- 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>
25 KiB
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-unpaidauf 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, dannphp artisan legacy:settle-unpaid --dry-runprüfen und ohne--dry-runausfü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, MusterBookingPaymentConfirmed(BCC anlegal.contact_email). ActivityLogfür den Verlauf, Permissionbilling:manage, Confirmation-Modals im Payments-Panel (Vorbild: „Bündel kündigen").- PDF-Primitiven (
text(),line(),buildPdf()) imLegacyInvoicePdfRenderer– 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:
hold→ erledigt.- Die 9 aus
holdabgeleiteten Bestandsschutz-Posten werden gekündigt (cancelBundleFor()je User, mitActivityLog-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/invoicesnicht mehr als „offen" anzeigen. Wenn du sie wirklich weg haben willst:--delete-holdals bewusste Option, löscht die 94 Zeilen (→ E7).
- Die 9 aus
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 alssupersededmarkiert (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.- 7
pending-Einzelkäufe (19,95 €). Fachliche Entscheidung (→ E8): entweder alsuncollectibleabschreiben (Vorschlag – Gmail-/SEO-Accounts, Aufwand > Ertrag) oder die PM depublizieren. Technisch ist beides ein Status-Update im Archiv bzw. ein Aufruf der bestehenden Depublikation. - Ursache beheben:
GrandfatherLegacySubscriptionsbekommt 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
- 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
ManualInvoiceService::invoiceFor()setzt künftiguser_payment_option_id.- 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.
config/billing.php→ Blockissuer(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 alsown_vat_id). Schließt L7 — und ist gleichzeitig die Datenquelle für die Mail (Bankverbindung) und das PDF.InvoicePolicy(analogLegacyInvoicePolicy):view/downloadPdffü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
- PDF-Primitiven aus
LegacyInvoicePdfRendererin einen TraitApp\Services\Billing\Concerns\DrawsPdf(oder abstrakte Basisklasse) ziehen — reine Verschiebung, Legacy-Renderer bleibt funktional identisch. ManualInvoicePdfRenderer(fürInvoice): Absender ausbilling.issuer, Empfänger ausinvoice_billing_addresses-Snapshot, Leistungsname auslegacy_conditions.namebzw.payment_option, Leistungszeitraum aus der Vereinbarung (current_period_start/endvor dem Weiterschalten — dafür genügt: Periodenstart =invoice_date, Ende = +1 Jahr/Monat), Netto/USt/Brutto,tax_note(Reverse-Charge/Drittland), Zahlungszieldue_date, Bankverbindung. Ergebnis wird nicht gespeichert (wie beim Archiv on-demand gerendert);pdf_pathbleibt frei für eine spätere Ablage.- Routen:
admin/invoices/{invoice}/pdf(Admin) undme/invoices/{invoice}/pdf(Kunde), ControllerInvoicePdfController, Policy-Check; für STR-Rechnungen 404 (dort bleibt Stripe der Beleg). - Kundenkonto: in
/admin/me/invoicesSpalte „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
ManualInvoiceDunningService(neu,App\Services\Billing), kapselt die vier Aktionen — jede transaktional, jede mitActivityLog-Eintrag (neuerActivityType::ManualInvoice):send(Invoice, User $actor)— nurstatus = openund ohnestripe_invoice_id; setztsent_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 wennsent_atgesetzt;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; ruftLegacySubscriptionService::cancelBundleFor($invoice->user); weitere offene MAN-Rechnungen desselben Users werden ebenfalls storniert (ein Kunde, ein Bündel, ein Vorgang).
- Mailable
ManualInvoiceIssued(queued,ShouldQueue, BCC anlegal.contact_emailwieBookingPaymentConfirmed), PDF als Anhang (attachDataaus dem Renderer), Viewemails/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.
- 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:
- 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 < heuteundopen, „storniert" mit Grund-Tooltip.
- Filter/Tab „nur MAN offen" oberhalb der Tabelle (Standardansicht dieses Vorgangs), KPI-Karte „Offene MAN-Rechnungen" (Anzahl + Summe, davon überfällig).
- 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 aufadmin.users.edit. Sobald die Adresse gepflegt ist, stellt der nächste 04:30-Lauf die Rechnung aus. Alternativ Button „Jetzt abrechnen" (ruftinvoiceFor()sofort auf) — spart das Warten bis zum nächsten Morgen. - 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-invoicesversendet 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