12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-12 18:32:33 +02:00
parent 405df0a122
commit 5b8bdf4182
779 changed files with 480564 additions and 6241 deletions

View file

@ -0,0 +1,316 @@
# 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)
- **Legacy-PaymentOptions werden nicht übernommen.** Neue Produkte werden vom Auftraggeber definiert und als Stripe-Prices angelegt.
- **Aktive `UserPaymentOption`-Einträge** (Status `active`, `valid_until >= today`) werden als `grandfathered` migriert:
- Neuer Datensatz in `user_payment_options` mit `status = 'grandfathered'`, `grandfathered_until = legacy.valid_until`, `legacy_conditions = {...Snapshot...}`.
- **Kein** Stripe-Subscription-Versuch (kein automatischer Import alter Abos in Stripe).
- Scheduler `ExpireGrandfatheredSubscriptions` erzeugt am `grandfathered_until` eine Customer-Benachrichtigung für Umstellung auf neues Produkt.
- Alle historischen `user_payments` werden als Information ins Archiv geschrieben (analog `legacy_invoices` optional).
### 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