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>
13 KiB
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
Abgleich-Doku: docs/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:
- 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.
- 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 zuautoPublishApproved()); Verzögerungsfenster (scoring.classification.green_delay_minutes) gilt für beide.PublishScheduledPressReleases: Kandidaten-Query vonclassification = greenaufclassification IN (green, yellow).- Admin-Review-Queue bleibt als Fallback bestehen: unklassifizierte PMs
(Job noch nicht gelaufen / KI-Ausfall ohne Fallback-Ergebnis) bleiben in
reviewund 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 vonpress_release_quota_used_this_monthentfernen. Stattdessen Guard: Einreichen erfordertpressReleaseQuotaRemaining() > 0(sonst würde eine grüne PM ohne verfügbaren Slot veröffentlicht).publish(): Increment beim Statuswechsel aufpublished, idempotent — nur wenn die PM zuvor noch nie veröffentlicht war (Prüfung überpress_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 überconfig/billing.php(billing.enforce_booking, Defaultfalse): solange das Tarif-Modul fehlt, gibt die Methodetruezurü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);UseristBillable, Cashier-Tabellen (subscriptions,subscription_items, Customer-Spalten) migriert. Die lokaleinvoices()-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).PlanSeederidempotent.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 inconfig/billing.php).- MAN-Kreis:
ManualInvoiceService+ Commandbilling:generate-manual-invoices(Scheduler täglich 04:30) — findet aktive/grandfathereduser_payment_optionsohnestripe_subscription_idmit erreichtemcurrent_period_end, friert die Rechnungsadresse als Snapshot ein, stellt eine MAN-Rechnung aus (Zahlungszielbilling.manual_due_days) und schaltet die Periode weiter. Konditions-Overrides pro Vereinbarung überlegacy_conditions(amount_cents/tax_cents/total_cents/interval); ohne Override Netto-Preis derpayment_option+billing.vat_rate. Nicht abrechenbare Fälle (fehlende Rechnungsadresse) werden geloggt und erneut versucht. User::hasActiveBooking()prüft jetzt echt (hinterbilling.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 inuser_payment_optionsmigriert werden (Tabelle ist aktuell leer — eigener Migrations-Schritt).
9E · Stripe (Laravel Cashier)
- Checkout für Abo (monatlich/jährlich) und Einmalzahlung (Einzel-PM, Credits);
Stripe-Produkte/Preise anlegen und IDs in
planspflegen. - Webhooks (Subscription-Status, Zahlungsausfall,
invoice.paid) + Spiegelung der Stripe-Rechnungen ininvoicesmit STR-Nummer aus demInvoiceNumberGenerator. - Slot-Logik auf Plan-Kontingent umstellen (siehe 9D-Rest), Stub ablösen.
- Benötigt
STRIPE_KEY/STRIPE_SECRET/STRIPE_WEBHOOK_SECRETin.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 inreviewmit 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 buildclean. - 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=falseals 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
classificationin 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