docs: dev-Doku für den Live-Betrieb neu sortiert — Archiv + Betrieb + Roadmap

Das System ist seit 20.07.2026 live; die dev-Doku spiegelt jetzt den
Betriebszustand statt der Release-Vorbereitung:

- dev/betrieb/: Deploy-Runbook (neu: §10 Betrieb nach Go-Live mit
  SFTP-Deploy-Checkliste inkl. queue:restart-Pflicht und den
  Betriebs-Erkenntnissen der Go-Live-Woche), Legacy-API-Parität, Umami.
- dev/roadmap.md: offene Nachlauf-Punkte (Go-Live-Mails, Merge nach main,
  Stripe-Test-Artefakte, Key-Rotation, Alt-System-Abschaltung) und
  Ideen-Backlog.
- dev/archive/: abgeschlossene Phasen — Migration-2026-Planung,
  Cutover-Runbook (mit Erledigt-Vermerk), Release-Plan/QA-Checkliste,
  Optimierungsphasen, Frontend-Konzepte + Responsive-Audits vom 16.07.,
  Altsystem-Reste.
- dev/README.md als Wegweiser; gitignore-Pfad der SQL-Dumps mitgezogen
  (dev/archive/migration-2026/sql/ bleibt untracked, ebenso dev/migration/
  und dev/api/).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-21 07:52:38 +00:00
parent 3d1bf38908
commit 665c94f162
360 changed files with 923 additions and 1 deletions

View file

@ -0,0 +1,163 @@
# 00 – Overview
## 1. Ausgangslage
### 1.1 Zwei Legacy-Portale (technisch identisch, organisatorisch getrennt)
| Eigenschaft | `presseecho` | `businessportal24` |
|---|---|---|
| Technologie | Symfony 1.4, Doctrine 1.2, PHP 5.6 | Symfony 1.4, Doctrine 1.2, PHP 5.6 |
| Server | Server A | Server B |
| Datenbank | `db_presseecho` (MySQL) | `db_businessportal` (MySQL) |
| Frontend-Domain | presseecho.de | businessportal24.com |
| Code-Base | identisch (nur `app.yml` + Templates abweichend) | identisch |
Code-Referenz im Repo: `/_businessportal24.com/` (exemplarisch; `presseecho` ist inhaltsgleich).
### 1.2 Neue Frontends (bereits in diesem Repo)
- `resources/views/web/` – Landingpages für `presseecho.test` und `businessportal24.test`
- Domain-basiertes Theme-System (`config/domains.php` + `ThemeServiceProvider`)
- Zwei Tailwind-/Vite-Configs (Portal vs. Web)
Diese Frontends sollen künftig **gegen das neue Backend arbeiten** – via direkter DB-Lesezugriffe (same-DB) oder API.
### 1.3 Neues Backend (Aufgabe dieses Projekts)
- Läuft unter `pressekonto.test` / `pressekonto.de`
- Stack: **Laravel 12, PHP 8.4, Livewire 4, Volt, Flux UI 2, MySQL 8, Tailwind 4**
- **Vorarbeiten bereits vorhanden**: Admin-UI-Gerüst, Routes, Auth-Stack
- **Noch zu tun**: Eloquent-Models, Migrations, Services, Daten-Migration, API, Payment (Stripe), Cron-Jobs
---
## 2. Ziele der Migration
### 2.1 Fachliche Ziele
1. **Ein Backend für beide Portale** – Schluss mit dupliziertem Code und doppeltem Betrieb.
2. **Ein Datenbestand** – beide Legacy-DBs in eine Ziel-DB konsolidiert, disambiguiert durch `portal` (`presseecho` | `businessportal24`).
3. **Moderne Admin-UX** – Livewire + Flux UI statt generierter Symfony-Admin-Tabellen.
4. **Saubere REST-API** – mit Sanctum-Auth und Laravel-API-Resources. Harter Cut-over auf Sanctum PATs ohne Legacy-Key-Kompatibilität; Legacy-Keys liefern 410 mit Migrationshinweis.
5. **Automatisierbarer Betrieb** – Laravel Scheduler + Queues statt Bash-Scripts.
### 2.2 Nicht-Ziele
- Kein Umbau der neuen Frontends (`presseecho.test`, `businessportal24.test`) – die laufen bereits.
- Kein 1:1-Port der alten Backend-Symfony-Templates – wir bauen das Admin neu (UI-Gerüst steht schon).
- **Keine Migration der Legacy-Payments/Invoices** als operative Daten – nur Archiv.
- **Keine Übernahme alter API-Keys** – Cut-over auf Sanctum-Tokens.
- **Keine Übernahme alter Passwörter** – User werden zum Reset aufgefordert.
- **Keine Promotion-Links** (Legacy-Feature entfällt).
- Keine Übernahme von PayPal Express, SPK-Berlin, Cortal Consors, Post/Bar, Rechnungszahlung – **einzig Stripe**.
- Kein CMS-Modul für redaktionelle Seiten (`page`-Modul im Legacy) – wird über Blade-Views im Frontend abgebildet.
---
## 3. Erfolgskriterien
| Kriterium | Messbar an |
|---|---|
| Backend unter `pressekonto.test` erreichbar | HTTP 200 auf Login-Seite |
| Admin-Login (Fortify + 2FA) funktioniert | Feature-Test `AuthTest` |
| Customer-Portal via Magic-Link erreichbar | Feature-Test `MagicLinkLoginTest` |
| Pressemitteilungen CRUD (Admin + Customer) | Livewire-Tests `PressReleaseIndex/Create/Edit/Show` |
| Firmen & Kontakte CRUD | Livewire-Tests |
| Sauberes Migrations-Skript (wiederholbar) | `artisan legacy:import` idempotent, läuft gegen die konfigurierten Legacy-DB-Verbindungen |
| Daten beider Portale importiert | `portal`-Verteilung plausibel, Counts Delta ≤ 1 % |
| Rechnungslauf (neu, Stripe) | Stripe-Webhook-Tests, `invoices:generate` Command |
| Legacy-Rechnungen im Archiv einsehbar | `legacy_invoices`-Table vollständig importiert; PDF wird bei Abruf aus Legacy-Daten generiert |
| API v1 mit Sanctum | API-Feature-Tests |
| Coverage ≥ 70 % auf Domain-Code | `./vendor/bin/pest --coverage` |
---
## 4. Phasen (High-Level)
Ausführlicher Plan in [`03-MIGRATION-PLAN.md`](./03-MIGRATION-PLAN.md).
| Phase | Titel | Ergebnis |
|---|---|---|
| P1 | Fundament | DB-Schema (Migrations), Models, Factories, Seeders, Config (countries/salutations) |
| P2 | Auth & Tenancy | Fortify-Profile, **Magic-Link Auth**, Rollen/Rechte (Spatie), `portal`-Scope auf Models |
| P3 | Admin-UI füllen | Volt-Komponenten an echte Models/Services anbinden (UI existiert) |
| P4 | **Customer-Portal** | Self-Service für Companies (PM erstellen, Rechnungen, Tokens, Profil) |
| P5 | Services & Domain-Logic | PressRelease-, Company-, Newsletter-Services; Slugging; Blacklist-Check |
| P6 | **Daten-Migration** ⭐ | Import-Commands aus beiden Legacy-DBs (**wiederholbar**), Bilder-Transfer, Legacy-Invoice-Archiv |
| P7 | API v1 | Sanctum, API-Resources, **Cut-over** (keine Legacy-Key-Kompatibilität) |
| P8 | Payment & Invoicing (Stripe, **neu**) | Neue Produkte, Checkout, Webhooks, PDF, Mahnwesen, Grandfathering |
| P9 | Scheduler & Queues | Laravel Scheduler + Queues |
| P10 | Testing & Härtung | Pest-Tests, Pint, statische Analyse |
| P11 | Deployment | Staging, Produktiv-Import-Rehearsal, DNS-Cutover, Produktivstart |
---
## 5. Entscheidungen (ehemals offene Fragen, 2026-04-23 geklärt)
| ID | Thema | Entscheidung |
|---|---|---|
| **Q-01** | Produktiv-DB-Dumps beider Legacy-Systeme | ✅ **Liegen vor** unter `dev/migration 2026/sql/` (`businessportal24_2026-04-23.sql` 578 MB, `presseecho_2026-04-23.sql` 369 MB). **Wichtig:** Die Migration muss als *sauberes, wiederholbares Skript* gebaut werden, sodass am Go-Live-Tag der **aktuelle Produktivstand** beider DBs in die neue Live-DB überführt werden kann. Auf dem Test-Server stehen Dumps **und direkter Read-Only-Zugriff** auf die Legacy-DBs zur Verfügung. |
| **Q-02** | Alte User-Passwörter | ❌ **Werden NICHT übernommen.** Beim Go-Live geht eine E-Mail an alle User mit Sicherheitshinweis und Passwort-Reset-Link. **Zusätzlich** wird ein **„Login per E-Mail-Link" (Magic Link / Einmal-Passwort)** eingebaut – insbesondere für Companies, die aktuell gar keinen Login haben, aber über die API Pressemitteilungen veröffentlichen. Über diese E-Mail können sie künftig auch auf ihre Pressemappe, Pressemitteilungen, Rechnungen und API-Tokens zugreifen. |
| **Q-03** | Legacy-API-Keys | ❌ **Werden NICHT übernommen.** Neue Token-Technik (Sanctum PATs). Tokens sind pro User im Backend abrufbar und verwaltbar. Alte API-Clients müssen einmalig auf den neuen Token umgestellt werden. |
| **Q-04a** | Alte Rechnungen (PDFs) | ✅ **Kommen ins Archiv** – nur lesbare Ansicht, kein aktiver Workflow. Separate `legacy_invoices`-Tabelle mit vollständigem DB-Import inkl. Status/User-Zuordnung; PDF wird für Legacy-Rechnungen bei Abruf aus diesen Daten generiert, nicht als neuer Stripe-Rechnungslauf. |
| **Q-04b** | Rechnungslauf | ✅ **Komplett neu entwickelt.** Keine Rechnungs-Zahlungsweise mehr. **Einzige Zahlungsart: Stripe.** |
| **Q-04c** | PaymentOptions | ✅ **Neue Produkte**, direkt über Stripe bezahlt. **Grandfathering:** Aktive User behalten ihre alten Konditionen bis Laufzeit-Ende, werden dann auf neue Produkte migriert. |
| **Q-05** | Media/Bilder | ✅ **Lokal** (Storage-Disk `public`, später `s3`). Funktionen werden **erweitert**: Pressemitteilungen dürfen künftig mehrere/größere Bilder enthalten, Companies bekommen ordentliche Logo-Varianten (Legacy war sehr minimalistisch). |
| **Q-06** | Pflicht-Datenfelder neu | ✅ `deleted_at` **überall** (Soft-Deletes). `portal`-Spalte auf allen mandantenfähigen Entitäten. Weitere sinnvolle Erweiterungen pro Entität: `uuid` (öffentliche Links), `gdpr_consent_at`, `last_seen_at` (User), `published_at` (PressRelease). |
| **Q-07** | Mehrsprachigkeit | ✅ **Deutsch + Englisch** werden direkt initialisiert (Admin-UI DE, Content zweisprachig DE/EN). |
| **Q-08a** | Promotion Links | ❌ **RAUS.** |
| **Q-08b** | Footer Code | ✅ Bleibt (deaktivierbar pro Company). |
| **Q-08c** | Newsletter | 🔁 **Neu aufgebaut**, nicht migriert. Nur die bestehenden Subscriber-Daten werden importiert. |
| **Q-08d** | **Kundenbereich / Customer-Portal** | ✅ **WICHTIG** (erweitert den Scope): Das Backend ist **NICHT nur Admin**. Drei Nutzer-Bereiche:<br>• **Admin** – interne Mitarbeiter, volle Verwaltung<br>• **Customer** – Company-User, eingeloggt *oder* via Magic-Link. Eigene Pressemitteilungen erstellen/bearbeiten, Rechnungen einsehen, API-Tokens verwalten, Company-/Contact-Daten pflegen<br>• **API-Only** – reine Token-Nutzung für automatisiertes Publishing |
| **Q-09** | Coupons | ⏸️ **Vertagt.** Kein Model/Feld in Phase 1. Kommt später separat dazu. |
| **Q-10** | Agency vs. Company | ✅ **Unterscheidung bleibt** (per `type`-Enum auf `companies`, kein Table-Inheritance). |
**Kernaussage:** Fokus dieser Migration liegt auf **Pressemitteilungen + Companies + Contacts** aus beiden Portalen sauber in eine neue DB zu überführen. **Zahlungen, Abwicklungen und Newsletter werden neu entwickelt** (nicht migriert). Das Backend dient **Admin-UND-Kunden** (Customer-Portal mit Magic-Link).
---
## 6. Stack-Vergleich
| Domäne | Legacy | Ziel |
|---|---|---|
| Framework | Symfony 1.4 | Laravel 12 |
| ORM | Doctrine 1.2 (YAML-Schema) | Eloquent |
| PHP | 5.6 | 8.4 |
| Auth | sfDoctrineGuardPlugin (sha1 + salt) | Fortify + bcrypt + **Magic-Link** |
| API-Auth | statischer `api_key`-String | **Sanctum PAT** (Cut-over, keine Legacy-Kompatibilität) |
| Rollen | `sfGuardGroup` / `sfGuardPermission` | Spatie/Permission |
| Admin-UI | Generator (auto-admin) | Livewire Volt + Flux UI |
| Forms | sfForm / sfFormDoctrine | Livewire + FormRequest + Flux |
| i18n | Doctrine `I18n` behavior | Spatie/Laravel-Translatable oder `*_translations`-Tabelle |
| Slugs | Doctrine `Sluggable` | `spatie/laravel-sluggable` |
| Images | `sfImageTransformPlugin` | `intervention/image` + Image Driver GD/Imagick |
| PDF | TCPDF (via sfpInvoicePdf) | `barryvdh/laravel-dompdf` |
| Cron | Bash-Scripts + Symfony-Tasks | Laravel Scheduler + Jobs/Queues |
| Mail | Swift Mailer | Laravel Mail (symfony/mailer) |
| Templates | PHP `.php` | Blade |
| Testing | kaum | Pest 3 |
| Assets | symlinks in `web/` | Vite 5 (Portal + Web getrennt) |
---
## 7. Risikopunkte
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| Inkonsistente Daten zwischen beiden Legacy-DBs (gleiche IDs, unterschiedliche Inhalte) | hoch | hoch | Import mit ID-Offset/Re-Mapping; `legacy_id` + `portal` als Unique-Key |
| Abhängigkeiten zwischen Frontends (presseecho.test) und neuem DB-Schema | mittel | hoch | Eloquent-Models können beide Frontends bedienen; Übergangsweise read-only Views |
| Differenz zwischen Test-Dump und Produktiv-Stand am Go-Live-Tag | hoch | hoch | **Wiederholbares Import-Skript**; Delta-Modus; Rehearsal-Import am Staging 1 Tag vor Cutover |
| API-Clients brechen nach Token-Umstellung | hoch | hoch | Frühe Kommunikation an alle API-User; Tokens im Backend self-service abrufbar; Übergangs-Banner |
| User können sich nicht einloggen (weil alte PW weg) | mittel | mittel | Magic-Link + Reset-Mail-Kampagne + gut sichtbare Hilfe |
| Grandfather-Abos falsch abgerechnet | mittel | hoch | Explizite `grandfathered_until`-Spalte, Tests, manuelle Liste |
| Bilder-Pfade ändern (Slug-basierte Thumbnails) | hoch | mittel | Redirect-Rules für alte `/thumbnails/...`-URLs; Import + URL-Map |
---
## 8. Nächste Schritte (konkret)
1. ✅ Entscheidungen geklärt (siehe §5, Stand 2026-04-23)
2. ✅ DB-Dumps liegen in `dev/migration 2026/sql/`
3. **Aktuell:** Phase 1 starten – Migrations, Models, Factories, Seeders (siehe [`03-MIGRATION-PLAN.md`](./03-MIGRATION-PLAN.md))
4. Parallel: Staging-DB-Verbindung zu Legacy-Systemen konfigurieren (Read-Only)
5. Phase 2: Magic-Link Auth + Customer-Bereich skizzieren

View file

@ -0,0 +1,282 @@
# 01 – Legacy-Analyse
Detailliertes Inventar des Symfony-1.4-Backends in `/_businessportal24.com/`. Das `presseecho`-Projekt ist code-identisch.
---
## 1. Projektstruktur (Top-Level)
```
_businessportal24.com/
├── apps/
│ ├── backend/ ← Admin-App (Routing, Config, Templates). Modules liegen in Plugins.
│ └── frontend/ ← Public/Frontend-App (wird NICHT migriert – neue Frontends existieren)
├── config/ ← Globale Config (Datenbanken, Projekt, i18n)
├── lib/ ← Global genutzte Klassen (Models liegen aber in Plugin)
├── plugins/ ← Fachliche Logik – der Hauptteil!
│ ├── PressePortalPlugin ★ Kern-Plugin (Entities, Modules, Forms)
│ ├── sfDoctrineGuardPlugin ← Auth
│ ├── sfDoctrineGuardPaymentPlugin ★ Payment, Invoice, Coupon
│ ├── sfDoctrineApplyPlugin ← Registrierung
│ ├── sfFormExtraPlugin ← Form-Widgets
│ ├── sfImageTransformPlugin ← Image-Thumbnailing
│ ├── sfImageTransformExtraPlugin
│ ├── sfCaptchaGDPlugin ← Captcha
│ ├── sfCsvPlugin ← CSV-Export
│ ├── isicsWidgetFormTinyMCEPlugin← WYSIWYG
│ ├── tnHttpErrorPlugin ← HTTP-Error-Handling
│ └── tnStrategicTesterResponsePlugin
├── symfony-1.4/ ← Framework (Vendor)
├── data/ ← Fixtures
├── web/ ← DocRoot, Public Assets
└── cron_script_{local,server}.sh
```
---
## 2. Admin-Module (`PressePortalPlugin/modules/`)
### 2.1 Verwaltung / CRUD
| Modul | Entität | Admin-URL | Migrations-Target |
|---|---|---|---|
| `adminCenter` | Dashboard / Navigationshub | `/` | Livewire `admin.dashboard` (existiert) |
| `adminSfGuardAuth` | Login/Logout | `/login`, `/logout` | Fortify (existiert) |
| `adminSfGuardUser` (über Routing) | User | `/sf_guard_user` | `admin.users.*` (existiert) |
| `adminSfGuardGroup` | Rollen | `/sf_guard_group` | Spatie Roles → `admin.roles.*` (existiert) |
| `adminSfGuardPermission` | Permissions | `/sf_guard_permission` | Spatie Permissions |
| `adminSalutation` | Anreden | `/salutation` | → Config (`config/salutations.php`) |
| `adminCountry` | Länder | `/country` | → Config (`config/countries.php`) |
| `adminCategory` | Kategorien (i18n) | `/category` | `admin.categories.*` (existiert) |
| `adminCompany` | Firmen/Agenturen | `/company` | `admin.companies.*` (existiert) |
| `adminContact` | Ansprechpartner | `/contact` | `admin.contacts.*` (existiert) |
| `adminPressrelease` | Pressemitteilungen | `/pressrelease` | `admin.press-releases.*` (existiert) |
| `adminPressreleaseimage` | Bilder zu PMs | `/pressreleaseimage` | Teil von PressRelease |
| `adminPromotionlink` | Werbeplätze/Links | `/promotionlink` | **Scope-Entscheidung nötig** |
| `adminFootercode` | Custom Footer-HTML | `/footercode` | **Scope-Entscheidung nötig** |
| `adminBlacklist` | Wort-Blacklist | `/blacklist` | `admin.blacklist.*` (neu) |
| `adminNewslettersubscription` | Newsletter-Subscriber | `/newslettersubscription` | `admin.newsletter.subscribers` (existiert) |
| `sfpCouponAdmin` | Gutscheine | `/coupon` | `admin.coupons.*` (existiert) |
| `sfpInvoiceAdmin` | Rechnungen | `/invoice` | `admin.invoices.*` (existiert) |
| `sfpInvoiceBillingAddressAdmin` | Rechn.-Adressen | `/invoice_billing_address` | Teil von Invoice |
| `sfpUserBillingAddressAdmin` | User-Rechn.-Adressen | `/user_billing_address` | Teil von User |
| `sfpUserPaymentOptionAdmin` | User-Abos/Payment | `/user_payment_option` | `admin.payments.*` (existiert) |
### 2.2 Service-/System-Module
| Modul | Zweck | Migrations-Target |
|---|---|---|
| `sfpCron` | HTTP-Trigger für Cron-Scripts | Laravel Scheduler (`routes/console.php`) |
| `sfpStore` | Abo-/Bestell-Flow | Stripe Checkout + Domain-Service |
| `sfpInvoicePdf` | PDF-Generation | `barryvdh/laravel-dompdf` + Blade-Template |
| `sfpUserBilling` | User-seitige Rechn.-Übersicht | Frontend-/Kundencenter-Views |
| `import` | CSV-/Daten-Import | **Neu als `php artisan` Command(s)** |
| `ajax` | Autocompletion für Admin-Forms | Livewire `wire:model`-Suche |
| `tnHttpError` | Error-Views | Laravel's Standard-Error-Pages |
### 2.3 Frontend-Module (werden NICHT migriert)
| Modul | Zweck |
|---|---|
| `company`, `customercenter`, `page`, `pressrelease`, `pressreleaseimage`, `category`, `profile`, `newsletter`, `presscontact`, `sfApply`, `sfGuardAuth`, `sfGuardUser` | Altes Symfony-Frontend. Wird ersetzt durch bestehendes `resources/views/web/*` und das neue Backend-API |
---
## 3. Datenmodell (aus `PressePortalPlugin/config/doctrine/schema.yml`)
### 3.1 Entitäten (Kern)
| Doctrine-Entität | Zeilen in Schema | Fachlicher Zweck |
|---|---|---|
| `Salutation` | 4–18 | Anrede (Herr/Frau, i18n) |
| `Country` | 23–32 | Länder |
| `sfGuardUser` | 38–84 | User |
| `sfGuardUserProfile` | 92–191 | User-Profil (Name, Adresse, Tel, E-Mail, API-Key, Tax-ID …) |
| `Company` | 197–278 | Firma (inkl. Logo, Login, Footer-Code-Disable) |
| `Agency` | 280–297 | Column-Aggregation-Inheritance von Company (ctype='agency') |
| `CompanyUser` | 300–318 | User ↔ Company Pivot |
| `ApiUser` | 321–339 | API-Only User-Markierung |
| `ResponsibleCompanyUser` | 342–360 | Verantwortliche User pro Company |
| `Contact` | 365–411 | Ansprechpartner einer Firma |
| `NewsletterSubscription` | 418–469 | Newsletter-Anmeldungen |
| `Category` | 476–494 | Kategorien (i18n, sluggable) |
| `PressRelease` | 500–575 | Pressemitteilungen (mit Teaser-Range, Status, SEO) |
| `PressReleaseContact` | 583–601 | PM ↔ Contact Pivot |
| `PressReleaseImage` | 608–644 | Bilder zu PMs |
| `PromotionLink` | 650–684 | Werbelinks |
| `PromotionLinkCategory` | 687–705 | PromotionLink ↔ Category Pivot |
| `FooterCode` | 707–733 | Custom Footer-HTML pro Kategorie/Sprache |
| `CategoryFooterCode` | 735–753 | Pivot |
| `Blacklist` | 755–768 | Worte für Spam-Check |
| `UserBillingAddress` | 771–807 | Rechn.-Adresse am User |
| `InvoiceBillingAddress` | 809–841 | Rechn.-Adresse auf Rechnung (Snapshot) |
| `UserPaymentOptionCompany` | 854–874 | Abo ↔ Firma |
### 3.2 Entitäten aus `sfDoctrineGuardPaymentPlugin`
| Entität | Zweck |
|---|---|
| `PaymentOption` | Produkt-Definition (Preis, Typ: recurring/onetime/single) |
| `PaymentOptionReference` | Self-referencing, Parent→Child (z. B. Aufpreispakete) |
| `PaymentOptionExcludeGroup` | Gruppen, die Option NICHT buchen dürfen |
| `PaymentOptionAccessGroup` | Gruppen, die durch Option Zugriff erhalten |
| `UserPaymentOption` | Gebuchtes Abo (User ↔ Produkt), Status, Laufzeit |
| `UserPaymentOptionReference` | Self-referencing |
| `UserPayment` | Zahlungsdatensatz |
| `Invoice` | Rechnung (mit Mahnungen, Fälligkeit) |
| `UserBillingAddress` | (hier nochmal definiert) |
| `InvoiceBillingAddress` | (hier nochmal definiert) |
| `Coupon` | Gutschein |
### 3.3 sfGuard-Tabellen
`sfGuardUser`, `sfGuardGroup`, `sfGuardPermission`, `sfGuardUserGroup`, `sfGuardGroupPermission`, `sfGuardUserPermission`, `sfGuardRememberKey` → Mapping auf Laravel `users` + Spatie `roles`/`permissions`/`model_has_*`.
### 3.4 Besondere Doctrine-Features
| Feature | Verwendung | Laravel-Äquivalent |
|---|---|---|
| `actAs: Timestampable` | auf fast allen Entitäten | `created_at`/`updated_at` (Standard) |
| `actAs: I18n` | `Salutation`, `Category`, `PaymentOption` | Translation-Tabellen oder `spatie/laravel-translatable` |
| `actAs: Sluggable` | `Company`, `Category`, `PressRelease`, `PressReleaseImage` | `spatie/laravel-sluggable` |
| `inheritance: column_aggregation` | `Agency extends Company` | Entweder gemeinsames Model mit `type`-Scope, oder Parent+Child-Model mit globalem Scope |
| `relations refClass` | viele M:N | Pivot-Tabellen, `belongsToMany` |
---
## 4. Routing (`apps/backend/config/routing.yml`)
Auszug – **15 `sfDoctrineRouteCollection`** (auto-generated REST für alle CRUD-Entitäten) + folgende Custom-Routes:
| Route | URL | Ziel |
|---|---|---|
| `homepage` | `/` | `adminCenter::index` |
| `change` | `/mychange` | `adminCenter::change` |
| `sf_guard_signin` | `/login` | Login |
| `sf_guard_signout` | `/logout` | Logout |
| `sf_guard_password` | `/request_password` | Password-Reset |
| `usercp` | `/usercp` | User-CP |
| `masterdata` | `/masterdata` | Stammdaten |
| `portalcp` | `/portalcp` | Portal-CP |
| `promotioncp` | `/promotioncp` | Promotion |
| `pressrelease_company_autocomplete` | `/pressrelease/companyautocomplete` | AJAX |
| `company_user_autocomplete` | `/company/userautocomplete` | AJAX |
| `press_release_autocomplete_url` | `/ajax/press_release_autocomplete` | AJAX |
| `sf_image` | `/thumbnails/sf_image/:format/:filepath.:sf_format` | Thumbnails |
| `slug_image` | `/thumbnails/:type/:format/:path/:slug-:id.:sf_format` | Thumbnails (Slug) |
→ Abbildung auf `routes/admin.php` (bereits zu 80 % vorhanden) + ein paar Livewire-Aktionen.
---
## 5. Admin-Menü (`config/adminMenu/hnav.yml`)
```
Home
├─ Master data
│ ├─ Countries
│ ├─ Salutations
│ └─ Blacklists
├─ User CP
│ ├─ Users
│ ├─ Groups
│ ├─ Permissions
│ └─ Newsletter subscription
├─ Billing
│ ├─ Payment options
│ ├─ Coupons
│ └─ Invoices
├─ Portal CP
│ ├─ Press releases
│ ├─ Press release images
│ ├─ Categories
│ ├─ Companies
│ └─ Contacts
└─ Promotions
├─ Promotion links
└─ Footer code
```
→ Diese Struktur ist im neuen Laravel-Projekt bereits in der `sidebar.blade.php`-Navigation abgebildet.
---
## 6. Cron-Scripts (`cron_script_server.sh`)
```bash
php symfony sfp:user-payment-create --application=frontend
php symfony sfp:invoice-create --application=frontend --culture=de
php symfony sfp:invoice-reminder --application=frontend --culture=de
# php symfony sfp:user-paymentoption-expire --application=frontend (deaktiviert)
```
Dazu `sfpCron::index` (HTTP-getriggert, sehr unsicher: Shared-Secret als URL-Parameter).
**Ziel-Mapping:**
| Legacy-Task | Neu |
|---|---|
| `sfp:user-payment-create` | Job `CreateRecurringPayments` (daily) |
| `sfp:invoice-create` | Job `GenerateInvoices` (daily) |
| `sfp:invoice-reminder` | Job `SendInvoiceReminders` (daily) |
| `sfp:user-paymentoption-expire` | Job `ExpirePaymentOptions` (daily) |
Alles über Laravel Scheduler → `routes/console.php`.
---
## 7. API (aus `lib/ApiActions.class.php` + Frontend-Modulen)
- **Auth**: Statischer `api_key` im User-Profil (String, SHA1-base64).
- **CSRF**: Für API-Requests in `prepareForm()` deaktiviert.
- **Endpoints** (exakte Liste siehe [`07-API-MIGRATION.md`](./07-API-MIGRATION.md)):
- `POST /pressrelease/create` – neue PM anlegen
- `POST /pressreleaseimage/upload` – Bild hochladen
- `GET /pressrelease/list` – PMs des Users
- `GET /company/list`
- `POST /newsletter/subscribe`
- …
---
## 8. Mailer (`lib/bpMailer.class.php`)
- Templating via `get_partial()` in Symfony.
- Versand via SwiftMailer.
→ Ersetzt durch:
- Laravel `Mailable` + Blade-Templates
- Queues (`mail` queue)
---
## 9. Frontend-Assets / Thumbnails
- URL-Format: `/thumbnails/pressreleaseimage/thumb_200/pr/<slug>-<id>.jpg`
- Generator: `sfImageTransformPlugin`
- Pfade referenzieren Slugs → bei Slug-Änderung Broken-Links
→ Ersatz: `intervention/image` + `/storage/images/...` + `public/thumbnails/...`. Für alte URLs **302-Redirect-Middleware**.
---
## 10. Vorarbeiten im Repository (nicht verlieren!)
1. **`_businessportal24.com/dev/`** – Bereits extrahiertes Core-Package mit DTOs, Enums, Value Objects, Domain-Events. Inhalte prüfen + dort wo sinnvoll in Laravel-App übernehmen.
2. **`_businessportal24.com/dev/migration/`** – Vorhandene Migrations-Doku (CHECKLIST, IMPLEMENTATION etc.) – als Referenz nutzen, aber **nicht mehr pflegen**.
3. **`dev/_old/Models/`** & **`dev/_old/migrations/`** – Früher automatisch erzeugte Eloquent-Modelle/-Migrations aus dem alten Schema (2024). Wert: Nützliches Mapping der Spalten-/Typ-Namen. **NICHT** 1:1 übernehmen – Schema wird modernisiert.
---
## 11. „Kritische" Stellen (Achtung bei Migration)
| Datei / Ort | Problem |
|---|---|
| `PressePortalPlugin/lib/helper/PaypalCredentialsHelper.php` (oder vergleichbar) | Hart-kodierte PayPal-Credentials → entfällt (nur Stripe) |
| `sfpCron/actions/actions.class.php` | Shared Secret `G8ZvEbnP8fEPfnWX4L` in URL; `exec()`-Call → entfällt |
| `sfGuardUser.password`-Feld | `sha1($salt.$password)` → Laravel bcrypt, Legacy-Hasher nötig |
| `Company.ctype` enum | Inheritance-Feld; bei Migration entscheiden, ob `type enum('agency','company')` oder Single-Table |
| `sfGuardUserProfile.api_key` | Wird von API-Clients genutzt → Umwandlung nach Sanctum + Keep-Alive |
| `PressRelease.text` | `string(40000)` – in MySQL 8 auf `TEXT` oder `MEDIUMTEXT` mappen |
| Thumbnails mit `/slug-id.ext` | Bei Slug-Änderung 404; neues Schema sollte `id` primär nutzen, Slug redirectbar |
| Mehrsprachigkeit `I18n` behavior | Aktuell `de` + `en` Daten in `*_translation`-Tabellen – siehe [`04-DATA-MODEL.md`](./04-DATA-MODEL.md) |

