Phase 9E (Abschluss): Checkout-Flows und Plan-Kontingent statt Quota-Stub
- Checkout-Backend: me.checkout.subscription (Tarif-Abo monatlich/jährlich)
und me.checkout.single-pm (Einzel-PM 19 € netto, pending-Kauf mit
Webhook-Erfüllung); StripeCheckoutService als mockbarer Stripe-Wrapper;
Stripe Tax via Cashier::calculateTaxes() (Netto-Preise, USt-ID-Abfrage)
- Slot-Logik: Kontingent aus dem Tarif (plans.press_release_quota) plus
bezahlte Einmalkäufe; Verbrauch bei Veröffentlichung zuerst aus dem
Plan-Zähler, danach Einlösung des ältesten Einmalkaufs (consumed +
PM-Verknüpfung); Grandfathered = unbegrenzt (Entscheidung 12.06.2026,
Bestandsschutz); Stub-Spalte users.press_release_quota entfernt
- billing:sync-stripe-plans legt zusätzlich das Einzel-PM-Produkt an
(STRIPE_PRICE_SINGLE_PM); Test-Mode-Sync gelaufen
- Buchungs-Seite: Rückmeldung nach Checkout (erfolg/abbruch/Guard-Hinweis)
- Tests: PressReleaseQuotaTest auf Plan-Semantik neu geschrieben,
CheckoutFlowTest (8 Tests), Modal-/API-Tests angepasst; Suite 510 passed
- Doku: Billing-und-Rechnungskreise (Kontingent-Tabelle, Checkout-Routen,
Webhook-Events, Stripe-CLI-Hinweis), PHASE-9-Plan 9E ✅, Checkliste,
STATUS-ABGLEICH, PROGRESS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
38fab64e10
commit
c8dc99c3c8
24 changed files with 775 additions and 100 deletions
|
|
@ -1,7 +1,8 @@
|
|||
# Billing & Rechnungskreise (hybrides Modell)
|
||||
|
||||
Stand: 12.06.2026 — Datenmodell, MAN-Kreis und USt-Behandlung umgesetzt
|
||||
(Phase 9D); Stripe-Checkout/Webhooks in Arbeit (Phase 9E).
|
||||
Stand: 12.06.2026 — Datenmodell, MAN-Kreis, USt-Behandlung (Phase 9D) sowie
|
||||
Stripe-Sync, Webhook-Verarbeitung, Checkout-Flows und Plan-Kontingent
|
||||
(Phase 9E) umgesetzt. Es fehlt die Checkout-UI (Phase 9F).
|
||||
|
||||
Dieses Dokument ist die zentrale Referenz für das Abrechnungssystem:
|
||||
Rechnungskreise, Tarif-Datenmodell, Steuerlogik, Befehle und Konfiguration.
|
||||
|
|
@ -44,6 +45,35 @@ eingelöster Einzel-/Extra-PM-Kauf **oder** eine aktive/grandfathered
|
|||
Legacy-Vereinbarung. Bestandskunden behalten damit nach Gate-Aktivierung
|
||||
volle Einreichungsrechte.
|
||||
|
||||
**PM-Kontingent** (`User::pressReleaseQuotaRemaining()`, null = unbegrenzt):
|
||||
|
||||
| Wer | Kontingent |
|
||||
|---|---|
|
||||
| Launch-Schalter `billing.enforce_booking` aus | unbegrenzt (Vor-Launch-Zustand) |
|
||||
| Bestandskunde (aktive/grandfathered Legacy-Vereinbarung) | **unbegrenzt** — Entscheidung 12.06.2026: Bestandsschutz gilt, das Alt-Produkt sah unbegrenzte PMs vor; eine Migration auf neue Tarife kommt ggf. später |
|
||||
| Abonnent | Monatskontingent des Tarifs (`plans.press_release_quota`) plus offene Einmalkäufe |
|
||||
| Nur Einmalkäufe | Anzahl bezahlter, noch nicht eingelöster Einzel-/Extra-PM-Käufe |
|
||||
|
||||
**Slot-Verbrauch** (erst bei Veröffentlichung, Decision-Update §3.2; Ablehnung
|
||||
kostet nichts; Re-Publish nach Archivierung zählt nicht doppelt): zuerst das
|
||||
Plan-Monatskontingent (Zähler `users.press_release_quota_used_this_month`,
|
||||
monatlicher Reset), danach wird der älteste bezahlte Einmalkauf eingelöst
|
||||
(`single_purchases.status → consumed`, verknüpft mit der PM). Die frühere
|
||||
Stub-Spalte `users.press_release_quota` ist entfernt.
|
||||
|
||||
**Checkout-Einstiege** (Phase 9E; UI-Anbindung folgt in 9F):
|
||||
|
||||
| Route | Zweck |
|
||||
|---|---|
|
||||
| `me.checkout.subscription` (`/admin/me/checkout/abo/{slug}/{monthly\|yearly}`) | Stripe-Checkout für ein Tarif-Abo |
|
||||
| `me.checkout.single-pm` (`/admin/me/checkout/einzel-pm`) | Stripe-Checkout Einzel-PM (legt `single_purchases`-Eintrag `pending` an; Webhook setzt `paid`) |
|
||||
|
||||
Erfolg/Abbruch landen auf der Buchungs-Seite (`?checkout=erfolg|abbruch`).
|
||||
Die Steuer ergänzt **Stripe Tax** automatisch (`Cashier::calculateTaxes()`
|
||||
im AppServiceProvider, Netto-Preise mit `tax_behavior: exclusive`) — nach
|
||||
denselben Regeln wie der VatResolver im MAN-Kreis, inkl. USt-ID-Abfrage im
|
||||
Checkout.
|
||||
|
||||
---
|
||||
|
||||
## 3. MAN-Kreis: Fälligkeitslauf für Legacy-Zahlungen
|
||||
|
|
@ -102,8 +132,9 @@ Rechnungsadresse bestimmt:
|
|||
| Befehl | Zweck | Scheduler |
|
||||
|---|---|---|
|
||||
| `billing:generate-manual-invoices` | MAN-Fälligkeitslauf (Abschnitt 3) | täglich 04:30 |
|
||||
| `billing:sync-stripe-plans` | Tarife + Einzel-PM als Netto-Produkte/Preise nach Stripe synchronisieren (idempotent; `--dry-run`) | manuell |
|
||||
| `legacy:grandfather-subscriptions` | Aktive Legacy-Abos aus dem Archiv migrieren | manuell (Migrations-Runbook) |
|
||||
| `press-releases:reset-monthly-quota` | Quota-Stub-Reset (entfällt mit Plan-Kontingent, 9E) | monatlich, 1. um 00:05 |
|
||||
| `press-releases:reset-monthly-quota` | Monatlicher Reset des Plan-Kontingent-Zählers (`press_release_quota_used_this_month`) | monatlich, 1. um 00:05 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -119,36 +150,50 @@ Rechnungsadresse bestimmt:
|
|||
| `vat_rate` | `BILLING_VAT_RATE` | `0.19` | USt-Satz für steuerpflichtige Fälle |
|
||||
| `eu_country_codes` | — | EU-27 ohne DE | Basis der Drittland-/EU-Unterscheidung |
|
||||
|
||||
Stripe/Cashier (`config/cashier.php`):
|
||||
Stripe/Cashier:
|
||||
|
||||
| ENV | Bedeutung |
|
||||
|---|---|
|
||||
| `STRIPE_KEY` / `STRIPE_SECRET` | Publishable/Secret Key (Test-Keys gesetzt) |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Signatur-Prüfung des Webhook-Endpoints — wird beim Einrichten des Endpoints gesetzt (9E) |
|
||||
| `CASHIER_CURRENCY` | Default `usd` → für uns `eur` setzen (9E) |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Signatur-Prüfung des Webhook-Endpoints (gesetzt; Endpoint `https://pressekonto.com/stripe/webhook` im Dashboard registriert, 12.06.2026) |
|
||||
| `STRIPE_PRICE_SINGLE_PM` | Stripe-Price-ID der Einzel-PM (legt `billing:sync-stripe-plans` an; ohne sie ist der Einzel-PM-Checkout deaktiviert) |
|
||||
| `CASHIER_CURRENCY` / `CASHIER_CURRENCY_LOCALE` | `eur` / `de_DE` (gesetzt) |
|
||||
|
||||
**Benötigte Webhook-Events** am Stripe-Endpoint: `invoice.payment_succeeded`
|
||||
(STR-Spiegelung), `checkout.session.completed` (Einmalkauf-Erfüllung) sowie
|
||||
die Cashier-Standardevents `customer.subscription.created/updated/deleted`,
|
||||
`customer.updated`, `customer.deleted` (Abo-Zustand).
|
||||
|
||||
**Lokal testen** (der registrierte Endpoint zeigt auf die Live-Domain und
|
||||
läuft bis zum Relaunch ins Leere — das ist unkritisch, Stripe versucht die
|
||||
Zustellung nur erneut): Stripe CLI verwenden —
|
||||
`stripe listen --forward-to pressekonto.test/stripe/webhook` und das von der
|
||||
CLI ausgegebene `whsec_…` temporär als `STRIPE_WEBHOOK_SECRET` in die `.env`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Offene Punkte (Stand 12.06.2026, nach 9E-Backbone)
|
||||
## 7. Offene Punkte (Stand 12.06.2026, nach Phase 9E)
|
||||
|
||||
0. **9E-Backbone erledigt**: Tarife liegen als Netto-Produkte/Preise in
|
||||
Stripe (Test-Mode, `billing:sync-stripe-plans`, IDs in `plans`);
|
||||
Webhook-Listener `ProcessStripeWebhook` spiegelt bezahlte
|
||||
Stripe-Rechnungen in den STR-Kreis (fortlaufende Nummer,
|
||||
Adress-Snapshot aus dem Stripe-Payload, idempotent) und erfüllt
|
||||
Einmalkäufe (`checkout.session.completed` →
|
||||
`single_purchases.status = paid`). Cashier-Route `POST /stripe/webhook`
|
||||
ist aktiv.
|
||||
1. **Phase 9E (Rest)**: Checkout-Flows (Abo + Einmalkauf) inkl.
|
||||
Buchungs-Seite, Webhook-Endpoint im Stripe-Dashboard registrieren +
|
||||
`STRIPE_WEBHOOK_SECRET` setzen, Slot-Logik von
|
||||
`users.press_release_quota`-Stub auf Plan-Kontingent umstellen
|
||||
(fachlich zu klären: Kontingent-Semantik für Grandfathered —
|
||||
Legacy-Produkt war „unbegrenzte PMs pro Pressemappe").
|
||||
2. **VIES-Validierung** der USt-ID (aktuell Formatprüfung).
|
||||
3. **PDF-Erzeugung** für MAN-/STR-Rechnungen (Layout inkl. `tax_note`);
|
||||
0. **9E erledigt**: Stripe-Sync (Tarife + Einzel-PM als Netto-Produkte,
|
||||
Test-Mode), Webhook-Verarbeitung (STR-Spiegelung, Einmalkauf-Erfüllung,
|
||||
Endpoint + Secret eingerichtet), Checkout-Flows (Abo + Einzel-PM,
|
||||
Routen siehe Abschnitt 2), Slot-Logik auf Plan-Kontingent umgestellt
|
||||
(Grandfathered = unbegrenzt, Entscheidung 12.06.2026), Stub-Spalte
|
||||
entfernt, Stripe Tax aktiviert (`Cashier::calculateTaxes()`).
|
||||
1. **Phase 9F**: Tarif-Seite/Buchungs-UI an die Checkout-Routen anbinden
|
||||
(die Buchungs-Seite ist noch Konzept-Mock mit deaktivierten Buttons);
|
||||
echte Tarif-/Buchungsdaten statt Platzhalter anzeigen.
|
||||
2. **Stripe Tax im Dashboard aktivieren** (Ursprungsadresse/Registrierung
|
||||
hinterlegen) — ohne das schlägt der Checkout mit automatischer Steuer
|
||||
fehl. Im Test-Mode prüfen, dann im Live-Mode wiederholen; dort auch
|
||||
`billing:sync-stripe-plans` erneut ausführen (Live-Produkt-IDs).
|
||||
3. **VIES-Validierung** der USt-ID (aktuell Formatprüfung; Stripe prüft
|
||||
die im Checkout erfasste USt-ID asynchron selbst).
|
||||
4. **PDF-Erzeugung** für MAN-/STR-Rechnungen (Layout inkl. `tax_note`);
|
||||
Archiv-PDFs existieren bereits on-demand.
|
||||
4. **`ExpireGrandfatheredSubscriptions`**: Benachrichtigung zur Umstellung
|
||||
auf neue Tarife am `grandfathered_until` (D-13-Rest).
|
||||
5. **Steuerberater-Abnahme** der USt-Regeln und Rechnungstexte vor dem
|
||||
5. **`ExpireGrandfatheredSubscriptions`**: Benachrichtigung zur Umstellung
|
||||
auf neue Tarife am `grandfathered_until` (D-13-Rest); erst dann wird
|
||||
das unbegrenzte Bestandskontingent ggf. migriert.
|
||||
6. **Steuerberater-Abnahme** der USt-Regeln und Rechnungstexte vor dem
|
||||
ersten produktiven MAN-/STR-Lauf.
|
||||
7. **Tageslimit** (`plans.daily_limit`) durchsetzen — Phase 9G.
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ Verbindliche Entscheidungen: `docs/Decision-Update Preisstruktur & Veröffentlic
|
|||
- [x] Aktive Legacy-Zahlungen migrieren (12.06.): `legacy:grandfather-subscriptions` leitet aus dem Rechnungsarchiv die aktiven jaehrlichen Vereinbarungen ab (22 im Test-Snapshot) und schreibt sie als `grandfathered` nach `user_payment_options` — Replay-faehig fuer den Lauf kurz vor Relaunch. Doku: `dev/migration 2026/05-DATABASE-MERGE.md` §5.6.
|
||||
- [x] USt-Behandlung (12.06.): alle neuen Preise netto; `VatResolver` (DE immer Steuer, EU nur mit USt-ID befreit/Reverse Charge, Drittland befreit), `vat_id` an Rechnungsadresse + Rechnungs-Snapshot, `tax_note` auf Rechnungen; Grandfathered rechnen auf Netto-Basis der letzten Legacy-Rechnung (Brutto bleibt fuer DE-Bestandskunden gleich).
|
||||
- [ ] VIES-Validierung der USt-ID (aktuell Formatpruefung) — vor Gate-/Checkout-Aktivierung.
|
||||
- [ ] Stripe-Checkout + Webhooks (Phase 9E): Produkte/Preise anlegen (netto, Steuer via Stripe Tax oder VatResolver), STR-Rechnungsspiegelung, Slot-Logik auf Plan-Kontingent umstellen, Quota-Stub abloesen. Benoetigt `STRIPE_KEY`/`STRIPE_SECRET` in `.env`.
|
||||
- [x] Stripe-Checkout + Webhooks (Phase 9E, 12.06.): Produkt-Sync nach Stripe (Tarife + Einzel-PM, netto, Stripe Tax), STR-Rechnungsspiegelung + Einmalkauf-Erfuellung per Webhook (Endpoint registriert), Checkout-Flows als Backend (`me.checkout.subscription`/`me.checkout.single-pm`), Slot-Logik auf Plan-Kontingent umgestellt (Grandfathered = unbegrenzt, Bestandsschutz), Quota-Stub-Spalte entfernt. UI-Anbindung folgt in 9F. Doku: `docs/user-admin/Billing-und-Rechnungskreise.md`.
|
||||
- [ ] Tageslimit je Tier (Business 2 / Pro 3 / Agency 5), gilt auch fuer Extra-PMs.
|
||||
- [ ] Launch-Credits: Extra-PM, Boost (nur gruene PMs), Veroeffentlichungsnachweis-PDF; Credit-Anker 1 Credit = 1 €.
|
||||
- [ ] Einzel→Abo-Bruecke (19 € Anrechnung innerhalb 30 Tagen).
|
||||
|
|
@ -158,6 +158,6 @@ Verbindliche Entscheidungen: `docs/Decision-Update Preisstruktur & Veröffentlic
|
|||
|
||||
- Phase 1, Phase 7 (PM-Form-Refactor), Phase 8 (User-Panel-Konsolidierung) und die KI-Pruef-Pipeline (Phasen 0–5) sind abgeschlossen — siehe Plan-Dokus oben.
|
||||
- Fuer Preise, Kontingente und den Veroeffentlichungs-Flow gilt ausschliesslich das Decision-Update vom 11.06.2026; aeltere Tarif-Tabellen in `Konzept-Update 1` und im Relaunch-Konzept sind ueberschrieben.
|
||||
- Der Quota-Stub (3 PM/Monat, zaehlt beim Einreichen) bleibt bis zum Tarif-Modul aktiv; die Umstellung auf Slot-Verbrauch bei Veroeffentlichung ist Teil des Launch-Blocks.
|
||||
- Das PM-Kontingent kommt aus dem Tarif (`plans.press_release_quota`) plus Einmalkaeufen; Bestandskunden (Grandfathered) sind unbegrenzt. Solange `billing.enforce_booking` aus ist, gilt kein Kontingent (Launch-Schalter).
|
||||
- Die KI-Klassifikation laeuft asynchron — in Produktion wird ein Queue-Worker fuer die Queue `classification` benoetigt (Test-Drain: `php artisan classification:work`).
|
||||
- Anhaenge sind aktuell aus Sicherheitsgruenden deaktiviert, Tabelle und Komponente bleiben aber erhalten und werden in einem separaten Audit-Track reaktiviert.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue