Mahnwesen — Mahnläufe (Payment Reminder)
Zweck
Das Mahnwesen erkennt überfällige Forderungen automatisch und erzeugt daraus Mahnungen je Kunde — entweder manuell per Knopfdruck oder als täglicher Auto-Lauf. Die Seite /payment-reminder-runs ist die operative Übersicht aller Mahnläufe: Sie zeigt pro Lauf, welche Kunden mit welcher Mahnstufe und welchem Gesamtbetrag erfasst wurden, und erlaubt den Versand der einzelnen Mahnungen per E-Mail.
Ein Mahnlauf bucht nichts und verändert keine offenen Posten — er ist eine Auswertung über die bestehenden Offenen Posten plus Versand-Workflow. Mahngebühren (surchargeAmount) werden in der Mahn-E-Mail ausgewiesen, aber nicht gebucht.
Die zugehörige Konfiguration (globale Aktivierung, Absender-Mailbox, Auto-Lauf-Zeit, Mahnstufen) liegt auf der eigenen Seite Mahnwesen — Einstellungen.
Voraussetzungen
Berechtigungen (CASL)
Frontend-Page-Guard (requiredAbility in src/routes.tsx):
| Action | Subject | Wirkung |
|---|---|---|
view | FE_PaymentReminder | Route /payment-reminder-runs und /settings/paymentreminder aufrufbar |
view | PaymentReminder | Inhalte (Läufe, Mahnungen) sichtbar |
API-Datenzugriff (caslMiddleware im BE-Router):
| Action | Subject | Endpoint(s) | Wirkung |
|---|---|---|---|
view | PaymentReminder | GET /payment-reminder-settings, …-levels, …-runs, …-notices, …/preview | Einstellungen, Stufen, Läufe, Mahnungen lesen |
create | PaymentReminder | POST /payment-reminder-runs | Mahnlauf manuell starten |
update | PaymentReminder | PATCH /payment-reminder-settings, …-levels/:id, POST …/send-all, POST …/:id/send | Einstellungen/Stufen ändern, Mahnungen versenden |
Die Subjects PaymentReminderLevel, PaymentReminderRun, PaymentReminderNotice und PaymentReminderNoticeItem existieren zusätzlich als feingranulare Lese-Subjects; im Standard reicht die Gruppe PaymentReminder.
Funktionsweise
Der Ablauf läuft in vier Stufen: Mahnstufen konfigurieren → Vorschlag berechnen → Mahnlauf erzeugen → Mahnungen versenden.
Vorschlag (computeDunningProposals)
Die Vorschlagsberechnung ist read-only und persistiert nichts. Sie:
- Lädt alle aktiven Mahnstufen (absteigend nach
daysOverdue). - Lädt alle Offenen Posten mit
parentType = SalesDocumentund filtert auf überfällig:outstandingAmount > 0,settlementStatus ≠ settled,dueDate < heute. - Löst Beleg, Belegnummer und Kunde (bzw. Standort) auf.
- Mahnsperre-Filter: Kunden bzw. Standorte mit
dunningBlock = truewerden komplett übersprungen. - Bestimmt je Kunde die höchste erreichte Mahnstufe anhand der maximalen Überfälligkeit aller seiner offenen Rechnungen.
- Ermittelt Aufschlag (
surchargeAmountder Stufe) und Empfänger-Adresse.
Mahnlauf (createDunningRun)
POST /payment-reminder-runs ruft die Vorschlagsberechnung auf und persistiert das Ergebnis atomar: einen PaymentReminderRun (status = completed), je Kunde eine PaymentReminderNotice (status = pending) und je überfälliger Rechnung eine PaymentReminderNoticeItem. Der Lauf trägt triggeredBy = manual (Button) oder auto (Scheduler).
Versand (sendDunningNotice)
Der Versand ist idempotent (eine bereits gesendete Mahnung wird nicht erneut verschickt). Pro Mahnung:
- Empfänger wird aus den Kunden-Attributen gezogen — Priorität „Email (Dunning)", sonst „Email (General)". Fehlt eine Adresse, wird die Mahnung auf
skippedgesetzt (kein Fehler). - Der E-Mail-Text kommt aus einem Textbaustein-Template (
emailCategory = dunning, Position = Mahnstufe); fehlt es, greift ein deutscher Standardtext. - Ist
attachInvoicesder Stufe aktiv, werden die Rechnungs-PDFs angehängt. - Versendet wird über die in den Einstellungen hinterlegte Absender-Mailbox; die erzeugte Mail wird mit Mahnung, Lauf, Kunde und Beleg verknüpft.
- Erfolg →
status = sent(mitsentMailId,sentAt,sentByEmployeeId); technischer Fehler →status = failedmiterrorMessage.
Schritt-für-Schritt-Anleitung
Mahnlauf starten und versenden
- Mahnläufe (
/payment-reminder-runs) öffnen. - Optional zuerst die Vorschau prüfen (
GET /payment-reminders/preview) — sie zeigt ohne Persistenz, wer aktuell gemahnt würde. - Mahnlauf starten → es entsteht ein neuer Lauf mit je einer Mahnung pro betroffenem Kunden (Status
pending). - Lauf öffnen → Liste der Mahnungen je Kunde mit Stufe, Gesamtbetrag und Empfänger-Adresse.
- Einzelne Mahnung Senden oder im Lauf Alle senden — die E-Mails gehen über die hinterlegte Absender-Mailbox raus.
- Status je Mahnung kontrollieren:
sent,skipped(keine Adresse) oderfailed(Fehlertext prüfen).
Mahnsperre für einen Kunden setzen
Damit ein Kunde (oder ein einzelner Standort) nicht gemahnt wird, setzen Sie im Kunden- bzw. Standort-Formular den Schalter Mahnsperre (dunningBlock). Gesperrte Kunden/Standorte werden in jedem Vorschlag und Lauf übersprungen — siehe Mahnsperre.
Datenmodell
Vier Modelle bilden das Mahnwesen ab (alle mit id, sequenceId, Soft-Delete deletedAt und Standard-Zeitstempeln):
PaymentReminderLevel — Mahnstufe
| Feldname | Pflicht | Datentyp | Beschreibung | Wirkung beim Ausfüllen | Voraussetzung |
|---|---|---|---|---|---|
level | ja | Integer (unique) | Mahnstufe (1, 2, 3 …) | Reihenfolge der Eskalation | je Stufe genau ein Eintrag |
active | ja | Boolean (Default false) | Stufe aktiv? | nur aktive Stufen werden berücksichtigt | — |
daysOverdue | ja | Integer (Default 14) | Schwelle: Tage nach Fälligkeit | ab so vielen Tagen Überfälligkeit greift diese Stufe | — |
surchargeAmount | nein | Decimal(10,2) | Mahngebühr (€) | wird in der E-Mail ausgewiesen, nicht gebucht | — |
attachInvoices | nein | Boolean | Rechnungs-PDFs anhängen? | hängt die Belege an die Mahn-E-Mail | — |
autoSend | nein | Boolean | im Auto-Lauf direkt versenden? | beim täglichen Lauf wird diese Stufe sofort gesendet, sonst bleibt sie pending | — |
PaymentReminderRun — Mahnlauf
| Feldname | Datentyp | Beschreibung |
|---|---|---|
runDate | Date | Datum des Laufs |
status | Enum (draft, completed) | bei Erstellung immer completed |
triggeredBy | Enum (manual, auto) | manuell (Button) oder Scheduler |
employeeId | UUID, nullable | wer hat den Lauf ausgelöst (bei manual) |
customerCount | Integer | Anzahl gemahnter Kunden |
noticeCount | Integer | Anzahl Mahnungen |
totalAmount | Decimal(12,2) | Gesamtbetrag (offene Posten + Aufschläge) |
PaymentReminderNotice — Mahnung (je Kunde)
| Feldname | Datentyp | Beschreibung |
|---|---|---|
runId | UUID | FK → PaymentReminderRun |
customerId | UUID | FK → Customer |
level | Integer | Mahnstufe dieser Mahnung |
totalOverdue | Decimal(12,2) | offener Gesamtbetrag des Kunden |
surchargeAmount | Decimal(10,2) | Aufschlag dieser Mahnung |
recipientEmail | String, nullable | Empfänger (aus Kunden-Attribut) |
status | Enum (pending, sent, skipped, failed) | Versand-Status |
sentMailId | UUID, nullable | FK → Mail (bei Versand) |
sentAt / sentByEmployeeId | Date / UUID | Versand-Zeitpunkt und -Person |
errorMessage | Text, nullable | Fehlertext bei failed |
PaymentReminderNoticeItem — Position (je Rechnung)
| Feldname | Datentyp | Beschreibung |
|---|---|---|
noticeId | UUID | FK → PaymentReminderNotice |
salesDocumentId | UUID | FK → SalesDocument (die Rechnung) |
openItemId | UUID, nullable | FK → OpenItem |
amount | Decimal(12,2) | offener Betrag dieser Rechnung |
dueDate | Date, nullable | Fälligkeitsdatum |
daysOverdue | Integer | berechnete Überfälligkeit in Tagen |
Mahnsperre (dunningBlock)
Die Mahnsperre ist ein Boolean-Feld dunningBlock (Default false) auf zwei Entitäten:
| Entität | Feld | Wirkung |
|---|---|---|
| Kunde | dunningBlock | alle Rechnungen des Kunden werden vom Mahnwesen ausgeschlossen |
| Standort | dunningBlock | Rechnungen, die an diesen Standort hängen, werden ausgeschlossen |
Im Frontend erscheint das Feld als Schalter (Label „Mahnsperre") im Kunden- bzw. Standort-Formular. Der Ausschluss greift sowohl in der Vorschau als auch in jedem Mahnlauf (manuell und automatisch).
Automatischer Mahnlauf (Scheduler)
Ist in den Einstellungen sowohl enabled als auch autoDailyRun aktiv, läuft täglich zur konfigurierten Uhrzeit (runTime, Default 06:00, Zeitzone Europe/Berlin) ein automatischer Mahnlauf über node-cron:
- Der Lauf wird mit
triggeredBy = autoerzeugt. - Nur Mahnungen, deren Stufe
autoSend = truehat, werden direkt versendet — alle übrigen bleibenpendingund warten auf manuelle Freigabe. - Fehler beim Einzelversand werden protokolliert, der Lauf selbst wird nicht zurückgerollt.
Verknüpfungen zu anderen Modulen
- Mahnwesen — Einstellungen — Aktivierung, Absender-Mailbox, Auto-Lauf, Mahnstufen.
- Offene Posten — Datenbasis: überfällige Forderungen.
- Forderungen Kunden — fokussierte Receivables-Sicht.
- Verkaufsbelege — erzeugen die offenen Posten, die gemahnt werden.
- Kunden / Standorte — tragen die Mahnsperre und die Empfänger-Adressen.
- Mail — Versandkanal; jede Mahnung wird mit der gesendeten Mail verknüpft.
Wiederverwendbare Konzepte
Häufige Fehler und Lösungen
| Fehler | Lösung |
|---|---|
Mahnung bleibt auf skipped | Kunde hat keine Adresse im Attribut „Email (Dunning)" oder „Email (General)". Adresse hinterlegen. |
Mahnung wird failed | Keine Absender-Mailbox in den Einstellungen oder Mailversand-Fehler — errorMessage prüfen. |
| Kunde fehlt im Lauf | Mahnsperre (dunningBlock) gesetzt, keine aktive Stufe für die Überfälligkeit erreicht, oder Posten bereits settled. |
| Auto-Lauf startet nicht | enabled oder autoDailyRun aus, keine aktive Stufe, oder geänderte runTime ohne Server-Neustart. |
| Aufschlag taucht in der Buchhaltung nicht auf | Korrekt — surchargeAmount wird nur in der E-Mail ausgewiesen, nicht gebucht. |
API/Schnittstellen
| Methode | Endpoint | Zweck | CASL |
|---|---|---|---|
GET | /payment-reminder-settings | Einstellungen lesen | view PaymentReminder |
PATCH | /payment-reminder-settings | Einstellungen ändern | update PaymentReminder |
GET | /payment-reminder-levels | Mahnstufen auflisten | view PaymentReminder |
PATCH | /payment-reminder-levels/:id | Mahnstufe ändern | update PaymentReminder |
GET | /payment-reminders/preview | Vorschau (read-only) | view PaymentReminder |
GET | /payment-reminder-runs | Mahnläufe auflisten | view PaymentReminder |
POST | /payment-reminder-runs | Mahnlauf starten | create PaymentReminder |
GET | /payment-reminder-runs/:id | Lauf-Detail | view PaymentReminder |
POST | /payment-reminder-runs/:id/send-all | alle offenen Mahnungen versenden | update PaymentReminder |
GET | /payment-reminder-notices | Mahnungen auflisten | view PaymentReminder |
GET | /payment-reminder-notice-items | Positionen auflisten | view PaymentReminder |
POST | /payment-reminder-notices/:id/send | einzelne Mahnung versenden | update PaymentReminder |
Versionshinweise
- 2026-06-11: Initiale Veröffentlichung — Mahnwesen-Modul (Mahnstufen, Mahnläufe, Mahnungen, Versand, Mahnsperre, Auto-Lauf). Verifiziert an
paymentReminder.service.ts,paymentReminder.router.ts, den vierpaymentReminder*-Modellen undroutes.tsx.