View file

@ -0,0 +1,351 @@
# 02 – Ziel-Architektur (Laravel 12)
## 1. Hohes Level
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Pressekonto Backend (pressekonto.test) │
│ Laravel 12 · PHP 8.4 · Livewire 4 · Volt · Flux UI 2 │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌─────────┐ │
│ │ Admin-UI │ │ Customer- │ │ Public API │ │ Console │ │
│ │ Livewire/Volt │ │ Portal │ │ v1 (Sanctum) │ │ Sched. │ │
│ │ (admin.*) │ │ (customer.*) │ │ │ │ + Jobs │ │
│ └──────┬────────┘ └──────┬────────┘ └──────┬────────┘ └────┬────┘ │
│ │ │ │ │ │
│ └──────────────────┴──────┬───────────┴──────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Application Services (app/Services/*) │ │
│ │ PressReleaseService · CompanyService · InvoiceService … │ │
│ └───────────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┼─────────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ Models │ │ Actions │ │ Integrations│ │
│ │ (Eloquent) │ │ (Invokable) │ │ Stripe, Mail│ │
│ └──────┬───────┘ └────────┬───────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MySQL 8 (einheitliche DB, `portal` disambiguiert Tenants) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Frontend-Apps (presseecho.test, businessportal24.test) nutzen entweder │
│ dieselbe DB (direkte Reads) oder /api/v1. │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## 2. Verzeichnis-Layout (Ziel)
```
app/
├── Actions/ ← Invokable Actions (1 Klasse = 1 Use-Case)
│ ├── PressRelease/
│ │ ├── CreatePressRelease.php
│ │ ├── PublishPressRelease.php
│ │ ├── RejectPressRelease.php
│ │ └── CheckForBlacklistedWords.php
│ ├── Company/
│ ├── Invoice/
│ │ ├── GenerateInvoicesForPeriod.php
│ │ └── SendInvoiceReminder.php
│ ├── Newsletter/
│ └── Fortify/ ← bereits von Fortify angelegt
│
├── Console/
│ └── Commands/
│ ├── Migration/ ← einmalige Daten-Migrations-Commands
│ │ ├── ImportLegacyUsers.php
│ │ ├── ImportLegacyCompanies.php
│ │ └── ImportLegacyPressReleases.php
│ └── Maintenance/
│ └── PurgeExpiredTokens.php
│
├── Enums/
│ ├── Portal.php ← Presseecho | Businessportal24
│ ├── PressReleaseStatus.php
│ ├── InvoiceStatus.php
│ ├── PaymentMethod.php
│ ├── PaymentStatus.php
│ ├── UserPaymentOptionStatus.php
│ ├── PaymentOptionType.php
│ ├── CompanyType.php
│ └── RegistrationType.php
│
├── Events/
│ ├── PressRelease/
│ │ ├── PressReleasePublished.php
│ │ └── PressReleaseRejected.php
│ └── Invoice/
│ ├── InvoiceCreated.php
│ └── InvoicePaid.php
│
├── Http/
│ ├── Controllers/
│ │ └── Api/
│ │ └── V1/
│ │ ├── PressReleaseController.php
│ │ ├── PressReleaseImageController.php
│ │ ├── CompanyController.php
│ │ └── NewsletterController.php
│ ├── Middleware/
│ │ ├── SetCurrentPortal.php ← setzt default-Portal aus Domain
│ │ └── EnsureCustomerRole.php ← für customer.*-Routen
│ ├── Requests/ ← FormRequests
│ │ ├── PressRelease/
│ │ ├── Company/
│ │ └── Api/V1/
│ └── Resources/V1/
│ ├── PressReleaseResource.php
│ ├── CompanyResource.php
│ └── ...
│
├── Jobs/
│ ├── GenerateInvoices.php
│ ├── SendInvoiceReminders.php
│ ├── CreateRecurringPayments.php
│ └── ExpirePaymentOptions.php
│
├── Livewire/
│ ├── Actions/ ← existiert
│ ├── Admin/ ← existiert (users)
│ ├── Customer/ ← NEU: Self-Service-Komponenten
│ │ ├── Dashboard.php
│ │ ├── PressReleases/Index.php
│ │ ├── PressReleases/Create.php
│ │ ├── Invoices/Index.php
│ │ ├── Tokens/Index.php
│ │ └── Profile.php
│ └── Auth/
│ ├── MagicLinkRequest.php ← NEU: "Login per Mail-Link"
│ └── MagicLinkConsume.php ← NEU: prüft signed URL
│
├── Mail/
│ ├── PressReleasePublished.php
│ ├── PressReleaseRejected.php
│ ├── InvoiceCreated.php
│ ├── InvoiceReminder.php
│ ├── MagicLoginLink.php ← NEU
│ ├── GoLivePasswordReset.php ← NEU: Kampagnen-Mail beim Go-Live
│ └── NewsletterConfirmation.php
│
├── Models/
│ ├── User.php ← existiert (erweitern)
│ ├── Profile.php ← entspr. sfGuardUserProfile
│ ├── Company.php
│ ├── Contact.php
│ ├── Category.php
│ ├── CategoryTranslation.php ← optional
│ ├── PressRelease.php
│ ├── PressReleaseImage.php
│ ├── Newsletter/
│ │ └── Subscription.php
│ ├── Invoice.php ← NEU-Billing (Stripe-getrieben)
│ ├── InvoiceBillingAddress.php
│ ├── LegacyInvoice.php ← ARCHIV (read-only)
│ ├── BillingAddress.php ← am User
│ ├── PaymentOption.php ← neue Produkte, Stripe-Price-Mapping
│ ├── PaymentOptionTranslation.php ← optional
│ ├── UserPaymentOption.php ← inkl. `grandfathered_until`
│ ├── UserPayment.php
│ ├── FooterCode.php ← bleibt (Q-08b)
│ ├── MagicLink.php ← NEU: Magic-Link-Token (siehe §5.2)
│ ├── Blacklist.php
│ └── LegacyImport/
│ └── ImportMap.php ← persistente ID-Mapping-Tabelle
│
│ (entfallen: Coupon, PromotionLink – D-14/D-16)
│
├── Policies/
│ ├── PressReleasePolicy.php
│ ├── CompanyPolicy.php
│ └── ...
│
├── Providers/
│ ├── AppServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── FortifyServiceProvider.php
│ └── ThemeServiceProvider.php ← existiert
│
├── Scopes/
│ ├── CurrentPortalScope.php ← optional: Default-Tenant-Filter
│ └── ActiveScope.php
│
├── Services/ ← domänenlogische Services
│ ├── PressReleaseService.php
│ ├── CompanyService.php
│ ├── InvoiceService.php
│ │ └── InvoiceNumberGenerator.php
│ ├── NewsletterService.php
│ ├── StripeService.php ← Wrapper um Laravel Cashier (optional) oder Stripe-PHP
│ ├── ImageService.php ← Intervention-Image + Storage
│ └── BlacklistService.php
│
└── Support/
├── Auth/
│ └── MagicLinkGenerator.php ← signed URLs mit kurzer TTL
└── Slug/
└── SluggableTrait.php
```
---
## 3. Konventionen
- **Naming**: `snake_case` für DB-Spalten, `camelCase`/`StudlyCase` für PHP, englisch für Code, deutsch für User-Facing-Strings.
- **Primary Keys**: `id` BIGINT unsigned, `ulid` optional für extern-sichtbare Ressourcen.
- **Timestamps überall** (`$timestamps = true`), Soft Deletes dort, wo Datenhistorie wichtig ist (User, Company, PressRelease, Invoice).
- **Portal-Scoping**: Spalte `portal` (`enum: presseecho | businessportal24 | both`) auf allen mandantenfähigen Entitäten. `CurrentPortalScope` filtert im Frontend; Admin-Panel zeigt beides (mit Filter-Dropdown).
- **Legacy-IDs**: Jede importierte Entität hat `legacy_portal` + `legacy_id` + Unique-Index. So bleiben API-Clients kompatibel (Mapping-Layer).
- **Enums**: Backed Enums mit Label-Methode:
```php
enum PressReleaseStatus: string
{
case Draft = 'draft';
case Review = 'review';
case Published = 'published';
case Rejected = 'rejected';
case Archived = 'archived';
public function label(): string { /* … */ }
}
```
- **Action-Pattern** über Controller hinaus: Controller/Livewire rufen Actions; Actions nutzen Services; Services kapseln Eloquent.
- **FormRequests** für Validierung; in Livewire über `rules()`-Methode + `ValidatesWhenResolvedTrait`.
---
## 4. Datenbank-Setup
- **Connection** `mysql` → Ziel-DB des neuen Backends.
- Optionale **Legacy-Connections**: `legacy_presseecho` + `legacy_businessportal` (read-only) – nur während der Import-Phase. Definition in `config/database.php`:
```php
'connections' => [
'mysql' => [/* Primary */],
'legacy_presseecho' => [/* read-only */],
'legacy_businessportal' => [/* read-only */],
],
```
---
## 5. Auth-Konzept
### 5.1 Rollen (Spatie Permission)
| Rolle | Wer | Zugang |
|---|---|---|
| `admin` | Interne Mitarbeiter | `admin.*`-Routen, alle Aktionen |
| `editor` | Interne Redaktion | `admin.*`-Routen mit eingeschränkten Permissions |
| `customer` | Company-Ansprechpartner | `customer.*`-Routen (Self-Service) |
| `api-only` | Automatisierte API-Clients | nur `/api/v1/*` via Sanctum, kein Web-Login |
### 5.2 Login-Wege
- **Passwort-Login (Admin + Customer)**: Fortify (bereits konfiguriert).
- **2FA**: Fortify-Standard (Spalten bereits in `users`). Pflicht für Admins, optional für Customer.
- **Magic-Link (Customer + Initial-Admin-Access)** ⭐ **NEU (D-10)**:
- Nutzer gibt E-Mail ein → System versendet signed URL (TTL 15 min, Single-Use).
- Tabelle `magic_links`: `id, user_id, token_hash, purpose (login|press_release_access|invoice_access), expires_at, consumed_at, ip_address`.
- Insbesondere für Companies ohne bestehenden Login (Legacy: viele Companies haben API-Key aber kein Passwort).
- Nach Verbrauch optional sofortiger Redirect zur Zielseite (z. B. bestimmte Pressemitteilung).
- **Passwort-Reset**: Fortify-Standard; beim Go-Live **Massen-Mailing** via `GoLivePasswordReset`-Mailable.
- **Legacy-Passwörter (sha1+salt)**: **werden NICHT übernommen (D-09)**. Beim ersten Login-Versuch mit Legacy-Account → Hinweis "Bitte Passwort zurücksetzen oder Magic-Link anfordern".
### 5.3 API-Auth
- **Sanctum Personal Access Tokens only**. Scopes: `press-releases:write`, `press-releases:read`, `companies:read`, etc.
- **Cut-over (D-05)**: Keine Kompatibilitätsschicht für Legacy `api_key`. Alte Clients bekommen Fehlermeldung mit Hinweis auf Migrations-URL.
- Tokens erstellbar / löschbar unter:
- Admin: `admin/users/{user}/tokens`
- Customer: `account/tokens`
---
## 6. Multi-Domain & Portal-Scope
- Pro Domain ein Frontend, aber alle Admin-Screens auf `pressekonto.test`.
- Middleware `SetCurrentPortal`:
- Web (presseecho.test / businessportal24.test) → `app()->instance('current_portal', Portal::Presseecho|Businessportal24)`
- Admin → Portal-Auswahl via Dropdown, gespeichert in Session (`current_portal`)
- Global Scope `CurrentPortalScope` auf `PressRelease`, `Company`, `Newsletter` etc. – abschaltbar via `->withoutGlobalScope(CurrentPortalScope::class)`.
---
## 7. Payment (Stripe only) – **komplett neu (D-03/D-12/D-13)**
- Paket: **`laravel/cashier` (Stripe)** für Subscription-Lifecycle (entscheiden in Phase 8).
- **Keine Rechnungs-Zahlungsweise mehr** – ausschließlich Stripe (Card, SEPA, optional Klarna).
- **Keine Legacy-PaymentOption-Migration** – neue Produkte/Stripe-Prices werden vom Auftraggeber definiert.
- **`PaymentOption`-Tabelle**: interne Produkt-Definition mit `stripe_product_id` + `stripe_price_id`.
- **Grandfathering**: Spalte `grandfathered_until` auf `UserPaymentOption`. Aktive Alt-User behalten Konditionen bis zu diesem Datum (aus Legacy-`contract_date` + Laufzeit abgeleitet). Scheduler-Job `ExpireGrandfatheredSubscriptions` stößt automatisch Umstellung an.
- Webhook-Endpoint `/webhooks/stripe` → Event-Handler aktualisieren `UserPaymentOption` + `UserPayment` + `Invoice`.
---
## 8. Invoicing (neu) + Legacy-Archiv
### Neuer Rechnungslauf
- PDF: `barryvdh/laravel-dompdf`, Template: `resources/views/pdf/invoice.blade.php`.
- Trigger: Stripe-Webhook `invoice.payment_succeeded` → eigene `Invoice` generieren, PDF erstellen, Mail senden.
- `InvoiceNumberGenerator`: Format `{yyyy}{mm}-{seq}` (modern, getrennt von Legacy-Nummernkreis).
- Mahnwesen: stark vereinfacht, nur für Stripe-Failed-Payments (automatische Stripe-Retry-Logik + zusätzliches Reminder-Mailing).
### Legacy-Archiv ⭐ D-12
- Separate Tabelle `legacy_invoices` (read-only).
- Vollständiger DB-Import aller Legacy-Rechnungen inkl. Status, Beträge, Datum, Zahlart, Raw-Snapshot und User-Zuordnung.
- PDFs werden bei Abruf/on demand aus den archivierten Legacy-Daten generiert. Ein abgelegter `pdf_path` ist nur Cache/Export, nicht die führende Quelle.
- Zugriff:
- Admin: volle Liste aller Legacy-Rechnungen
- Customer: nur eigene (Filter über `user_id`/`company_id`-Mapping aus Legacy-Import)
- **Keine Neuausstellung, kein Mahnwesen** auf Legacy-Rechnungen.
---
## 9. Images / Media
- Storage-Disk `public` (lokal) für Dev, `s3` für Prod.
- `ImageService` kapselt Upload, Thumbnail-Generierung (Intervention Image), Cleanup.
- URL-Schema neu: `/storage/images/press-releases/{id}/{variant}.{ext}`.
- **Alte URLs**: Redirect-Regel in `routes/web.php` oder dedizierte Middleware.
---
## 10. i18n-Strategie (D-07: DE + EN initial)
Legacy nutzt Doctrine-I18n-Behavior → eigene `*_translation`-Tabellen (Salutation_Translation, Category_Translation, PaymentOption_Translation).
Ziel:
- **UI-Locales**: `de` (primär), `en` (initial aktiviert).
- **Content-Locales**: `de`, `en` – mindestens für `Category`, optional für `PressRelease`.
| Option | Für / Gegen |
|---|---|
| `spatie/laravel-translatable` (JSON-Spalten) | + schlank, – keine FTS/Sortierung per Sprache |
| Klassische Translation-Tables (Modell `CategoryTranslation`) | + SQL-fähig, – mehr Code |
**Empfehlung:** Klassische Translation-Tables für `Category` (weil Slug/Name pro Sprache gebraucht werden); JSON für Static-Master wie Salutation.
Beim Seeding werden DE + EN direkt für Kategorien und Salutations angelegt.
---
## 11. Testing
- **Pest 3**; Factories, Seeders, `RefreshDatabase`-Trait.
- Feature-Tests für Livewire-Screens, Actions, API-Endpoints.
- Architektur-Tests (Pest `arch()`) für Namenskonventionen.
- Datenimport-Tests: Import-Command gegen Sample-SQL-Dumps in `database/legacy-fixtures/`.
---
## 12. Code-Qualität
- **Laravel Pint** (bereits konfiguriert) für Code-Style.
- **PHPStan/Larastan** Level 5–6 auf `app/` (empfohlen, kein Muss in Phase 1).
- **PHP-CS-Fixer** nicht nötig (Pint deckt ab).
- **Pre-commit-Hook**: `pint --dirty` + Tests.

View file

@ -0,0 +1,270 @@
# 03 – Migrations-Plan (Phasenweise)
Jede Phase hat einen Satz klar abgegrenzter Aufgaben, ein greifbares Deliverable und Exit-Kriterien. Prioritäten: 🔴 hoch · 🟡 mittel · 🟢 niedrig.
**Legende Status:** ⬜ offen · 🔄 in Arbeit · ✅ fertig · ⏸️ blockiert · ❌ verworfen
> **Status-Abgleich 2026-04-29:** Diese Datei wurde gegen den realen Code-Stand abgeglichen. `08-PROGRESS.md` bleibt das chronologische Log; die Tabellen hier bilden den operativen Phasenstatus ab.
> **Scope-Anpassung 2026-04-23:** Nach den Entscheidungen des Auftraggebers (siehe [`00-OVERVIEW.md §5`](./00-OVERVIEW.md#5-entscheidungen-ehemals-offene-fragen-2026-04-23-geklärt)) wurde der Plan deutlich geschärft:
> - **P2** enthält jetzt Magic-Link-Auth, keine Legacy-Passwort-Übernahme.
> - **P4** (Customer-Portal) ist neu eingezogen worden.
> - **P7** (API) macht einen sauberen Cut-over, keine Legacy-Key-Kompatibilität.
> - **P8** (Billing) wird komplett neu gebaut – keine Übernahme alter Rechnungen in den neuen Rechnungskreis; Legacy-Rechnungen werden vollständig als DB-basiertes Archiv importiert, Grandfathering bleibt separat.
> - Promotion Links und Coupons entfallen in Phase 1.
---
## Phase 0 – Vorbereitung ✅ (weitgehend erledigt)
| # | Aufgabe | Status |
|---|---|---|
| 0.1 | Entscheidungen in [`00-OVERVIEW.md §5`](./00-OVERVIEW.md) mit Auftraggeber klären | ✅ 2026-04-23 |
| 0.2 | Legacy-DB-Dumps beschaffen → `dev/migration 2026/sql/` | ✅ |
| 0.3 | `.env` anpassen: DB-Connections für `mysql`, `legacy_presseecho`, `legacy_businessportal` | ✅ lokal/testfähig, da P6-Import gelaufen; Staging/Prod-Credentials bleiben P11-Thema |
| 0.4 | Lokale MySQL-Schemas `legacy_pe_snapshot` / `legacy_bp_snapshot` aus Dumps erstellen | ✅ für bisherigen Vollimport erledigt |
| 0.5 | Stripe-Test-Account + API-Keys vom Auftraggeber | ⬜ |
| 0.6 | Liste der neuen Stripe-Produkte (Preisliste) vom Auftraggeber | ⬜ |
| 0.7 | Liste der aktiven Alt-Abos für Grandfathering (oder Kriterien) vom Auftraggeber | ⬜ |
**Exit:** DB-Dumps lokal importierbar, `.env` konfiguriert, Stripe-Zugang vorhanden.
---
## Phase 1 – Fundament (1–2 d) 🔴
**Ziel:** Alle Eloquent-Models + Migrations + Factories + Seeders existieren und `php artisan migrate:fresh --seed` läuft durch.
| # | Aufgabe | Abhängigkeiten | Status |
|---|---|---|---|
| 1.1 | `config/countries.php` + `config/salutations.php` aus Legacy-Daten exportieren | 0.4 | ✅ |
| 1.2 | Enums anlegen (`app/Enums/*`) inkl. `Portal`, `PressReleaseStatus`, `UserPaymentOptionStatus` (mit `grandfathered`), `InvoiceStatus` (Stripe-nah) | – | ✅ |
| 1.3 | Migrations schreiben (Tabellen aus [`04-DATA-MODEL.md`](./04-DATA-MODEL.md)):<br>`magic_links`, `companies`, `contacts`, `profiles`, `categories`, `category_translations`, `press_releases`, `press_release_images`, `press_release_contact`, `newsletter_subscriptions`, `billing_addresses`, `invoice_billing_addresses`, `invoices`, `payment_options`, `payment_option_translations`, `user_payment_options`, `user_payments`, `legacy_invoices`, `legacy_import_map`, `contact_user`, `api_usage_logs`, `admin_presets`, `footer_codes`, `category_footer_code`.<br>**Scope-Hinweis:** `blacklists` und Coupons sind weiterhin nicht produktiv umgesetzt bzw. vertagt. | 1.2 | ✅ Kernschema (inkl. Footer-Codes 2026-05-04) |
| 1.4 | `users`-Tabelle erweitern: `portal`, `registration_type`, `gdpr_consent_at`, `legacy_portal`, `legacy_id`, `last_seen_at`. **`password` auf NULLABLE**. | – | ✅ |
| 1.5 | Soft-Deletes + `portal`-Spalte auf allen relevanten Tabellen | 1.3 | ✅ |
| 1.6 | Models mit Relationships + Casts + Global Scopes anlegen | 1.3 | ✅ Kernmodels |
| 1.7 | Factories für alle Models (Faker-DE-Locale) | 1.6 | ✅ Kernfactories |
| 1.8 | Seeder: `RolesAndPermissionsSeeder` (admin/editor/customer/api-only), `CategorySeeder` (DE+EN), `DefaultAdminUserSeeder`, `SalutationTranslationSeeder` | 1.6 | 🔄 Rollen, Kategorien, PaymentOptions, AdminPresets vorhanden; kein separater DefaultAdmin/SalutationSeeder |
| 1.9 | `php artisan migrate:fresh --seed` läuft grün | 1.3–1.8 | ✅ laut `08-PROGRESS.md` |
| 1.10 | Erste Pest-Tests (Smoke): alle Factories erzeugen valide Models | 1.7 | ✅/🔄 Billing/Auth/Admin/API-Featuretests vorhanden; keine zentrale Factory-Smoke-Suite |
**Exit:** DB-Struktur stabil, Test-Daten vorhanden, Tests grün. Keine PromotionLinks/Coupons-Tabellen (D-14/D-16).
---
## Phase 2 – Auth & Tenancy (1.5 d) 🔴
| # | Aufgabe | Status |
|---|---|---|
| 2.1 | Spatie-Seeder mit Rollen: `admin`, `editor`, `customer`, `api-only` + passende Permissions | ✅ |
| 2.2 | `User`-Model: `canAccessAdmin()` + `canAccessCustomer()` + Policies | ✅ |
| 2.3 | **Magic-Link Auth** ⭐ (D-10): `MagicLinkGenerator` + `MagicLinkConsume`-Controller/Livewire + `MagicLoginLink`-Mailable | ✅ |
| 2.4 | Fortify-Login-View erweitern um "Login per E-Mail-Link" Toggle | ✅ |
| 2.5 | Middleware `SetCurrentPortal` + `CurrentPortalScope` (Global Scope) | ✅ `SetCurrentPortal`, `PortalScope`, `CurrentPortalContext` |
| 2.6 | Portal-Dropdown im Admin-Header (Flux UI Dropdown) | ✅ `admin.portal-switcher` in Sidebar |
| 2.7 | `GoLivePasswordReset`-Mailable + Command `auth:send-go-live-mails` (vor Go-Live, dry-runnable) | ✅ Command + Mailable vorbereitet; Versand bleibt P11 |
| 2.8 | Tests: Passwort-Login, Magic-Link-Flow (happy + expired + consumed), Portal-Scope | ✅ Auth-/MagicLink-/Access-Tests vorhanden |
**Exit:** Neue Nutzer können sich per Passwort + Magic-Link einloggen. Portal-Scope filtert Datensätze.
---
## Phase 3 – Admin-UI an Models binden (2–3 d) 🔴
Die Views existieren bereits als Blade/Livewire-Stubs (`resources/views/admin/*`, `resources/views/livewire/admin/*`). Aufgabe: **echte Daten-Queries + Actions** einbauen.
| # | Bereich | Aufgaben | Status |
|---|---|---|---|
| 3.1 | Dashboard | Widgets: Anzahl PMs, neue Companies, neue Newsletter-Abos | ✅ echte Counts + Review-Queue |
| 3.2 | Press Releases | Index, Create, Edit, Show, Publish/Reject-Actions, mehrere Bilder | ✅ 2026-05-04: CRUD/Workflow + Multi-Image-Upload mit Sortierung/Preview-Flag/Löschen via `livewire/components/press-release-images-manager` (gleicher Component für Admin + Customer), API generiert thumb/medium/large-Varianten |
| 3.3 | Categories | Index mit DE+EN-Translations, Create/Edit (Slug-Live-Preview) | ✅ 2026-05-04: Index, Create und Edit (DE+EN Slugs mit Live-Preview, Parent-Hierarchie + Loop-Schutz, Lösch-Schutz bei verknüpften PMs/Sub-Kategorien). Tests (8 Cases). |
| 3.4 | Companies | CRUD inkl. erweitertem Logo-Upload (Varianten) | ✅ 2026-05-04: Admin-Logo-Upload nutzt jetzt `ImageService::storeCompanyLogo()`, generiert sq/wide-Varianten und schreibt `logo_variants`. Logo-Entfernen-Flow inklusive. |
| 3.5 | Contacts | CRUD, Verknüpfung zu Company | ✅ |
| 3.6 | Users & Roles | CRUD + Role-Assign + Permission-Editor + Legacy-Archivrechnungen-Tab | ✅ Kern umgesetzt; Legacy-Rechnungs-Tab im Admin separat prüfen |
| 3.7 | Newsletter | Subscriber-Liste, Ex-/Import CSV | ⏸️ **Vertagt 2026-05-04** – wird nach dem Pressemitteilungs-Veröffentlichungs-Block angegangen. Aktuell vorhanden: Newsletter-Sync-UI für die Legacy-Maillisten. |
| 3.8 | Invoices (neu) | Liste, Detail, Stripe-Status, PDF-Download | ⬜ Admin-View noch Dummy; wartet auf P8 |
| 3.9 | **Legacy-Invoices (Archiv)** ⭐ | Read-only Liste, Filter, PDF-Download | ✅ 2026-05-04: Admin-Archivübersicht mit Suche, Portal-/Status-/Mapping-/PDF-Filtern, Hinweisen für unzugeordnete Legacy-User und on-demand PDF-Download |
| 3.10 | Payments | Liste (Filter), Detail | ⬜ Admin-View noch Dummy; wartet auf P8 |
| 3.11 | Footer-Codes | CRUD (blieb im Scope) | ✅ 2026-05-04: Schema (`footer_codes` + `category_footer_code`), Model + Factory, Admin-Volt (Index mit Suche/Filter, Create, Edit, Soft-Delete, Toggle-Active), Sidebar-Link, Tests (9 Cases). |
**Exit:** Alle CRUD-Screens funktionsfähig mit echten Daten. Promotions-Admin entfällt.
---
## Phase 4 – Customer-Portal ⭐ NEU (1.5–2 d) 🔴
**Ziel (D-11):** Endkunden (Company-User) können sich selbst bedienen – kein Admin nötig für tägliche Aufgaben.
> **Architektur-Update 2026-05-04 (Panel-Konsolidierung):** Customer-Bereich ist als "Mein Bereich" ins gemeinsame Admin-Panel überführt. URL-Prefix `/admin/me/*`, Routenamen `me.*`. Alte `/customer/*`-Pfade als 301-Redirects erhalten. Login-Redirect erfolgt rollenbasiert (Admin/Editor → `/dashboard`, Customer → `/admin/me`). Sidebar zeigt "Mein Bereich" auch Admin/Editor; Admin-Bereiche bleiben Admin/Editor vorbehalten.
| # | Aufgabe | Status |
|---|---|---|
| 4.1 | Route-Gruppe `me.*` unter `/admin/me/*` mit `auth:web` + `EnsureUserIsCustomer` (admin/editor/customer) | ✅ 2026-05-04: konsolidiert ins Admin-Panel; alte `customer.*`-Routen als 301-Redirects erhalten. |
| 4.2 | Dashboard (`Livewire\Customer\Dashboard`): aktive Abos, letzte eigene PMs, offene Rechnungen | 🔄 Dashboard + eigene PMs fertig; aktive Abos/offene neue Rechnungen warten auf P8 |
| 4.3 | Press Releases: Index, Create, Edit, Submit-for-Review (Status `review`) | ✅ |
| 4.4 | Rechnungen: neue + Legacy-Archivrechnungen einsehen, PDF-Download | 🔄 Legacy-Archiv + PDF-Download vorbereitet; neue Rechnungen warten auf P8 |
| 4.5 | API-Tokens: Self-Service Create/Revoke, Scope-Auswahl | ✅ |
| 4.6 | Company-/Contact-Profile pflegen (inkl. Logo-Upload) | ✅ Customer-Profile editierbar inkl. Logo-Upload mit Varianten |
| 4.7 | Passwort / 2FA / E-Mail ändern | ✅ `customer.security` mit Passwort-/Mail-Update + Fortify 2FA-Aktivierung/Recovery-Codes |
| 4.8 | Policies auf allen Aktionen (Customer darf nur eigenes sehen) | ✅ `PressReleasePolicy`, `CompanyPolicy`, `ContactPolicy`, `LegacyInvoicePolicy` |
| 4.9 | Livewire-Feature-Tests | ✅ Token + Legacy-Rechnungen + Profile/Security/Policies abgedeckt |
**Exit:** Ein Test-Customer kann per Magic-Link einsteigen, PM erstellen, Token generieren, Legacy-Rechnung ansehen.
---
## Phase 5 – Domain-Services & Geschäftslogik (2 d) 🟡
| # | Aufgabe | Status |
|---|---|---|
| 5.1 | `PressReleaseService` + `PublishPressRelease`/`RejectPressRelease`-Actions + Mailable | ✅ 2026-05-04: `PressReleaseService` mit Submit/Publish/Reject (mit Pflicht-Begründung)/BackToDraft/Archive/Auto-Reject bei Blacklist; **Audit-Log via `press_release_status_logs`** mit Quelle (admin/customer/blacklist) und Bearbeiter; rollenbasierte Mail-URLs (`me.*` für Customer, `admin.*` für Admin); Cache-Invalidation für Stats + Review-Counter. |
| 5.2 | `BlacklistService` (Wort-Prüfung im Text; Integration in Publish-Flow) | ✅ 2026-05-04: `App\Services\PressRelease\BlacklistService` + `BlacklistViolationException`, in `submitForReview()`/`publish()` integriert (auto-rejected + Mail an Autor), Wortliste in `config/blacklist.php` |
| 5.3 | `NewsletterService` neu (Subscribe/Confirm/Unsubscribe mit Token) | ⏸️ **Vertagt 2026-05-04** – wird nach dem Pressemitteilungs-Veröffentlichungs-Block angegangen. Heute genutzt: `NewsletterSyncService`/Null-Client für Legacy-Listen. |
| 5.4 | `CompanyService` (Aktivierung, Logo-Upload, Footer-Code-Management) | ⬜ |
| 5.5 | `ImageService` (Intervention Image, Varianten, Thumbnail-Redirect) | ✅ 2026-05-04: Logos + PressRelease-Bilder mit thumb/medium/large via Cover-Resize. Verwendet GD ohne neue Composer-Dep, Aufruf von Customer/Admin/API/Sync. Legacy-Thumbnail-Redirect noch offen, aber kein Blocker. |
| 5.6 | `SluggableTrait` für Models + Unique-per-Language-Logik | ✅ 2026-05-04: `App\Models\Concerns\HasUniqueSlug` mit konfigurierbarem Scope; PressRelease (`portal+language`) und Company (`portal`) nutzen ihn. Category bleibt aussen vor (Slugs in Translations). |
| 5.7 | Domain-Events + Listeners (`PressReleasePublished` → `SendPublishNotification`) | 🔄 Mailables vorhanden, keine dedizierten Domain-Events |
| 5.8 | Tests pro Service (Feature-Tests + Unit-Tests) | 🔄 PressRelease/Admin/API-Pfade abgedeckt; keine vollständige Service-Suite |
---
## Phase 6 – Daten-Migration ⭐ KERN (3–4 d) 🔴
**Ziel:** Alle relevanten Daten aus beiden Legacy-DBs in die neue DB – **als wiederholbares Skript** (D-18).
> **Stand 2026-04-28:** Kern-Import vollständig abgeschlossen ✅
| # | Aufgabe | Status |
|---|---|---|
| 6.1 | Legacy-Zugriff via direkte DB-Connection (kein Legacy-Model nötig) | ✅ |
| 6.2 | `legacy_import_map`-basierte Upsert-Logik (idempotent) | ✅ |
| 6.3 | `--source`, `--step`, `--dry-run`, `--force` auf Master-Command | ✅ |
| 6.4 | Import-Commands: `categories → users → companies → contacts → press_releases` (inkl. Bilder + Kontakt-Pivot) | ✅ beides Portale |
| 6.5 | `legacy:archive-invoices`: erster Archivlauf mit 864 Rechnungen in `legacy_invoices` | ✅ in 6.5d technisch erweitert |
| 6.5b | `legacy:fix-timestamps`: Historische Timestamps auf allen Entitäten korrekt | ✅ |
| 6.5c | `legacy:import --step=link-associations`: 16.968 User↔Kontakt-Links | ✅ |
| 6.5d | **Legacy-Rechnungen Vollimport**: alle bestehenden Rechnungen beider Portale inkl. Status, Beträgen, Datumsfeldern, Zahlart, vollständigem Raw-Snapshot, `pdf_payload` und User-Zuordnung importieren; PDF bleibt DB-basiert/on-demand auf Knopfdruck statt Datei-Archiv | ✅ 2026-05-04 |
| 6.6 | `legacy:grandfather-subscriptions`: aktive Alt-Abos übernehmen | ⬜ warten auf Kriterien |
| 6.7 | Konflikt-Handling-Reports | ✅ via Importer-Fehlerlog |
| 6.8 | Bilder-Transfer (Mediendateien) | ✅ 2026-05-04: `legacy:sync-company-logos` + `legacy:sync-press-release-images` decken beide Asset-Typen ab. Generiert Varianten via `ImageService`, idempotent, Dry-Run + Reports vorhanden. |
| 6.9 | `legacy:verify`: Konsistenz-Checks | ✅ 0 Fehler / 0 Warnungen |
| 6.10 | **Rehearsal-Lauf** gegen produktiven Snapshot auf Staging | ⬜ |
**Datenstand (2026-04-28):**
- Users: 66.150 | Companies: 107.836 | Contacts: 16.964 | PMs: 174.688 | Rechnungen: 864 im bisherigen Archivlauf
**Klarstellung Legacy-Rechnungen (2026-05-04):** `legacy:archive-invoices` übernimmt jede Legacy-Rechnung idempotent, erhält den Originalstatus unverändert und löst die Zuordnung zum neuen User über `legacy_import_map` auf. Unzuordenbare Rechnungen bleiben im Archiv sichtbar und werden im Import-Report gezählt. `pdf_payload` speichert zusätzlich Billing-Adresse und User-Payment-Snapshot für die DB-basierte PDF-Erzeugung. Neue Stripe-Rechnungen werden erst in P8 separat konzipiert.
**Exit:** Kompletter Import beider Portale auf Staging erfolgreich. Rehearsal-Lauf dokumentiert. `legacy:verify` grün.
---
## Phase 7 – API v1 (1.5 d) 🔴
| # | Aufgabe | Status |
|---|---|---|
| 7.1 | Analyse Legacy-Access-Logs (letzte 90 Tage), Priorisierung Endpoints | ✅ Tooling fertig; Legacy-App-Logs nicht vorhanden, Entscheidung über migrierte Daten/Codeprüfung getroffen |
| 7.2 | `routes/api.php`: Prefix `v1`, Middleware `api,auth:sanctum` | ✅ |
| 7.3 | Controller + Resources für: PressRelease (CRUD), PressReleaseImage (minimal), Company (read), Category (read), Newsletter (subscribe) | ✅ `newsletter/unsubscribe` bewusst aus P7 entfernt; neuer Newsletter-Dienst folgt später |
| 7.4 | Request-Validation via FormRequests | ✅ |
| 7.5 | Sanctum Token-Abilities (`press-releases:read|write`, `companies:read`, etc.) | ✅ |
| 7.6 | **Legacy-Cut-Handler** (410 Gone bei `api_key`-Param) | ✅ |
| 7.7 | Rate-Limiting pro Token | ✅ 60/min pro Sanctum-Token bzw. Bearer-Token-Fingerprint |
| 7.8 | API-Doku (Scribe oder OpenAPI YAML) | ✅ OpenAPI YAML unter `docs/api/v1.yml` + Route `/docs/api/v1` |
| 7.9 | Feature-Tests für alle Endpoints | ✅ Kern-Endpunkte + Images + Token-Freigabe + API-Usage-Logging abgedeckt |
| 7.10 | Legacy-API-Kundenkommunikation vorbereiten | ✅ Kundenreport fertig: `api:legacy-customers-report`; operative Mailtexte folgen außerhalb P7 |
---
## Phase 8 – Payment & Invoicing (Stripe, **neu gebaut**) (2.5 d) 🔴
| # | Aufgabe | Status |
|---|---|---|
| 8.1 | Stripe-Setup (`.env` Keys, Entscheidung Cashier vs. raw `stripe-php`) | ⬜ |
| 8.2 | `PaymentOption`-Admin mit neuen Produkten + Sync zu Stripe Products/Prices (Command `stripe:sync-products`) | ⬜ |
| 8.3 | Checkout-Flow (Livewire im Customer-Portal): Stripe Checkout Session, Rückkehr-URL | ⬜ |
| 8.4 | Webhook-Controller `/webhooks/stripe` + Event-Handler (`checkout.session.completed`, `invoice.paid`, `invoice.payment_failed`, `customer.subscription.deleted`) | ⬜ |
| 8.5 | `InvoiceNumberGenerator` (neuer Kreislauf `{YYYY}{MM}-{seq}`) | ⬜ |
| 8.6 | PDF-Blade + `InvoiceService` | ⬜ |
| 8.7 | Mahnwesen vereinfacht (`invoices:remind` nur für Stripe-Fails) | ⬜ |
| 8.8 | **Grandfathering-Handler** ⭐: `ExpireGrandfatheredSubscriptions`-Scheduler + Customer-Benachrichtigung | ⬜ |
| 8.9 | Legacy-Rechnungen Archiv-UI im Customer-/Admin-Bereich anbinden | ✅ Customer- und Admin-Archiv inkl. DB-basierter PDF-Erzeugung vorhanden |
| 8.10 | Tests mit Stripe-Test-Mode (Fake-Webhooks) | ⬜ |
---
## Phase 9 – Scheduler & Queues (0.5 d) 🟡
| # | Aufgabe | Status |
|---|---|---|
| 9.1 | Supervisor/Queue-Worker-Setup (Dev + Prod Docs) | ⬜ |
| 9.2 | `routes/console.php`: daily `invoices:remind`, `magic-links:purge`, `grandfather:expire`, monthly `newsletter:campaign-digest` (optional) | 🔄 `magic-links:purge` täglich und `press-releases:purge-drafts` wöchentlich vorhanden; P8/P5-Jobs offen |
| 9.3 | Failure-Alerting (Log-Channel + Mail bei fehlgeschlagenem Job) | ⬜ |
---
## Phase 10 – Testing & Härtung (1 d) 🔴
| # | Aufgabe | Status |
|---|---|---|
| 10.1 | Coverage-Ziel 70 % auf `app/Services`, `app/Actions` | ⬜ |
| 10.2 | Architecture-Tests (Pest `arch()`) | ⬜ |
| 10.3 | `php artisan test --parallel` grün | ⬜ |
| 10.4 | Manuelle QA-Checkliste (siehe [`06-FEATURES-SCOPE.md §15`](./06-FEATURES-SCOPE.md)) durchgehen | ⬜ |
| 10.5 | Security-Review: CSRF, CORS, Rate-Limits, File-Uploads, Magic-Link-TTL | ⬜ |
---
## Phase 11 – Deployment (1 d) 🔴
| # | Aufgabe | Status |
|---|---|---|
| 11.1 | Produktiv-DB-Server provisionieren, Backups konfigurieren | ⬜ |
| 11.2 | Staging-Deployment + Smoke-Tests | ⬜ |
| 11.3 | Produktiv-Import-**Rehearsal** gegen aktuellen Legacy-Snapshot | ⬜ |
| 11.4 | **Go-Live-Mailing**: Passwort-Reset + Sicherheitshinweis an alle User | 🔄 Command/Mailable fertig; Versand und finale Texte offen |
| 11.5 | **API-Kunden-Kommunikation** (Token-Migration) | 🔄 Kundenreport fertig; Freigabeliste und operative Mailtexte offen |
| 11.6 | DNS-Cutover-Plan: `pressekonto.de` auf neuen Server | ⬜ |
| 11.7 | Read-Only-Modus auf Legacy-Systemen während Final-Import | ⬜ |
| 11.8 | Abschalten der alten Symfony-Server (nach Review-Periode) | ⬜ |
---
## Aufwandsschätzung (grob, nach Scope-Schärfung)
| Phase | Aufwand (MT) |
|---|---|
| 0 | 0.25 (Rest) |
| 1 | 1.5 |
| 2 | 1.5 |
| 3 | 2.5 |
| 4 | 2.0 |
| 5 | 2.0 |
| 6 | 3.5 |
| 7 | 1.5 |
| 8 | 2.5 |
| 9 | 0.5 |
| 10 | 1.0 |
| 11 | 1.0 |
| **Σ** | **~19.75 MT** (∼4 Wochen Vollzeit) |
Grund für Erhöhung ggü. ursprünglich 16 MT: Customer-Portal (P4) ist neu, Billing ist komplett Neubau statt Migration, Daten-Migration aufwändiger wegen Wiederholbarkeits-Anforderung.
---
## Abhängigkeitsgraph
```
P0 → P1 → P2 ┬→ P3 ┐
├→ P4 ┤
│ ├→ P5 ┐
│ │ ├→ P7 ┐
│ │ │ ├→ P10 → P11
│ │ ├→ P8 ┘
│ │ │
│ └→ P6 ┘
│
└→ P9
```
P6 (Daten-Migration) ist der kritische Pfad – kann parallel zu P3–P5 auf einem Staging-Branch vorbereitet werden, blockiert aber P10/P11.

View file

@ -0,0 +1,441 @@
# 04 – Datenmodell (Mapping Doctrine → Eloquent)
Dieses Dokument beschreibt das **neue DB-Schema** und wie die Legacy-Tabellen dorthin gemappt werden. Ziel: Saubere Laravel-Konventionen, `portal`-Scoping und Legacy-IDs für Imports.
## Gemeinsame Konventionen
- Jede Tabelle hat: `id` (BIGINT UNSIGNED AI), `created_at`, `updated_at`.
- Fachliche Entitäten haben: `deleted_at` (Soft-Delete).
- Multi-Portal-Entitäten haben zusätzlich: `portal` (ENUM `presseecho`, `businessportal24`, `both` – `both` nur für Stammdaten).
- Importierte Datensätze haben: `legacy_portal` (ENUM), `legacy_id` (BIGINT), UNIQUE `(legacy_portal, legacy_id)`.
- FK-Spalten: `*_id` BIGINT UNSIGNED, constrainted, i.d.R. `ON DELETE` wie in Legacy.
- UUIDs nur dort, wo externe Referenzen nötig (z.B. öffentliche PM-Links): Spalte `uuid`, indiziert.
---
## 1. Users & Profil
### `users` (erweitert das bestehende Schema)
| Spalte | Typ | Legacy-Feld | Bemerkung |
|---|---|---|---|
| id | BIGINT PK | — | Neu |
| name | VARCHAR(120) | — | Display-Name |
| email | VARCHAR(190) UNIQUE | `sfGuardUserProfile.email` | |
| email_verified_at | TIMESTAMP | – | |
| password | VARCHAR(255) NULL | – | bcrypt; **null erlaubt** (Magic-Link-Only-User wie viele Companies) |
| remember_token | VARCHAR(100) | sfGuardRememberKey | |
| two_factor_* | (bestehend) | – | Fortify |
| registration_type | ENUM('agency','company','apiuser','existing_*') | `sfGuardUserProfile.registration_type` | |
| language | CHAR(2) DEFAULT 'de' | `sfGuardUserProfile.language` | |
| portal | ENUM('presseecho','businessportal24','both') | – | Standard Portal |
| is_active | BOOL DEFAULT 1 | `sfGuardUser.is_active` | |
| is_super_admin | BOOL DEFAULT 0 | `sfGuardUser.is_super_admin` | nur Migrationshilfe; später via Spatie-Rolle |
| last_login_at | TIMESTAMP | `sfGuardUser.last_login` | |
| last_login_ip | VARCHAR(45) | `sfGuardUser.ip_address` | |
| gdpr_consent_at | TIMESTAMP NULL | – | Neu, für Consent-Tracking |
| legacy_portal | ENUM | – | Import-Spur |
| legacy_id | BIGINT | `sfGuardUser.id` | |
| deleted_at | TIMESTAMP | – | |
> **Nicht übernommen (D-09):** Legacy-Password-Hash (`sha1+salt`) und Legacy-`api_key` wandern **nicht** in die neue `users`-Tabelle. Bei Go-Live bekommen alle User eine Reset-Mail; API-Clients müssen sich neuen Sanctum-Token generieren.
### `profiles` (1:1 zu User, entspricht Rest von `sfGuardUserProfile`)
| Spalte | Typ | Legacy-Feld |
|---|---|---|
| id, user_id (UK) | – | `user_id` |
| salutation_key | VARCHAR(20) | `salutation_id` → Config-Key (z.B. `mr`,`mrs`) |
| title | VARCHAR(80) | `title` |
| first_name, last_name | VARCHAR(80) | |
| phone | VARCHAR(40) | |
| address | VARCHAR(1000) | |
| country_code | CHAR(2) | `country_id` → ISO Code aus Config |
| birthdate | DATE | |
| backlink_url | VARCHAR(255) | |
| show_stats | BOOL | |
| validation_date | TIMESTAMP | |
| contract_date | TIMESTAMP | |
| validate_token | VARCHAR(32) | `validate` |
| tax_id_number | VARCHAR(255) | |
| tax_exempt | BOOL | |
| tax_exempt_reason | VARCHAR(1000) | |
| disable_footer_code | BOOL | |
| timestamps | | |
### Rollen/Rechte (Spatie)
- Tabellen: `roles`, `permissions`, `model_has_roles`, `model_has_permissions`, `role_has_permissions` (von Spatie).
- Initial-Rollen: `admin`, `editor`, `customer`, `api-only`.
- Migration: `sfGuardGroup` → Mapping auf neue Rollen (nicht 1:1!), `sfGuardPermission` → Permissions.
### `magic_links` ⭐ NEU (D-10)
| Spalte | Typ | Bemerkung |
|---|---|---|
| id | BIGINT PK | |
| user_id | FK `users.id` | |
| token_hash | CHAR(64) UNIQUE | sha256 des ausgegebenen Tokens |
| purpose | ENUM('login','press_release_access','invoice_access','initial_setup') | |
| payload | JSON NULL | z. B. `{"press_release_id": 42}` |
| expires_at | TIMESTAMP | TTL typisch 15 min |
| consumed_at | TIMESTAMP NULL | Single-Use |
| ip_requested | VARCHAR(45) | |
| ip_consumed | VARCHAR(45) NULL | |
| timestamps | | |
Scheduler-Job `PurgeExpiredMagicLinks` räumt alte/konsumierte Einträge > 30 Tage auf.
---
## 2. Companies & Contacts
### `companies`
| Spalte | Typ | Legacy (`Company`) |
|---|---|---|
| id | BIGINT PK | |
| portal | ENUM | – |
| owner_user_id | BIGINT NULL | `user_id` |
| type | ENUM('agency','company') DEFAULT 'company' | `ctype` |
| name | VARCHAR(255) | |
| slug | VARCHAR(255) UNIQUE WITHIN portal | (Sluggable) |
| address | VARCHAR(1000) | |
| country_code | CHAR(2) | `country_id` |
| phone | VARCHAR(40) | |
| fax | VARCHAR(40) | |
| email | VARCHAR(190) | |
| website | VARCHAR(190) | |
| logo_path | VARCHAR(255) | `logo` (Legacy, minimal) |
| logo_variants | JSON NULL | – (neu, D-05: `{sq,wide,dark}`) |
| is_active | BOOL DEFAULT 1 | |
| disable_footer_code | BOOL | |
| legacy_portal, legacy_id | | |
| timestamps, deleted_at | | |
### `company_user` (Pivot)
| user_id (PK) | company_id (PK) | role ENUM('member','responsible','owner') |
→ Vereinigt `CompanyUser` + `ResponsibleCompanyUser` in eine Pivot-Tabelle mit `role`.
### `contact_user` (Pivot)
| user_id (PK) | contact_id (PK) |
→ Direkte User-Kontakt-Zuordnung für den Admin- und Customer-Kontext. Im Legacy-System wurde diese Zugehörigkeit indirekt aus `company_user`/`responsible_company_user` plus `contacts.company_id` abgeleitet. Der Import befüllt die Tabelle über `legacy:import --step=link-associations` idempotent aus bestehenden Firmenzuordnungen.
### `contacts`
| Spalte | Typ | Legacy (`Contact`) |
|---|---|---|
| id | BIGINT | |
| company_id | FK `companies.id` | |
| salutation_key | VARCHAR(20) | `salutation_id` |
| title | VARCHAR(80) | |
| first_name, last_name | VARCHAR(80) | |
| responsibility | VARCHAR(255) | |
| phone, fax | VARCHAR(40) | |
| email | VARCHAR(190) | |
| legacy_portal, legacy_id | | |
| timestamps, deleted_at | | |
---
## 3. Kategorien
### `categories`
| id | parent_id NULL | portal | is_active | legacy_*, timestamps |
### `category_translations`
| id | category_id FK | locale CHAR(2) | name VARCHAR(255) | slug VARCHAR(255) | description TEXT |
| UNIQUE (category_id, locale) |
---
## 4. Pressemitteilungen
### `press_releases`
| Spalte | Typ | Legacy (`PressRelease`) |
|---|---|---|
| id | BIGINT | |
| uuid | CHAR(36) UNIQUE | – neu für öffentliche Links |
| portal | ENUM | – |
| user_id | FK | |
| company_id | FK NULL | |
| category_id | FK | |
| language | CHAR(2) | |
| title | VARCHAR(255) | |
| slug | VARCHAR(255) | (unique: portal+language+slug) |
| text | MEDIUMTEXT | |
| backlink_url | VARCHAR(255) | |
| keywords | VARCHAR(255) | |
| status | ENUM('draft','review','published','rejected','archived') | `status` string |
| hits | INT UNSIGNED DEFAULT 0 | |
| teaser_begin | INT UNSIGNED | |
| teaser_end | INT UNSIGNED | |
| no_export | BOOL | |
| published_at | TIMESTAMP NULL | – neu |
| legacy_portal, legacy_id | | |
| timestamps, deleted_at | | |
### `press_release_images` (erweitert, D-05)
Spalten: `id`, `press_release_id FK`, `disk VARCHAR(30) DEFAULT 'public'`, `path VARCHAR(512)`, `variants JSON` (thumb/medium/large), `title VARCHAR(120)`, `description VARCHAR(500)`, `copyright VARCHAR(255)`, `is_preview BOOL`, `sort_order INT UNSIGNED`, `width INT`, `height INT`, `mime VARCHAR(60)`, `legacy_*`, `timestamps`, `deleted_at`.
Legacy war minimalistisch – künftig dürfen PMs mehrere, größere Bilder enthalten (kein hartes Limit, UI-Limit konfigurierbar).
### `press_release_contact` (Pivot, wie Legacy `PressReleaseContact`)
| press_release_id | contact_id |
---
## 5. Newsletter
### `newsletter_subscriptions`
| Spalte | Typ | Legacy (`NewsletterSubscription`) |
|---|---|---|
| id | | |
| portal | ENUM | – |
| user_id | FK NULL | `user_id` |
| salutation_key | VARCHAR(20) | |
| first_name, last_name | VARCHAR(80) | |
| email | VARCHAR(190) (unique per portal) | |
| ip_address | VARCHAR(45) | |
| is_confirmed | BOOL | `is_active` |
| confirmation_token | VARCHAR(32) | `validate` |
| subscribed_at | TIMESTAMP | `subscribe_date` |
| unsubscribed_at | TIMESTAMP | `unsubscribe_date` |
| legacy_portal, legacy_id | | |
| timestamps | | |
---
## 6. Blacklist / FooterCode
### `blacklists`
| id | portal | title VARCHAR(255) | content MEDIUMTEXT | timestamps |
### `footer_codes` (bleibt, Q-08b)
| id | portal | language CHAR(2) NULL | title VARCHAR(255) | content TEXT | is_global BOOL | timestamps |
### `category_footer_code` (Pivot)
| footer_code_id | category_id |
> **Entfällt (D-14):** `promotion_links`, `promotion_link_category` – werden nicht migriert und nicht neu angelegt.
---
## 7. Billing & Payment (komplett neu, D-03/D-12/D-13)
**Wichtig:** Legacy-PaymentOptions, -UserPaymentOptions, -UserPayments und -Coupons werden **nicht 1:1 in die neue Billing-Logik migriert**. Nur *aktive* Subscriptions werden als "grandfathered" übernommen. Legacy-Rechnungen werden dagegen vollständig als read-only Archivdaten importiert (`legacy_invoices`) und nicht in den neuen Rechnungskreis (`invoices`) umgebucht.
### `billing_addresses` (entspricht `UserBillingAddress`)
| id | user_id FK UNIQUE | salutation_key | title | name | address1 | address2 | postal_code | city | country_code | timestamps |
### `invoice_billing_addresses`
Wie `billing_addresses`, aber ohne `user_id`-FK; dafür Snapshot zum Zeitpunkt der Rechnung.
### `payment_options` (neu definiert)
| Spalte | Typ | Bemerkung |
|---|---|---|
| id | BIGINT PK | |
| article_number | VARCHAR(60) UNIQUE | Interne Artikelnummer |
| type | ENUM('recurring','onetime') | Stripe-Subscription oder One-Off |
| price_cents | INT UNSIGNED | |
| currency | CHAR(3) DEFAULT 'EUR' | |
| interval | ENUM('monthly','yearly','once') NULL | Für Recurring |
| is_hidden | BOOL | |
| stripe_product_id | VARCHAR(60) | |
| stripe_price_id | VARCHAR(60) | |
| timestamps, deleted_at | | |
### `payment_option_translations` (DE/EN)
| id | payment_option_id FK | locale | name | description |
### `user_payment_options` (aktive Abos)
| Spalte | Typ | Bemerkung |
|---|---|---|
| id | BIGINT PK | |
| user_id | FK | |
| payment_option_id | FK | |
| status | ENUM('active','past_due','cancelled','grandfathered') | |
| **grandfathered_until** | DATE NULL | ⭐ D-13: Alt-Konditionen bis zu diesem Datum |
| legacy_conditions | JSON NULL | Snapshot alter Konditionen für Grandfathering |
| current_period_start | DATE | |
| current_period_end | DATE | |
| stripe_subscription_id | VARCHAR(60) NULL | |
| cancelled_at | TIMESTAMP NULL | |
| timestamps | | |
### `user_payment_option_company` (Pivot)
| user_payment_option_id | company_id | is_active | timestamps |
### `user_payments` (nur noch Stripe-Charges)
| id | user_payment_option_id FK NULL | amount_cents | currency | status ENUM('pending','succeeded','failed','refunded') | stripe_charge_id | stripe_invoice_id NULL | timestamps |
### `invoices` (neuer Rechnungskreis)
| Spalte | Bemerkung |
|---|---|
| id | |
| user_id FK | |
| user_payment_id FK NULL | |
| invoice_billing_address_id FK | Snapshot |
| number VARCHAR(40) UNIQUE | neue Nummernkreis, z. B. `{YYYY}{MM}-{seq}` |
| status | ENUM('open','paid','void','uncollectible') (Stripe-nah) |
| amount_cents / tax_cents / total_cents | |
| currency CHAR(3) | |
| is_netto BOOL | |
| invoice_date / due_date / paid_at | |
| stripe_invoice_id VARCHAR(60) NULL | |
| pdf_path | |
| timestamps, deleted_at | |
> **Entfällt:** `payment_method`-Enum – es gibt nur noch Stripe. Keine Rechnungszahlungs-Variante mehr.
### `legacy_invoices` ⭐ ARCHIV (D-12)
Separate, read-only Archivtabelle mit allen Rechnungen aus beiden Legacy-Portalen. Ziel ist der vollständige Import der vorhandenen Rechnungsdaten inkl. Status und User-Zuordnung; die PDF-Erzeugung für Legacy-Rechnungen bleibt DB-basiert und erfolgt bei Abruf/on demand.
| Spalte | Typ | Legacy-Quelle |
|---|---|---|
| id | BIGINT PK | (neu) |
| legacy_portal | ENUM | — |
| legacy_id | BIGINT | `invoice.id` |
| user_id | FK `users.id` NULL | Via Import-Map auflösbar |
| legacy_user_id | BIGINT | Original-User-ID im Legacy |
| number | VARCHAR(40) | Original-Rechnungsnummer |
| amount_cents / tax_cents / total_cents | | |
| status | VARCHAR(40) | Original-Status-String |
| invoice_date / due_date / paid_at | | |
| payment_method | VARCHAR(40) | Nur als Info (SPK, PayPal, Invoice …) |
| pdf_path | VARCHAR(512) NULL | Optionaler Cachepfad, falls ein generiertes PDF abgelegt wird; nicht primäre Quelle |
| raw_snapshot | JSON | Vollständige Original-Zeile bzw. alle für die Legacy-PDF-Erzeugung nötigen Daten für Audit/Rendering |
| pdf_payload | JSON NULL | Snapshot für PDF-Erzeugung (`invoice`, `invoice_billing_address`, `user_payment`) |
| pdf_generated_at | TIMESTAMP NULL | Zeitpunkt der letzten on-demand PDF-Erzeugung |
| imported_at | TIMESTAMP | |
UNIQUE `(legacy_portal, legacy_id)`.
**DB-Vorbereitung vor Go-Live:** `pdf_payload` enthält die für die on-demand PDF-Erzeugung nötigen Legacy-Snapshots. Der neue `invoices`-Kreislauf bleibt davon getrennt.
> **Coupons vertagt (D-16):** Keine Tabelle in Phase 1. Wird später, ggf. direkt als Stripe-Coupons, nachgezogen.
---
## 8. Import-Tracking
### `legacy_import_map`
| Spalte | Typ | Zweck |
|---|---|---|
| id | BIGINT | |
| legacy_portal | ENUM('presseecho','businessportal24') | |
| legacy_table | VARCHAR(80) | z. B. `sf_guard_user`, `press_release` |
| legacy_id | BIGINT | |
| target_table | VARCHAR(80) | |
| target_id | BIGINT | |
| imported_at | TIMESTAMP | |
| UNIQUE (legacy_portal, legacy_table, legacy_id) | | |
Diese Tabelle erlaubt idempotente Imports und Rückverfolgung.
---
## 9. Tabellen-Übersicht (Total)
| # | Tabelle | Quelle / Herkunft |
|---|---|---|
| 1 | users | sfGuardUser + sfGuardUserProfile (teilw.) |
| 2 | profiles | sfGuardUserProfile |
| 3 | magic_links | NEU (D-10) |
| 4 | roles / permissions / … | Spatie |
| 5 | companies | Company + Agency |
| 6 | company_user | CompanyUser + ResponsibleCompanyUser |
| 7 | contacts | Contact |
| 8 | categories | Category |
| 9 | category_translations | Category_Translation |
| 10 | press_releases | PressRelease |
| 11 | press_release_images | PressReleaseImage (erweitert) |
| 12 | press_release_contact | PressReleaseContact |
| 13 | newsletter_subscriptions | NewsletterSubscription (nur Daten, Workflow neu) |
| 14 | blacklists | Blacklist |
| 15 | footer_codes, category_footer_code | FooterCode (bleibt) |
| 16 | billing_addresses | UserBillingAddress |
| 17 | invoice_billing_addresses | InvoiceBillingAddress |
| 18 | payment_options | NEU (nicht aus Legacy) |
| 19 | payment_option_translations | NEU |
| 20 | user_payment_options | Teil-Übernahme aktiver Abos (Grandfathering) |
| 21 | user_payment_option_company | s.o. |
| 22 | user_payments | NEU (Stripe-Charges) |
| 23 | invoices | NEU (Stripe-getriebener Rechnungskreis) |
| 24 | **legacy_invoices** | ⭐ ARCHIV aller alten Rechnungen |
| 25 | legacy_import_map | NEU (technisch) |
**Raus aus der DB, rein in Config/Code:**
| Legacy-Tabelle | Ziel |
|---|---|
| `Country` | `config/countries.php` |
| `Salutation` + `Salutation_Translation` | `config/salutations.php` |
**Ganz raus / entfallen:**
| Legacy-Tabelle | Grund |
|---|---|
| `ApiUser` | Duplikat zu `users.registration_type = apiuser` / Rolle `api-only` |
| `sfGuardRememberKey` | Laravel `remember_token`-Spalte reicht |
| `sfGuardForgotPassword` | Fortify löst Reset selbst |
| `PromotionLink` / `PromotionLinkCategory` | Feature entfällt (D-14) |
| `Coupon` | Vertagt (D-16) |
| `PaymentOption_ExcludeGroup` / `_AccessGroup` | Ersetzt durch Spatie-Permissions + direkte Product-Whitelist |
| `Invoice` (als aktive Tabelle) | Ersetzt durch neuen Rechnungskreis + `legacy_invoices`-Archiv |
---
## 10. Indizes & Performance
- `press_releases`: Index `(portal, status, published_at DESC)` für Public-Listing; Unique `(portal, language, slug)`.
- `press_releases`: Index `uuid` (öffentliche Share-Links).
- `newsletter_subscriptions`: Unique `(portal, email)`.
- `magic_links`: Index `(user_id, purpose, consumed_at)`; Unique `token_hash`.
- `invoices`: Index `(user_id, invoice_date)`, `stripe_invoice_id`.
- `legacy_invoices`: Unique `(legacy_portal, legacy_id)`; Index `(user_id, invoice_date DESC)`.
- `legacy_import_map`: Unique-Index siehe oben.
---
## 11. Migrationen (Reihenfolge)
1. `users` (Alterung der bestehenden Tabelle durch Zusatz-Spalten, `password` NULLABLE)
2. `profiles`, `billing_addresses`
3. `magic_links`
4. Spatie-Migrationen (+ Seeder für Rollen `admin`, `editor`, `customer`, `api-only`)
5. `companies`, `company_user`, `contacts`
6. `categories`, `category_translations` (inkl. EN-Seed)
7. `blacklists`, `footer_codes`, `category_footer_code`
8. `press_releases`, `press_release_images`, `press_release_contact`
9. `newsletter_subscriptions`
10. `payment_options`, `payment_option_translations`
11. `user_payment_options`, `user_payment_option_company`, `user_payments`
12. `invoice_billing_addresses`, `invoices`
13. `legacy_invoices` (Archiv)
14. `legacy_import_map`
> Separate Phase (Phase 6): Import-Commands füllen diese Tabellen aus den Legacy-DBs.

View file

@ -0,0 +1,325 @@
# 05 – Zusammenführung der zwei Legacy-Datenbanken
Beide Legacy-Portale (`presseecho` und `businessportal24`) haben **strukturell nahezu identische** Datenbanken. Sie werden in **eine Ziel-DB** konsolidiert.
## Ist-Stand Dumps (2026-04-23)
| Portal | Datei | Größe | Tabellen |
|---|---|---|---|
| businessportal24 | `dev/migration 2026/sql/businessportal24_2026-04-23.sql` | 578 MB | 42 |
| presseecho | `dev/migration 2026/sql/presseecho_2026-04-23.sql` | 369 MB | 43 |
**Strukturelle Unterschiede** zwischen den beiden Dumps:
- Nur **businessportal24** hat: `press_release_image_old` (legacy Altbestand – ignorierbar, nur Archivwert)
- Nur **presseecho** hat: `category_pe_data`, `press_release_pe_data` (zusätzliche PE-spezifische Felder)
Die 41 gemeinsamen Tabellen haben identische DDL – Import kann mit einem Code-Pfad laufen (Portal als Parameter).
---
## 1. Grundprinzipien
1. **Die Ziel-DB ist NEU.** Wir kopieren keine Tabellen 1:1. Stattdessen werden Daten transformiert eingespielt (siehe [`04-DATA-MODEL.md`](./04-DATA-MODEL.md)).
2. **Legacy-IDs bleiben nachvollziehbar** via `legacy_portal` + `legacy_id` auf jedem Datensatz + zentraler `legacy_import_map`.
3. **Kollisionen werden deterministisch aufgelöst** (siehe §3).
4. **Importe sind idempotent** – wiederholbare Skripte, bei erneutem Lauf passiert nichts doppeltes.
5. **Wiederholbar** (D-18): Das gesamte Import-Skript muss sowohl gegen die Test-Dumps als auch am Go-Live-Tag gegen den dann aktuellen Produktivstand laufen können.
6. **Dry-Run zuerst**, Produktiv-Run danach mit Snapshot-Backup vorher.
---
## 2. Konnektivität
In `config/database.php` drei Connections:
```php
'connections' => [
'mysql' => [
// Ziel-DB
],
'legacy_presseecho' => [
'driver' => 'mysql',
'host' => env('LEGACY_PE_DB_HOST'),
'database' => env('LEGACY_PE_DB_NAME'),
'username' => env('LEGACY_PE_DB_USER'),
'password' => env('LEGACY_PE_DB_PASS'),
'charset' => 'utf8mb4',
'prefix' => '',
'strict' => false,
],
'legacy_businessportal' => [
// analog
],
],
```
### Datenquellen-Modi (wichtig für Wiederholbarkeit)
Das Import-Skript unterstützt drei Modi – alle liefern dieselben Ergebnisse:
| Modus | Quelle | Einsatz |
|---|---|---|
| **`dump`** | Import der SQL-Dumps in lokale MySQL-Schemas `legacy_pe_snapshot` / `legacy_bp_snapshot`, dann darauf lesen | Lokal + CI |
| **`direct`** | Read-Only-Verbindung zu den Produktiv-/Staging-Legacy-DBs | Test-Server + Go-Live-Tag |
| **`fixture`** | Kleine Pest-Fixtures | Automatisierte Tests |
Artisan-Flag: `--source=dump|direct|fixture`.
---
## 3. Kollisions-Handling
Beide DBs haben z.B. `sfGuardUser.id = 1`. Regeln:
### 3.1 User-Konflikt via E-Mail
Wenn in beiden Portalen die **gleiche E-Mail** existiert:
| Fall | Handling |
|---|---|
| Gleiche E-Mail, gleicher User | → **Zusammenführung zu einem User**. Neuer User hat `portal = both`, behält beide `legacy_import_map`-Einträge. Daten (Companies, PMs) aus beiden Seiten hängen am selben User. |
| Gleiche E-Mail, offensichtlich andere Person | → **Report + manuelle Entscheidung**. Standard: erst `presseecho` importieren; Konflikt im BP24-Import wird auf eine Konflikt-Liste gesetzt, die der Auftraggeber freigibt. |
| Unterschiedliche E-Mails | → Zwei separate User, `portal = presseecho` bzw. `businessportal24`. |
### 3.2 Company-Konflikt via `name`
- Wenn gleicher Name + zusammengeführter User → Merge (`portal = both`).
- Sonst zwei separate Companies; Slug per Portal-Suffix dedupen (`acme-ag`, `acme-ag-bp24`).
### 3.3 Press-Release- und andere Entitäten
- Unique-Constraints werden auf `(legacy_portal, legacy_id)` relaxed; neuer Slug ggf. mit Portal-Präfix bei Kollision.
- Alle FKs werden via `legacy_import_map` neu aufgelöst.
---
## 4. Import-Ablauf (High-Level)
> **Ist-Stand 2026-04-29:** Implementiert ist der Master-Command `legacy:import` mit `--source=presseecho|businessportal24|all` und `--step=categories|users|companies|contacts|press-releases|link-associations|all`. Die früher geplanten Commands `legacy:import-all` und `legacy:images-copy` existieren aktuell nicht als produktive Commands; Medienübernahme bleibt offen.
```bash
# 0. Leere Ziel-DB (frische Migration)
php artisan migrate:fresh --seed
# 1. Dry-Run (Reports ohne Writes)
php artisan legacy:import --source=all --dry-run
php artisan legacy:archive-invoices --dry-run
# 2. Reports prüfen (Konflikte, fehlende FKs, Counts)
php artisan legacy:verify --no-report
# 3. Echter Import
php artisan legacy:import --source=presseecho --step=categories --force
php artisan legacy:import --source=all --step=users --force
php artisan legacy:import --source=presseecho --step=companies --force
php artisan legacy:import --source=businessportal24 --step=companies --force
php artisan legacy:import --source=presseecho --step=contacts --force
php artisan legacy:import --source=businessportal24 --step=contacts --force
php artisan legacy:import --source=presseecho --step=press-releases --force
php artisan legacy:import --source=businessportal24 --step=press-releases --force
php artisan legacy:import --step=link-associations --force
php artisan legacy:archive-invoices
php artisan legacy:fix-timestamps
# 4. Verifikation
php artisan legacy:verify
# 5. Bilder-/Medien-Transfer
# Noch offen: finaler Scope/Command
# 6. Final-QA auf Staging
```
### Master-Command `legacy:import`
Ruft die Importer in der richtigen Reihenfolge auf, wenn `--step=all` genutzt wird. Jeder Importer ist selbst idempotent (via `legacy_import_map`).
```
legacy:import
├─ categories (+Translations DE/EN)
├─ users (+Profile + Rollen)
├─ companies (+company_user)
├─ contacts
├─ press-releases (+press_release_images + press_release_contact)
└─ link-associations (contact_user aus Firmenzuordnungen)
```
Ergänzende Commands:
```
legacy:archive-invoices (→ legacy_invoices)
legacy:fix-timestamps
legacy:verify
```
Noch offen bzw. blockiert:
- `legacy:grandfather-subscriptions` für aktive Alt-Abos (Kriterien fehlen)
- Medien-/Bilddatei-Transfer
Reihenfolge relevant wegen FKs; jede Subtabelle nutzt `legacy_import_map`, um bereits importierte Datensätze zu überspringen oder zu aktualisieren (Upsert).
---
## 5. Pro Entität: Transformationsregeln
### 5.1 Users
| Legacy-Feld | Ziel | Transform |
|---|---|---|
| `sfGuardUser.username` | `users.name` | Display-Name; wenn `username` wie Mail aussieht + `email`-Feld leer → `users.email` |
| `sfGuardUser.password` + `.salt` | – | **Wird nicht übernommen (D-09)**. `users.password = null`. |
| `sfGuardUserProfile.email` | `users.email` | `lower()`, trim; UNIQUE-Check mit Konflikt-Handling §3.1 |
| `is_active`, `is_super_admin`, `last_login`, `ip_address` | `users.*` | 1:1 |
| `api_key` | – | **Nicht übernommen (D-05)**. User muss neuen Sanctum-Token erzeugen. |
| Rolle | Spatie | `sfGuardGroup` → Mapping auf `admin` / `editor` / `customer` / `api-only` (konfigurierbare Mapping-Tabelle im Command) |
Profilfelder in `profiles` anlegen (FK `user_id`).
### 5.2 Companies
1. Alle Companies aus beiden DBs lesen.
2. Gleichnamig + selber Owner → Merge (`portal = both`).
3. Sonst separate Company, Slug ggf. suffixen.
4. `CompanyUser` + `ResponsibleCompanyUser` in `company_user`-Pivot mit `role`.
5. Logo: alte Datei kopieren in `storage/app/public/companies/{id}/logo-original.{ext}`, Varianten (`sq`, `wide`, `dark`) werden beim ersten Admin-Aufruf erzeugt (lazy).
### 5.3 Press Releases
- Owner-User via `legacy_import_map` auflösen.
- `portal`-Spalte = Quell-Portal.
- Slug: gleiche Regel wie Legacy, aber Unique auf `(portal, language, slug)`.
- `text`: identisch übernehmen; Mojibake-Check (UTF-8 vs. Latin-1) einbauen.
- `status`-String in neues Enum mappen (`'published' → published`, `''|null → draft`, `'rejected' → rejected`).
- `uuid` neu generieren (für öffentliche Share-Links).
- `published_at` aus `created_at` ableiten, falls `status=published`.
### 5.4 Images
- DB-Record erst nach Datei-Transfer anlegen (Atomicität).
- Pfad nach neuem Schema: `press-releases/{id}/{original-name}.ext`.
- Alte Slug-Thumbnails werden NICHT kopiert – neu generiert on upload.
- Redirect-Middleware sorgt für alte `/thumbnails/...`-URLs.
### 5.5 Rechnungen ⭐ ARCHIV (D-12)
**Keine operative Migration in den neuen Rechnungskreis.** Alle Legacy-Rechnungen kommen vollständig in die read-only Archivtabelle `legacy_invoices`:
- Command `legacy:archive-invoices` importiert alle Rechnungen beider Portale idempotent.
- Jede Legacy-Zeile → ein `legacy_invoices`-Datensatz inkl. Status, Betrag, Datumsfeldern, Zahlart, User-Zuordnung, `raw_snapshot` und `pdf_payload` als JSON.
- Für PDF-Abruf bleibt die Legacy-Logik erhalten: PDF wird aus den importierten DB-Daten bei Bedarf generiert. Es wird kein verpflichtender PDF-Dateibestand migriert; `pdf_path` ist höchstens Cache/Export.
- Import-Report: Counts pro Portal, Statusverteilung, Summenvergleich, unzugeordnete `legacy_user_id`.
- Kein Mahnwesen, keine Statuswechsel.
Vor dem Go-Live-Rehearsal muss der Report gegen den aktuellen Produktiv-Snapshot geprüft werden. Unzugeordnete Rechnungen sind erlaubt, müssen aber fachlich bewertet werden.
### 5.6 Payment ⭐ NEU + GRANDFATHERING (D-13)
> **Umgesetzt 2026-06-12** mit präzisierten Kriterien des Auftraggebers:
> Quelle der Aktiv-Erkennung ist **ausschließlich das Rechnungsarchiv**
> (`legacy_invoices`, D-12) — nicht die Legacy-Payment-Tabellen direkt.
> Command: `legacy:grandfather-subscriptions` (idempotent, Replay-fähig).
- **Legacy-PaymentOptions werden nicht übernommen.** Neue Produkte werden vom Auftraggeber definiert und als Stripe-Prices angelegt. Für die Grandfathered-Vereinbarungen entstehen versteckte Katalog-Platzhalter (`payment_options.article_number = LEGACY-{PE|BP}-{Artikel}`, `is_hidden = true`); die verbindlichen Beträge liegen pro Vereinbarung in `legacy_conditions`.
- **Aktiv-Regel** (aus dem Archiv abgeleitet): jüngste Rechnung pro (Portal, Legacy-`user_payment_option`) mit `pdf_payload.payment_option.type = 'recurring'` und `pdf_payload.user_payment_option.status = 'active'`; `next_due_date` darf höchstens `--grace-months` (Default 12) überfällig sein, sonst gilt die Vereinbarung als stale und bleibt reines Archiv. Einmal-Käufe (`type = single`) werden nie übernommen.
- **Übernahme** als `grandfathered` in `user_payment_options`:
- `status = 'grandfathered'`, `grandfathered_until = legacy.valid_until_date` (nullable), `stripe_subscription_id = null`.
- `current_period_start = service_period_begin_date` der jüngsten Rechnung, `current_period_end = next_due_date` → der tägliche MAN-Kreis-Lauf (`billing:generate-manual-invoices`) stellt die nächste Rechnung zum gewohnten (jährlichen) Rhythmus aus.
- **Beträge (Klarstellung 12.06.):** Legacy fakturierte **brutto** (Steuer inkludiert); steuerbefreite Kunden erhielten den Netto-Ausweis (`is_netto`). Die Migration leitet daraus die **Netto-Vertragsbasis** ab (`legacy_conditions.net_cents`; brutto ÷ 1,19 bzw. Netto-Betrag direkt). Die Steuer bestimmt der `VatResolver` pro Rechnung neu: DE immer mit Steuer, EU nur mit gültiger USt-ID befreit (Reverse Charge), Drittland befreit — für deutsche Bestandskunden bleibt der Bruttobetrag unverändert, die Steuer wird künftig sauber ausgewiesen (`invoices.tax_note` bei Befreiung).
- **Kein** Stripe-Subscription-Versuch (kein automatischer Import alter Abos in Stripe). Neue manuelle Rechnungen entstehen im **MAN-Rechnungskreis** (`invoices`), nie im Archiv.
- Scheduler `ExpireGrandfatheredSubscriptions` (Customer-Benachrichtigung am `grandfathered_until`) bleibt offen — folgt mit dem Stripe-Billing-Block.
- Alle historischen `user_payments` werden als Information ins Archiv geschrieben (analog `legacy_invoices` – optional).
- **Replay (D-18)**: Re-Runs aktualisieren bestehende Einträge anhand `legacy_conditions.legacy_portal` + `legacy_user_payment_option_id` — der Lauf kurz vor dem Relaunch übernimmt damit den dann aktuellen Stand ohne Duplikate.
### 5.7 Coupons
Vertagt (D-16) – werden in Phase 1 nicht migriert/gebaut.
### 5.8 Newsletter
- Nur `newsletter_subscriptions` übernehmen (E-Mails + Doppel-Opt-In-Status).
- Neue Workflows/Kampagnen-Komponenten werden separat gebaut.
### 5.9 Promotion Links / FooterCode
- Promotion Links **ignorieren** (D-14).
- FooterCode **übernehmen** (inkl. `category_footer_code`-Pivot).
---
## 6. Verifikations-Checks (`legacy:verify`)
Command prüft:
1. **Counts**: pro Entität `legacy_source_count` vs. `target_count` + Delta.
2. **Foreign Keys**: keine dangling FKs (z. B. `press_releases.user_id` existiert in `users`).
3. **User-Merges**: Alle `portal = both`-User auflisten.
4. **E-Mail-Duplikate** (Unique-Verletzungen hätten Imports blockiert – Check trotzdem).
5. **Invoice-Summen**: Σ `amount_cents` pro Portal in `legacy_invoices` vs. Summe aus Legacy-DB.
6. **Press Release Counts per Kategorie pro Portal**.
7. **Grandfathered Subscriptions**: Liste aller `user_payment_options.status = grandfathered` mit `grandfathered_until`.
Ausgabe als Tabelle und `storage/app/migration/verify-{timestamp}.json`.
---
## 7. Wiederholbarkeit / Produktiv-Replay ⭐ D-18
**Ziel:** Am Go-Live-Tag wird derselbe Code gegen den dann aktuellen Produktivstand laufen – deshalb:
- **Idempotenz via `legacy_import_map`:** Wiederholte Läufe aktualisieren existierende Datensätze (Upsert-Strategie) oder überspringen sie (je nach Entität). Kein Duplikat-Import.
- **Delta-Modus:** aktuell noch nicht implementiert; bei Bedarf vor Go-Live ergänzen oder über frischen Vollimport/Rehearsal ersetzen.
- **Rehearsal-Pflicht:** Vor dem Go-Live wird der komplette Importlauf mindestens einmal auf einem frischen Staging mit einem **aktuellen Produktiv-Snapshot** getestet (< 24 h alt).
- **Reset-Option:** aktuell nicht als produktiver Command dokumentiert; Staging-Rehearsals sollten auf frischer Ziel-DB laufen.
### 7.1 Go-Live-Runbook (Kurz)
```
1. Maintenance-Mode aktivieren (alte Systeme read-only)
2. Frischer Produktiv-Dump beider Legacy-DBs → Staging importieren (Rehearsal)
3. Nach erfolgreichem Rehearsal: frischer Zielsystem-Dump als Restore-Point
4. Migration auf Produktiv-Zielsystem: `php artisan migrate:fresh --force`
5. Importfolge aus `MIGRATION-STEPS.md` ausführen (`legacy:import`, `legacy:archive-invoices`, `legacy:fix-timestamps`)
6. Medien-/Bilddateien übertragen, sobald finaler Scope/Command feststeht
7. `php artisan legacy:verify`
8. Go-Live-Mailing rausschicken (Passwort-Reset an alle aktiven User)
9. DNS-Cutover
10. Alte Systeme in Kalt-Archiv verschieben
```
---
## 8. Rollback
Produktiv-Import erfolgt ausschließlich auf einer **frischen Ziel-DB**, die vor dem Lauf vollständig gedumpt wird (`mysqldump`). Bei Problemen: Dump zurückspielen.
Zusätzlich bietet jeder `legacy:import-*`-Command eine `--reset`-Option (nur in Staging).
---
## 9. Zeitplan
| Schritt | Dauer |
|---|---|
| Legacy-Models + Connections + Dumps lokal importieren | 0.5 d |
| Users + Companies + Contacts Import + Konflikt-Logik | 1.0 d |
| PressReleases + Images + Categories Import | 1.0 d |
| Newsletter + Blacklist + FooterCode | 0.25 d |
| Legacy-Invoices-Archiv + PDF-Transfer | 0.5 d |
| Grandfather-Subscriptions-Mapping | 0.25 d |
| Verifikation + Reporting | 0.5 d |
| Rehearsal-Lauf gegen produktiven Snapshot | 0.5 d |
| **Σ** | **~4.5 d** |
---
## 10. Pre-Flight-Checkliste (vor Produktiv-Import)
- [ ] Beide Legacy-DB-Dumps oder direkte Read-Connections aktuell (<24 h)
- [ ] Staging-Import abgeschlossen + verifiziert
- [ ] Rehearsal-Lauf erfolgreich
- [ ] Auftraggeber hat Konflikt-Fälle (Mails, Firmen) freigegeben
- [ ] Grandfathered-Liste vom Auftraggeber abgenommen
- [ ] Backup der Ziel-DB gemacht
- [ ] Alte Systeme auf read-only (Maintenance-Page)
- [ ] Monitoring läuft (Log-Channel, Slack/Mail-Alerts)
- [ ] Go-Live-Mailing vorbereitet + Versandzeitpunkt festgelegt

View file

@ -0,0 +1,244 @@
# 06 – Feature-Scope (IN / OUT / ANGEPASST)
Diese Matrix ist die **fachliche Scope-Grundlage**. **Stand: 2026-04-23 – Entscheidungen vom Auftraggeber bestätigt.**
Legende: **IN** = wird migriert · **NEU** = wird neu gebaut (nicht aus Legacy portiert) · **OUT** = entfällt · **ANGEPASST** = kommt rein, aber in veränderter Form · **ARCHIV** = nur lesbar für Nachschau.
---
## 1. Authentifizierung
| Feature | Legacy | Scope | Anmerkung |
|---|---|---|---|
| Login/Logout | ✓ | **IN** | Fortify |
| Registrierung (neue User) | ✓ | **IN** | Fortify-View anpassen; User-Typ bei Anmeldung wählen |
| Passwort zurücksetzen | ✓ | **IN** | Fortify |
| E-Mail-Verifizierung | ✓ | **IN** | Fortify |
| 2FA (TOTP) | – | **IN** (neu) | Fortify 2FA-Spalten liegen bereits in `users` |
| Remember-me | ✓ | **IN** | Laravel Standard |
| **Magic-Link Login** (Login per E-Mail-Link) | – | **NEU** ⭐ | Für Companies ohne Passwort + als Alternative für alle; kurzlebige signed URLs |
| Legacy-Passwort-Hashes weiter gültig | sha1 | **OUT** | Go-Live-Mailing mit Reset-Link + Sicherheitshinweis |
| Legacy-API-Key-Kompatibilität | ✓ | **OUT** | Sauberer Cut-over auf Sanctum; Kunden werden über neue Tokens informiert |
| Captcha beim Login | sfCaptchaGDPlugin | **OUT** | Ersetzt durch Rate-Limiting + Honeypot |
---
## 2. Benutzer / Profile
| Feature | Legacy | Scope |
|---|---|---|
| User-CRUD im Admin | ✓ | **IN** |
| User-Rollen (Groups) | sfGuardGroup | **IN** (Spatie): `admin`, `editor`, `customer`, `api-only` |
| User-Permissions | sfGuardPermission | **IN** (Spatie) |
| User-Profile (Adresse, Tel, Tax-ID …) | ✓ | **IN** |
| Sanctum-Tokens verwalten (Self-Service) | – | **NEU** | Customer verwaltet eigene Tokens unter `/account/tokens` |
| User-Profile `backlink_url` | ✓ | **IN** |
| User-Profile `show_stats` | ✓ | **IN** |
| User-Profile `disable_footer_code` | ✓ | **IN** | Footer-Code bleibt Feature |
| User-Profile `contract_date` | ✓ | **IN** |
| ApiUser als eigener Typ | ✓ | **ANGEPASST** | Als Rolle `api-only` (Spatie), keine eigene Tabelle |
---
## 3. Firmen & Kontakte
| Feature | Legacy | Scope |
|---|---|---|
| Company-CRUD | ✓ | **IN** |
| Agency als Subtyp | Inheritance | **IN** | Als `type`-Enum auf `companies` (entschieden in D-17/Q-10) |
| Mehrere verantwortliche User pro Company | `ResponsibleCompanyUser` | **IN** | Pivot mit `role` |
| Company-Login (eigenes Passwort) | ✓ | **OUT** | Stattdessen: User-Zugang pro Ansprechpartner; Company-E-Mail bekommt Magic-Link |
| **Company-Logo-Upload (verbessert)** | ✓ (minimal) | **ANGEPASST** ⭐ | Mehrere Varianten (sq / wide / darkmode), größere Auflösung, Drag-&-Drop |
| Footer-Code deaktivieren pro Company | ✓ | **IN** |
| Contact-CRUD | ✓ | **IN** |
| Contact-Autocomplete in PM-Formular | ✓ | **IN** |
---
## 4. Pressemitteilungen
| Feature | Legacy | Scope |
|---|---|---|
| CRUD (Admin) | ✓ | **IN** |
| **CRUD (Customer-Self-Service)** | ✓ | **IN** ⭐ | Customer-Portal: eigene PMs erstellen/bearbeiten |
| Multi-Kategorie? | Nein (1 Kategorie) | **IN** (1:1, N:M optional später) |
| Slugging per Titel+Sprache | ✓ | **IN** |
| Teaser-Range `teaser_begin`/`teaser_end` | ✓ | **IN** |
| Status (`draft`, `published`, `rejected`, `archived`) | lose Strings | **IN** | Enum |
| Publish/Reject-Workflow | ✓ | **IN** | Actions + Events |
| Automatische Blacklist-Prüfung | ✓ | **IN** |
| **Image-Upload mehrere pro PM (verbessert)** | ✓ | **ANGEPASST** ⭐ | Mehrere Bilder, größere Auflösungen, Varianten; Legacy war sehr minimalistisch |
| Preview-Bild | ✓ | **IN** |
| Hits-Zähler | ✓ | **IN** | Atomar `increment` |
| `no_export`-Flag | ✓ | **IN** |
| `portal`-Spalte (portal_type) | – | **IN** ⭐ | Pflichtfeld für Portal-Zuordnung (presseecho / businessportal24) |
| SEO-Felder (Meta Title, Description) | teilw. | **ANGEPASST** | Explizite SEO-Spalten |
| Kontakte zu PMs zuordnen | ✓ | **IN** |
| **Öffentlicher Magic-Link zum Teilen / Vorschau** | – | **NEU** | UUID-basierter Share-Link für noch unveröffentlichte PMs |
---
## 5. Kategorien
| Feature | Legacy | Scope |
|---|---|---|
| CRUD mit Slug | ✓ | **IN** |
| Mehrsprachigkeit (DE + EN) | ✓ | **IN** | `category_translations`, DE + EN direkt initialisiert |
| Hierarchie (Parent/Child) | ✗ | **IN** (neu, optional) | `parent_id` vorgesehen |
| Footer-Code pro Kategorie | ✓ | **IN** |
---
## 6. Newsletter
| Feature | Legacy | Scope |
|---|---|---|
| Subscribe mit Double-Opt-In | ✓ | **NEU** | Komplett neu gebaut |
| Unsubscribe-Link | ✓ | **NEU** |
| Admin: Subscriber-Liste | ✓ | **NEU** |
| Admin: Kampagnen (manuell versenden) | ✗ | **NEU** |
| CSV-Export / -Import | ✗ | **NEU** |
| Bestandssubscriber (aus Legacy) | ✓ | **IN** (Datenimport) | Nur Subscriber-Daten werden übernommen, kein Workflow-Code |
---
## 7. Billing / Payment / Invoicing
**Großer Umbau** (D-03, D-12, D-13): Komplett neuer Billing-Lauf, nur Stripe, alte Rechnungen → Archiv.
| Feature | Legacy | Scope |
|---|---|---|
| **PaymentOption-Verwaltung (neu definiert)** | ✓ | **NEU** | Neue Produkte, Stripe Prices als Quelle |
| **Grandfathering** (aktive Alt-User behalten Konditionen) | – | **NEU** ⭐ | `grandfathered_until`-Feld, automatische Migration auf neue Produkte bei Ablauf |
| Recurring via Stripe Subscription | – | **NEU** | Stripe Subscriptions |
| Onetime / Single-Purchase | ✓ | **NEU** | Stripe Checkout |
| Coupons | ✓ | **OUT** (vertagt, D-16) | Kein Model in Phase 1; evtl. Stripe-Coupons später. Aktuelle Coupon-Routen/Views gelten nur als UI-Skeleton ohne Priorisierung in P1. |
| Mehrfach-Buchung pro Company | ✓ | **IN** (neu modelliert) |
| Zahlungsart: SPK Berlin / Cortal Consors / Bar / Post / PayPal | ✓ | **OUT** |
| **Zahlungsart: Rechnung** | ✓ | **OUT** ⭐ | Entfällt komplett, nur noch Stripe |
| Zahlungsart: Stripe (Karte, SEPA, Klarna optional) | ✗ | **NEU** |
| Invoice-Generation (neu) | ✓ | **NEU** | Trigger: erfolgreicher Stripe-Charge oder periodische Subscription-Invoices |
| Invoice-PDF | ✓ (TCPDF) | **ANGEPASST** | DomPDF |
| Mahnwesen | ✓ (3 Stufen) | **NEU** | Vereinfacht, da nur noch Stripe-Zahlungsausfälle abzufangen sind |
| Abweichende Rechnungs-Adresse | ✓ | **IN** |
| Tax-Exempt / Reverse-Charge | ✓ | **IN** |
| VAT-Berechnung | ✓ | **IN** (aktuelle Sätze) |
| **Legacy-Rechnungen (PDFs)** | ✓ | **ARCHIV** ⭐ | Separate Tabelle `legacy_invoices` mit vollständigem DB-Import inkl. Status/User-Zuordnung; PDF wird read-only bei Abruf aus Legacy-Daten generiert |
---
## 8. REST-API
Details: [`07-API-MIGRATION.md`](./07-API-MIGRATION.md)
| Endpoint | Legacy | Scope |
|---|---|---|
| POST Pressrelease | ✓ | **IN** (neu implementiert, Sanctum-Auth) |
| GET Pressrelease-List | ✓ | **IN** |
| GET/PATCH Pressrelease | ✓ | **IN** |
| POST Image-Upload | ✓ | **IN** |
| GET Companies | ✓ | **IN** |
| GET Categories | ✓ | **IN** |
| POST Newsletter subscribe | ✓ | **IN** |
| **Auth: Legacy `api_key`-Query-Param** | ✓ | **OUT** ⭐ | Sanctum-Bearer-Token only; Kunden-Migration per E-Mail |
| (alles andere Undokumentierte) | ? | ❓ | Legacy-Log analysieren |
---
## 9. Marketing / Redaktion
| Feature | Legacy | Scope |
|---|---|---|
| **Promotion-Links** | ✓ | **OUT** ⭐ | Entfällt komplett (D-14) |
| Footer-Code pro Kategorie/Sprache | ✓ | **IN** |
| „page"-Modul (statische Seiten) | ✓ | **OUT** | Static Pages im Frontend-Repo als Blade |
---
## 10. Cron / Scheduler
| Job | Legacy | Scope |
|---|---|---|
| `sfp:user-payment-create` | ✓ | **OUT** | Abgelöst durch Stripe Subscriptions |
| `sfp:invoice-create` | ✓ | **NEU** | Stripe-Webhook-getrieben statt Cron |
| `sfp:invoice-reminder` | ✓ | **NEU** | Laravel-Scheduler-Job für unbezahlte Stripe-Invoices |
| `sfp:user-paymentoption-expire` | deaktiviert | **NEU** | Grandfather-Ablauf-Handler |
| **Magic-Link-Cleanup** | – | **NEU** | Expired tokens löschen |
| Newsletter-Versand-Queue | – | **NEU** |
| Log-Rotation / Prune Failed Jobs | – | **NEU** |
---
## 11. Bilder / Media
| Feature | Legacy | Scope |
|---|---|---|
| Thumbnail-Generierung | sfImageTransformPlugin | **ANGEPASST** | Intervention Image, on-upload |
| Varianten (thumb_200, thumb_500, etc.) | ✓ | **IN** |
| Storage lokal | ✓ | **IN** (Dev + Prod initial) |
| Storage S3-kompatibel | ✗ | **IN** (optional später) |
| Alte Image-URLs (`/thumbnails/...`) | ✓ | **ANGEPASST** | Redirect-Middleware |
| **Company-Logo größer & mehrere Varianten** | ✓ (minimal) | **NEU** ⭐ |
| **Press-Release mehrere Bilder** | teils | **NEU** ⭐ |
---
## 12. Kundenbereich / Customer-Portal ⭐ NEU AUFGEWERTET
**Entscheidung D-11:** Das Backend ist **nicht nur Admin-UI**, sondern bedient auch Endkunden.
| Feature | Scope | Anmerkung |
|---|---|---|
| Customer-Login (Passwort oder Magic-Link) | **NEU** | Eigene Route-Gruppe `customer.*` |
| Eigenes Dashboard (Übersicht aktive Abos, letzte PMs, Rechnungen) | **NEU** |
| Eigene Pressemitteilungen: Liste / Erstellen / Bearbeiten | **NEU** |
| Eigene Rechnungen einsehen + PDF | **NEU** | Inkl. Legacy-Archivrechnungen |
| API-Tokens verwalten (Sanctum-PATs) | **NEU** |
| Company-/Contact-Profile pflegen | **NEU** |
| Zwei-Faktor-Auth aktivieren | **NEU** |
| Passwort ändern / Mail ändern | **NEU** |
---
## 13. Frontend-Integration
| Feature | Legacy | Scope |
|---|---|---|
| Altes Symfony-Frontend (presseecho/businessportal24.com) | ✓ | **OUT** |
| Neue Frontends (presseecho.test / businessportal24.test) | vorhanden | **IN** | Greifen künftig auf neue Models/API zu |
---
## 14. Offene Scope-Entscheidungen (Restliste)
| ID | Thema | Vorschlag / Status |
|---|---|---|
| S-01 | Multi-Kategorie pro PM | Bei 1:1 belassen; N:M-Ausbau nur auf Anfrage |
| S-02 | Coupons-Wiedereinführung | Vertagt (siehe D-16) |
| S-03 | E-Mail-Duplikate zwischen Portalen | Merge wenn gleiche E-Mail (siehe 05-DATABASE-MERGE §3.1) |
| S-04 | S3 für Medien in Prod | Optional, erst wenn Volumen es erfordert |
| S-05 | Definition der neuen Stripe-Produkte | Preisliste + Laufzeiten vom Auftraggeber noch zu liefern |
---
## 15. QA-Checkliste (manuell, vor Go-Live)
- [ ] Admin-Login (inkl. 2FA) funktioniert
- [ ] **Customer-Login via Magic-Link** funktioniert
- [ ] **Customer-Login via Passwort** funktioniert
- [ ] Passwort-Reset-Flow funktioniert (für Go-Live-Mailing kritisch)
- [ ] Portal-Scoping: Admin sieht mit Filter Presseecho-only, BP24-only, alle
- [ ] Press Release: Admin: Create → Save Draft → Publish → Mail → sichtbar im Frontend
- [ ] Press Release: **Customer**: Create → Save Draft → Submit → Admin sieht zur Prüfung
- [ ] Pressrelease Image-Upload, mehrere Bilder, Preview-Bild-Flag
- [ ] Company-Logo-Upload (mehrere Varianten)
- [ ] Blacklist-Wort blockiert Publish
- [ ] Newsletter Double-Opt-In End-to-End (**neu**)
- [ ] Stripe-Checkout End-to-End inkl. Webhook, Invoice-PDF wird erzeugt
- [ ] Grandfather-User: aktives Alt-Abo wird korrekt weiterberechnet/angezeigt
- [ ] Legacy-Rechnungen im Archiv sichtbar (Admin + Customer)
- [ ] API: `POST /v1/press-releases` mit Sanctum-Token
- [ ] API: Alter `api_key`-Parameter wird abgewiesen (Error-Hinweis mit Migrationslink)
- [ ] Alter `/thumbnails/...`-Link redirectet korrekt
- [ ] Scheduler-Jobs greifen (Runbook: `php artisan schedule:run` und `queue:work`)
- [ ] **Import-Rehearsal**: aktuelles Runbook aus `MIGRATION-STEPS.md` läuft auf frischer Ziel-DB gegen produktiven DB-Snapshot erfolgreich durch

View file

@ -0,0 +1,241 @@
# 07 – API-Migration
## 1. Ausgangslage (Legacy)
- Authentifizierung: Statischer `api_key`-String im User-Profil (`sfGuardUserProfile.api_key`).
- Transport via HTTP-Header oder Query-Param, je nach Client (historisch gewachsen).
- Symfony-Module `pressrelease`, `pressreleaseimage`, `company`, `category`, `newsletter` stellten Actions bereit (ApiActions-Basisklasse).
- Format: **JSON** (teils auch XML) – wurde via `renderText(json_encode(...))` erzeugt.
- CSRF für API-Requests in `prepareForm()` abgeschaltet.
- **Undokumentiert** – Schnittstellen-Wissen liegt in Server-Logs und im Symfony-Code verstreut.
Vor der finalen Migration: **Zugriffs-Log des Prod-Servers der letzten 3 Monate** analysieren, um vollständige Liste der tatsächlich genutzten Endpoints zu erhalten. (Aufgabe zu Beginn Phase 7.)
---
## 2. Ziel-Architektur
- **URL-Prefix**: `/api/v1`
- **Auth**: **Nur Laravel Sanctum Personal Access Tokens** (Bearer).
- ⭐ **Entscheidung D-05 / Q-03:** Kein Kompatibilitätsmodus für Legacy-Keys. Cut-over statt Keep-Alive.
- **Format**: JSON only; `Accept: application/json` empfohlen. Validation-Fehler als RFC 7807 (Laravel Default).
- **Versionierung**: `v1` = neuer Stand, saubere Ressourcen (kein Legacy-Feld-Alias mehr nötig, weil Clients eh umstellen müssen).
- **Rate-Limiting**: Pro Sanctum-/Bearer-Token 60/min.
- **Dokumentation**: OpenAPI-YAML (`docs/api/v1.yml`) + optional Scribe-Renderer.
### Token-Scopes (Sanctum Abilities)
| Ability | Zweck |
|---|---|
| `press-releases:read` | Listen + Details lesen |
| `press-releases:write` | Erstellen / Update / Delete |
| `press-release-images:write` | Upload / Delete |
| `companies:read` | Firmen lesen |
| `newsletter:subscribe` | Newsletter-Opt-In |
Token können im Customer-Portal (und Admin) mit individuellen Scopes erstellt werden.
---
## 3. Cut-Over-Strategie für Legacy-API-Clients
**Kein Parallelbetrieb.** Kommunikation in 3 Phasen:
| Phase | Zeitpunkt | Aktion |
|---|---|---|
| **Ankündigung** | T-30 Tage | Mail an alle User mit `api_key_legacy != null`: neue Technik kommt, Token-Migration nötig, Link zur Doku |
| **Erinnerung** | T-7 Tage | Zweite Mail |
| **Cut-over** | Go-Live | Alter `api_key` wird abgewiesen. Response `410 Gone` mit `message: "Legacy API keys are no longer supported."`, `migration_url` und `docs_url` |
Aktive Kunden bekommen im Backend / Customer-Portal eine prominent platzierte Migrations-Anleitung + Token-Generator.
---
## 4. Endpoint-Mapping (vorläufig)
| Legacy-Route | Methode | Neu | Ressource |
|---|---|---|---|
| `pressrelease/create` | POST | `POST /v1/press-releases` | `PressReleaseResource` |
| `pressrelease/list` | GET | `GET /v1/press-releases?page=&status=` | collection |
| `pressrelease/show/:id` | GET | `GET /v1/press-releases/{id}` | |
| `pressrelease/update/:id` | POST | `PATCH /v1/press-releases/{id}` | |
| `pressrelease/delete/:id` | POST | `DELETE /v1/press-releases/{id}` | |
| `pressreleaseimage/upload` | POST | `POST /v1/press-releases/{id}/images` | `multipart/form-data`, max. 5 MB |
| `pressreleaseimage/list` | GET | `GET /v1/press-releases/{id}/images` | collection |
| `pressreleaseimage/delete/:id` | POST | `DELETE /v1/press-release-images/{id}` | |
| `company/list` | GET | `GET /v1/companies` | |
| `company/show/:id` | GET | `GET /v1/companies/{id}` | |
| `category/list` | GET | `GET /v1/categories?lang=de\|en` | |
| `newsletter/subscribe` | POST | `POST /v1/newsletter/subscribe` | |
| `newsletter/unsubscribe` | – | **nicht Teil von P7** | wird später im neuen Frontend mit sauberem Newsletter-Dienst neu implementiert |
### Beispiel: PressReleaseResource
```php
return [
'id' => $this->id,
'uuid' => $this->uuid,
'legacy' => [
'portal' => $this->legacy_portal,
'id' => $this->legacy_id,
],
'portal' => $this->portal->value,
'title' => $this->title,
'slug' => $this->slug,
'language' => $this->language,
'status' => $this->status->value,
'text' => $this->text,
'backlink_url' => $this->backlink_url,
'keywords' => $this->keywords,
'teaser' => [
'begin' => $this->teaser_begin,
'end' => $this->teaser_end,
],
'category' => CategoryResource::make($this->whenLoaded('category')),
'company' => CompanyResource::make($this->whenLoaded('company')),
'images' => PressReleaseImageResource::collection($this->whenLoaded('images')),
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
];
```
---
## 5. Authentifizierung (Sanctum only)
Route-Definition:
```php
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::apiResource('press-releases', PressReleaseController::class);
Route::get('press-releases/{pressRelease}/images', [PressReleaseImageController::class, 'index']);
Route::post('press-releases/{pressRelease}/images', [PressReleaseImageController::class, 'store']);
Route::delete('press-release-images/{pressReleaseImage}', [PressReleaseImageController::class, 'destroy']);
Route::apiResource('companies', CompanyController::class)->only(['index', 'show']);
Route::get('categories', [CategoryController::class, 'index']);
Route::post('newsletter/subscribe', [NewsletterController::class, 'subscribe']);
});
```
Abilities werden pro Controller-Action geprüft:
```php
$request->user()->tokenCan('press-releases:write') or abort(403);
```
### Legacy-Handler (One-Shot)
Ein kleiner Handler fängt Requests mit `api_key`-Param ab und antwortet mit einer freundlichen Fehlermeldung inkl. Link zur Migrationsdoku (statt generischem 401):
```php
if ($request->has('api_key') || $request->header('X-Api-Key')) {
return response()->json([
'message' => 'Legacy API keys are no longer supported.',
'migration_url' => url('/account/tokens'),
'docs_url' => url('/docs/api/v1'),
], 410); // 410 Gone – signalisiert deutlich: "Feature ist weg, hier ist der Ersatz"
}
```
---
## 6. Authorization (Policies)
- `PressReleasePolicy::create` → User muss `press-releases:write`-Ability UND Mitglied einer Company mit `role` ∈ {owner, responsible} sein.
- `PressReleasePolicy::update/delete` → Ownership + Status-Check (nur `draft` / `rejected` editierbar).
- `PressReleaseImageController` → List nur eigene Pressemitteilung, Upload/Delete nur eigene `draft` / `rejected` Pressemitteilung, Upload max. 5 MB.
- `NewsletterController::subscribe` → öffentlich, aber `newsletter:subscribe`-Ability erforderlich bei authentifiziertem Zugriff.
- `newsletter/unsubscribe` → bewusst nicht in P7 umgesetzt; neuer Newsletter-Dienst folgt nach der Migration.
- Fehlermeldungen sprachlich konsistent (deutsch im `message`-Feld, abhängig von `Accept-Language`).
---
## 7. Fehlerformat
Standard Laravel:
```json
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"message": "The given data was invalid.",
"errors": {
"title": ["Der Titel wird benötigt."],
"text": ["Der Text darf nicht leer sein."]
}
}
```
Legacy-Key-Versuch:
```json
HTTP/1.1 410 Gone
Content-Type: application/json
{
"message": "Legacy API keys are no longer supported.",
"migration_url": "https://pressekonto.de/account/tokens",
"docs_url": "https://pressekonto.de/docs/api/v1"
}
```
---
## 8. Versioning & Zukunft
- `v1` = neuer Stand ab Go-Live (keine Legacy-Altlasten).
- `v2` erst auf Anfrage / bei breaking changes.
---
## 9. Testing
- **Feature-Tests** pro Endpoint:
- Happy-Path mit Sanctum-Token (mit passender Ability)
- Auth-Fehlerfall (kein Token)
- Ability-Fehlerfall (403)
- **Legacy-Key-Versuch → 410** mit migration_url
- Validation-Fehler (422)
- Policy-Fehler (403)
- **Smoke-Test**-Skript (`php artisan api:smoke`) das gegen Staging 10 kritische Endpoints fährt.
---
## 10. OpenAPI-Dokumentation
Ablegen unter `docs/api/v1.yml`, generiert aus:
- Scribe (`knuckleswtf/scribe`) – PHP-Annotations → YAML, oder
- manuell (konsistenter, wenn Team es pflegt).
**Empfehlung**: Mit Scribe starten, bei Bedarf manuell überschreiben.
---
## 11. Migrationsschritte in Phase 7
1. Log-Analyse: alle in den letzten 90 Tagen in Produktion aufgerufenen URLs auflisten
2. Endpoints priorisieren (Top-20 zuerst)
3. Routen, Controller, Resources, Requests, Policies pro Endpoint
4. Sanctum-Ability-Seeder für Standard-Scopes
5. Legacy-410-Handler
6. Integrationstests
7. Smoke-Tests gegen Staging mit echtem Client
8. **Kommunikation an API-Partner** (E-Mail-Verteiler) mit Token-Migrations-Anleitung (T-30, T-7, T-0)
9. Self-Service Token-Management im Customer-Portal verfügbar machen
### 11.1 Log-Analyse-Tooling
Für Schritt 1 gibt es ein wiederholbares Artisan-Werkzeug:
```bash
php artisan api:analyze-legacy-access-logs /var/log/nginx/access.log "/var/log/nginx/access.log.*" --top=20
```
Der Command erkennt Legacy-API-Routen (`pressrelease/*`, `pressreleaseimage/*`, `company/*`, `category/*`, `newsletter/*`), zählt Requests mit `api_key`, maskiert Beispielrequests und speichert nur API-Key-Fingerprints statt Klartext-Keys.
Der JSON-Report landet unter:
```bash
storage/app/private/migration/legacy-api-access-*.json
```

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,380 @@
# 09 – Qualitätsreview: Bisherige Umsetzung
**Datum:** 2026-04-27
**Reviewer:** Claude Code (KI-gestütztes Code-Review)
**Geprüfte Phasen:** P0, P1, P2, P3 (laut `08-PROGRESS.md`)
> **Status-Hinweis 2026-04-29:** Dieses Review ist ein historischer Befund vom 2026-04-27. Die kritischen Punkte aus §2 und mehrere Punkte aus §3 wurden laut `08-PROGRESS.md` und Code-Abgleich inzwischen behoben oder teilweise umgesetzt (u. a. `EnsureUserIsAdmin`, `SetCurrentPortal`/`PortalScope`, Magic-Link-Härtung, Factories, Billing-Address-Bugfix, `contact_user`-Import). Für den aktuellen operativen Status siehe `03-MIGRATION-PLAN.md` und `08-PROGRESS.md`; dieses Dokument bleibt als Review-Quelle erhalten.
Dieses Dokument bewertet die bisherige Implementierung nach vier Dimensionen:
1. Code-Qualität & Laravel-Konventionen
2. Datenmigrations-Bereitschaft (Legacy-Import)
3. Admin-UI/UX
4. Offene Risiken & Prioritäten
**Legende:** 🔴 kritisch · 🟡 sollte korrigiert werden · 🟢 gut / verbesserbar · ✅ keine Beanstandung
---
## 1. Was gut gemacht wurde
Bevor die Probleme beschrieben werden, eine ehrliche Bestandsaufnahme der Stärken:
- **Saubere Enum-Nutzung** (`Portal`, `PressReleaseStatus`, etc.) – konsequent in Migrations, Casts und Validation eingesetzt.
- **Migrations sind gut strukturiert** – jede Tabelle hat `deleted_at`, `portal`, `legacy_portal/id` wo nötig; `down()` ist vollständig implementiert.
- **MagicLink-Implementierung** – korrekt: Token-Hash in DB, Single-Use-Flag, TTL, Anti-Enumeration bei unbekannter E-Mail.
- **Idempotente Seeder** – `firstOrCreate` / `updateOrCreate` durchgängig; `PaymentOptionSeeder` mit DE/EN-Translations.
- **`UserRolePermissionSyncService`** – gute Vorbereitung, damit der spätere Legacy-Import denselben Rollenmechanismus nutzt.
- **Validation in User-Edit** – vollständig, typsicher, nutzt `Rule::exists()`/`Rule::unique()`.
- **Live-Suche mit Limits** – Firmenlookup und Kontaktlookup ab 2 Zeichen, max. 40 Treffer, serverseitig – performant und korrekt.
- **PHPDoc `@var` Array-Shape-Typen** – z. B. `contactForms` im User-Edit korrekt annotiert.
- **Tests vorhanden** für alle P3-Features (User-CRUD, Company-CRUD, Kontakt-CRUD, Filter-Presets, etc.).
---
## 2. Kritische Probleme (🔴)
### 2.1 Admin-Middleware fehlt: Jeder eingeloggte User kann Admin-Bereich nutzen
**Datei:** `routes/admin.php`
```php
Route::middleware(['auth'])->group(function () {
Volt::route('admin/users', ...);
Volt::route('admin/roles', ...); // Rollenverwaltung!
// ...
});
```
Alle Admin-Routen sind nur mit `auth` geschützt – **nicht** mit einer Rolle-/Permission-Prüfung. Ein frisch registrierter Customer-User kann `/admin/roles` aufrufen und Rollen bearbeiten.
**Fix:** Middleware oder Gate-Checks auf der Routengruppe.
```php
Route::middleware(['auth', 'verified', function ($request, $next) {
abort_unless($request->user()?->canAccessAdmin(), 403);
return $next($request);
}])->group(function () {
// ...
});
```
Alternativ ein dediziertes `EnsureUserIsAdmin`-Middleware.
---
### 2.2 Kein Portal-Scoping: Mandantentrennung existiert nicht
**Geplant:** Phase 2.5 – `SetCurrentPortal`-Middleware + `CurrentPortalScope` (Global Scope)
**Status:** ⬜ Nicht implementiert
Alle Admin-Queries liefern aktuell Daten beider Portale unkontrolliert. Ein `admin`-User von `presseecho` sieht alle Firmen, Kontakte, PMs von `businessportal24` – und umgekehrt. Das ist für ein Produktivsystem inakzeptabel.
Der Global Scope müsste bei allen Multi-Portal-Modellen (Company, Contact, PressRelease, NewsletterSubscription, etc.) automatisch nach dem aktiven Portal filtern.
**Auswirkung auf bestehenden Code:** Nach Einführung des Scopes werden Tests und Queries, die aktuell portalübergreifend arbeiten, Ergebnisse verlieren. Das ist ein Breaking Change mit breiter Wirkung – deshalb **muss P2.5 vor weiteren P3-Implementierungen** erfolgen.
---
### 2.3 `persistUserLinks()` ohne Authorization-Check: N+1 bei jedem Klick
**Datei:** `resources/views/livewire/admin/users/edit.blade.php`, Zeile ~540
`persistUserLinks()` wird bei jedem Klick auf „Zuordnen"/„Entfernen" ausgeführt. Darin:
1. `User::query()->find($this->id)` – neuer DB-Query
2. `$user->companies()->sync(...)` – Pivot-Tabelle schreiben
3. `$user->contacts()->sync(...)` – Pivot-Tabelle schreiben
Es gibt **keinen Check**, ob der aktuell eingeloggte Admin tatsächlich diesen User bearbeiten darf. In einem Multi-Portal-Setup könnten Portal-A-Admins damit User von Portal B manipulieren.
Zusätzlich: `sync()` auf Kontakten schreibt **alle** `linkedContactIds` inklusive IDs, die nicht zu den verlinkten Firmen gehören – es gibt keinen serverseitigen Konsistenz-Check.
---
### 2.4 `MagicLinkConsumeController`: N+1 + Lazy-Load auf ungeprüftem Objekt
**Datei:** `app/Http/Controllers/Auth/MagicLinkConsumeController.php`
```php
$magicLink = MagicLink::query()->where(...)->first();
// Zeile 28: $magicLink->user->is_active ← eager load fehlt
// Zeile 37: $magicLink->user->update(...) ← zweite Abfrage
// Zeile 42: Auth::guard('web')->login($magicLink->user) ← dritte Abfrage
```
Der User wird dreimal separat geladen. Mit `->with('user')->first()` wäre es eine Abfrage.
Außerdem: Wenn der User gelöscht ist (SoftDelete), gibt `$magicLink->user` `null` zurück, aber es gibt keinen `null`-Check – das führt zu einem `TypeError` statt einem sicheren Redirect.
**Fix:**
```php
$magicLink = MagicLink::query()
->with('user')
->where('token_hash', hash('sha256', $token))
->where('purpose', 'login')
->first();
if (! $magicLink || ! $magicLink->user || ! $magicLink->user->is_active) { ... }
```
---
### 2.5 Doppelte Permission-Zuweisung ist ein Anti-Pattern
**Datei:** `app/Services/Auth/UserRolePermissionSyncService.php`
```php
$user->assignRole($role);
$user->syncPermissions(
$user->getPermissionsViaRoles()->pluck('name')->all()
);
```
Spatie Permissions ist so konzipiert, dass Rollen die Permissions tragen – `$user->can('...')` prüft automatisch über Rollen. Die **direkte Zuweisung derselben Permissions an den User** (zusätzlich zur Rolle) schafft Inkonsistenzen:
- Bei Änderung der Permissions einer Rolle bleiben die direkt zugewiesenen User-Permissions unverändert.
- `$user->getAllPermissions()` enthält Duplikate.
- Der Legacy-Import-Command muss diesen Doppelmechanismus kennen – fehleranfällig.
**Empfehlung:** Direkte Permission-Zuweisung an User komplett entfernen. Ausschließlich über Rollen arbeiten. Das entspricht dem Spatie-Standardmodell.
---
## 3. Sollte korrigiert werden (🟡)
### 3.1 Billing-Address-Update kann Felder nicht leeren
**Datei:** `resources/views/livewire/admin/users/edit.blade.php`, Zeile ~398–414
```php
$mergedBillingPayload = array_merge(
$billingAddress->only([...]),
collect($billingPayload)
->filter(static fn ($value) => filled($value)) // ← Problem
->all()
);
```
`filter(fn ($v) => filled($v))` entfernt alle leeren/null-Werte aus dem Payload. Das bedeutet: Hat ein Admin-User eine `address2` gesetzt und will sie wieder löschen (Feld leer lassen), bleibt der alte Wert stehen. Felder können einmal gesetzt nicht mehr entfernt werden.
**Fix:** Statt `filled()` nur `null !== $value` prüfen, oder den Payload direkt ohne Merge verwenden.
---
### 3.2 Drei Migrations für eine Feature-Idee am selben Tag
**Dateien:**
- `2026_04_24_120000_create_user_filter_presets_table.php`
- `2026_04_24_121500_add_is_default_to_user_filter_presets_table.php`
- `2026_04_24_123000_add_last_used_at_to_user_filter_presets_table.php`
Drei Migrations innerhalb von 90 Minuten für dasselbe Feature sind ein Zeichen dafür, dass die Tabelle nicht ausreichend vorab modelliert wurde. Solange die Tabelle noch nicht in Produktion ist, sollten diese zu **einer einzigen Migration** zusammengeführt werden.
Dies ist vor dem Go-Live wichtig, da viele kleine Patch-Migrations die `php artisan migrate` Ausführungszeit und Fehlerdiagnose erschweren.
---
### 3.3 Fehlende Factories für 12+ Modelle
Aktuell haben nur 8 Modelle Factories (`User`, `BillingAddress`, `InvoiceBillingAddress`, `Invoice`, `PaymentOption`, `PaymentOptionTranslation`, `UserPaymentOption`, `UserPayment`).
Folgende Modelle haben **keine Factory** und sind im Testcode mit rohen `Model::query()->create()` Calls aufgerufen:
- `Company`, `Contact`, `Profile`, `Category`, `CategoryTranslation`
- `PressRelease`, `PressReleaseImage`, `NewsletterSubscription`
- `MagicLink`, `UserFilterPreset`
In `UserManagementTest.php` sieht man z.B.:
```php
$companyA = Company::query()->create([
'name' => 'Alpha GmbH',
'slug' => 'alpha-gmbh',
]);
```
Das ist fragil: Wenn die `companies`-Tabelle ein NOT-NULL-Feld ohne Default bekommt, brechen alle Tests. Mit Factories werden die Defaults vom Framework verwaltet.
---
### 3.4 `contact_user` Pivot-Tabelle ist nicht im Datenmodell (04-DATA-MODEL.md)
Die Tabelle `contact_user` (viele-zu-viele User ↔ Kontakte) wurde in P3 eingeführt, ist aber in der Datenmodell-Dokumentation nicht beschrieben. Das schafft zwei Probleme:
1. **Legacy-Import (Phase 6) weiß nicht, dass diese Tabelle befüllt werden muss.** Der Import füllt Kontakte über `company_id` – die expliziten User-Kontakt-Verknüpfungen in `contact_user` müssen separat abgeleitet werden (welcher User hatte welche Kontakte?).
2. **Dual-Relationship-Komplexität:** Kontakte sind jetzt sowohl über `company_user` (indirekt via Firma) als auch direkt über `contact_user` mit Usern verknüpft. Der Fallback-Code in `mount()` (`if ($this->linkedContactIds === [])`) zeigt, dass die Logik bereits jetzt nicht trivial ist.
**Empfehlung:** `04-DATA-MODEL.md` um `contact_user` ergänzen und den Import-Ansatz für diese Tabelle klären.
---
### 3.5 `UserManagementTest.php` ist eine 820-Zeilen-Monodatei
Alle Admin-Tests (User-CRUD, Company-CRUD, Kontakt-CRUD, Filter-Presets, Company-Contacts-Tab, etc.) liegen in einer Datei. Das macht:
- Debugging schwerer (ein Fehler lässt 19+ Tests rot werden)
- Test-Isolation schwerer verständlich
- CI-Runs langsamer (kein paralleles Splitting)
**Empfehlung:** Aufteilen in:
- `tests/Feature/Admin/UserCrudTest.php`
- `tests/Feature/Admin/CompanyCrudTest.php`
- `tests/Feature/Admin/ContactCrudTest.php`
- `tests/Feature/Admin/FilterPresetTest.php`
---
### 3.6 `session()->flash()` in Livewire-Komponenten ist unzuverlässig
In vielen Volt-Komponenten wird `session()->flash('success', ...)` genutzt (z.B. in `addLinkedCompany()`, `removeLinkedContact()`). In Livewire 4 sind Flash-Messages über Session nach einem Livewire-Request (kein Full-Page-Reload) oft nicht sichtbar oder kommen mit Delay.
**Der Livewire-4-Standard** ist `$this->dispatch('notify', message: '...')` mit einem Alpine.js-Event-Listener, oder Flux-UI-eigene Notification-Komponenten. Session-Flash ist für Redirects nach `$this->redirect()` korrekt – aber nicht für inline Actions ohne Redirect.
---
### 3.7 ENUM-Spalten erschweren spätere Schema-Änderungen
Viele Tabellen nutzen `$table->enum(...)` mit PHP-Enum-Werten (z.B. `portal`, `registration_type`, `status`). Das ist funktional korrekt, hat aber einen Nachteil:
MySQL-ENUMs können nur via `ALTER TABLE` geändert werden – das sperrt die Tabelle kurz. Für `companies` und `contacts` (potentiell Hunderttausende Zeilen) kann das beim Go-Live-Rehearsal ein Problem werden.
**Alternative:** `VARCHAR` mit Laravel-seitiger Validation und Enum-Casts. Der Enum wird auf PHP-Ebene erzwungen, die DB ist flexibler.
Dies ist ein Low-Priority-Thema für den aktuellen Entwicklungsstand, aber beim Go-Live-Rehearsal (Phase 6.10) beachten.
---
## 4. Verbesserungsideen (🟢)
### 4.1 PressRelease-CRUD: Das Kern-Feature ist noch Dummy-Data
**Betroffene Dateien:**
- `resources/views/livewire/admin/press-releases/index.blade.php`
- `resources/views/livewire/admin/press-releases/create.blade.php`
- `resources/views/livewire/admin/press-releases/edit.blade.php`
- `resources/views/livewire/admin/press-releases/show.blade.php`
Alle vier Pressemitteilungs-Views enthalten noch Dummy-Daten (`// TODO: Implementierung mit echten Models nach Migration`). Pressemitteilungen sind **das Kernprodukt** beider Portale. Ohne diese Implementierung kann der Migrationserfolg nicht validiert werden.
Ebenso: `categories/index.blade.php`, `invoices/index.blade.php`, `payments/index.blade.php` sind alle noch nicht angebunden.
---
### 4.2 Kein `PressRelease`-Factory → keine Tests möglich
Das `PressRelease`-Model hat keine Factory und `HasFactory` fehlt. Für Phase 5 (`PressReleaseService` mit Tests) und Phase 6 (Import-Verification) wird ein vollständiger Test-Datensatz benötigt.
---
### 4.3 `UserFilterPreset`-Feature: zu früh für P3
Filter-Presets mit Default-Flag und Last-Used-Timestamp sind ein UX-Komfort-Feature. Zum aktuellen Zeitpunkt (die Admin-Hauptfunktionen sind noch nicht alle angebunden) binden sie Entwicklungszeit und Migration-Komplexität. Das Feature ist gut implementiert – aber die Priorisierung in P3 war verfrüht.
---
### 4.4 Keine Policies für Datenzugriff
Aktuell gibt es keine `Policy`-Klassen. Ein `admin`-User kann jede Company, jeden Kontakt, jeden User bearbeiten – egal ob er zu seinem Portal gehört. Mit der Einführung des Portal-Scopes (P2.5) wird das teilweise gelöst, aber spezifischere Zugriffsregeln (z.B. „Editor darf nur PMs im Status `draft` bearbeiten") benötigen Policies.
---
### 4.5 `MigratePresseData`-Command: Legacy-Import-Ansatz fehlt
**Datei:** `app/Console/Commands/MigratePresseData.php` (laut git status geändert)
Der Import-Command ist eine der kritischsten Anforderungen (D-18: wiederholbares Skript). Aus dem Progress-Log ist nicht ersichtlich, ob dieser Command bereits eine funktionierende Grundstruktur hat oder ob er noch ein Stub ist. Phase 6 (Import) ist der kritische Pfad für Go-Live.
---
### 4.6 Keine `gdpr_consent_at`-Erfassung beim Registrierungs-Flow
Das Feld `gdpr_consent_at` ist in der `users`-Tabelle vorhanden und im `fillable`-Array. Es wird aber nirgends im Registrierungs- oder User-Create-Flow befüllt. Für DSGVO-Konformität (besonders bei `presseecho.de` mit deutschen Nutzern) ist eine dokumentierte Einwilligung Pflicht.
---
## 5. Daten-Migration: Bereitschaft
| Aspekt | Status | Bewertung |
|---|---|---|
| Legacy-Tabellen-Mapping dokumentiert | ✅ `04-DATA-MODEL.md` vollständig | ✅ gut |
| `legacy_import_map` Tabelle | ✅ Migration + Model vorhanden | ✅ gut |
| Legacy-DB-Connections in `.env` | ⬜ `0.3` noch offen | 🔴 blockiert P6 |
| Legacy-Models unter `app/Models/Legacy/` | ⬜ nicht vorhanden | 🔴 P6 nicht startbar |
| Import-Commands (`legacy:import-*`) | ⬜ nicht vorhanden | 🔴 P6 nicht startbar |
| `legacy_invoices` Archiv-Migration | ✅ P6.5d umgesetzt | ✅ Vollimport-Command inkl. Status/User-Zuordnung/PDF-Payload; Rehearsal gegen Produktiv-Snapshot bleibt Pflicht |
| Portal-Scoping für Imports | ⬜ P2.5 fehlt | 🟡 Import ohne Scope unsicher |
| Idempotenz-Mechanismus | ✅ `legacy_import_map` Unique-Index | ✅ gut |
| Rehearsal-Plan dokumentiert | ✅ `05-DATABASE-MERGE.md` | ✅ gut |
| Factories für Import-Testdaten | ⬜ 12+ Modelle ohne Factory | 🟡 Tests schwer schreibbar |
---
## 6. Admin-UI/UX: Bewertung
### Was gut ist
- **Company-Detailseite mit Tabs** (Übersicht / Kontakte) ist ein gutes UX-Pattern.
- **Live-Suche** für Firma- und Kontaktzuordnung ist intuitiv und performant.
- **Soft-Delete mit Warn-Modal** für Companies und Kontakte verhindert versehentliches Löschen.
- **Pagination** in allen Index-Views vorhanden.
### Was fehlt / verbessert werden kann
- **Dashboard zeigt keine echten Daten** – die Widget-Queries (Anzahl PMs, neue Firmen, Newsletter-Abos) sind Stubs.
- **Breadcrumb-Navigation fehlt** – bei tief verschachtelten Views (Company → Kontakt → Edit) gibt es keine Orientierungshilfe.
- **Kein Portal-Switcher** (P2.6 offen) – Admins mit `portal = both` können nicht zwischen den Portal-Ansichten wechseln.
- **Pressemitteilungs-Workflow** (Draft → Review → Published) ist die Hauptfunktion des Systems und noch nicht implementiert.
- **Keine Suche in der Rollenübersicht** – bei vielen Custom-Rollen wird die Tabelle unübersichtlich.
- **User-Edit**: Rollenänderung und Firmenzuordnungsrolle werden nicht sofort persistiert (nur bei Save), während Firmen- und Kontaktzuordnung sofort persistiert werden – inkonsistentes UX-Verhalten.
---
## 7. Priorisierte To-Do-Liste
### Sofort (vor weiteren P3-Features)
| # | Maßnahme | Warum |
|---|---|---|
| A1 | Admin-Middleware: `canAccessAdmin()` auf Routengruppe erzwingen | Sicherheitslücke |
| A2 | `MagicLinkConsumeController`: `->with('user')` + null-Check | Laufzeitfehler bei gelöschten Usern |
| A3 | `UserRolePermissionSyncService`: Direkte Permission-Zuweisung entfernen | Anti-Pattern, bricht spätere Sync-Logik |
| A4 | P2.5 umsetzen: `SetCurrentPortal`-Middleware + Portal Global Scopes | Mandantentrennung ist Grundvoraussetzung |
### Kurzfristig (vor Go-Live-Rehearsal)
| # | Maßnahme | Warum |
|---|---|---|
| B1 | Drei `user_filter_presets`-Migrations zu einer zusammenführen | Migrations-Hygiene |
| B2 | Factories für Company, Contact, PressRelease, Category anlegen | Testbarkeit |
| B3 | `contact_user` in `04-DATA-MODEL.md` dokumentieren + Import-Strategie klären | Import-Korrektheit |
| B4 | Billing-Address-Update: Felder leerbar machen | Funktionsbug |
| B5 | `session()->flash()` durch Livewire-Events ersetzen | UX-Zuverlässigkeit |
| B6 | `gdpr_consent_at` im Registrierungsflow befüllen | DSGVO |
### Mittelfristig (P3-Abschluss)
| # | Maßnahme | Warum |
|---|---|---|
| C1 | PressRelease-CRUD auf echte Daten umstellen | Kernfeature |
| C2 | Categories-CRUD auf echte Daten umstellen | Abhängigkeit von PressRelease |
| C3 | Dashboard-Widgets mit echten Queries | Monitoring |
| C4 | `UserManagementTest.php` aufteilen | Testbarkeit |
| C5 | Policies für feinere Zugriffskontrolle | Sicherheit |
### P6-Vorbereitung (kritischer Pfad)
| # | Maßnahme | Warum |
|---|---|---|
| D1 | `.env` mit Legacy-DB-Connections konfigurieren (0.3) | Phase 6 blockiert |
| D2 | `app/Models/Legacy/`-Models anlegen | Phase 6 nicht startbar |
| D3 | `legacy:import-users` als ersten Command implementieren | Kritischer Pfad |
| D4 | Import-Command mit `--dry-run` und `--since` | D-18 |
---
## 8. Gesamtbewertung
**Umsetzungsqualität (bisherige Phasen):** Solide. Die Kern-Architektur ist gut durchdacht, die Entscheidungen (D-01 bis D-18) sind konsequent umgesetzt, das DB-Schema entspricht dem dokumentierten Zielmodell. Die Implementierungen in P3 (Company-CRUD, Kontakt-CRUD, User-CRUD) zeigen gute Livewire/Volt-Praxis.
**Größtes Risiko:** Phase P2.5 (Portal-Scoping) wurde übersprungen, aber in P3 weitergebaut. Wenn der Global Scope nachgezogen wird, müssen alle bestehenden Queries und Tests auf Korrektheit geprüft werden. Je länger das aufgeschoben wird, desto teurer wird die Nacharbeit.
**Kern-Gap:** Das Hauptfeature des Systems – Pressemitteilungen erstellen, einreichen, prüfen, veröffentlichen – ist vollständig als Dummy-Data implementiert. Das sollte die nächste Priorität nach der Security-Middleware sein.
**Import-Bereitschaft:** Die Tabellenstruktur ist bereit. Die Import-Commands, Legacy-Models und `.env`-Konfiguration fehlen noch vollständig. Phase 6 ist 3–4 Manntage Arbeit und der kritische Pfad zum Go-Live.

View file

@ -0,0 +1,174 @@
# 10 – Legacy-API-Kunden & Zugriffsbereinigung
## Ausgangspunkt
Das Legacy-System speichert API-Zugriffe nicht applikationsseitig. In Produktion ist Symfony-Logging deaktiviert (`logging_enabled: false`, `sfNoLogger`). API-Nutzung ist daher nicht zuverlässig aus der Anwendung rekonstruierbar.
Stattdessen wird der API-Cutover aus den migrierten Daten abgeleitet:
- Legacy-API-User sind in `users.registration_type = apiuser` bzw. über die Rolle `api-only` erkennbar.
- Legacy-API-Keys wurden bewusst nicht übernommen.
- Legacy-API-Credentials wie `apiReadAccess` / `apiWriteAccess` wurden nicht 1:1 pro User persistiert, sondern in neue Sanctum-Abilities übersetzt.
- Zahlungs-/Freigabestatus kann nur über importierte `legacy_invoices` bzw. später über Grandfathering-Daten bewertet werden.
## Legacy-Regeln
Im alten Symfony-Frontend gilt:
- API-fähige Routen sind in `apps/frontend/config/routing.yml` mit `api_access: true` markiert.
- `ApiAccessFilter` weist `X-ApiKey` auf nicht-API-Routen mit HTTP 501 ab.
- `ApiKeyGuardAuthFilter` liest `X-ApiKey` und sucht `sfGuardUserProfile.api_key`.
- Wird ein Profil gefunden, wird dessen User für den Request eingeloggt.
- Wenn bereits eine gültige Web-Session existiert, kann die API technisch auch ohne gültigen API-Key laufen, sofern Credentials passen.
- `SecureApiAccessFilter` verlangt:
- `GET` / `HEAD` → `apiReadAccess`
- `POST` / `PUT` / `DELETE` → `apiWriteAccess`
- andere Methoden → HTTP 405
- Einzelne Actions verlangen zusätzlich Symfony-Credentials wie `editor`.
- `ApiKeyGuardAuthFilter` prüft nicht selbst `is_active`.
## Neue Bewertungsregel
Für den Cutover erhalten nur noch Kunden Zugriff, die im migrierten System fachlich freigabefähig sind:
1. Kandidat ist `registration_type = apiuser` oder hat Rolle `api-only`.
2. User ist aktiv (`is_active = true`).
3. Die letzte importierte Legacy-Rechnung des Users hat `status = paid`.
Alle anderen Kandidaten werden nicht automatisch freigeschaltet:
- `needs_review`: aktiver API-Kandidat ohne importierte Legacy-Rechnung.
- `blocked`: inaktiver API-Kandidat oder letzte Rechnung nicht bezahlt.
## Report-Command
```bash
php artisan api:legacy-customers-report
php artisan api:legacy-customers-report --classification=eligible
php artisan api:legacy-customers-report --portal=businessportal24
php artisan api:legacy-customers-report --no-report
```
Der Command schreibt standardmäßig:
```bash
storage/app/private/migration/legacy-api-customers-*.json
```
Der Report enthält:
- Kandidatenregel und bekannte Datenlücken
- Zusammenfassung nach `eligible`, `needs_review`, `blocked`
- User-ID, E-Mail, Portal, Legacy-ID
- Rollen und Registration-Type
- letzte Legacy-Rechnung inkl. Status, Datum, Betrag
- empfohlene Aktion:
- `invite_to_generate_sanctum_token`
- `manual_billing_review_required`
- `do_not_grant_api_access`
## Aktuelle Datenlage
In der migrierten Datenbank wurden bisher gefunden:
- 195 User mit `registration_type = apiuser`
- 1 API-User mit archivierter Legacy-Rechnung
- diese Rechnung ist bezahlt
Damit ist nach aktueller Datenlage nur ein API-User automatisch freigabefähig. Die übrigen API-User müssen entweder manuell geprüft werden oder benötigen zusätzliche Legacy-Billing-/Vertragsdaten.
## Noch fehlende Daten
Für eine harte finale Entscheidung fehlen ggf.:
- vollständige Legacy-Zuordnung von API-Usern zu zahlenden Vertrags-/Billing-Usern, falls API-User nicht selbst Rechnungsempfänger waren
- explizite Legacy-Credential-Zuordnung `apiReadAccess` / `apiWriteAccess` pro User, falls feiner als `apiuser`/`api-only` benötigt
- aktuelle fachliche Liste aktiver API-Kunden vom Auftraggeber, falls Rechnungsarchiv nicht ausreicht
Ohne diese Daten ist die sichere Standardeinstellung:
- nur `eligible` automatisch zur Token-Migration einladen
- `needs_review` nicht automatisch freischalten
- `blocked` nicht freischalten
## Token-Erstellung im neuen System
Die Customer-Seite für API-Tokens ist zusätzlich technisch abgesichert. Ein User kann nur dann einen Sanctum-Token erstellen, wenn:
1. `users.is_active = true`
2. mindestens eine der folgenden Bedingungen erfüllt ist:
- aktiver neuer Zahlungsstatus in `user_payment_options` (`status = active`, aktueller Zeitraum gültig)
- aktiver Bestandsschutz in `user_payment_options` (`status = grandfathered`, `grandfathered_until >= today`)
- Übergangsweise: letzte importierte Legacy-Rechnung ist `paid`
Damit bekommen reaktivierte oder neue Kunden künftig keinen API-Zugang über alte Rechnungen, sondern erst über die neue aktive Zahlungs-/Vertragslogik. Bereits erstellte Tokens inaktiver User werden zusätzlich bei API-Requests blockiert.
## API-Usage-Logging
Neue API-Requests werden persistent in `api_usage_logs` protokolliert:
- `user_id`
- `personal_access_token_id`
- HTTP-Methode
- Pfad
- Route-Name
- Statuscode
- IP-Adresse
- User-Agent
- Dauer in Millisekunden
- Zeitpunkt
Nicht gespeichert werden:
- Bearer-Token im Klartext
- Legacy-`api_key`
- Request-Payloads
Auch abgewiesene Legacy-Key-Requests (`410 Gone`) und Requests inaktiver User (`403`) werden geloggt.
## API-Rate-Limiting
Die `/api/v1`-Routen sind zusätzlich per Middleware `EnsureApiTokenRateLimit` limitiert:
- Limit: 60 Requests pro Minute
- bevorzugter Schlüssel: Sanctum-/Bearer-Token-ID
- Fallback: SHA-256-Fingerprint des Bearer-Tokens, danach User-ID/IP
- inaktive User werden vorher per `EnsureApiUserIsActive` blockiert
- überschrittene Limits liefern HTTP 429 mit `Retry-After`, `X-RateLimit-Limit` und `X-RateLimit-Remaining`
Damit wird ein einzelner stark nutzender Client begrenzt, ohne andere Tokens desselben Users sofort mitzusperren.
## API-Usage-Reporting
Die protokollierten API-Requests können per Artisan-Command ausgewertet werden:
```bash
php artisan api:usage-report
php artisan api:usage-report --from=2026-04-01 --to=2026-04-30
php artisan api:usage-report --user=123
php artisan api:usage-report --status=403
php artisan api:usage-report --no-report
```
Der Command schreibt standardmäßig:
```bash
storage/app/private/migration/api-usage-*.json
```
Der Report enthält:
- Gesamtzahl Requests
- eindeutige User und Sanctum-Tokens
- 2xx-/4xx-/5xx-Verteilung
- durchschnittliche Dauer
- Top-Pfade
- Statuscode-Verteilung
- Top-User und Top-Tokens
- letzte Requests im gefilterten Zeitraum
Damit ist die neue API-Nutzung ab Cutover nicht mehr von Webserver-Logs abhängig und kann regelmäßig exportiert oder später in eine Admin-UI übernommen werden.
## P7-Abgrenzung Newsletter
`newsletter/unsubscribe` wird bewusst nicht als Legacy-API-Endpoint umgesetzt. Es gibt dafür keinen relevanten Legacy-API-Client. Newsletter-Abmeldung und externer Newsletter-Dienst werden nach der Migration im neuen Frontend sauber neu implementiert.

View file

@ -0,0 +1,390 @@
# 11 – Noch zu tun: Optimierungen
**Stand:** 2026-04-29
**Kontext:** Admin-Performance, Livewire/Volt-Aufteilung, Slow-Request-Auswertung
Dieses Dokument fasst die noch offenen Optimierungspunkte nach der bisherigen Admin-Performance-Runde zusammen. Bereits umgesetzt sind u. a. aggregierte Dashboard-Queries, Admin-Statistik-Caches, Fulltext-Suche mit Fallback, kleinere Pagination, optimierte `withCount()`-Nutzung, Slow-Admin-Request-Logging, eigener `admin_slow`-Log-Kanal, Artisan-Report und eine Admin-Report-Seite.
---
## 1. Slow-Admin-Reports mit echten Daten auswerten
**Priorität:** hoch
**Ziel:** Nicht weiter auf Verdacht optimieren, sondern anhand echter Slow-Requests.
### Aktueller Stand
Slow Admin Requests werden in den eigenen Daily-Log-Kanal geschrieben:
```bash
storage/logs/admin-slow-*.log
```
Die Auswertung ist verfügbar über:
```bash
php artisan admin:slow-requests
```
und im Admin unter:
```text
Reports > Performance
```
### Noch zu tun
1. Admin mit realistischem Datenbestand bedienen.
2. Typische langsame Seiten gezielt öffnen:
- Dashboard
- Benutzerliste
- Benutzer bearbeiten
- Firmenliste
- Firma anzeigen
- Kontakte
- Pressemitteilungen
- Reports > Performance
3. Danach den Report auswerten:
```bash
php artisan admin:slow-requests --top=20 --limit=50
```
4. Auffällige Routen priorisieren:
- höchste `duration_ms`
- höchste `database_time_ms`
- höchste `query_count`
- wiederkehrende Slow Queries
### Ergebnis
Die nächsten Optimierungen sollten aus dem Report abgeleitet und nicht pauschal durchgeführt werden.
---
## 2. EXPLAIN für Top-Slow-Queries prüfen
**Priorität:** hoch
**Ziel:** Sicherstellen, dass neue Indexe und Fulltext-Indexe von MySQL/PostgreSQL tatsächlich genutzt werden.
### Aktueller Stand
Der Slow-Request-Reporter versucht für SELECT/CTE-Slow-Queries automatisch ein `EXPLAIN` bzw. bei SQLite `EXPLAIN QUERY PLAN`.
### Noch zu prüfen
Besonders relevant sind Queries auf:
- `press_releases`
- `companies`
- `contacts`
- `users`
- `categories`
- `category_translations`
### Prüffragen
- Wird bei Suchabfragen der Fulltext-Index genutzt?
- Wird bei Listen `ORDER BY created_at` / `ORDER BY id` ein passender Index genutzt?
- Sind Sortierung und Filter in derselben Index-Reihenfolge abgedeckt?
- Gibt es noch `SCAN TABLE` / Full Table Scans auf großen Tabellen?
- Gibt es `whereHas()`/`has()`-Queries, die durch `distinct()`/Aggregationen ersetzt werden sollten?
### Mögliche Folgearbeiten
- Zusätzliche zusammengesetzte Indexe ergänzen.
- Sortier-/Filterkombinationen in Listen begrenzen.
- Query-Formulierungen vereinfachen, wenn der Planner Indexe nicht nutzt.
---
## 3. Livewire-Komponenten gezielt aufteilen
**Priorität:** hoch bis mittel
**Ziel:** Große Admin-Komponenten wartbarer machen und teure Teilbereiche als Livewire-Inseln isolieren.
Livewire-Aufteilung ist nicht pauschal ein Performance-Gewinn. Sie lohnt sich vor allem, wenn ein Teilbereich eigene Daten lädt, eigene Live-Suche hat oder unabhängig rerendern kann.
### 3.1 `admin.users.edit` aufteilen
**Priorität:** hoch
**Datei:** `resources/views/livewire/admin/users/edit.blade.php`
Diese Komponente ist der größte Refactoring-Kandidat. Sie bündelt derzeit:
- User-Basisdaten
- Profil
- Rollen
- Firmenzuordnung
- Kontaktzuordnung
- Kontaktbearbeitung
- Rechnungsadresse
- Live-Suchen
- direkte Persistierung bei Link/Unlink-Aktionen
### Empfohlenes Zielbild
`admin.users.edit` bleibt Page-Wrapper für:
- User-ID
- Basisdaten
- Save-Flow
- grobe Navigation / Layout
Auslagern:
1. `admin.users.partials.profile-form`
- zunächst Blade-Partial ohne eigenes Livewire
- geringes Risiko
2. `admin.users.partials.billing-address-form`
- zunächst Blade-Partial ohne eigenes Livewire
- geringes Risiko
3. `admin.users.company-links`
- eigene Volt-Komponente
- verwaltet Firmen-Live-Suche, Rollen pro Firma, Link/Unlink
- reduziert Rerenders im Hauptformular
4. `admin.users.contact-links`
- eigene Volt-Komponente
- verwaltet Kontakt-Live-Suche und Kontakt-Zuordnung
- sollte nach `company-links` umgesetzt werden
### Empfohlene Reihenfolge
1. Nur Blade-Partials extrahieren, keine Logik ändern.
2. Tests unverändert laufen lassen.
3. Firmenzuordnung in eigene Komponente ziehen.
4. Kontaktzuordnung in eigene Komponente ziehen.
5. Danach Slow-Reports vergleichen.
### Risiken
- Aktuell persistieren Link/Unlink-Aktionen direkt.
- Firmen- und Kontaktzuordnung hängen fachlich zusammen.
- Zu starke Trennung kann Events/State-Synchronisierung unnötig komplex machen.
Deshalb nur schrittweise aufteilen.
---
## 4. `companies.show` Kontakte-Tab isolieren
**Priorität:** mittel
**Datei:** `resources/views/livewire/admin/companies/show.blade.php`
### Aktueller Stand
Die Komponente lädt Übersichtsdaten, Pressemitteilungen, User, Counts und bei aktivem Kontakte-Tab zusätzlich Kontakte und Kontakt-Lookup.
### Zielbild
`admin.companies.show` bleibt für:
- Firmenkopf
- Übersicht
- Pressemitteilungen
- User-Zuordnungen
Neue Komponente:
```text
admin.companies.contacts-tab
```
Aufgaben:
- Kontaktliste laden
- Kontakt-Suche
- vorhandene Kontakte zuordnen
- Limit-/Hinweislogik für große Kontaktmengen
### Nutzen
- Kontakte-Tab lädt unabhängig.
- Suchen im Kontakte-Tab rerendern nicht die komplette Firmen-Detailseite.
- Klarere Verantwortlichkeit.
---
## 5. Listen erst nach Messwerten weiter aufteilen
**Priorität:** mittel bis niedrig
Diese Komponenten sind bereits deutlich optimiert und sollten nicht ohne Slow-Report-Befund refactored werden:
- `admin.users`
- `admin.contacts.index`
- `admin.companies.index`
- `admin.press-releases.index`
- `admin.categories.index`
### Bereits umgesetzt
- `simplePaginate(50)`
- aggregierte Stats
- zentrale Cache-Invalidierung
- Fulltext-Suche mit Fallback
- limitierte Combobox-/Lookup-Ergebnisse
- `withCount()` nur gezielt oder nachträglich für sichtbare Datensätze
### Noch möglich
Nur bei messbarer Langsamkeit:
- weitere Sortieroptionen begrenzen
- zusätzliche zusammengesetzte Indexe
- einzelne Filter als eigene Komponenten
- Export-/Bulk-Actions asynchronisieren
---
## 6. Billing-Bereiche finalisieren
**Priorität:** mittel
**Dateien:**
- `resources/views/livewire/admin/invoices/index.blade.php`
- `resources/views/livewire/admin/payments/index.blade.php`
- `resources/views/livewire/admin/coupons/index.blade.php`
### Aktueller Stand
Diese Bereiche enthalten noch Dummy-/Platzhalterdaten.
### Noch zu tun bei echter Implementierung
- echte Models anbinden
- `simplePaginate()` statt Vollmengen
- aggregierte Stats statt Einzelcounts
- Indexe für Filter/Sortierung
- Suchfilter mit passenden Datenbankindizes
- Tests für Filter, Pagination und Berechtigungen
---
## 7. Rollen-Index nur bei Bedarf optimieren
**Priorität:** niedrig
**Datei:** `resources/views/livewire/admin/roles/index.blade.php`
### Aktueller Stand
Die Rollenübersicht lädt alle Rollen mit:
```php
withCount(['users', 'permissions'])
```
Das ist bei wenigen Systemrollen unproblematisch.
### Optimieren, falls:
- viele Custom-Rollen entstehen
- der Slow-Report `admin.roles.index` auffällig zeigt
### Mögliche Maßnahmen
- Pagination
- Cache für Rollenübersicht
- Counts nur bei Bedarf anzeigen
---
## 8. Optional: Slow-Requests zusätzlich in Datenbank persistieren
**Priorität:** optional
**Ziel:** Langfristige Trends statt reiner Log-Auswertung.
### Aktueller Stand
Logs sind bewusst schlank und reichen für Debugging.
### Datenbank wäre sinnvoll, wenn:
- historische Trends benötigt werden
- Performance-Regressionen über Releases verglichen werden sollen
- Reports filterbar über längere Zeiträume bleiben müssen
- Log-Rotation Daten zu früh entfernt
### Mögliche Tabelle
```text
admin_slow_requests
```
Felder:
- `route_name`
- `path`
- `method`
- `status_code`
- `user_id`
- `duration_ms`
- `database_time_ms`
- `query_count`
- `slow_queries` JSON
- `requested_at`
Vorerst nicht zwingend notwendig.
---
## 9. Optional: Report-UI erweitern
**Priorität:** niedrig bis mittel
Die neue Seite `Reports > Performance` kann später erweitert werden um:
- CSV-Export
- Zeitraum-Schnellfilter: heute, 7 Tage, 30 Tage
- Gruppierung nach Tag/Stunde
- direkte Links zu Admin-Routen, sofern Parameter rekonstruierbar sind
- Badges für Schwellenwerte
- Vergleich vor/nach Optimierung
---
## 10. Empfohlene nächste Umsetzung
Wenn keine neuen Slow-Report-Befunde vorliegen:
1. `users.edit` in Blade-Partials für Profil und Rechnungsadresse aufteilen.
2. Tests unverändert laufen lassen.
3. Firmenzuordnung als eigene Volt-Komponente extrahieren.
4. Kontaktzuordnung als eigene Volt-Komponente extrahieren.
5. Danach Slow-Report erneut vergleichen.
Wenn Slow-Report-Befunde vorliegen:
1. langsamste Route identifizieren.
2. Top-Slow-Queries mit EXPLAIN prüfen.
3. nur diese Route/Query optimieren.
4. Test ergänzen.
5. erneute Messung.
---
## 11. Aktuelle Empfehlung
Der nächste konkrete Code-Schritt sollte sein:
```text
admin.users.edit refactor phase 1
```
Umfang:
- Profil-Formular in Partial auslagern
- Rechnungsadresse in Partial auslagern
- keine Logikänderung
- bestehende `UserManagementTest`-Tests weiterverwenden
Danach:
```text
admin.users.company-links als eigene Volt-Komponente
```
Das bringt den besten Mix aus Wartbarkeit, geringerem Rerender-Risiko und überschaubarem Refactoring-Risiko.

View file

@ -0,0 +1,286 @@
# 12 – Nächste Schritte (Roadmap)
> ⚠️ **VERALTET (eingefroren am 04.05.2026).** Die aktive Arbeitsliste ist seit dem
> 03.07.2026 **`dev/release/entwicklungsplan-release.md`** — dieses Dokument nur noch
> als historische Referenz nutzen. Wichtigste Abweichungen zum Stand unten:
> Phase 8 (Stripe-Billing inkl. Wallet/Add-ons/Grandfathering) ist seit 12.06. **fertig**,
> die Legacy-Rechnungen sind importiert/archiviert, Widerrufs- und Kündigungsbutton
> (§ 312k BGB) sind umgesetzt, die Test-Suite liegt bei **900+ Tests grün**.
> Offen sind im Wesentlichen noch [EXTERN]-Punkte (Stripe-Live-Keys, Rechtstexte,
> Kontaktdaten, Favicons) sowie QA/Deploy — Details im Release-Plan.
**Stand:** 2026-05-04
**Quellen:** [`03-MIGRATION-PLAN.md`](./03-MIGRATION-PLAN.md), [`08-PROGRESS.md`](./08-PROGRESS.md), [`11-NOCH-ZU-TUN-OPTIMIERUNGEN.md`](./11-NOCH-ZU-TUN-OPTIMIERUNGEN.md), [`09-REVIEW-QUALITAET.md`](./09-REVIEW-QUALITAET.md)
Dieses Dokument bündelt **alles, was laut Entwicklungsplan noch offen ist**, in einer einzigen Übersicht – nach Priorität und Phase sortiert. Stand der Test-Suite: **180 Tests grün, 3 geskippt, 0 Fehler**.
**Legende:** 🔴 hoch · 🟡 mittel · 🟢 niedrig · ⏸️ vertagt · ⬜ offen · 🔄 in Arbeit
---
## 1. Schnell-Status
### Was ist fertig
- Phase 0 – Vorbereitung (bis auf Stripe-Daten vom Auftraggeber)
- Phase 1 – Fundament (Schema, Models, Factories, Seeder, Migrations grün)
- Phase 2 – Auth & Tenancy (Magic-Link, Portal-Scope, Portal-Switcher, Roll-Aware-Login-Redirect)
- Phase 3 – Admin-UI (Press Releases, Categories, Footer-Codes, Companies, Contacts, Users, Roles **alle real angebunden**)
- Phase 4 – Customer-Portal (jetzt als „Mein Bereich" unter `/admin/me/*` ins Admin-Panel konsolidiert)
- Phase 5 (teilweise) – PressReleaseService inkl. Reject-Reason + Audit-Log, BlacklistService, ImageService, HasUniqueSlug-Trait
- Phase 6 – Daten-Migration: Kern komplett (Users/Companies/Contacts/PMs/Bilder, idempotent, Bilder-Sync); **Legacy-Rechnungen brauchen einen Vollimport-Nachziehschritt**
- Phase 7 – API v1 vollständig (Endpunkte, Sanctum, Rate-Limit, OpenAPI, Tests, Legacy-Cut, Kundenreport)
### Was steht als Nächstes an
1. **Externe Abhängigkeit klären** (siehe §2): Stripe-Keys + Grandfathering-Kriterien vom Auftraggeber.
2. **Legacy-Rechnungen vollständig importieren** (siehe §8): DB-Struktur prüfen/erweitern, Import-Skript finalisieren, Status + User-Zuordnung erhalten, PDF-Erzeugung aus Legacy-Daten beim Abruf.
3. **PM-Veröffentlichung abrunden**: Inline-Edit durch Admin, Bulk-Aktionen, In-App-Notifications (Bell-Icon).
4. **Newsletter wieder aufnehmen** (P3.7 + P5.3 – aktuell vertagt).
5. **Phase 8 (Stripe-Billing)** als größter Brocken – sobald Stripe-Daten da sind.
6. **Phase 10 (Härtung) + 11 (Deployment)** – Security-Review, Coverage-Ziel, Staging-Rehearsal, DNS-Cutover.
---
## 2. Externe Blocker (Auftraggeber liefert)
Diese Punkte können wir nicht selbst anstoßen – ohne sie blockt P8 und teilweise P11.
| # | Aufgabe | Plan-Ref | Wirkung |
|---|---|---|---|
| ⬜ 0.5 | Stripe-Test-Account + API-Keys | 03-MP §0 | P8 komplett blockiert |
| ⬜ 0.6 | Liste der neuen Stripe-Produkte (Preisliste) | 03-MP §0 | P8.2 blockiert |
| ⬜ 0.7 | Liste der aktiven Alt-Abos (oder Kriterien) für Grandfathering | 03-MP §0 | P6.6 + P8.8 blockiert |
| ⬜ 11.4 | Finale Texte fürs Go-Live-Mailing (Passwort-Reset/Sicherheitshinweis) | 03-MP §11 | Versand am Go-Live blockiert; Command + Mailable selbst sind fertig |
| ⬜ 11.5 | Freigabeliste + operative Mailtexte für API-Kunden-Migration | 03-MP §11 | Versand blockiert; Kundenreport selbst ist fertig |
**Empfehlung:** Diese fünf Punkte als kompakte Mail/Liste an den Auftraggeber – ohne sie kann der Go-Live-Pfad nicht abgeschlossen werden.
---
## 3. Pressemitteilungs-Block: Nice-to-Have-Erweiterungen
Der Kern (Erstellen → Submit → Review → Publish/Reject mit Reason + Audit-Log) ist fertig. Diese Erweiterungen würden den Workflow noch runder machen:
| # | Aufgabe | Priorität | Aufwand |
|---|---|---|---|
| ⬜ pm-x1 | **Inline-Edit + „Speichern & Veröffentlichen"** auf einer Seite im Admin – heute zwei separate Schritte | 🟡 | 0.25 d |
| ⬜ pm-x2 | **Bulk-Aktionen im Admin-Index**: mehrere PMs auf einmal `Veröffentlichen`/`Ablehnen` (mit gemeinsamer Begründung) | 🟡 | 0.5 d |
| ⬜ pm-x3 | **In-App-Notifications** (Bell-Icon in der Sidebar) für Customer: „PM veröffentlicht/abgelehnt" – ergänzt heutige Mail | 🟡 | 0.5–1 d |
| ⬜ pm-x4 | **Permission-Granularität**: `press-releases:review` (lesen + freigeben) vs. `press-releases:publish` (final veröffentlichen) trennen, falls Editor nicht direkt veröffentlichen soll | 🟢 | 0.25 d |
---
## 4. Phase 1 – Restarbeiten
| # | Aufgabe | Priorität | Status |
|---|---|---|---|
| 1.8 | `DefaultAdminUserSeeder` + `SalutationTranslationSeeder` ergänzen | 🟢 | 🔄 |
| 1.10 | Zentrale Factory-Smoke-Suite (alle Factories erzeugen valide Models) | 🟢 | 🔄 |
---
## 5. Phase 3 – Admin-UI: Restarbeiten
| # | Bereich | Aufgabe | Priorität | Status |
|---|---|---|---|---|
| 3.7 | Newsletter | Subscriber-Liste + CSV Im-/Export | 🟡 | ⏸️ vertagt 2026-05-04 |
| 3.8 | Invoices (neu) | Liste, Detail, Stripe-Status, PDF-Download | 🔴 | ⬜ wartet auf P8 |
| 3.9 | Legacy-Invoices Admin | Admin-Spiegel zur Customer-Sicht finalisieren | 🟡 | ✅ Übersicht mit Filtern, Mapping-Hinweisen und PDF-Download umgesetzt; Navigation heißt **Legacy Rechnungen** |
| 3.10 | Payments | Liste + Detail | 🔴 | ⬜ wartet auf P8 |
---
## 6. Phase 4 – Customer-Portal: Restarbeiten
| # | Aufgabe | Priorität | Status |
|---|---|---|---|
| 4.2 | Customer-Dashboard: aktive Abos + offene neue Rechnungen | 🟡 | 🔄 wartet auf P8 |
| 4.4 | Neue Rechnungen einsehen & PDF-Download | 🔴 | 🔄 wartet auf P8 |
---
## 7. Phase 5 – Domain-Services: Restarbeiten
| # | Aufgabe | Priorität | Status |
|---|---|---|---|
| 5.3 | `NewsletterService` neu (Subscribe / Confirm / Unsubscribe mit Token) | 🟡 | ⏸️ vertagt 2026-05-04 |
| 5.4 | `CompanyService` (Aktivierung, Logo-Upload, Footer-Code-Management) | 🟡 | ⬜ |
| 5.7 | Domain-Events + Listeners (`PressReleasePublished` → `SendPublishNotification` etc.) | 🟢 | 🔄 nur Mailables, keine echten Events |
| 5.8 | Vollständige Service-Test-Suite (Coverage > 70 %) | 🟡 | 🔄 |
> **Hinweis:** `ImageService` (5.5) hat noch einen offenen Restpunkt: **Legacy-Thumbnail-Redirect** – kein Blocker, aber nice für SEO-Kontinuität alter Bild-URLs.
---
## 8. Phase 6 – Daten-Migration: Restarbeiten
| # | Aufgabe | Priorität | Status |
|---|---|---|---|
| 6.5d | **Legacy-Rechnungen Vollimport**: alle bestehenden Rechnungen aus den Legacy-DBs inkl. Status, Beträgen, Datum, Zahlart, vollständigem Raw-Snapshot und User-Zuordnung importieren; `legacy:archive-invoices` schreibt Import-Report + PDF-Payload; PDF-Erzeugung bleibt DB-basiert/on-demand statt Datei-Migration | 🔴 | ✅ umgesetzt 2026-05-04; Rehearsal gegen Produktiv-Snapshot bleibt P6.10 |
| 6.6 | `legacy:grandfather-subscriptions` (aktive Alt-Abos übernehmen) | 🔴 | ✅ umgesetzt 2026-06-12 (Kriterien vom Auftraggeber: Quelle ist das Rechnungsarchiv — jüngste Rechnung pro Vereinbarung mit `recurring` + `active`; Übernahme als `grandfathered` mit `current_period_end = next_due_date`, MAN-Kreis fakturiert weiter; Replay-fähig, Rehearsal bleibt P6.10) |
| 6.10 | **Rehearsal-Lauf** gegen produktiven Snapshot auf Staging | 🔴 | ⬜ |
**Wichtig für 6.5d:** `legacy:archive-invoices` importiert jetzt Rechnungsdaten, Billing-Adress-Snapshot und User-Payment-Snapshot in `legacy_invoices.raw_snapshot`/`pdf_payload`, zählt unzugeordnete Legacy-User im Report und lässt diese Rechnungen trotzdem im Archiv. Für Legacy-Rechnungen bleibt die bestehende Logik erhalten: Rechnung liegt als Datenbankdatensatz vor und das PDF wird bei Bedarf auf Knopfdruck aus diesen Daten erzeugt. Neue Stripe-Rechnungen werden separat in P8 geplant. Der finale Nachweis der Vollständigkeit erfolgt weiterhin im Staging-Rehearsal mit aktuellem Produktiv-Snapshot.
---
## 9. Phase 8 – Stripe-Billing (komplett offen, größter Brocken)
Hängt komplett an §2 (Stripe-Account + Produktliste).
| # | Aufgabe | Priorität | Aufwand |
|---|---|---|---|
| 8.1 | Stripe-Setup (`.env`, Cashier vs. raw `stripe-php`) | 🔴 | 0.25 d |
| 8.2 | `PaymentOption`-Admin + Sync zu Stripe Products/Prices (`stripe:sync-products`) | 🔴 | 0.5 d |
| 8.3 | Checkout-Flow im Customer-Portal (Stripe Checkout Session + Rückkehr-URL) | 🔴 | 0.5 d |
| 8.4 | Webhook-Controller `/webhooks/stripe` + Event-Handler (checkout-session-completed, invoice-paid, invoice-payment-failed, subscription-deleted) | 🔴 | 0.5 d |
| 8.5 | `InvoiceNumberGenerator` (`{YYYY}{MM}-{seq}`) | 🟡 | 0.25 d |
| 8.6 | PDF-Blade + `InvoiceService` | 🟡 | 0.25 d |
| 8.7 | Mahnwesen vereinfacht (`invoices:remind` für Stripe-Fails) | 🟡 | 0.25 d |
| 8.8 | **Grandfathering-Handler** (`ExpireGrandfatheredSubscriptions`-Scheduler + Customer-Mail) | 🔴 | 0.25 d |
| 8.9 | Legacy-Rechnungen Archiv-UI im Admin finalisieren + DB-basierte Legacy-PDF-Erzeugung absichern | 🟡 | ✅ |
| 8.10 | Tests mit Stripe-Test-Mode (Fake-Webhooks) | 🔴 | 0.25 d |
**Gesamt P8:** ~2.5–3 Manntage netto, sobald Stripe-Daten vorliegen.
---
## 10. Phase 9 – Scheduler & Queues
| # | Aufgabe | Priorität | Status |
|---|---|---|---|
| 9.1 | Supervisor/Queue-Worker-Setup dokumentieren (Dev + Prod) | 🟡 | ⬜ |
| 9.2 | Cron-Jobs für `invoices:remind`, `grandfather:expire`, `newsletter:campaign-digest` (optional) ergänzen | 🟡 | 🔄 (`magic-links:purge` + `press-releases:purge-drafts` da, P8/P5-Jobs fehlen) |
| 9.3 | Failure-Alerting (Log-Channel + Mail bei fehlgeschlagenen Jobs) | 🟡 | ⬜ |
---
## 11. Phase 10 – Testing & Härtung
| # | Aufgabe | Priorität | Status |
|---|---|---|---|
| 10.1 | Coverage-Ziel **70 %** auf `app/Services` und `app/Actions` | 🔴 | ⬜ |
| 10.2 | Architecture-Tests (Pest `arch()`) – z. B. „kein Volt-Volt darf direkt `Mail::send` aufrufen" | 🟡 | ⬜ |
| 10.3 | `php artisan test --parallel` grün | 🟡 | ⬜ |
| 10.4 | Manuelle QA-Checkliste durchgehen (siehe `06-FEATURES-SCOPE.md §15`) | 🔴 | ⬜ |
| 10.5 | **Security-Review**: CSRF, CORS, Rate-Limits, File-Uploads, Magic-Link-TTL | 🔴 | ⬜ |
---
## 12. Phase 11 – Deployment
| # | Aufgabe | Priorität | Status |
|---|---|---|---|
| 11.1 | Produktiv-DB-Server provisionieren + Backups konfigurieren | 🔴 | ⬜ |
| 11.2 | Staging-Deployment + Smoke-Tests | 🔴 | ⬜ |
| 11.3 | Produktiv-Import-**Rehearsal** gegen aktuellen Legacy-Snapshot | 🔴 | ⬜ |
| 11.4 | Go-Live-Mailing (Passwort-Reset + Sicherheitshinweis) versenden | 🔴 | 🔄 (Code fertig, Texte/Versand offen) |
| 11.5 | API-Kunden-Kommunikation (Token-Migration) versenden | 🔴 | 🔄 (Report fertig, Texte/Versand offen) |
| 11.6 | DNS-Cutover-Plan: `pressekonto.de` auf neuen Server | 🔴 | ⬜ |
| 11.7 | Read-Only-Modus auf Legacy-Systemen während Final-Import | 🔴 | ⬜ |
| 11.8 | Abschalten der alten Symfony-Server (nach Review-Periode) | 🟡 | ⬜ |
---
## 13. Performance-Optimierungen (aus 11-NOCH-ZU-TUN)
Nicht zwingend für Go-Live nötig, aber fürs Polishing:
| # | Aufgabe | Priorität | Quelle |
|---|---|---|---|
| ⬜ opt-1 | Slow-Admin-Reports mit echten Daten füllen + auswerten | 🔴 | 11-NOCH §1 |
| ⬜ opt-2 | EXPLAIN auf Top-Slow-Queries laufen lassen, Indexe nachziehen | 🔴 | 11-NOCH §2 |
| ⬜ opt-3 | `admin.users.edit` aufteilen (Profil-Partial → BillingAddress-Partial → Volt-Komponente `company-links` → Volt-Komponente `contact-links`) | 🟡 | 11-NOCH §3.1 |
| ⬜ opt-4 | `admin.companies.show` – Kontakte-Tab als eigene Volt-Komponente isolieren | 🟡 | 11-NOCH §4 |
| ⬜ opt-5 | Listen weiter splitten (nur bei messbarer Langsamkeit, nicht prophylaktisch) | 🟢 | 11-NOCH §5 |
| ⬜ opt-6 | Billing-Dummy-Views durch echte Implementierung ersetzen | 🔴 | 11-NOCH §6 (= P3.8/3.10/3.11 in P8) |
| ⬜ opt-7 | Rollen-Index nur bei Bedarf optimieren (Pagination/Cache) | 🟢 | 11-NOCH §7 |
| ⬜ opt-8 | Slow-Requests zusätzlich in DB persistieren (Trends über Releases) | 🟢 | 11-NOCH §8 |
| ⬜ opt-9 | Report-UI erweitern (CSV-Export, Zeitraum-Schnellfilter, Vergleichsmodi) | 🟢 | 11-NOCH §9 |
---
## 14. Restpunkte aus dem Qualitätsreview (09-REVIEW)
Die kritischen Punkte aus §2 sind durchgängig behoben. Aus §3/§4 stehen noch aus:
| # | Aufgabe | Priorität | Quelle |
|---|---|---|---|
| ⬜ rev-1 | `gdpr_consent_at` im Registrierungsflow befüllen | 🔴 | 09-REVIEW §3 / §4.6 |
| ⬜ rev-2 | `UserManagementTest.php` (820 Zeilen) in `tests/Feature/Admin/UserCrudTest.php`, `CompanyCrudTest.php`, `ContactCrudTest.php`, `FilterPresetTest.php` aufteilen | 🟡 | 09-REVIEW §3.5 |
| ⬜ rev-3 | `session()->flash()` in inline Volt-Aktionen (ohne Redirect) durch `$this->dispatch('notify', ...)` ersetzen | 🟡 | 09-REVIEW §3.6 |
| ⬜ rev-4 | `04-DATA-MODEL.md` um `contact_user`-Pivot ergänzen (Doku-Lücke) | 🟢 | 09-REVIEW §3.4 |
| ⬜ rev-5 | Drei `user_filter_presets`-Migrations zu einer zusammenführen (Hygiene vor Go-Live) | 🟢 | 09-REVIEW §3.2 |
| ⬜ rev-6 | ENUM → VARCHAR mit Enum-Cast prüfen, falls beim Rehearsal Lock-Probleme auf großen Tabellen auftreten | 🟢 | 09-REVIEW §3.7 |
---
## 15. Empfohlene Reihenfolge der nächsten Sessions
Folgende Tranchen-Reihenfolge priorisiert das Erreichen der Produktionsreife:
### Tranche A – Legacy-Rechnungen Vollimport ✅ erledigt 2026-05-04
1. P6.5d Source-Audit: Legacy-Rechnungstabellen, Statuswerte, benötigte PDF-Daten und User-Mapping geprüft
2. `legacy:archive-invoices` zu vollständigem, idempotentem Import-/Report-Skript ausgebaut
3. DB-Vorbereitung mit `pdf_payload` + `pdf_generated_at` nachgezogen
4. Customer-PDF-Abruf auf DB-basierte Legacy-PDF-Erzeugung abgesichert
### Tranche B – PM-Block abrunden (~1 Tag, sofort möglich)
5. `pm-x1` Inline-Edit + „Speichern & Veröffentlichen" im Admin
6. `pm-x2` Bulk-Aktionen im Admin-Index
7. `pm-x3` In-App-Notifications (Bell-Icon)
8. `rev-1` `gdpr_consent_at` im Registrierungsflow (DSGVO)
### Tranche C – Newsletter-Wiederaufnahme (~1 Tag)
9. P5.3 `NewsletterService` (Subscribe/Confirm/Unsubscribe mit Token)
10. P3.7 Admin-Newsletter-UI (Subscriber-Liste + CSV)
### Tranche D – Service-Härtung & Test-Coverage (~1 Tag)
11. P5.4 `CompanyService`
12. P5.7 Domain-Events einführen
13. P10.1 Coverage auf 70 % heben
14. P10.2 `arch()`-Tests
### Tranche E – Performance-Polishing (~0.5 Tage, optional)
15. `opt-1` + `opt-2` Echte Slow-Reports auswerten + EXPLAIN
16. `opt-3` `admin.users.edit` aufteilen
### Tranche F – Wartet auf Auftraggeber: P8 Stripe (~2.5–3 Tage)
17. Sobald Stripe-Daten vorliegen: P8 komplett (8.1 – 8.10)
18. P3.8 + P3.10 (Invoices/Payments-Admin echt anbinden)
19. P4.2 + P4.4 (Customer-Dashboard + neue Rechnungen)
### Tranche G – Phase 9 + 10 + 11 (~2–3 Tage)
20. P9.1 – 9.3 Queue/Scheduler/Alerting
21. P10.4 + 10.5 QA-Checkliste + Security-Review
22. P11.1 – 11.3 Staging-Deployment + Rehearsal
23. P11.4 + 11.5 Mailings versenden
24. P11.6 – 11.8 DNS-Cutover + Legacy-Read-Only + Abschaltung
---
## 16. Aufwandsschätzung Rest
| Block | Aufwand | Abhängigkeiten |
|---|---|---|
| Tranche A (Legacy-Rechnungen Vollimport) | ✅ erledigt | Produktiv-Snapshot-Rehearsal bleibt P6.10 |
| Tranche B (PM-Polishing) | ~1 d | – |
| Tranche C (Newsletter) | ~1 d | – |
| Tranche D (Service-Härtung) | ~1 d | – |
| Tranche E (Performance) | ~0.5 d | – |
| Tranche F (Stripe + Billing) | ~3 d | Stripe-Daten ⬜ |
| Tranche G (Deployment) | ~2.5 d | Tranche F + Auftraggeber-Texte |
| **Σ Restaufwand** | **~9.5–10 Manntage** | – |
Der Endspurt zum Go-Live (Tranche F + G) ist davon ~5.5 Tage und hängt komplett am Auftraggeber-Input.
---
## 17. Zusammenfassung: Was kann **heute** ohne externen Input passieren?
- Tranche B (PM-Polishing) – 1 Tag
- Tranche C (Newsletter) – 1 Tag
- Tranche D (Service-Härtung & Tests) – 1 Tag
- Tranche E (Performance-Polishing) – 0.5 Tage
- Restpunkte aus §14 (Quality-Review-Cleanup) – 0.5 Tage
= **~4 Manntage produktive Arbeit**, bevor wir auf Stripe-/Go-Live-Daten vom Auftraggeber warten müssen.

View file

@ -0,0 +1,127 @@
# Migration Steps – aktuelles Runbook
Stand: 2026-06-17. Spiegelt den aktuell implementierten Command-Stand. Details und Go-Live-Kontext stehen in `05-DATABASE-MERGE.md` und `08-PROGRESS.md`; die Live-Settings/Commands rund um den Go-Live in `../../docs/weiteres/Live-Deployment-Checkliste (WS-7).md`.
## Zwei-Phasen-Strategie (kurzer Ausfall)
Der Import ist **wiederholbar**. Vorgesehener Ablauf:
1. **Phase 1 – Voll-Import (jetzt):** Neues System auf `pressekonto.com` aufsetzen, Schema migrieren, kompletten Bestand mit `--force` importieren, Frontends auf Test-Domain verifizieren.
2. **Phase 2 – Delta-Lauf (beim Cutover):** Altsystem kurz einfrieren, dieselben Commands **ohne `--force`** erneut laufen lassen → bestehende Datensätze bleiben unangetastet, nur neue/zwischenzeitlich entstandene werden nachgezogen. Danach DNS umstellen.
> **Die `--force`-Disziplin ist entscheidend:**
> - **MIT `--force`** (nur Phase 1): überschreibt bestehende Datensätze.
> - **OHNE `--force`** (Phase 2): überspringt bereits importierte Datensätze (`legacy_import_map`), importiert ausschließlich Neue. Genau das Verhalten für einen sauberen Delta-Lauf.
---
## Phase 0 – Schema (einmalig, vor dem ersten Import)
```bash
php artisan migrate --force
```
Ohne das Schema schlägt jeder Import-Schritt fehl. Auf dem Live-System ist das Teil der WS-7-Checkliste.
## Dry-Run (vor jedem echten Lauf)
```bash
php artisan legacy:import --source=all --dry-run
php artisan legacy:archive-invoices --dry-run
php artisan legacy:grandfather-subscriptions --dry-run
php artisan legacy:verify --no-report
php artisan legacy:migrate-media --portal=all --type=all --base-path=dev/migration --dry-run
```
Hinweis: `legacy:archive-invoices` importiert die Legacy-Rechnungen vollständig in `legacy_invoices`, inkl. Status/User-Zuordnung, `raw_snapshot`, `pdf_payload` und Report. Die PDF-Erzeugung erfolgt im Customer-Bereich bei Abruf aus diesen Archivdaten.
---
## Phase 1 – Voll-Import (mit `--force`, korrekte Reihenfolge)
```bash
php artisan legacy:import --source=presseecho --step=categories --force
php artisan legacy:import --source=all --step=users --force
php artisan legacy:import --source=presseecho --step=companies --force
php artisan legacy:import --source=businessportal24 --step=companies --force
php artisan legacy:import --source=presseecho --step=contacts --force
php artisan legacy:import --source=businessportal24 --step=contacts --force
php artisan legacy:import --source=presseecho --step=press-releases --force
php artisan legacy:import --source=businessportal24 --step=press-releases --force
php artisan legacy:import --step=link-associations --force
php artisan legacy:archive-invoices
php artisan legacy:grandfather-subscriptions
php artisan legacy:fix-timestamps
php artisan legacy:migrate-media --portal=all --type=all --base-path=dev/migration
php artisan legacy:verify
```
Reihenfolge ist wegen FKs Pflicht (companies vor contacts vor press-releases; users zuerst). Der Schritt `--step=users` importiert neben `sf_guard_user` auch `sf_guard_user_profile` in die neue Tabelle `profiles`.
### Datums-Migration der Pressemitteilungen (wichtig für die Frontend-Sortierung)
Das Frontend sortiert/clustert nach `published_at`. Der Importer setzt daher:
- `created_at`/`updated_at` = Legacy-`created_at`/`updated_at` (per `forceFill`, da nicht fillable) — das echte Anlage-/Bearbeitungsdatum, **nicht** das Importdatum.
- `published_at` = Legacy-`created_at` (Publikationsdatum) für veröffentlichte Meldungen. Bewusst **nicht** `updated_at` — dort steht bei vielen Alt-Datensätzen der Massen-/Migrationsstempel (z. B. `2026-04-…`), der sonst als „frisches" Veröffentlichungsdatum erschiene.
`legacy:fix-timestamps` ist der schnelle Cross-DB-Resync (gleicher MySQL-Server) und korrigiert `created_at`, `updated_at` **und** `published_at` (für `status=published`) authoritativ. Bei einem reinen Delta-Lauf ohne Re-Import diesen Schritt mitlaufen lassen.
---
## Phase 2 – Delta-Lauf beim Cutover (OHNE `--force`)
Altsystem einfrieren, dann:
```bash
# Vorab sehen, was neu dazukommt
php artisan legacy:import --source=all --dry-run
# Nur neue Datensätze nachziehen (kein --force!)
php artisan legacy:import --source=all
php artisan legacy:archive-invoices
php artisan legacy:grandfather-subscriptions # replay-fähig: aktualisiert bestehende, ergänzt neue
php artisan legacy:fix-timestamps # korrigiert created_at/updated_at/published_at
php artisan legacy:verify
# Medien NICHT mit --force – würde im neuen System gepflegte Logos/Bilder überschreiben.
# Nur falls neue Medien fehlen, gezielt ohne --force nachladen:
# php artisan legacy:migrate-media --portal=all --type=all --base-path=dev/migration
```
Danach DNS auf das neue System umstellen. `legacy:grandfather-subscriptions` läuft **nach** `legacy:archive-invoices`, weil es die aktiven, jährlich wiederkehrenden Zahlungsvereinbarungen aus dem Rechnungsarchiv ableitet (jüngste Rechnung pro Vereinbarung mit `payment_option.type = recurring` und `user_payment_option.status = active`) und als `grandfathered` in `user_payment_options` schreibt (Beträge netto in `legacy_conditions.net_cents`). Die nächste Rechnung stellt danach der tägliche MAN-Kreis-Lauf (`billing:generate-manual-invoices`) zum gewohnten Rhythmus aus. Optionen: `--dry-run`, `--as-of=`, `--grace-months=12` (älter überfällige Vereinbarungen gelten als stale und bleiben reines Archiv).
---
## Idempotenz / Replay-Sicherheit (Kurzreferenz)
Alle Schritte schreiben per `updateOrCreate`/`insertOrIgnore` über `(legacy_portal, legacy_id)` bzw. `legacy_import_map`; **kein Truncate, kein Delete**. Re-Run-Verhalten:
| Schritt | ohne `--force` | mit `--force` | zieht Neue nach |
|---|---|---|---|
| categories / users / companies / contacts | SKIP | UPDATE | ja |
| press-releases | SKIP | UPDATE (uuid + slug bleiben erhalten) | ja |
| link-associations | `insertOrIgnore` (idempotent) | — | ja |
| archive-invoices | SKIP | UPDATE | ja |
| grandfather-subscriptions | UPDATE/INSERT je Vereinbarung | — | ja |
| fix-timestamps | Raw-UPDATE (idempotent) | — | ja |
| migrate-media | SKIP (Pfad-Check) | **OVERWRITE** (Vorsicht) | ja |
Vor Phase 2 immer `legacy:verify` laufen lassen, um Lücken aus Phase 1 (z. B. fehlgeschlagene User-Importe → verwaiste Rechnungen) zu erkennen.
---
## Noch nicht finalisiert
- Medien-/Bilddateien-Transfer: Scope/Command vorhanden (`legacy:migrate-media`), finale Base-Path-Strategie auf Live noch festzulegen.
- Staging-Rehearsal mit aktuellem Produktiv-Snapshot bleibt Pflicht vor Go-Live.
## Legacy-Rechnungsreport
`legacy:archive-invoices` schreibt standardmäßig:
```bash
storage/app/private/migration/legacy-invoices-*.json
```
Der Report enthält pro Portal: Source-Count, importierte/übersprungene/fehlerhafte Rechnungen, Summe in Cent, Statusverteilung, Anzahl unzugeordneter Legacy-User, Anzahl erzeugter PDF-Payloads.

View file

@ -0,0 +1,102 @@
# Migration 2026 – Pressekonto Backend
> **Migration des Backends** der beiden Legacy-Portale `presseecho` und `businessportal24` (Symfony 1.4, PHP 5.6) in ein **gemeinsames, modernes Laravel 12 Backend** unter der Domain `pressekonto.test` / `pressekonto.de`.
**Start:** 23.04.2026
**Lead-Technologie:** Laravel 12 · PHP 8.4 · Livewire 4 · Volt · Flux UI 2 · Tailwind CSS 4 · MySQL 8
**Quell-Projekt:** `/_businessportal24.com/` (Symfony 1.4 / Doctrine 1 / PHP 5.6)
**Ziel-Projekt:** `/` (dieses Repository)
---
## Kontext in einem Absatz
Historisch existieren zwei technisch identische Symfony-1.4-Installationen (`presseecho` und `businessportal24`), die auf zwei verschiedenen Servern mit **zwei getrennten Datenbanken** laufen. Im neuen Setup werden beide Front-Seiten über **ein gemeinsames Laravel-Backend** gesteuert. Das Backend verantwortet User-, Firmen-, Kontakt-, Pressemitteilungs-, Kategorie-, Rechnungs-, Zahlungs- und Newsletter-Verwaltung sowie die **REST-API**, an der die beiden Front-Seiten und Dritt-Clients hängen.
---
## Dokumentstruktur
| # | Dokument | Zweck |
|---|---|---|
| 00 | [OVERVIEW](./00-OVERVIEW.md) | Kontext, Scope, Ziele, Erfolgskriterien |
| 01 | [LEGACY-ANALYSIS](./01-LEGACY-ANALYSIS.md) | Inventar des Symfony-1.4-Backends (Module, Models, Routes, Plugins) |
| 02 | [TARGET-ARCHITECTURE](./02-TARGET-ARCHITECTURE.md) | Laravel 12 Zielarchitektur (Struktur, Schichten, Konventionen) |
| 03 | [MIGRATION-PLAN](./03-MIGRATION-PLAN.md) | Phasenplan mit priorisierten Aufgaben |
| 04 | [DATA-MODEL](./04-DATA-MODEL.md) | Mapping Doctrine-Schema → Eloquent-Models |
| 05 | [DATABASE-MERGE](./05-DATABASE-MERGE.md) | Zusammenführung der zwei Legacy-Datenbanken |
| 06 | [FEATURES-SCOPE](./06-FEATURES-SCOPE.md) | Feature-Matrix: IN / OUT / ANGEPASST |
| 07 | [API-MIGRATION](./07-API-MIGRATION.md) | API-Endpoints, API-Keys → Sanctum |
| 08 | [PROGRESS](./08-PROGRESS.md) | Laufendes Fortschrittslog |
| 09 | [REVIEW-QUALITAET](./09-REVIEW-QUALITAET.md) | Qualitätsreview vom 2026-04-27; später teilweise behoben |
| 10 | [LEGACY-API-KUNDEN](./10-LEGACY-API-KUNDEN.md) | API-Kundenreport, Freigabelogik und Kommunikationsrisiko |
---
## Schnellüberblick: Zahlen & Fakten (aus der Legacy-Analyse)
- **40 Symfony-Module** (Admin + Frontend + API + Cron)
- **~30 Doctrine-Entitäten** (plus i18n-Tabellen, Ref-Tables, sfGuard-*)
- **12 Plugins**, davon fachlich relevant: `PressePortalPlugin`, `sfDoctrineGuardPlugin`, `sfDoctrineGuardPaymentPlugin`
- **2 Cron-Jobs**: Rechnungserstellung, Mahnwesen, Recurring-Payments
- **REST-API** auf Basis eines statischen `api_key`-Feldes im User-Profil
- **DB-Dumps** (2026-04-23): `businessportal24` 42 Tabellen / 578 MB, `presseecho` 43 Tabellen / 369 MB – 41 Tabellen sind strukturell identisch, 3 abweichend (`press_release_image_old` nur BP24, `category_pe_data` + `press_release_pe_data` nur PE)
Aktueller Implementierungsstand (Code-Abgleich 2026-04-29):
- ✅ **Domain-basiertes Theme-System** (`pressekonto` / `presseecho` / `businessportal24`)
- ✅ **Admin-UI** mit echten Daten für Dashboard, Users/Roles, Companies, Contacts, Categories-Index und PressRelease-CRUD/-Workflow
- ✅ **Auth-Stack**: Fortify, Sanctum, Spatie/Permission
- ✅ **Admin-Schutz und Portal-Scoping**: `EnsureUserIsAdmin`, `SetCurrentPortal`, `PortalScope`, Sidebar-Portal-Switcher
- ✅ **Customer-Portal-Grundumfang**: eigene Pressemitteilungen, Legacy-Rechnungsarchiv, API-Token Self-Service, Profilansicht
- ✅ **Eloquent-Modelle, Migrations, Factories und Seeder** für den Kernbestand
- ✅ **Daten-Migration P6-Kern**: wiederholbarer `legacy:import`, Rechnungsarchiv, Timestamps-Fix, `legacy:verify`
- ✅ **API v1 P7**: Sanctum, Kernendpoints, Legacy-Key-410, OpenAPI, Usage-Logging, Rate-Limit, API-Kundenreport
- ✅ **Dual-Vite-Setup** (Portal + Web)
Weiterhin offen / releasekritisch:
- ⬜ **P8 Stripe/Billing/Invoicing**: Checkout, Webhooks, neue Rechnungen, Invoice-PDF, Grandfathering
- ⬜ **Medienübernahme**: Legacy-Bilddateien, Thumbnail-/Redirect-Strategie, Logo-Varianten
- ⬜ **Go-Live-Ops P9–P11**: Queue-/Worker-Betrieb, Staging-Rehearsal, DNS-/Read-only-Cutover, finale Kommunikation
- ⬜ **API-Kundenfreigabe**: viele Legacy-API-User benötigen manuelle fachliche Prüfung, siehe `10-LEGACY-API-KUNDEN.md`
---
## Arbeitsweise
1. Vor jeder Phase → das passende Dokument in diesem Ordner lesen
2. Nach jedem Teilschritt → `08-PROGRESS.md` aktualisieren
3. Änderungen am Plan (Scope, Technik, Entscheidungen) → direkt im betreffenden MD-Dokument + Hinweis im PROGRESS-Log
4. **Keine neuen Dateien in den Legacy-Ordnern** (`_businessportal24.com/`, `dev/_old/`, `dev/migration/`) mehr anlegen – diese dienen nur noch als Referenz.
---
## Entscheidungen, die bereits feststehen
| # | Entscheidung | Begründung |
|---|---|---|
| D-01 | **Neues Schema**, keine 1:1-Übernahme der Legacy-Tabellen | Moderne Laravel-Konventionen (snake_case, `id`/`uuid`, `created_at`/`updated_at`, Soft Deletes wo sinnvoll) |
| D-02 | **Zwei Legacy-DBs → eine Ziel-DB** | Quelle `portal_type` (`presseecho` \| `businessportal24`) als Spalte auf relevanten Entitäten (User, Company, PressRelease …) |
| D-03 | **Nur Stripe als Payment-Gateway** – keine Rechnungs-Zahlungsweise mehr | PayPal Express / SPK-Berlin / Cortal Consors / Rechnung entfallen komplett. |
| D-04 | **`Country` und `Salutation` werden Config-Dateien** (`config/countries.php`, `config/salutations.php`) | Statisches Stammdatum; keine DB-Tabelle nötig |
| D-05 | **API-Keys: Cut-over auf Sanctum PATs** (keine Legacy-Kompatibilität) | Kundenkommunikation + Self-Service Token-Verwaltung im Backend |
| D-06 | **Admin-Backend = Livewire Volt + Flux UI**, keine Inertia/Vue | Konsistenz mit bereits erstellter Struktur |
| D-07 | **Sprache: Deutsch + Englisch** werden direkt initialisiert | UI DE, Content zweisprachig |
| D-08 | Altes **Symfony-Frontend wird nicht** übernommen | Neue Frontends sind bereits separat in diesem Repo (`resources/views/web/*`) |
| D-09 | **Passwörter werden NICHT übernommen** | Bei Go-Live gehen Reset-Mails an alle User mit Sicherheitshinweis |
| D-10 | **Magic-Link Login** (E-Mail-Link / Einmal-Passwort) wird eingebaut | Insbesondere für Companies ohne bisherigen Login; Self-Service-Zugriff auf Pressemitteilungen, Rechnungen, Tokens |
| D-11 | **Backend ist Admin + Customer-Portal** | Drei Bereiche: Admin (intern), Customer (Company-Self-Service), API-Only |
| D-12 | **Legacy-Rechnungen → Archiv**, neuer Billing-Lauf komplett neu gebaut | Separate `legacy_invoices`-Tabelle, vollständiger DB-Import inkl. Status/User-Zuordnung, PDFs read-only on demand |
| D-13 | **Neue PaymentOptions / Stripe-Produkte**; Grandfathering für Alt-User | Alt-User behalten Konditionen bis Laufzeit-Ende |
| D-14 | **Promotion Links entfallen**; Footer Code bleibt | Scope-Reduktion |
| D-15 | **Newsletter wird neu aufgebaut**, nur Subscriber-Daten migriert | Legacy-Workflow-Code wird nicht portiert |
| D-16 | **Coupons: vertagt** (erstmal kein Model) | Wird später separat evaluiert |
| D-17 | **`portal`-Spalte + `deleted_at`** auf allen mandantenfähigen Entitäten verpflichtend | Multi-Tenant-Trennung + Soft-Deletes |
| D-18 | **DB-Dumps liegen vor** (`dev/migration 2026/sql/`), Import muss **wiederholbar** sein | Go-Live benötigt Replay gegen produktiven DB-Stand |
Entscheidungsdetails mit Begründungen: siehe [`00-OVERVIEW.md` §5](./00-OVERVIEW.md).
---
## Kontakt / Zuständigkeiten
Alle Planungsdokumente in diesem Ordner sind die **Single Source of Truth**. Bei Änderungen am Plan bitte Commit-Message mit `migration-2026:` prefixen.