billing: MAN-Rechnungen fuer Ueberweisungskunden bearbeiten, hold-Altlast bereinigen

Bestandskunden aus dem Altsystem zahlen per Ueberweisung; nach dem 04:30-Lauf
fehlte bisher der gesamte Bearbeitungsweg (kein PDF, keine Mail, kein
Zahlungseingang, kein Bestandsschutz-Ende). Plan und Befund:
dev/lagacy-rechnungen/entwicklungsplan-2026-09-16.md.

Bereinigung (legacy:settle-unpaid, wiederholbar):
- Legacy-Status hold = nie bezahlt, ersatzlos erledigt. 9 daraus abgeleitete
  Bestandsschutz-Buendel werden gekuendigt, MAN-00001/00002 storniert.
  Archivzeilen bleiben erhalten, sind aber in Admin-Archiv und Kundenkonto
  standardmaessig ausgeblendet.
- pending/reminder (recurring) = offene Vorgaenge: Periode auf den
  Leistungsbeginn der unbezahlten Rechnung zurueckgedreht und als MAN-Rechnung
  neu ausgestellt (Archiv-Status superseded, replaces_legacy_invoice_number).
- pending/reminder (single) = abgeschrieben (uncollectible).
- legacy:grandfather-subscriptions wertet den Zahlungsstatus jetzt selbst aus.

Workflow in /admin/payments (ManualInvoiceDunningService, je Aktion mit
Eintrag im Aktivitaets-Verlauf):
- Rechnung senden: Mail mit PDF-Anhang, Bankverbindung aus billing.issuer,
  Hinweis auf Bestandsschutz-Ende; Zahlungsziel zaehlt ab Versand.
- Erinnerung: gleiche Mail im Mahn-Ton, Nachfrist bei verstrichener Frist.
- Bezahlt: Zahlungseingang laut Kontoauszug verbuchen.
- Bestandsschutz beenden: Rechnung storniert (nie geloescht), Buendel
  gekuendigt, weitere offene MAN-Rechnungen des Kunden storniert, optionale
  Kundenmail.
- Panel "Blockierte Vereinbarungen": faellige Posten ohne Rechnung (meist
  fehlende Rechnungsadresse) mit "Jetzt abrechnen" oder "Entfernen".

Schema: invoices bekommt user_payment_option_id, sent_at, reminded_at,
reminder_count, voided_at, void_reason, replaces_legacy_invoice_number
(inkl. Backfill der bestehenden MAN-Rechnungen). PDF-Primitiven des
Archiv-Renderers in den Trait DrawsPdf ausgelagert; MAN-PDF on-demand ueber
admin.invoices.pdf / me.invoices.manual-pdf (InvoicePolicy).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-16 10:56:29 +00:00
parent f581e1eb3a
commit a00f9dc79a
33 changed files with 3606 additions and 195 deletions

View file

