presseportale/dev/archive/legacy-rechnungen/entwicklungsplan-2026-09-16.md
Kevin Adametz 8e3554bf91 dev: Doku aufgeräumt, neue Roadmap als einzige Liste offener Punkte
- dev/roadmap.md neu: alle offenen Punkte aus Plänen, Audits und Protokollen,
  gegen Code und Git abgeglichen (Stand v1.7.0)
- Abgeschlossenes ins Archiv: 2026-09-01 → archive/2026-09-nutzergewinnung,
  2026-09-11-audit → archive/2026-09-audit, lagacy-rechnungen →
  archive/legacy-rechnungen, Deploy-Protokoll v1.1.0, Umami-Snippets,
  Go-Live-Roadmap vom 20.07.
- Aktiv bleiben eigene Ordner bildrechte/ (Plan Fassung 2, Decision-Update)
  und changelog/ (Decision-Update Changelog)
- dev/README.md als Ordnerkarte neu, Verweise und zwei Code-Kommentare auf
  die neuen Pfade

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:25:09 +00:00

25 KiB
Raw Blame History

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