presseportale/docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md
Kevin Adametz 38fab64e10 Phase 9E (Backbone): Stripe-Produkt-Sync und Webhook-Verarbeitung mit STR-Spiegelung
- billing:sync-stripe-plans: legt die Tarife als Netto-Produkte/Preise
  (tax_behavior exclusive, EUR) in Stripe an und pflegt die IDs zurueck
  nach plans; idempotent, --dry-run. Gegen Stripe Test-Mode ausgefuehrt —
  alle 4 Tiers verknuepft.
- ProcessStripeWebhook (Listener auf Cashier WebhookReceived):
  - invoice.payment_succeeded -> Spiegelung in den lokalen STR-Kreis
    (fortlaufende Nummer via InvoiceNumberGenerator, Adress-Snapshot
    bevorzugt aus dem Stripe-Payload inkl. lokaler USt-ID, Status paid,
    idempotent gegen doppelte Zustellung)
  - checkout.session.completed -> markiert den referenzierten
    single_purchases-Datensatz als bezahlt (Metadata single_purchase_id)
- CASHIER_CURRENCY=eur (+ Locale de_DE); Cashier-Webhook-Route aktiv
- Doku: Billing-Referenz §7 + Phase-9-Plan (9E-Backbone) aktualisiert

Offen fuer 9E-Rest: Checkout-Flows (Abo + Einmalkauf), Webhook-Endpoint
im Stripe-Dashboard + STRIPE_WEBHOOK_SECRET, Slot-Logik auf
Plan-Kontingent (fachliche Frage: Grandfathered = unbegrenzt?).

Tests: StripeWebhookProcessingTest (7, inkl. Event-Wiring).
Suite: 497 passed, 4 skipped. Pint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:07:09 +00:00