@ -0,0 +1,348 @@
# Legacy-Rechnungen per Überweisung: Versand, Zahlungseingang, Bestandsschutz-Ende
Stand: 16.09.2026 (Analyse auf DEV-DB vom 15.09.2026) · Branch `technik/betriebsfixes-2026-09`
## Status 16.09.2026: umgesetzt (Phase B, 0, 1, 2, 3)
- Branch `technik/betriebsfixes-2026-09`, alle 1670 Tests grün.
- Entscheidungen wie vorgeschlagen; E7 = Archiv **behalten und ausblenden**.
- `legacy:settle-unpaid` auf der DEV-DB gelaufen: 9 Bündel gekündigt, MAN-00001/00002 storniert,
6 Vorgänge zurückgedreht, 7 Einzelkäufe abgeschrieben. **Nur 1 der 6** offenen Vorgänge konnte
sofort neu ausgestellt werden (MAN-00003 ersetzt 43541-7) — die anderen **5 haben keine
Rechnungsadresse** (in der Alt-Rechnung stand nur der Name) und stehen im Panel „Blockierte
Vereinbarungen": Adresse im User-Profil nachtragen → „Jetzt abrechnen" → „Rechnung senden".
- Auf Live nach dem Deploy: `php artisan migrate`, dann `php artisan legacy:settle-unpaid --dry-run`
prüfen und ohne `--dry-run` ausführen.
- Phase 4 (Automatisierung) bewusst offen.
## 0. Worum es geht
Bestandskunden aus dem Altsystem zahlen weiter per **Überweisung** auf das eigene
Konto. Neue Abschlüsse laufen ausschließlich über Stripe. Für die Bestandskunden
fehlt heute der gesamte Bearbeitungsweg *nach* der Rechnungserzeugung:
> Rechnung fällig → **Knopfdruck**: Mail mit Rechnung (PDF), Zahlungsbitte und Hinweis
> „ohne Zahlung endet der Bestandsschutz in 14 Tagen" → Zahlung eingegangen: **als bezahlt
> markieren** → keine Zahlung: **Bestandsschutz beenden** (Rechnung stornieren, Bündel kündigen).
---
## 1. Ist-Zustand (was schon da ist, was fehlt)
### 1.1 Drei Datenwelten — nur eine davon ist relevant
| Tabelle | Inhalt | Rolle in diesem Vorhaben |
|---|---|---|
| `legacy_invoices` (887 Zeilen, 91 ohne `paid_at`) | Read-only-Archiv der Alt-Rechnungen (D-12), eigener PDF-Renderer | Historie, bleibt read-only. Die „unbezahlten" teilen sich in 94 `hold` (nie bezahlt → erledigt, s. 1.4) und 13 `pending`/`reminder` (offene Vorgänge 2026, die sauber abzuarbeiten sind). |
| `user_payment_options` mit `status = grandfathered` (22 laufend) | Die Bestandsschutz-Vereinbarungen (je Portal ein Posten, nur als **Bündel** kündbar – `LegacySubscriptionService::cancelBundleFor()`) | Das ist der „Bestandsschutz", der bei Nichtzahlung beendet wird. |
| `invoices` ohne `stripe_invoice_id` = **MAN-Kreis** (2 Zeilen) | Neue Rechnungen, die der 04:30-Lauf `billing:generate-manual-invoices` beim Erreichen von `current_period_end` erzeugt (`ManualInvoiceService`) | **Hier setzt alles an.** Die Rechnung existiert bereits als Datensatz — aber sonst nichts. |
### 1.2 Was der MAN-Kreis heute kann
- Fälligkeit erkennen, Rechnungsadresse einfrieren, USt via `VatResolver` (DE 19 %, EU mit USt-ID
befreit, Drittland befreit), MAN-Nummer vergeben, `due_date = invoice_date + 14 Tage`
(`billing.manual_due_days`), Periode um ein Jahr weiterschalten.
- Anzeige in `/admin/payments` → Tabelle „Rechnungsausgang (STR/MAN)" (nur lesen).
- Anzeige im Kundenkonto `/admin/me/invoices` (Spalte „Beleg" zeigt für MAN nur „–").
### 1.3 Was fehlt (Lücken, die der Plan schließt)
| # | Lücke | Konsequenz heute |
|---|---|---|
| L1 | **Kein PDF** für MAN-Rechnungen (`LegacyInvoicePdfRenderer` kann nur `LegacyInvoice`) | Kunde bekommt keinen Beleg, Admin kann nichts verschicken |
| L2 | **Keine Mail** – weder bei Erzeugung noch per Hand | Rechnungen MAN-00001/MAN-00002 sind seit **07.08.2026** offen, seit 21.08. überfällig und wurden **nie zugestellt** |
| L3 | **Kein Link Rechnung → Vereinbarung** (`invoices.user_payment_id` ist NULL, zeigt ohnehin auf `user_payments`, nicht auf `user_payment_options`) | Beim „Bestandsschutz beenden" lässt sich nicht sicher sagen, zu welchem Posten die Rechnung gehört; man muss über den User gehen |
| L4 | **Keine Admin-Aktionen**: senden, erinnern, bezahlt markieren, stornieren | Zahlungseingänge lassen sich nicht verbuchen → Kunden bleiben ewig „offen" |
| L5 | **Keine Versand-/Mahnhistorie** (kein `sent_at`, `reminded_at`) | Man sieht nicht, was der Kunde wann bekommen hat |
| L6 | **Blockierte Vereinbarungen sind unsichtbar**: Posten ohne Rechnungsadresse werden täglich übersprungen (nur Log-Warning) | UPO #15 (User 59128) und #18 (User 62129, netto 1.000 €) hängen seit **02./14.08.2026** fest — und weil `current_period_end` in der Vergangenheit liegt, gelten sie per `scopeGrandfatheredNow()` als **nicht mehr aktiv** („Buchung erforderlich"), obwohl nie eine Rechnung gestellt wurde |
| L7 | **Rechnungsaussteller** ist nirgends konfiguriert; der Legacy-Renderer hat adametz.media (BP24) und Stern Consulting (Presseecho) inkl. IBAN **hartcodiert** | Für neue Rechnungen braucht es eine Quelle für Absender, Bankverbindung, Steuernummer |
### 1.4 Befund 16.09.: Der Bestandsschutz wurde aus **unbezahlten** Rechnungen abgeleitet
Einordnung (Kevin, 16.09.): Legacy-Status `hold` = nie bezahlt, überfällig, Kunde oft nicht mehr
erreichbar → **ersatzlos erledigt**. Nur `pending` und `reminder` sind noch offene, sauber
abzuarbeitende Vorgänge.
`legacy:grandfather-subscriptions` hat diese Unterscheidung **nicht** gemacht: Es filtert nur auf
`payment_option.type = recurring` und `user_payment_option.status = active`, nicht auf den
Zahlungsstatus der Quellrechnung. Ergebnis über die 22 laufenden Bestandsschutz-Posten:
| Quellrechnung | Posten | Bewertung |
|---|---|---|
| `paid` | 7 (#2, #3, #5, #6, #7, #11, #22) | **Echte Bestandskunden.** Regelbetrieb über den MAN-Kreis, nächste Fälligkeit Okt. 2026 – Jun. 2027. |
| `hold` | **9** (#4, #8, #10, #14, #15, #16, #17, #18, #19) | **Karteileichen.** Zahlungshistorie durchgehend `hold` seit 2018–2022 (z. B. #8: letzte Zahlung 12/2017, seither 8× unbezahlt). Darunter genau die beiden Kunden, an die **MAN-00001/00002** gestellt wurden (User 59455: 5× hold seit 2021, User 60911: nur hold, nie gezahlt) – und die beiden „blockierten" Posten #15/#18 aus L6. |
| `reminder` | 5 (#12, #13, #20, #21, #23) | **Offene Vorgänge 2026.** Rechnungen vom 30.06.2026, im Altsystem bereits gemahnt, fällig 14.07.2026, Leistungszeitraum bis Mitte 2027. Gemischte Historie (#13: 2024/25 bezahlt; #12: seit 2020 hold, 2026 trotzdem gemahnt). |
| `pending` (recurring) | 1 (#1, User 9338) | **Offener Vorgang.** 49 € Presseecho, Rechnung 07.07.2026, Kunde seit 2011, zuletzt wechselnd. |
Die sechs offenen Vorgänge haben eine Tücke: Ihre Legacy-Rechnung (Juni/Juli 2026) ist **unbezahlt**,
aber die Migration hat `current_period_end` bereits auf **2027** gesetzt (= `next_due_date` der
Rechnung). Der MAN-Kreis würde sie also erst 2027 wieder abrechnen – die offene 2026-Forderung
kennt das neue System nicht.
Daneben gibt es **7 `pending`-Einzelkäufe** (19,95 €, Juli 2026, BP24): Gmail-/SEO-Accounts, PM ist
jeweils `published`, nie bezahlt. Kein Bestandsschutz betroffen, aber Teil des „sauber abarbeiten".
Die 94 `hold`-Archivrechnungen (17.299,93 €, 2018–2026) haben alle eine User-Zuordnung.
Was bereits **mitgenutzt** werden kann (kein Neubau nötig):
- Globales Mail-Gate `GateOutgoingMail` (Bounce-Sperrliste) → jede neue Mailable ist automatisch geschützt.
- Mail-Layout `x-emails.layout` / `x-emails.button`, Muster `BookingPaymentConfirmed` (BCC an `legal.contact_email`).
- `ActivityLog` für den Verlauf, Permission `billing:manage`, Confirmation-Modals im Payments-Panel (Vorbild: „Bündel kündigen").
- PDF-Primitiven (`text()`, `line()`, `buildPdf()`) im `LegacyInvoicePdfRenderer` – ohne externe PDF-Library.
---
## 2. Zielbild: der Workflow in vier Schritten
```
04:30-Lauf Admin (Knopf) Admin (Knopf) Admin (Knopf)
─────────── ───────────── ───────────── ─────────────
Periode endet ──▶ MAN-Rechnung ──▶ „Rechnung senden" ──▶ „Als bezahlt markieren" oder
(status open) Mail + PDF-Anhang paid_at, status paid „Bestandsschutz beenden"
due_date = +14 Tage status void + Bündel gekündigt
sent_at gesetzt (cancelBundleFor)
│
└──▶ optional „Erinnerung senden"
(gleiche Mail, Mahn-Ton, reminded_at)
```
Rechnungen werden **nicht gelöscht**, sondern auf `void` („Storniert") gesetzt: die
MAN-Nummernfolge bleibt lückenlos, Buchhaltung/GoBD sind sauber, und der Vorgang bleibt
nachvollziehbar. Löschen im Wortsinn passiert beim **Bestandsschutz** (Posten → `cancelled`),
nicht bei der Rechnung.
---
## 3. Entwicklungsplan
Vier Phasen, jede für sich testbar und deploybar. Phase 0–3 ergeben zusammen den
gewünschten Knopfdruck-Workflow; Phase 4 ist optionale Automatisierung.
### Phase B — Bereinigung der Altlast (zuerst, unabhängig vom Rest) · Aufwand: klein
Ein einmaliger, wiederholbarer Befehl `legacy:settle-unpaid` (`--dry-run`, JSON-Report wie die
anderen Migrationsbefehle), der die Befunde aus 1.4 auflöst:
1. **`hold` → erledigt.**
- Die **9 aus `hold` abgeleiteten Bestandsschutz-Posten** werden gekündigt
(`cancelBundleFor()` je User, mit `ActivityLog`-Eintrag „Bestandsschutz beendet: Quellrechnung
nie bezahlt"). Damit fallen auch die „blockierten" Posten #15/#18 aus L6 weg – die brauchen
keine Rechnungsadresse mehr.
- **MAN-00001/00002** werden storniert (`void`, Grund „Kunde hat Vorjahresrechnung nie bezahlt") —
nicht versendet. Damit erledigt sich E6.
- Die 94 `hold`-Archivrechnungen: Vorschlag **im Archiv belassen** (read-only, D-12; Löschen von
Rechnungsdatensätzen ist auch bei nie bezahlten unschön), aber im Admin-Archiv standardmäßig
ausblenden (Filter „ohne hold"), und in der Kundenansicht `/admin/me/invoices` nicht mehr als
„offen" anzeigen. Wenn du sie wirklich weg haben willst: `--delete-hold` als bewusste Option,
löscht die 94 Zeilen (→ E7).
2. **`reminder`/`pending` (recurring, 6 Posten) → in den neuen Workflow überführen.**
Einfachster Weg: die Periode der 6 Posten **um ein Jahr zurückdrehen** (`current_period_start/end`
= Werte der offenen Legacy-Rechnung, also Ende = 30.06./07.07.2026). Dann stellt der nächste
04:30-Lauf ganz normal eine MAN-Rechnung für 2026/27 aus, und ab da gilt der Knopfdruck-Workflow
aus Phase 2/3 (senden → 14 Tage → bezahlt oder beenden). Die Legacy-Rechnung wird im Archiv als
`superseded` markiert (neue Status-Zeichenkette, verweist auf die MAN-Nummer), damit nicht zwei
Forderungen für denselben Zeitraum nebeneinander stehen. Kein zweiter Rechnungsweg, kein
Sonderfall im Code.
3. **7 `pending`-Einzelkäufe (19,95 €).** Fachliche Entscheidung (→ E8): entweder als `uncollectible`
abschreiben (Vorschlag – Gmail-/SEO-Accounts, Aufwand > Ertrag) oder die PM depublizieren.
Technisch ist beides ein Status-Update im Archiv bzw. ein Aufruf der bestehenden Depublikation.
4. **Ursache beheben:** `GrandfatherLegacySubscriptions` bekommt einen Filter auf den Zahlungsstatus
der Quellrechnung (`paid` → Bestandsschutz; `pending`/`reminder` → Bestandsschutz **mit** offener
Forderung, Periode = Rechnungsperiode; `hold` → überspringen). Der Befehl ist laut Doku
replay-fähig; so kann das nicht noch einmal passieren.
**Tests:** `LegacySettleUnpaidCommandTest` (hold-Posten gekündigt, MAN-Rechnungen void, reminder-Posten
zurückgedreht, paid-Posten unangetastet, dry-run schreibt nichts); `GrandfatherLegacySubscriptionsTest`
um die drei Status-Fälle erweitern.
**Ergebnis nach Phase B:** 7 gesunde Bestandskunden + 6 offene Vorgänge, die der MAN-Kreis binnen
eines Tages als Rechnungen ausstellt. Erst dann lohnen sich die Knöpfe.
### Phase 0 — Datenmodell & Konfiguration (Fundament) · Aufwand: klein
1. **Migration `invoices`** (Tabelle ist winzig, kein FULLTEXT → unkritisch):
- `user_payment_option_id` (nullable FK) — Link zur Vereinbarung (schließt L3)
- `sent_at`, `reminded_at`, `reminder_count` (tinyint) — Versandhistorie (L5)
- `voided_at`, `void_reason` (string, nullable) — Nachvollziehbarkeit beim Storno
2. **`ManualInvoiceService::invoiceFor()`** setzt künftig `user_payment_option_id`.
3. **Backfill** für MAN-00001/00002 in derselben Migration (User 59455 → UPO #14, User 60911 → UPO #16;
eindeutig, weil je genau ein Grandfathered-Posten) – auch wenn beide in Phase B storniert werden,
soll der Storno an der richtigen Vereinbarung hängen.
4. **`config/billing.php` → Block `issuer`** (Absender der MAN-Rechnungen, alles per `.env` überschreibbar):
Firmenname, Inhaber, Straße, PLZ/Ort, Telefon, E-Mail, Web, IBAN, BIC, Bank, Steuernummer,
USt-ID (letztere existiert schon als `own_vat_id`). Schließt L7 — und ist gleichzeitig die
Datenquelle für die Mail (Bankverbindung) und das PDF.
5. **`InvoicePolicy`** (analog `LegacyInvoicePolicy`): `view`/`downloadPdf` für Eigentümer und Admin,
`manage` = `billing:manage`.
**Tests:** Migration + Backfill (Pest, `RefreshDatabase`), `invoiceFor()` schreibt `user_payment_option_id`.
### Phase 1 — PDF für MAN-Rechnungen · Aufwand: mittel
1. PDF-Primitiven aus `LegacyInvoicePdfRenderer` in einen Trait `App\Services\Billing\Concerns\DrawsPdf`
(oder abstrakte Basisklasse) ziehen — reine Verschiebung, Legacy-Renderer bleibt funktional identisch.
2. **`ManualInvoicePdfRenderer`** (für `Invoice`): Absender aus `billing.issuer`, Empfänger aus
`invoice_billing_addresses`-Snapshot, Leistungsname aus `legacy_conditions.name` bzw. `payment_option`,
Leistungszeitraum aus der Vereinbarung (`current_period_start/end` **vor** dem Weiterschalten —
dafür genügt: Periodenstart = `invoice_date`, Ende = +1 Jahr/Monat), Netto/USt/Brutto,
`tax_note` (Reverse-Charge/Drittland), Zahlungsziel `due_date`, Bankverbindung.
Ergebnis wird **nicht** gespeichert (wie beim Archiv on-demand gerendert); `pdf_path` bleibt frei
für eine spätere Ablage.
3. **Routen**: `admin/invoices/{invoice}/pdf` (Admin) und `me/invoices/{invoice}/pdf` (Kunde),
Controller `InvoicePdfController`, Policy-Check; für STR-Rechnungen 404 (dort bleibt Stripe der Beleg).
4. **Kundenkonto**: in `/admin/me/invoices` Spalte „Beleg" für MAN → Button „PDF öffnen" statt „–".
**Tests:** Renderer liefert gültiges PDF (`%PDF-`-Header, Rechnungsnummer/Beträge im Stream), Route
403/404-Fälle, Legacy-Renderer-Snapshot unverändert (bestehende Tests grün).
### Phase 2 — Mail & Service (die eigentliche Logik) · Aufwand: mittel
1. **`ManualInvoiceDunningService`** (neu, `App\Services\Billing`), kapselt die vier Aktionen —
jede transaktional, jede mit `ActivityLog`-Eintrag (neuer `ActivityType::ManualInvoice`):
- `send(Invoice, User $actor)` — nur `status = open` und ohne `stripe_invoice_id`;
setzt `sent_at`, **`due_date = heute + manual_due_days`** (die 14 Tage zählen ab Zustellung,
nicht ab dem 04:30-Lauf — s. Entscheidung E1), verschickt Mailable.
- `remind(Invoice, User $actor)` — nur wenn `sent_at` gesetzt; `reminded_at`, `reminder_count++`,
gleiche Mailable im Mahn-Ton (Variante über Konstruktor-Flag).
- `markPaid(Invoice, Carbon $paidAt, User $actor)` — `status = paid`, `paid_at`.
- `terminateForNonPayment(Invoice, User $actor, ?string $note)` — `status = void`, `voided_at`,
`void_reason`; ruft `LegacySubscriptionService::cancelBundleFor($invoice->user)`; **weitere offene
MAN-Rechnungen desselben Users** werden ebenfalls storniert (ein Kunde, ein Bündel, ein Vorgang).
2. **Mailable `ManualInvoiceIssued`** (queued, `ShouldQueue`, BCC an `legal.contact_email` wie
`BookingPaymentConfirmed`), PDF als **Anhang** (`attachData` aus dem Renderer), View
`emails/billing/manual-invoice-issued.blade.php`:
- Anrede, Bezug auf Bestandsschutz-Leistung und Portal, Rechnungsnummer/-datum, Netto/USt/Brutto
- **Bankverbindung** (aus `billing.issuer`) und Verwendungszweck (= Rechnungsnummer)
- Zahlungsziel als Datum (`due_date`)
- Klar formulierter Hinweis: *„Geht die Zahlung nicht bis zum {due_date} ein, endet Ihr
Bestandsschutz (…) zu diesem Datum. Ihre bisherigen Pressemitteilungen bleiben online; neue
Veröffentlichungen sind dann nur über die aktuellen Tarife möglich."* (Wortlaut → E3)
- Button „Rechnung im Konto ansehen", Hilfe-Link
- Mahn-Variante: Betreff „Zahlungserinnerung …", Einleitung „wir haben noch keinen Zahlungseingang
festgestellt", Frist identisch (`due_date`), sonst gleicher Inhalt.
- Transaktionsmail → kein Opt-out; Bounce-Sperrliste greift automatisch über `GateOutgoingMail`.
3. **Kunden-Mail bei Beendigung?** Vorschlag: **ja, eine kurze Bestätigung** („Bestandsschutz beendet,
Rechnung storniert, so buchen Sie neu"). Ist eine zweite kleine Mailable `GrandfatheringTerminated`.
Kann in Phase 3 mit einer Checkbox im Modal optional gemacht werden (→ E4).
**Tests (`tests/Feature/Billing/ManualInvoiceDunningTest.php`):** `Mail::fake()`; send setzt `sent_at`
und neue `due_date`, Mail an User mit PDF-Anhang und BCC; send auf `paid`/STR-Rechnung wirft
Exception; remind ohne send verweigert; markPaid; terminate → Rechnung `void`, alle Grandfathered-Posten
`cancelled`, zweite offene MAN-Rechnung ebenfalls `void`; gesperrte Adresse → Mail wird vom Gate
gestoppt, `sent_at` bleibt trotzdem gesetzt? → **nein**: Service prüft vorher
`EmailSuppressionService::isSuppressed()` und verweigert mit Hinweis (Admin soll es sehen).
### Phase 3 — Admin-Oberfläche · Aufwand: mittel
Alles in der bestehenden Volt-Seite `livewire/admin/payments/index.blade.php`, kein neuer Screen:
1. **Tabelle „Rechnungsausgang (STR/MAN)"** — neue Spalte „Aktionen" (nur MAN-Zeilen):
- „PDF" (öffnet Phase-1-Route)
- Status `open`, noch nicht gesendet → **„Rechnung senden"** (Modal: Empfänger-Adresse, Betrag,
neues Zahlungsziel, Warnung falls Adresse gesperrt)
- Status `open`, gesendet → **„Erinnerung senden"**, **„Als bezahlt markieren"** (Modal mit
Datumsfeld, Default heute), **„Bestandsschutz beenden"** (Danger-Modal wie „Bündel kündigen",
zeigt die betroffenen Posten + weitere offene Rechnungen des Users, optional Checkbox „Kunde
informieren")
- Zusatzinfos in der Status-Zelle: „gesendet 17.09.", „erinnert 01.10. (1×)", Badge **„überfällig"**
wenn `due_date < heute` und `open`, „storniert" mit Grund-Tooltip.
2. **Filter/Tab „nur MAN offen"** oberhalb der Tabelle (Standardansicht dieses Vorgangs), KPI-Karte
**„Offene MAN-Rechnungen"** (Anzahl + Summe, davon überfällig).
3. **Panel „Blockierte Vereinbarungen"** (schließt L6; nach Phase B aktuell leer, bleibt aber als
Frühwarnung sinnvoll): Grandfathered-Posten mit `current_period_end <
heute`, für die keine offene MAN-Rechnung existiert — mit Grund („keine Rechnungsadresse") und
Link auf `admin.users.edit`. Sobald die Adresse gepflegt ist, stellt der nächste 04:30-Lauf die
Rechnung aus. Alternativ Button **„Jetzt abrechnen"** (ruft `invoiceFor()` sofort auf) — spart das
Warten bis zum nächsten Morgen.
4. **Abrechnungs-Sektion im Bestandsschutz-Panel**: pro Posten die letzte MAN-Rechnung (Nummer, Status)
inline anzeigen, damit man vom Kunden zur Rechnung kommt, nicht nur umgekehrt.
**Tests (`AdminPaymentsPageTest` erweitern):** Buttons nur für `billing:manage`; `send` aus der Seite
ruft Service (Mail::fake); `markPaid`, `terminate` mit Erfolgsmeldung; Blockiert-Panel listet UPO ohne
Rechnungsadresse.
### Phase 4 — Optional: Automatisierung · Aufwand: klein, aber Entscheidungssache
Erst nach ein paar Wochen Handbetrieb sinnvoll, wenn der Wortlaut und der Ablauf stehen:
- `billing:generate-manual-invoices` versendet direkt nach Erzeugung (`--send`), statt auf den Knopf
zu warten.
- Scheduler-Task `billing:remind-manual-invoices`: automatische Erinnerung z. B. 7 Tage nach
Zahlungsziel, **nie** automatisches Beenden — das bleibt bewusst ein Klick.
- Tägliche Admin-Notiz (bestehende Monitoring-Mail-Infrastruktur) „N MAN-Rechnungen > 14 Tage
überfällig".
---
## 4. Entscheidungen, die ich brauche
| # | Frage | Vorschlag |
|---|---|---|
| **E1** | Läuft die 14-Tage-Frist ab **Rechnungsdatum** (04:30-Lauf) oder ab **Versand**? Der Knopf kann Tage später gedrückt werden. | Ab Versand: beim „Senden" `due_date = heute + 14`. Rechnungsdatum bleibt das Erzeugungsdatum. |
| **E2** | Aussteller: **adametz.media für beide Portale**? (Presseecho-Altrechnungen liefen auf Stern Consulting GmbH.) | Ja, ein Aussteller, in `billing.issuer` konfiguriert. Bitte Bankverbindung/Steuernummer bestätigen — ich übernehme sonst die Werte aus dem Legacy-Renderer (Sparkasse Bielefeld, DE96 …, St.-Nr. 349/5001/4350, USt-ID DE298729654). |
| **E3** | Wortlaut des Bestandsschutz-Hinweises — insbesondere: bleiben veröffentlichte PMs online? Was passiert mit der API (Bestandskunden mit Token)? | Mein Entwurf in Phase 2; du korrigierst den Text im Blade. |
| **E4** | Soll der Kunde bei „Bestandsschutz beenden" eine Mail bekommen? | Ja, optional per Checkbox im Modal (Default: an). |
| **E5** | Bestätigung Phase B: alle **9** aus `hold` abgeleiteten Bestandsschutz-Posten kündigen – auch #13-artige Grenzfälle gibt es dort nicht, alle haben mehrjährige hold-Ketten. Betroffen sind u. a. #18 (1.000 € netto, seit 2022 nie bezahlt). | Ja, alle 9 kündigen. |
| **E6** | MAN-00001/00002 gehören zu zwei dieser hold-Kunden. | Stornieren, nicht senden (erledigt sich mit E5). |
| **E7** | Die 94 `hold`-Archivrechnungen: im Archiv behalten und ausblenden, oder physisch löschen? | Behalten + ausblenden. Löschen nur auf ausdrücklichen Wunsch (`--delete-hold`). |
| **E8** | 7 unbezahlte Einzelkäufe (19,95 €, Juli 2026, PMs online): abschreiben (`uncollectible`) oder PM depublizieren? | Abschreiben; optional zusätzlich depublizieren, wenn du das Signal setzen willst. |
| **E9** | Die 6 offenen Vorgänge (`reminder`/`pending`): Periode zurückdrehen und über den MAN-Kreis neu ausstellen (Rechnungsdatum = jetzt, Zahlungsziel +14 Tage ab Versand) — auch wenn der Kunde im Altsystem schon eine Rechnung vom 30.06. hat? | Ja: eine saubere neue Rechnung mit Hinweis „ersetzt Rechnung Nr. … vom 30.06.2026" im Text, statt zwei Rechnungswege zu pflegen. |
---
## 5. Reihenfolge & Aufwand (grob)
| Phase | Inhalt | Aufwand | Abhängig von |
|---|---|---|---|
| B | `legacy:settle-unpaid` (hold kündigen/stornieren, offene Vorgänge zurückdrehen), Grandfather-Filter | ~3 h | E5, E7, E8, E9 |
| 0 | Migration, Backfill, `billing.issuer`, Policy | ~2 h | E2 |
| 1 | PDF-Renderer, Routen, Kundenkonto-Button | ~4 h | 0 |
| 2 | Service + Mailable(s), Tests | ~4 h | 0, 1, E1, E3 |
| 3 | Admin-Aktionen, Blockiert-Panel, KPI | ~4–5 h | 2, E4 |
| 4 | Automatisierung | ~2 h | Erfahrung aus Handbetrieb |
**Einfachster Weg zum Ziel = Phase B → 0 → 1 → 2 → 3**, etwa 2 Arbeitstage inkl. Tests. Phase B
ist dabei die wichtigste: ohne sie würden die neuen Knöpfe Rechnungen an neun Kunden schicken, die
seit Jahren nicht zahlen, und die sechs echten offenen Forderungen aus 2026 blieben unsichtbar. Alles läuft
in bestehenden Dateien/Mustern (Volt-Seite, Mail-Layout, PDF-Primitiven, ActivityLog), keine neue
Abhängigkeit, keine neue Tabelle — nur fünf Spalten auf `invoices`.
Nach dem Deploy: Doku `docs/user-admin/Billing-und-Rechnungskreise.md` Abschnitt 3 und 7 (Punkt 4
„PDF-Erzeugung") aktualisieren.
---
## 6. Betroffene Dateien (Überblick)
```
neu
app/Console/Commands/SettleUnpaidLegacyInvoices.php (Phase B, `legacy:settle-unpaid`)
tests/Feature/Billing/LegacySettleUnpaidCommandTest.php
app/Services/Billing/ManualInvoicePdfRenderer.php
app/Services/Billing/ManualInvoiceDunningService.php
app/Services/Billing/Concerns/DrawsPdf.php (aus LegacyInvoicePdfRenderer extrahiert)
app/Mail/ManualInvoiceIssued.php
app/Mail/GrandfatheringTerminated.php (E4)
app/Http/Controllers/InvoicePdfController.php
app/Policies/InvoicePolicy.php
database/migrations/2026_09_xx_add_dunning_columns_to_invoices_table.php
resources/views/emails/billing/manual-invoice-issued.blade.php
resources/views/emails/billing/grandfathering-terminated.blade.php
tests/Feature/Billing/ManualInvoiceDunningTest.php
tests/Feature/Billing/ManualInvoicePdfTest.php
geändert
app/Console/Commands/GrandfatherLegacySubscriptions.php (Filter auf Zahlungsstatus der Quellrechnung)
resources/views/livewire/admin/invoices/index.blade.php (hold standardmäßig ausblenden)
app/Models/Invoice.php (fillable, casts, Relation userPaymentOption, Scopes manual/openManual)
app/Services/Billing/ManualInvoiceService.php (user_payment_option_id setzen)
app/Services/Billing/LegacyInvoicePdfRenderer.php (Primitiven in Trait)
app/Enums/ActivityType.php (ManualInvoice)
config/billing.php (issuer-Block)
routes/admin.php, routes/customer.php (PDF-Routen)
resources/views/livewire/admin/payments/index.blade.php
resources/views/livewire/customer/invoices.blade.php
tests/Feature/Billing/AdminPaymentsPageTest.php
tests/Feature/Billing/ManualInvoiceGenerationTest.php
```