presseportale/docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md
Kevin Adametz 23ac8bc7f1 Phase 9F: Tarif-Seite mit Stripe-Checkout und Billing Portal
- Buchungs-Seite zeigt das echte 4-Tier-Raster aus plans (Monat/Jahr-
  Toggle, Jahrespreis als "2 Monate gratis") mit Checkout-Buttons,
  Einzel-PM als separaten No-Abo-Block und Enterprise-Hinweis;
  Credit-Konzept-Mock entfernt (Credits folgen mit 9I bzw. Phase 2)
- Aktueller-Tarif-Panel real: Abo (Preis, Kontingent, Kündigungsstatus),
  Bestandstarif (unbegrenzt, nächste MAN-Rechnung), offene Einzelkäufe;
  Kontingent-Kachel zeigt "Unbegrenzt" bei Bestandsschutz
- "Abo verwalten" über das Stripe Billing Portal
  (me.checkout.billing-portal; Zahlungsmethode, Rechnungen, Kündigung)
- Aktive Buchungen + Verlauf aus echten Daten (Abo, Legacy-Vereinbarung,
  offene/eingelöste Einzelkäufe mit PM-Verknüpfung)
- Tests: BookingsPageTest (9 Tests), PanelConsolidationTest angepasst;
  Suite 519 passed / 4 skipped
- Doku: PHASE-9-Plan 9F , Billing-Doku (Routen, Stripe Tax aktiviert),
  STATUS-ABGLEICH, Checkliste, PROGRESS

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

289 lines
15 KiB
Markdown
Raw Permalink 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: Produkt-Sync (Tarife + Einzel-PM), Webhook-Verarbeitung (STR-Spiegelung, Einmalkauf-Erfüllung, Endpoint registriert), Checkout-Flows (Backend), Slot-Logik auf Plan-Kontingent (Grandfathered = unbegrenzt), Stripe Tax | L | mittel |
| **9F** ✅ | Tarif-Seite + Checkout-UI: Buchungs-Seite mit echtem 4-Tier-Raster (Monat/Jahr-Toggle, „2 Monate gratis"), Einzel-PM-Block, Bestandstarif-Anzeige, „Abo verwalten" (Stripe Billing Portal), 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~~ → mit 9E erledigt: Slot-Logik auf Plan-Kontingent
umgestellt, Stub-Spalte entfernt.
### 9E · Stripe (Laravel Cashier) ✅ (12.06.2026)
- ✅ Produkt-Sync: `billing:sync-stripe-plans` legt Tarife (Monats-/Jahres-
preise) und das Einzel-PM-Produkt als Netto-Preise in Stripe an
(Test-Mode gelaufen; IDs in `plans` bzw. `STRIPE_PRICE_SINGLE_PM`).
- ✅ Webhooks: `ProcessStripeWebhook` spiegelt bezahlte Stripe-Rechnungen
mit STR-Nummer in `invoices` und erfüllt Einmalkäufe; Endpoint
`https://pressekonto.com/stripe/webhook` registriert, Secret gesetzt.
- ✅ Checkout-Flows (Backend): `me.checkout.subscription` +
`me.checkout.single-pm` (CheckoutController → StripeCheckoutService);
Stripe Tax via `Cashier::calculateTaxes()` (Netto-Preise). UI-Anbindung
der Buttons folgt in 9F.
- ✅ Slot-Logik: Plan-Kontingent + Einmalkauf-Verbrauch statt Stub;
**Grandfathered = unbegrenzt** (Entscheidung 12.06.2026, Bestandsschutz).
Details: `docs/user-admin/Billing-und-Rechnungskreise.md` §2.
- Offen → §7 der Billing-Doku: Stripe Tax im Dashboard aktivieren,
Live-Mode-Sync vor Relaunch.
### 9F · Tarif-Seite + Checkout-UI ✅ (12.06.2026)
- ✅ „Buchungen & Add-ons" zeigt das echte 4-Tier-Raster aus `plans`
(Monat/Jahr-Toggle, Jahrespreis als „2 Monate gratis") mit
Checkout-Buttons auf `me.checkout.subscription`; Einzel-PM als
separater No-Abo-Block (`me.checkout.single-pm`); Enterprise als
dezenter Hinweis unter dem Raster. Der Credit-Konzept-Mock ist
abgelöst (Credits → 9I bzw. Phase 2).
- ✅ Aktueller Tarif real: Abo (Preis, Kontingent, Kündigungsstatus),
Bestandstarif (unbegrenzt, nächste MAN-Rechnung) oder offene
Einzelkäufe; Kontingent-Kachel (`Unbegrenzt` bei Bestandsschutz).
- ✅ „Abo verwalten" → Stripe Billing Portal (`me.checkout.billing-portal`:
Zahlungsmethode, Rechnungen, Kündigung).
- ✅ Aktive Buchungen + Verlauf aus echten Daten (Abo, Legacy-Vereinbarung,
offene/eingelöste Einzelkäufe mit PM-Verknüpfung).
- Einstieg aus dem Submit-Gate-Hinweis (9C) führt bereits hierher.
### 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