presseportale/docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md
Kevin Adametz 894a9436b0 USt-Behandlung: Netto-Preise, VatResolver und Steuer-Ausweis im MAN-Kreis
Einwand/Entscheidung 12.06.2026: Legacy fakturierte brutto (Steuer
inkludiert, z. B. 199 Euro; steuerbefreite Kunden mit Netto-Ausweis
167,23). Alle neuen Preise sind netto; die Steuer wird zur
Rechnungsstellung sauber validiert und ausgewiesen.

- VatResolver + VatTreatment: DE grundsaetzlich immer mit Steuer, EU nur
  mit (formal plausibler) USt-ID befreit (Reverse Charge inkl.
  Pflichthinweis), Drittlaender grundsaetzlich befreit;
  EU-Laenderliste + vat_rate in config/billing.php
- Schema: billing_addresses.vat_id + invoice_billing_addresses.vat_id
  (Snapshot pro Rechnung), invoices.tax_note; Profil-Formular schreibt
  die vorhandene USt-ID jetzt auch an die Rechnungsadresse
- ManualInvoiceService: rechnet auf Netto-Vertragsbasis
  (legacy_conditions.net_cents bzw. Netto-Katalogpreis) und bestimmt
  Steuer/is_netto/tax_note pro Rechnung ueber den VatResolver
- legacy:grandfather-subscriptions: leitet net_cents aus der letzten
  Legacy-Rechnung ab (brutto / 1,19 bzw. is_netto-Betrag direkt);
  fuer DE-Bestandskunden bleibt der Bruttobetrag unveraendert
  (199 brutto -> 167,23 netto + 31,77 USt = 199,00)
- Doku: Decision-Update 2.1 (Netto-Klarstellung), Phase-9-Plan,
  Checkliste, 05-DATABASE-MERGE 5.6; offen: VIES-Validierung der USt-ID

Tests: VatResolverTest (Datasets fuer alle Faelle), Reverse-Charge/
EU-/Drittland-Rechnungen, Netto-Ableitung; Suite 490 passed, 4 skipped.
Pint clean.

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

14 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).
  • 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