273 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 9 · Veröffentlichungs-Flow (Launch) & Tarif-Modul
Stand: 2026-06-12 — **Block 1 (9A9C) abgeschlossen**; Review-Stopp vor
Block 2 (9D9J, Tarif-Modul). Suite nach Block 1: 451 passed, 4 skipped.
Vorgänger: Phase 8 (User-Panel-Konsolidierung) + KI-Prüf-Pipeline (beide abgeschlossen).
Verbindliche Entscheidungen: [`docs/Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](./Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md)
Abgleich-Doku: [`docs/STATUS-ABGLEICH-USER-PANEL.md`](./STATUS-ABGLEICH-USER-PANEL.md)
---
## 0. Worum es geht
Phase 9 setzt das Decision-Update vom 11./12.06.2026 um — in zwei Blöcken:
1. **Block 1 — Veröffentlichungs-Flow (9A9C)**: Die Flow-Regeln, die
unabhängig vom Tarif-Modul gelten und auf denen das Tarif-Modul aufsetzt.
Funktioniert vollständig mit dem vorhandenen Quota-Stub.
2. **Block 2 — Tarif-Modul (9D9I)**: Zahlung, Tarife, Einzel-PM, Tageslimit
und die drei Launch-Credit-Posten. Löst den Quota-Stub ab.
**Leitplanken aus dem Decision-Update:**
- Gelb geht zum Launch **direkt live** wie Grün — keine manuelle Prüf-Queue.
Nur Rot wird abgelehnt (Meldung mit Begründung an den Autor).
- Der PM-Slot zählt **bei Veröffentlichung** runter, nicht bei der Prüfung.
Rot verbraucht keinen Slot.
- „Speichern" bleibt immer frei; „Speichern & zur Prüfung einreichen" ist
hinter eine aktive Buchung gegated (der Button konvertiert, er verschwindet
nicht).
- Kein Re-Check zum Launch: eine Einreichung = eine Prüfung = (bei Gelb/Grün)
eine Veröffentlichung. Vorab-Prüfung/Redigieren sind Phase 2.
---
## 1. Sub-Päckchen-Übersicht
| ID | Thema | Größe | Risiko |
|---|---|---|---|
| **9A** ✅ | Gelb-Routing auf Direkt-Live umstellen (Routing, Scheduler, Tests) | S | gering |
| **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, Einzelkäufe, Cashier, Rechnungskreise STR-/MAN-, MAN-Fälligkeitslauf (Stub-Ablösung folgt mit 9E) | L | hoch (Datenmodell) |
| **9E** 🔄 | Stripe-Anbindung — Backbone ✅ (Produkt-Sync nach Stripe, Webhook-Listener mit STR-Spiegelung + Einmalkauf-Erfüllung); offen: Checkout-Flows, Webhook-Endpoint registrieren, 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 |
| **9I** | Launch-Credits: Extra-PM, Boost (nur Grün), Veröffentlichungsnachweis-PDF | L | mittel |
| **9J** | Abschluss: Tests, Pint, Build, Doku-Sync, PROGRESS-Eintrag | S | keine |
Nach jedem Päckchen Review-Stopp mit dem User; vor 9D ein größerer
(Datenmodell-Entscheidungen + Cashier-Freigabe).
---
## 2. Block 1 — Veröffentlichungs-Flow
### 9A · Gelb-Routing auf Direkt-Live
**Entscheidung (12.06.2026)**: Rot = nicht veröffentlichbar (rechtlich/
inhaltlich) → Ablehnung mit Meldung. Gelb/Grün = veröffentlichbar → geht in
der ersten Phase direkt online. Gelb bleibt als interne Markierung erhalten
(nicht boostbar, Admin-Signal), löst aber keine manuelle Prüfung aus.
**Anpassungen:**
- `PressReleaseService::routeByClassification()`: Gelb durchläuft denselben
Auto-Publish-Pfad wie Grün (`autoPublishGreen()` → generalisiert zu
`autoPublishApproved()`); Verzögerungsfenster
(`scoring.classification.green_delay_minutes`) gilt für beide.
- `PublishScheduledPressReleases`: Kandidaten-Query von
`classification = green` auf `classification IN (green, yellow)`.
- Admin-Review-Queue bleibt als **Fallback** bestehen: unklassifizierte PMs
(Job noch nicht gelaufen / KI-Ausfall ohne Fallback-Ergebnis) bleiben in
`review` und sind manuell behandelbar. KI-Badge und Klassifikations-Filter
im Admin bleiben unverändert.
**Tests:** `PressReleaseClassificationJobTest` (Gelb-sofort → published,
Gelb-geplant → bleibt review bis Termin), `PressReleaseSchedulingTest`
(gelbe fällige PM wird publiziert).
### 9B · Slot-Verbrauch bei Veröffentlichung
**Regel:** Der Slot zählt genau einmal pro PM, beim **ersten** Übergang zu
`published`. Rot abgelehnte PMs verbrauchen nichts.
**Anpassungen:**
- `submitForReview()`: Increment von `press_release_quota_used_this_month`
**entfernen**. Stattdessen Guard: Einreichen erfordert
`pressReleaseQuotaRemaining() > 0` (sonst würde eine grüne PM ohne
verfügbaren Slot veröffentlicht).
- `publish()`: Increment beim Statuswechsel auf `published`, idempotent —
nur wenn die PM zuvor noch nie veröffentlicht war (Prüfung über
`press_release_status_logs`, kein neues Schema-Feld). Zählt auf den
PM-Eigentümer (`user_id`).
- Veröffentlichungs-Modal: Text von „wird bei Einreichung verbraucht" auf
„wird bei Veröffentlichung verbraucht; abgelehnte PMs kosten keinen Slot".
**Tests:** Submit verbraucht keinen Slot; Publish (Admin, Auto-Publish,
Scheduler) verbraucht genau einen; Rot → kein Verbrauch; Archivieren +
erneutes Publizieren zählt nicht doppelt; Submit bei 0 Rest-Slots blockiert.
### 9C · Submit-Gate-Schnittstelle
**Ziel:** Das Gate aus dem Decision-Update §5.1, gebaut gegen eine schmale
Schnittstelle, die zunächst ein Stub bedient und in 9D/9E vom Tarif-Modul
implementiert wird — Modal und Service müssen dann nicht mehr angefasst werden.
**Anpassungen:**
- `User::hasActiveBooking(): bool` — Launch-Schnittstelle. Stub-Verhalten
über `config/billing.php` (`billing.enforce_booking`, Default `false`):
solange das Tarif-Modul fehlt, gibt die Methode `true` zurück; mit
aktiviertem Flag (und später echter Subscription-Prüfung) greift das Gate.
- Einreichungs-Modal (`press-release-submit-modal`): ohne aktive Buchung
zeigt das Modal statt des Prüf-Flows einen Buchungs-Hinweis mit CTA
(„Buchung erforderlich" → Tarif-Seite). Der Button bleibt sichtbar.
- Server-Guard: `submitForReview()` wirft ohne aktive Buchung eine Exception
(UI allein reicht nicht); API-Submit-Route antwortet mit **402**.
**Tests:** Gate aus (Default) → Verhalten unverändert; Gate an →
Modal-Hinweis statt Checkboxen, `submitForReview` wirft, API gibt 402.
---
## 3. Block 2 — Tarif-Modul (nach Review-Stopp)
### Entscheidung 12.06.2026 — Hybride Rechnungsarchitektur
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).
- **USt-Behandlung (Einwand 12.06.)**: Alle neuen Preise sind **netto**.
`VatResolver` bestimmt die Steuer pro Rechnung aus der Rechnungsadresse:
DE immer mit Steuer, EU nur mit (formal plausibler) USt-ID befreit
(Reverse Charge inkl. Pflichthinweis in `invoices.tax_note`), Drittland
befreit. `vat_id` an `billing_addresses` + Snapshot, gepflegt über das
bestehende USt-ID-Feld im Profil. Grandfathered-Vereinbarungen rechnen
auf der Netto-Basis der letzten Legacy-Rechnung (`net_cents`, brutto ÷
1,19 bzw. Netto-Ausweis direkt). **Offen**: echte VIES-Validierung der
USt-ID (aktuell Formatprüfung) — Folgeschritt, vor Gate-Aktivierung
empfohlen.
- **Legacy-Migration (12.06.)**: `legacy:grandfather-subscriptions` leitet
die aktiven, jährlich wiederkehrenden Vereinbarungen aus dem
Rechnungsarchiv ab und schreibt sie als `grandfathered` in
`user_payment_options` (Replay-fähig — die Kern-Migration läuft kurz
vor dem Relaunch erneut). Details:
`dev/migration 2026/05-DATABASE-MERGE.md` §5.6 + `MIGRATION-STEPS.md`.
- **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.
### 9E · Stripe (Laravel Cashier)
- 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
- Raster mit 4 Tiers; Einzel-PM als separater No-Abo-Block (nicht als
billigste Spalte); Enterprise als dezenter Sales-Hinweis unter der Tabelle.
- Jahrespreis kommuniziert als „2 Monate gratis".
- Einstieg aus dem Submit-Gate-Hinweis (9C) und aus „Buchungen & Add-ons".
### 9G · Tageslimit
- `plans.daily_limit` (Starter ohne Limit); Prüfung beim Veröffentlichen
(nicht beim Einreichen), zählt veröffentlichte PMs des Users pro Kalendertag
(Europe/Berlin); gilt auch für Extra-PMs. Überschreitung → PM bleibt in
`review` mit Hinweis, Veröffentlichung am Folgetag durch den Scheduler.
### 9H · Einzel-PM + Abo-Brücke
- Einzel-PM-Kauf 19 € → genau eine Einreichung/Veröffentlichung.
- Brücke: Abo-Abschluss innerhalb 30 Tagen rechnet 19 € auf den ersten
Monat an (Stripe-Coupon oder Rabatt-Position).
### 9I · Launch-Credits
- Credit-Wallet (1 Credit = 1 € Listenpreis, Pakete mit Volumenrabatt).
- Posten: **Extra-PM** (Kontingent voll → einzelne PM nachkaufen),
**Boost** (nur für grün klassifizierte PMs, nachträglich),
**Veröffentlichungsnachweis-PDF**.
- Kein Dofollow-Backlink-Verkauf (bewusst ausgeschlossen).
### 9J · Abschluss
- Volle Suite grün, Pint clean, `npm run build` clean.
- Doku-Sync: `STATUS-ABGLEICH`, `checkliste-user-backend.md`, dieses Dokument,
`PROGRESS.md`-Eintrag.
---
## 4. Was außerhalb von Phase 9 bleibt
- Vorab-KI-Prüfung, Redigieren/Re-Check-Loop, Prüfzähler, Credit-Overflow
(Decision-Update §7 — Phase 2)
- Score-Feinstufung für Boost („nur Geprüft/Hochwertig boostbar")
- Magic-Link-Flow, Statistik-/Abrechnungs-Tabs, Anhänge-Reaktivierung,
Trust-Score, Notice-and-Action
---
## 5. Risiken & Annahmen
- **Idempotenz Slot-Verbrauch (9B)**: Prüfung über Status-Logs statt neuem
Feld — bei Alt-Daten mit unvollständigen Logs schlimmstenfalls ein
doppelter Zähler; akzeptabel für den Stub, wird mit 9D-Periodenzähler
sauber.
- **Gate-Stub (9C)**: `enforce_booking=false` als Default hält das System
bis zum Tarif-Modul voll funktionsfähig; das Flag erlaubt Tests und
frühe Aktivierung.
- **9D/9E Datenmodell + Cashier**: größter Block, eigener Review-Stopp davor;
Stub-Ablösung (`press_release_quota`-Spalten entfernen) erst nach
verifizierter Migration.
- **Rechtstexte** (Einreichungs-Modal) sind weiterhin Platzhalter —
anwaltliche Prüfung läuft parallel, unabhängig von Phase 9.
- **Betrieb**: Queue-Worker für `classification` in Produktion bleibt
Go-Live-Voraussetzung (unabhängig von Phase 9).
---
## 6. Akzeptanzkriterien Phase 9 gesamt
- [ ] Gelb klassifizierte PMs gehen ohne manuelle Prüfung live (sofort/Termin)
- [ ] Rot verbraucht keinen Slot; Slot zählt genau einmal, bei Veröffentlichung
- [ ] Einreichen ohne aktive Buchung zeigt Buchungs-Hinweis (UI) und wird
serverseitig abgelehnt (Gate aktiviert)
- [ ] Tarife buchbar (4 Tiers, monatlich/jährlich), Einzel-PM kaufbar
- [ ] Tageslimit greift je Tier, auch für Extra-PMs
- [ ] Extra-PM, Boost (nur Grün) und PDF-Nachweis als Credits kaufbar
- [ ] Quota-Stub vollständig abgelöst, `pressReleaseQuotaRemaining()` stabil
- [ ] Tests grün, Pint clean, Build clean, Doku synchron