- 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>
289 lines
15 KiB
Markdown
289 lines
15 KiB
Markdown
# 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: 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
|