presseportale/docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md
Kevin Adametz d548f4b235 Phase 9D: Tarif-Datenmodell, Cashier und hybride Rechnungskreise STR-/MAN-
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>
2026-06-12 10:15:46 +00:00

13 KiB
Raw Blame History

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 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:

  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 (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).
  • 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 in user_payment_options migriert 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 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