Phase 9D: Tarif-Datenmodell, Cashier und hybride Rechnungskreise STR-/MAN-
Tarif-Datenmodell (Decision-Update): - plans: Starter/Business/Pro/Agency mit Monats-/Jahrespreis (Jahres = 10 x Monat), PM-Kontingent, Tageslimit, Stripe-IDs; idempotenter Seeder - single_purchases: Einzel-PM, Extra-PM, Boost, PDF-Nachweis mit Status-Lifecycle und Stripe-Checkout-Referenzen - laravel/cashier ^16.5 installiert (freigegeben); User ist Billable, Cashier-Migrationen published + ausgefuehrt; lokale invoices()-Relation ueberschreibt bewusst die Cashier-Methode Hybride Rechnungskreise (Entscheidung 12.06.2026): - invoice_number_sequences + InvoiceNumberGenerator: atomare fortlaufende Nummern pro Kreis (STR- fuer den neuen Stripe-Shop, MAN- fuer den manuellen Legacy-Kreis); Alt-Archiv legacy_invoices bleibt unveraendert - ManualInvoiceService + billing:generate-manual-invoices (Scheduler taeglich 04:30): prueft aktive/grandfathered user_payment_options ohne Stripe-Subscription auf erreichtes Periodenende, friert die Rechnungsadresse als Snapshot ein, stellt die MAN-Rechnung aus (Zahlungsziel billing.manual_due_days) und schaltet die Periode weiter; Konditions-Overrides via legacy_conditions, sonst Netto-Preis + billing.vat_rate; nicht abrechenbare Faelle werden geloggt und beim naechsten Lauf erneut geprueft Submit-Gate: - User::hasActiveBooking() prueft jetzt echt (hinter billing.enforce_booking): Cashier-Abo, bezahlter Einzel-/Extra-PM-Kauf oder laufende Legacy-Vereinbarung (MAN-Kreis) Suite: 468 passed, 4 skipped (17 neue Billing-Tests). Pint clean. Offen fuer 9E: Stripe-Checkout/Webhooks, STR-Spiegelung, Slot-Logik auf Plan-Kontingent, Migration der aktiven Legacy-Zahlungen in user_payment_options. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4419d9ff43
commit
d548f4b235
28 changed files with 1545 additions and 25 deletions
|
|
@ -40,8 +40,8 @@ Phase 9 setzt das Decision-Update vom 11./12.06.2026 um — in zwei Blöcken:
|
|||
| **9B** ✅ | Slot-Verbrauch von Einreichung auf Veröffentlichung umstellen (Rot = kein Slot) | M | mittel (Idempotenz) |
|
||||
| **9C** ✅ | Submit-Gate-Schnittstelle (`hasActiveBooking()`-Stub, Modal-Hinweis, Server-Guard) + Fix: Create-Form lief am Funnel vorbei | M | gering |
|
||||
| — | **Review-Stopp mit User** | | |
|
||||
| **9D** | Tarif-Datenmodell: Pläne, Subscriptions, Einzel-PM-Käufe; Quota-Stub ablösen | L | hoch (Datenmodell) |
|
||||
| **9E** | Stripe-Anbindung (Laravel Cashier — **Dependency-Freigabe nötig**) | L | mittel |
|
||||
| **9D** ✅ | Tarif-Datenmodell: Pläne, Einzelkäufe, Cashier, Rechnungskreise STR-/MAN-, MAN-Fälligkeitslauf (Stub-Ablösung folgt mit 9E) | L | hoch (Datenmodell) |
|
||||
| **9E** | Stripe-Anbindung (Cashier installiert ✅): Checkout, Webhooks, STR-Rechnungsspiegelung, Slot-Logik auf Plan-Kontingent | L | mittel |
|
||||
| **9F** | Tarif-Seite + Checkout-UI (Raster, Einzel-PM-Block, „2 Monate gratis", Enterprise-Hinweis) | M | gering |
|
||||
| **9G** | Tageslimit je Tier (Business 2 / Pro 3 / Agency 5; gilt auch für Extra-PMs) | S | gering |
|
||||
| **9H** | Einzel-PM-Kauf (19 €) + Einzel→Abo-Brücke (Anrechnung 30 Tage) | M | mittel |
|
||||
|
|
@ -126,25 +126,60 @@ Modal-Hinweis statt Checkboxen, `submitForReview` wirft, API gibt 402.
|
|||
|
||||
## 3. Block 2 — Tarif-Modul (nach Review-Stopp)
|
||||
|
||||
### 9D · Tarif-Datenmodell
|
||||
### Entscheidung 12.06.2026 — Hybride Rechnungsarchitektur
|
||||
|
||||
- Tabellen (Arbeitsstand, final beim Review-Stopp vor 9D):
|
||||
`plans` (Starter/Business/Pro/Agency: Preis mtl./jährl., PMs/Monat,
|
||||
Tageslimit), `subscriptions` (User, Plan, Zyklus, Status, Periodenstart/-ende),
|
||||
`single_purchases` (Einzel-PM, Extra-PM, Boost, PDF — Typ, Preis, Status,
|
||||
`applied_to_press_release_id`).
|
||||
- `User::hasActiveBooking()` prüft echte Subscription oder offenen Einzel-PM-Kauf.
|
||||
- Slot-Logik wechselt von `users.press_release_quota` auf Plan-Kontingent +
|
||||
Periodenzähler; Stub-Spalten werden nach Migration entfernt.
|
||||
- Kontingent-Anzeige (Modal, Editor) liest aus der neuen Quelle —
|
||||
Schnittstelle `pressReleaseQuotaRemaining()` bleibt stabil.
|
||||
Alle **neuen** Abschlüsse und Zahlungen laufen über **Stripe**. Die Umsetzung
|
||||
ist hybrid mit zwei getrennten Rechnungskreisen (plus Altbestand):
|
||||
|
||||
| Kreis | Präfix | Inhalt |
|
||||
|---|---|---|
|
||||
| **Stripe-Shop** | `STR-` | Alles Neue (Abos, Einzel-PM, Credits) — komplette Abwicklung über Stripe, fortlaufende Nummer im STR-Kreis |
|
||||
| **Manuell/Legacy** | `MAN-` | Laufende, noch aktive Alt-Zahlungen ab Relaunch: Fälligkeit wird im Hintergrund geprüft, Rechnung wie im Legacy-System ausgestellt |
|
||||
| Alt-Archiv | — | Die importierten Alt-Rechnungen (`legacy_invoices`, 864 Stück) bleiben unverändert bestehen |
|
||||
|
||||
### 9D · Tarif-Datenmodell — ✅ umgesetzt (12.06.2026)
|
||||
|
||||
- **Cashier installiert** (`laravel/cashier` ^16.5, freigegeben); `User` ist
|
||||
`Billable`, Cashier-Tabellen (`subscriptions`, `subscription_items`,
|
||||
Customer-Spalten) migriert. Die lokale `invoices()`-Relation überschreibt
|
||||
bewusst die Cashier-Methode.
|
||||
- **`plans`**: Starter/Business/Pro/Agency mit Monats-/Jahrespreis
|
||||
(Jahres = 10 × Monat), PM-Kontingent, Tageslimit, Stripe-IDs (nullable,
|
||||
werden in 9E gepflegt). `PlanSeeder` idempotent.
|
||||
- **`single_purchases`**: Einzel-PM, Extra-PM, Boost, PDF-Nachweis mit
|
||||
Status Pending/Paid/Consumed/Refunded und Stripe-Checkout-Referenzen.
|
||||
- **`invoice_number_sequences` + `InvoiceNumberGenerator`**: atomare,
|
||||
fortlaufende Nummern pro Kreis (`STR-00001`, `MAN-00001`; Padding
|
||||
konfigurierbar in `config/billing.php`).
|
||||
- **MAN-Kreis**: `ManualInvoiceService` + Command
|
||||
`billing:generate-manual-invoices` (Scheduler täglich 04:30) — findet
|
||||
aktive/grandfathered `user_payment_options` **ohne**
|
||||
`stripe_subscription_id` mit erreichtem `current_period_end`, friert die
|
||||
Rechnungsadresse als Snapshot ein, stellt eine MAN-Rechnung aus
|
||||
(Zahlungsziel `billing.manual_due_days`) und schaltet die Periode weiter.
|
||||
Konditions-Overrides pro Vereinbarung über `legacy_conditions`
|
||||
(`amount_cents`/`tax_cents`/`total_cents`/`interval`); ohne Override
|
||||
Netto-Preis der `payment_option` + `billing.vat_rate`. Nicht abrechenbare
|
||||
Fälle (fehlende Rechnungsadresse) werden geloggt und erneut versucht.
|
||||
- **`User::hasActiveBooking()`** prüft jetzt echt (hinter
|
||||
`billing.enforce_booking`): Cashier-Abo ∨ bezahlter Einzel-/Extra-PM-Kauf
|
||||
∨ aktive/grandfathered Legacy-Vereinbarung (MAN-Kreis).
|
||||
- **Noch offen in 9D** (folgt mit 9E, braucht Checkout/Webhooks):
|
||||
Slot-Logik von `users.press_release_quota`-Stub auf Plan-Kontingent +
|
||||
Periodenzähler umstellen und Stub-Spalten entfernen. Voraussetzung:
|
||||
Die aktiven Legacy-Zahlungen müssen noch in `user_payment_options`
|
||||
migriert werden (Tabelle ist aktuell leer — eigener Migrations-Schritt).
|
||||
|
||||
### 9E · Stripe (Laravel Cashier)
|
||||
|
||||
- **Vor Start freizugeben:** `laravel/cashier` als neue Dependency.
|
||||
- Checkout für Abo (monatlich/jährlich) und Einmalzahlung (Einzel-PM, Credits).
|
||||
- Webhooks (Subscription-Status, Zahlungsausfall) + lokale Spiegelung.
|
||||
- Rechnungen an bestehende `invoices`-Struktur anbinden (Klärung beim Review).
|
||||
- Checkout für Abo (monatlich/jährlich) und Einmalzahlung (Einzel-PM, Credits);
|
||||
Stripe-Produkte/Preise anlegen und IDs in `plans` pflegen.
|
||||
- Webhooks (Subscription-Status, Zahlungsausfall, `invoice.paid`) + Spiegelung
|
||||
der Stripe-Rechnungen in `invoices` mit STR-Nummer aus dem
|
||||
`InvoiceNumberGenerator`.
|
||||
- Slot-Logik auf Plan-Kontingent umstellen (siehe 9D-Rest), Stub ablösen.
|
||||
- Benötigt `STRIPE_KEY`/`STRIPE_SECRET`/`STRIPE_WEBHOOK_SECRET` in `.env`
|
||||
(aktuell nicht gesetzt).
|
||||
|
||||
### 9F · Tarif-Seite + Checkout-UI
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue