# Phase 9 · Veröffentlichungs-Flow (Launch) & Tarif-Modul Stand: 2026-06-12 — **Block 1 (9A–9C) abgeschlossen**; Review-Stopp vor Block 2 (9D–9J, 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 (9A–9C)**: 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 (9D–9I)**: 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 (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 | | **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