14 KiB
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
- Die Ziel-DB ist NEU. Wir kopieren keine Tabellen 1:1. Stattdessen werden Daten transformiert eingespielt (siehe
04-DATA-MODEL.md). - Legacy-IDs bleiben nachvollziehbar via
legacy_portal+legacy_idauf jedem Datensatz + zentralerlegacy_import_map. - Kollisionen werden deterministisch aufgelöst (siehe §3).
- Importe sind idempotent – wiederholbare Skripte, bei erneutem Lauf passiert nichts doppeltes.
- 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.
- Dry-Run zuerst, Produktiv-Run danach mit Snapshot-Backup vorher.
2. Konnektivität
In config/database.php drei Connections:
'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_mapneu aufgelöst.
4. Import-Ablauf (High-Level)
Ist-Stand 2026-04-29: Implementiert ist der Master-Command
legacy:importmit--source=presseecho|businessportal24|allund--step=categories|users|companies|contacts|press-releases|link-associations|all. Die früher geplanten Commandslegacy:import-allundlegacy:images-copyexistieren aktuell nicht als produktive Commands; Medienübernahme bleibt offen.
# 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-subscriptionsfü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
- Alle Companies aus beiden DBs lesen.
- Gleichnamig + selber Owner → Merge (
portal = both). - Sonst separate Company, Slug ggf. suffixen.
CompanyUser+ResponsibleCompanyUserincompany_user-Pivot mitrole.- 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_mapauflö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).uuidneu generieren (für öffentliche Share-Links).published_atauscreated_atableiten, fallsstatus=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-invoicesimportiert alle Rechnungen beider Portale idempotent. - Jede Legacy-Zeile → ein
legacy_invoices-Datensatz inkl. Status, Betrag, Datumsfeldern, Zahlart, User-Zuordnung,raw_snapshotundpdf_payloadals 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_pathist 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 (Statusactive,valid_until >= today) werden alsgrandfatheredmigriert:- Neuer Datensatz in
user_payment_optionsmitstatus = 'grandfathered',grandfathered_until = legacy.valid_until,legacy_conditions = {...Snapshot...}. - Kein Stripe-Subscription-Versuch (kein automatischer Import alter Abos in Stripe).
- Scheduler
ExpireGrandfatheredSubscriptionserzeugt amgrandfathered_untileine Customer-Benachrichtigung für Umstellung auf neues Produkt.
- Neuer Datensatz in
- Alle historischen
user_paymentswerden als Information ins Archiv geschrieben (analoglegacy_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:
- Counts: pro Entität
legacy_source_countvs.target_count+ Delta. - Foreign Keys: keine dangling FKs (z. B.
press_releases.user_idexistiert inusers). - User-Merges: Alle
portal = both-User auflisten. - E-Mail-Duplikate (Unique-Verletzungen hätten Imports blockiert – Check trotzdem).
- Invoice-Summen: Σ
amount_centspro Portal inlegacy_invoicesvs. Summe aus Legacy-DB. - Press Release Counts per Kategorie pro Portal.
- Grandfathered Subscriptions: Liste aller
user_payment_options.status = grandfatheredmitgrandfathered_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