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:
parent
f581e1eb3a
commit
a00f9dc79a
33 changed files with 3606 additions and 195 deletions
348
dev/lagacy-rechnungen/entwicklungsplan-2026-09-16.md
Normal file
348
dev/lagacy-rechnungen/entwicklungsplan-2026-09-16.md
Normal 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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue