# Zahlungsmittel und Mahnlauf

Entwurf vom 31. Juli 2026. Zwei Lieferungen, in dieser Reihenfolge.

## Was heute passiert, wenn eine Abbuchung scheitert

Gemessen im Code, nicht erinnert:

- `ApplyStripeBillingEvent::invoicePaymentFailed()` setzt den Vertrag auf
  `past_due` und schreibt einen `SubscriptionRecord` je Versuch.
- **Sonst nichts.** Keine Mail, keine Anzeige im Portal, keine Sperre. Der
  Kopfkommentar nennt auch den Grund: „cutting a customer off on the first
  failed attempt would punish an expired card as though it were a refusal to
  pay."
- Das Portal hat **keinerlei Zahlungsmittel-Verwaltung**. Kein
  `payment_method`, kein Billing-Portal, kein Bezahllink. Ein Kunde kann seine
  Karte nicht austauschen — nirgends.
- `ProxmoxClient::shutdownVm()` existiert (geordnet, mit Zeitlimit). Das
  Werkzeug zum Abschalten ist da, es benutzt nur niemand dafür.

Der letzte Punkt der Aufzählung ist der wichtigste: **ein Mahnlauf über einem
Portal ohne Kartenwechsel mahnt jemanden, der die Ursache gar nicht beheben
kann.** Deshalb die Zweiteilung, und deshalb diese Reihenfolge.

## Getroffene Entscheidungen

| Frage | Entscheidung |
|---|---|
| Wer führt den Mahnlauf | **CluPilot.** Stripes eigene Mahn-Mails werden im Stripe-Dashboard einmal von Hand abgeschaltet. Stripe versucht weiter automatisch abzubuchen; Fristen, Stufen, Gebühren, Sperre und alle Kundenmails kommen von hier. |
| Wie die Gebühr eingezogen wird | **Eigene Gebührenrechnung neben der gescheiterten.** Keine Stornierung, keine Rechnungsnummer wird angefasst. „Jetzt bezahlen" begleicht beide in einem Vorgang. |
| Wie weit die Sperre reicht | **Nur der betroffene Vertrag.** Andere Verträge desselben Kunden laufen weiter. |
| Kartenwechsel | **Eigene Maske mit Stripe Elements.** Bleibt im Design des Portals; der Publishable Key wird damit erstmals wirklich gelesen. |
| Gebührenhöhe (Vorgabe) | **Steigend: 5 € zur zweiten, 10 € zur dritten Mahnung.** Startstufe und Beträge im Adminbereich einstellbar. |

Eine Stripe-Abo-Rechnung ist beim Scheitern bereits finalisiert; eine Gebühr
lässt sich nachträglich nicht auf sie draufbuchen. Das ist der Grund für die
zweite Zeile, nicht Bequemlichkeit.

---

# Lieferung 1 — Zahlungsmittel und Nachzahlen

Für sich allein nützlich, auch ohne den Mahnlauf: ein Kunde mit abgelaufener
Karte kann sie heute nicht tauschen.

## Bestandteile

- **Kartenmaske im Portal** (Stripe Elements). Ein SetupIntent wird
  serverseitig erzeugt, die Maske bestätigt ihn im Browser, das entstandene
  Zahlungsmittel wird als Vorgabe am Stripe-Kunden gesetzt. SCA-Rückfragen
  laufen über Elements' eigene Bestätigung.
- **Offene Rechnungen bezahlen.** Liste der offenen Stripe-Rechnungen des
  Kunden mit einem Knopf je Rechnung und einem „alles offene bezahlen".
  Serverseitig `POST /invoices/{id}/pay`.
- **Banner im Portal**, sobald ein Vertrag `past_due` ist: was gescheitert ist,
  was offen ist, und die zwei Knöpfe.
- **`STRIPE_KEY`/`STRIPE_KEY_TEST`** wird ab hier gelesen. Fehlt er, zeigt die
  Maske das und bietet nichts an, statt eine leere Karte zu zeichnen.

## Prüfungen

- Ein Kunde ohne hinterlegtes Zahlungsmittel bekommt die Maske, nicht eine
  Fehlerseite.
- Ein bestätigter SetupIntent macht das Zahlungsmittel zur **Vorgabe** — sonst
  scheitert die nächste Abbuchung mit derselben kaputten Karte weiter.
- Bezahlen einer offenen Rechnung, die inzwischen anderweitig beglichen wurde,
  ist folgenlos und meldet keinen Fehler.
- Der Publishable Key des falschen Betriebsmodus wird abgewiesen, nicht
  benutzt.

---

# Lieferung 2 — Mahnlauf, Gebühren, Sperre

## Der Zustand lebt in einer eigenen Tabelle

`dunning_cases` — eine Zeile je **Rückstands-Episode**:

| Spalte | Bedeutung |
|---|---|
| `subscription_id` | welcher Vertrag |
| `stripe_invoice_id` | die gescheiterte Rechnung |
| `level` | 0 = Hinweis, 1–3 = Mahnstufe, 4 = gesperrt |
| `next_step_at` | wann der nächste Schritt fällig ist |
| `opened_at`, `settled_at` | Beginn und Ende der Episode |
| `fee_invoice_ids` | die ausgestellten Gebührenrechnungen |

**Nicht als Spalte an `subscriptions`.** Ein Kunde kann mehr als einmal in
Rückstand geraten; eine Spalte überschriebe die vorige Episode — und genau die
will jemand sehen, wenn der Kunde anruft und fragt, warum schon wieder Gebühren
anfallen.

## Die Teile

| Teil | Aufgabe |
|---|---|
| `App\Actions\OpenDunningCase` | Hängt an `invoicePaymentFailed()`. Je Rechnung genau einmal — ein zweiter fehlgeschlagener Versuch derselben Rechnung eröffnet keinen zweiten Fall. |
| `App\Services\Billing\DunningSchedule` | Fristen, Startstufe, Beträge aus `Settings`. Eine Stelle, an der die Vorgaben stehen. |
| `App\Console\Commands\AdvanceDunning` | Täglich. Fällige Fälle eine Stufe weiter: Mail, Gebührenrechnung, auf der letzten Stufe abschalten. |
| `App\Actions\SettleDunningCase` | Wenn **beide** Rechnungsarten bezahlt sind: Fall schließen, VM hochfahren, Marke entfernen. |
| Portal | Banner mit Grund und Betrag, „Zahlung nachholen". |
| Konsole | `admin.dunning` — Einstellungen oben, laufende Fälle darunter. |

## Vorgabe-Fristen

| Tag | Was passiert |
|---|---|
| 0 | Hinweis-Mail. **Keine** Mahnung, keine Gebühr — eine abgelaufene Karte ist keine Zahlungsverweigerung. |
| 3 | Erste Mahnung |
| 10 | Zweite Mahnung, **+5 €** |
| 17 | Dritte Mahnung, **+10 €** |
| 24 | Cloud wird abgeschaltet |

Alles einstellbar, einschließlich der Stufe, ab der eine Gebühr anfällt.

## Abschalten heißt abschalten

`shutdownVm()` — geordnet, mit Zeitlimit. Dazu `instances.suspended_at`, damit
weder Monitoring noch ein Wiederanlauf die Maschine versehentlich zurückholt.
**Nichts wird gelöscht, keine Platte angefasst.** Beim Begleichen: hochfahren,
Marke weg.

**Anmelden und Bezahlen bleiben immer offen.** Eine Sperre, die den Kunden von
der Rechnung aussperrt, ist ein Fehler, kein Druckmittel.

## Konsole: `admin.dunning`

Eigener Eintrag unter **Betrieb**, neben „Rechnungen" — das liest jemand,
während er sich um offenes Geld kümmert, nicht während er die Installation
einrichtet. Berechtigung `site.manage`, wie die zwei Seiten daneben.

Oben die Einstellungen. Darunter die laufenden Fälle mit Kunde, Vertrag, Stufe,
offenem Betrag und Fälligkeit — und den zwei Eingriffen, die ein Mensch
braucht: **Frist verlängern** und **Fall schließen** (wenn jemand überwiesen
hat). Beide mit Begründungsfeld, beide im Fall vermerkt.

## Mails

Sechs Vorlagen über `MailPurpose::BILLING`: Hinweis (Tag 0), Mahnung 1–3,
Sperre, Freigabe. Jede nennt den offenen Betrag, die Gebühren getrennt
ausgewiesen, und führt auf denselben Knopf im Portal.

## Prüfungen

- Ein Fall schreitet **nur** fort, wenn `next_step_at` erreicht ist, und ein
  zweiter Lauf am selben Tag tut nichts.
- Gebührenrechnungen entstehen **genau einmal je Stufe**, auch wenn der Befehl
  zweimal läuft.
- `SettleDunningCase` schließt erst, wenn Rückstand **und** Gebühren bezahlt
  sind — die halbe Zahlung schließt nichts.
- Die Sperre fährt herunter und **löscht nie**; ein Test, der das festhält,
  gehört zu den teuersten, die hier fehlen könnten.
- Ein beglichener Fall fährt die Maschine wieder hoch.
- Die Sperre verhindert **nicht** Anmeldung, Portal oder Bezahlseite.
- Ein Vertrag in Sperre lässt andere Verträge desselben Kunden unberührt.

## Was ausdrücklich nicht dazugehört

- **Kein Inkasso, keine Kündigung.** Nach der Sperre passiert nichts weiter
  von selbst; was dann geschieht, entscheidet ein Mensch.
- **Keine anteilige Rückrechnung.** Die laufende Periode bleibt, wie sie ist.
- **Keine zweite Stufe der Sperre** (ganzes Konto). Wenn sie gebraucht wird,
  ist sie ein eigener Entwurf.
