30 KiB
Umsetzung: Neustrukturierung Customer / Lead / Booking
Status: Phase 1 auf Live abgeschlossen (2026-04-17); Hotfix gegen Import-Bug ausgespielt (2026-04-18); Phase 2 auf Test erfolgreich migriert und verifiziert — Phase-2-Live-Deploy nach Stabilitätsphase.
Erstellt: April 2025
Letzte Aktualisierung: 2026-04-18
Konzept: konzept.md
Deploy-Handbücher: phase-1-live-deploy.md · phase-2-live-deploy.md
Hotfix-Dokumentation: ../hotfix-2026-04-18/HOTFIX.md
Aktueller Fortschritt
| Phase | Status | Deployed auf Test? | Deployed auf Live? |
|---|---|---|---|
| Phase 1 — Contact-Deduplizierung | ✅ Abgeschlossen | ✅ Ja | ✅ 2026-04-17 |
| Phase 1 — Contacts-Modul (neuer Code) | ✅ Abgeschlossen | ✅ Ja | ✅ 2026-04-17 |
Phase 2 — App-Code (Models/Repos/Controller/Views auf contacts/inquiries/inquiry_id) |
✅ Abgeschlossen | ✅ Ja | ⬜ Nein (Deploy-Handbuch fertig) |
| Phase 2 — Tabellen-Migrationen (3 Migrationsdateien) | ✅ Abgeschlossen | ✅ Ja, Batch 27–29 | ⬜ Nein (Deploy-Handbuch fertig) |
Phase 2 — Smoke-Test-Fixes 2026-04-17 (Lead::bookings() FK; orderColumn-SQL in 4 Controllern; Blade-Column-name: in contact/index.blade.php) |
✅ Abgeschlossen | ✅ Ja | ⬜ Nein (in Phase-2-Deploy gebündelt) |
Phase-1-Hotfix — nullable Parameter (?int $mailDirId, ?string $subdir) in 3 Service-Klassen (Mail-Dialoge mit NULL-Werten) |
✅ Abgeschlossen | ✅ Ja | ⬜ Nein (in Phase-2-Deploy gebündelt, siehe Handbuch) |
Hotfix 2026-04-18 — BookingImport.php auf Live: 'inquiry_id' → 'lead_id' (Phase-2-Datei versehentlich beim Phase-1-Deploy gerutscht, neue Bookings mit lead_id=NULL); Reparatur-DB-Query + 2 Artisan-Commands (bookings:resync-from-travel-bookings, bookings:repair-from-travel-bookings) |
✅ Abgeschlossen | ✅ Ja | ✅ 2026-04-18 |
| Phase 3 — Participants konsolidieren | ⬜ Ausstehend (Migrationen geschrieben, Code noch nicht) | ⬜ Nein | ⬜ Nein |
| Phase 4 — Communications / Notices / Attachments | ⬜ Ausstehend (Migrationen geschrieben, Code noch nicht) | ⬜ Nein | ⬜ Nein |
Verifikation Phase 2 auf Test (2026-04-17)
Smoke-Queries nach erfolgter Migration:
Tabellen:
customer NOT FOUND ← umbenannt ✓
contacts EXISTS ← neu ✓
lead NOT FOUND ← umbenannt ✓
inquiries EXISTS ← neu ✓
booking EXISTS ✓
booking-Spalten:
lead_id NOT FOUND ← umbenannt ✓
inquiry_id EXISTS ← neu ✓
Eloquent-Queries:
Contact::count() = 19.156 (ohne merged)
Customer::count() = 22.283 (alle, inkl. merged — Legacy-Model)
Lead::count() = 19.543
Booking::count() = 10.648
Booking->inquiry_id funktioniert
Booking->lead() Relation mit expl. FK inquiry_id funktioniert
Smoke-Test-Findings 2026-04-17 (Test-Durchlauf nach Phase-2-Restore)
Während der UI-Smoke-Tests auf mein.sterntours.test traten drei unterschiedliche Bug-Klassen auf, alle auf Test gefixt und in die Phase-2-Deploy-Liste aufgenommen.
1. Eloquent-Relation ohne expliziten FK — Lead::bookings()
- Fehler:
/leads→Column not found: 'booking.lead_id'. - Ursache:
$this->hasMany(Booking::class)ohne 2. Argument ⇒ Laravel leitet den FK aus dem Model-Namen ab (Lead→lead_id), der nach dem Rename nicht mehr existiert. - Fix:
app/Models/Lead.phpZeile 249–253:$this->hasMany(Booking::class, 'inquiry_id').
2. Hartcodierte Tabellenqualifier in orderColumn-SQL-Strings + Blade-Column-name:
- Fehler A:
/contacts→Column not found: 'customer.id' in 'order clause'. Behoben durch SQL-Umstellung im Controller — blieb aber bestehen, weil Yajra bei fehlendemorderColumn-Match denname:-String aus der Blade-Column-Definition als raw SQL verwendet. - Fehler B (gleiche Ursache, 2. Runde):
/contacts→ derselbe Fehler trotz Controller-Fix. Root Cause: Inresources/views/contact/index.blade.phpZeile 188 standname: 'customer.id'. Yajra matchedorderColumn()auf diesenname:-String — mein erster Fix mitorderColumn('id', …)hat also nicht getroffen. - Ursache zusammengefasst: Yajra-DataTables-
orderColumn()bekommt als 2. Argument eine Raw-SQL-Expression; und dername:-Wert einer DataTable-Column muss exakt zum 1. Argument vonorderColumn()/filterColumn()passen — sonst fällt Yajra zurück auf "Name wörtlich als SQL einsetzen". - Fix in 5 Dateien:
app/Http/Controllers/ContactController.php—orderColumn('contacts.id' / 'deleted_at', …),filterColumn('contacts.id', …)resources/views/contact/index.blade.php— Column-Namecustomer.id→contacts.idapp/Http/Controllers/Admin/ReportController.php— 2 Blöcke (customer.firstname $1→contacts.firstname $1etc.)app/Http/Controllers/Admin/ReportBookingController.php— 2 Blöckeapp/Http/Controllers/Admin/ReportLeadsController.php—$orderByNum-Array
- Bewusst nicht angefasst: die 1. Argumente von
addColumn/rawColumns(customer.fullName,lead.status_id) — das sind reine DataTables-Frontend-Identifier, die Yajra über den Eloquent-Relation-Mechanismus auflöst (ModelCustomer/Lead→$table = 'contacts'/'inquiries'), und die 3-teiligenbooking.customer.firstname-Strings inReportController.phpundReportProviderController.php(Yajra-Relation-Notation bzw. seit jeher kein gültiges SQL, durch Phase 2 nicht schlimmer).
3. Strict-Type-Regression aus Phase 1 (Laravel-10/PHP-8) — zwei Varianten
- Fehler A:
App\Services\Booking::getCustomerMailName(): Argument #2 ($mailDirId) must be of type int, null giveninmodal-show-mail-inner.blade.phpZeile 92. - Fehler B:
App\Services\Booking::setOutputDirs(): Argument #2 ($subdir) must be of type string, null givenin derselben kompilierten View, Zeile 101. - Ursache: Blade-Views übergeben
$mail_dir_idbzw.$mail_sdir_iddirekt aus DB-Feldern, die nullable sein können (Entwurfs-Mails, Top-Level-Ordner, Mails ohne Subdir). Unter Laravel 8 / PHP 7 wurdenullstillschweigend zu0/""gecastet; PHP 8 strict types verweigern das. - Fix in drei Dateien:
app/Services/Booking.php—getCustomerMailName(),getCustomerMailEmails():int $mailDirId→?int;setOutputDirs():string $subdir→?string.app/Services/Lead.php— identische Änderungen (symmetrische API).app/Services/MailDirService.php—getCustomerMailName(),getCustomerMailEmails(),resolveModel()auf?int;setOutputDir()auf?string; inresolveModel()zusätzlich ein früherreturn null-Guard.
- Das ist fachlich ein Phase-1-Bug, der nichts mit Customer→Contacts zu tun hat. Wir deployen ihn trotzdem zusammen mit Phase 2, weil die Services keine DB-Abhängigkeit haben und ein separates Wartungsfenster unnötig wäre.
Was in Phase 1 umgesetzt wurde
Datenbank-Migrationen (alle auf Testsystem eingespielt):
merged_into_id+merged_ataufcustomer-Tabelle → Duplikat-Trackingdeleted_ataufcustomer-Tabelle → Soft Delete für neue Contacts-UI
Artisan Commands:
contacts:find-duplicates— findet Duplikate nach E-Mail / Name+Geburtsdatum / Name+PLZcontacts:merge-duplicates— führt Duplikate zusammen (dry-run-Modus vorhanden)
Contacts-Modul (neuer paralleler Code, alter Code bleibt unberührt):
app/Models/Contact.php— Global Scope schließt Duplikate + gelöschte Kontakte ausapp/Repositories/ContactRepository.phpapp/Http/Controllers/ContactController.php— index, detail, store, destroy, getContactsresources/views/contact/— index, detail, partials- Neue Routes unter
/contactsund/contact/* - Neuer Navigationspunkt "Kontakte" im Sidenav
Contacts-Übersicht — Features:
- DataTable mit Schnellsuche (Name, Vorname, E-Mail, Telefon gleichzeitig)
- PLZ/Ort-Suche mit OR-Logik über beide Felder
- Schnellfilter: Alle / Mit Anfragen / Mit Buchungen
- Anfragen- und Buchungs-Zähler pro Kontakt als klickbare Badges
- History-Modal: Klick auf Anfragen- oder Buchungs-Badge öffnet Modal mit vollständiger Verlaufsübersicht (AJAX,
_detail_history.blade.phpmit$modal=true, Links öffnen in neuem Tab) - Löschen mit Bootstrap-Bestätigungs-Modal + Fehler-Toast (blockiert wenn Anfragen/Buchungen vorhanden)
Übersicht der Migrations-Dateien
Alle Migrations-Dateien liegen in database/migrations/ und können gezielt auf den Live-Server eingespielt werden. Jede Phase ist unabhängig deploybar — das System bleibt nach jeder Phase voll funktionsfähig.
| Datei | Phase | Beschreibung |
|---|---|---|
2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php |
1 | merged_into_id + merged_at zu customer hinzufügen |
2025_04_15_100002_phase1_add_soft_delete_to_customer_table.php |
1 | deleted_at (Soft Delete) zu customer hinzufügen |
2025_04_15_200001_phase2_rename_customer_to_contacts.php |
2 | customer → contacts umbenennen |
2025_04_15_200002_phase2_rename_lead_to_inquiries.php |
2 | lead → inquiries umbenennen |
2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php |
2 | booking.lead_id → booking.inquiry_id |
2025_04_15_300001_phase3_create_participants_unified_table.php |
3 | participants_unified erstellen + Daten migrieren |
2025_04_15_300002_phase3_drop_old_participant_tables.php |
3 | lead_participant + participant droppen (nach Test!) |
2025_04_15_400001_phase4_create_communications_table.php |
4 | communications erstellen + Daten migrieren |
2025_04_15_400002_phase4_create_notices_table.php |
4 | notices erstellen + Daten migrieren |
2025_04_15_400003_phase4_create_attachments_table.php |
4 | attachments erstellen + Daten migrieren |
2025_04_15_400004_phase4_drop_old_communication_tables.php |
4 | lead_mails + customer_mails droppen (nach Test!) |
2025_04_15_400005_phase4_drop_old_notice_tables.php |
4 | lead_notices + booking_notices droppen (nach Test!) |
2025_04_15_400006_phase4_drop_old_attachment_tables.php |
4 | lead_files + booking_files droppen (nach Test!) |
Voraussetzungen
# Auf dem Live-Server ausführen:
# 1. Datenbank-Backup erstellen (vor JEDER Phase!)
mysqldump -u root -ppassword stern_crm > backup_$(date +%Y%m%d_%H%M%S).sql
# 2. Migration-Status prüfen
php artisan migrate:status
Phase 1 — Contact-Deduplizierung
Schritt 1: Migration einspielen
php artisan migrate --path=database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php
php artisan migrate --path=database/migrations/2025_04_15_100002_phase1_add_soft_delete_to_customer_table.php
Schritt 2: Duplikate analysieren (dry-run)
php artisan contacts:find-duplicates
# Optional: CSV exportieren
php artisan contacts:find-duplicates --export=storage/app/duplicates.csv
# Nur bestimmte Konfidenz-Stufe
php artisan contacts:find-duplicates --confidence=HIGH
Schritt 3: Duplikate zusammenführen
# Erst Vorschau ohne Änderungen
php artisan contacts:merge-duplicates --dry-run
# Dann ausführen (HIGH-Konfidenz zuerst, sicherste Duplikate)
php artisan contacts:merge-duplicates --confidence=HIGH --force
php artisan contacts:merge-duplicates --confidence=MEDIUM --force
# LOW nur nach manueller Prüfung der CSV
php artisan contacts:merge-duplicates --confidence=LOW --force
Ergebnis-Prüfung
-- Wie viele Duplikate wurden zusammengeführt?
SELECT COUNT(*) FROM customer WHERE merged_into_id IS NOT NULL;
-- Gibt es noch aktive Duplikate (gleiche E-Mail)?
SELECT email, COUNT(*) FROM customer
WHERE merged_into_id IS NULL AND email IS NOT NULL AND email != ''
GROUP BY email HAVING COUNT(*) > 1;
Rollback Phase 1
php artisan migrate:rollback --path=database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php
Phase 2 — Tabellen umbenennen
Wichtig: Vor Phase 2 muss der App-Code bereits auf die neuen Tabellennamen vorbereitet sein, ODER die Migration wird deployed bevor der Code-Release erfolgt (mit sofortigem Rollback-Plan).
Der App-Code ist vorbereitet (siehe Abschnitt "Erledigte Code-Änderungen"). Empfehlung für den Live-Deploy: Maintenance-Mode-Window — Code-Release und Migrationen atomar in einem Wartungsfenster einspielen, damit Code und DB immer synchron sind.
Erledigte Code-Änderungen (App-Code ist Phase-2-ready)
Models (app/Models/):
Customer.php:protected $table = 'contacts';(mit Doku-Kommentar zu Modul 3 Phase 2)Lead.php:protected $table = 'inquiries';(mit Doku-Kommentar — Model-Name bleibt aus Kompatibilitätsgründen)Contact.php:protected $table = 'contacts';(ohne Fallback-Kommentar zu "pre-Phase-2")Booking.php:@property int $inquiry_id(ersetzt$lead_id)$casts['inquiry_id']+$fillableenthalten'inquiry_id'statt'lead_id'lead()-Relation:return $this->belongsTo(Lead::class, 'inquiry_id');— Methodenname bleibt für Legacy-Kompatibilität- Zusätzlich
inquiry()-Alias-Relation für semantische Klarheit im neuen Code
Repositories (app/Repositories/):
BookingPDFRepository.php: alle 9 Vorkommen von$this->model->lead_id→$this->model->inquiry_id. Inbooking_documents.lead_id(Shadow-Feld, Spaltenname bleibt) wird jetzt$this->model->inquiry_idgeschrieben.LeadRepository::createBooking(): Booking-Create nutzt'inquiry_id' => $this->model->id.CustomerMailRepository:customer_mails.lead_id-Spalte bleibt erhalten, Wert kommt aus$booking->inquiry_id. Alle 6 Vorkommen angepasst.
Controllers + Services + Commands:
RequestController.php: alle Booking-Querieswhere('lead_id', ...)→where('inquiry_id', ...); Datatables-SQL-Sort aufinquiry_id.API/BookingController.php: API-Feldnamelead_idbleibt (Abwärtskompatibilität), Wert aus$booking->inquiry_id.Admin/ReportController.php+Admin/ReportProviderController.php: alle 16 Vorkommen von$v->booking->lead_id/$export->booking->lead_id→...->inquiry_id.Admin/ReportLeadsController.php+LeadController.php: qualifizierte SubquerywhereColumn('lead_id', 'lead.id')→whereColumn('lead_id', 'inquiries.id').ContactController.php:select('customer.*')→select('contacts.*'),where('customer.id', ...)→where('contacts.id', ...),DB::table('customer')/DB::table('lead')→DB::table('contacts')/DB::table('inquiries').CustomerController.php:select('customer.*')→select('contacts.*').Services/BookingImport.php: Booking-Create-Array nutzt'inquiry_id'.Console/Commands/ContactsMergeDuplicates.php+ContactsFindDuplicates.php: alleDB::table('customer')/DB::table('lead')umgestellt.Console/Commands/SyncNewsletterKulturreisen.php:metadata['lead_id']-Key bleibt (ist Payload-Konvention), Wert aus$booking->inquiry_id.
Views (resources/views/):
pdf/components/booking_header.blade.php,pdf/components/booking_head.blade.php,customer/mail/modal-show-mail-inner.blade.php:{{ $booking->lead_id }}→{{ $booking->inquiry_id }}.- Alle weiteren View-Treffer zu
lead_idsind Lead-Kontext (lead_mails.lead_id,lead_notices.lead_id, HTML-Data-Attribute) und bleiben unverändert — die Spalten werden von Phase 2 nicht umbenannt.
Was nicht umgestellt wurde (mit Absicht):
- FK-Spalten
lead_mails.lead_id,lead_notices.lead_id,lead_files.lead_id,lead_participant.lead_id,inquiry.lead_id,status_history.lead_id,customer_mails.lead_id,booking_documents.lead_id— Spaltennamen bleiben, FKs zeigen nachRENAME TABLEautomatisch aufinquiries.id. - API-Feld
lead_idinAPI/BookingController::import-Response — Abwärtskompatibilität für API-Konsumenten. - Metadaten-Keys
lead_idin Newsletter-Payload. - HTML-Data-Attribute
data-lead_id+ DataTables-Spaltennamenlead_id(UI-seitige Konventionen, kein DB-Bezug). - Routen-Pfade
/lead/*— reine UX-Arbeit, nicht blockierend für DB-Rename. Nachträglich als 301-Redirect-Paket umsetzbar. - Routen-Namen (
lead_detail,lead_index) — bleiben als Aliase, um Links in Views/Mails/Logs nicht zu brechen.
Schritt 1: Migrationen einspielen
Auf Test (2026-04-17 erfolgreich durchgeführt, Batch 27–29):
php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force
php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force
php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force
Wichtig —
--pathverwenden! Ohne--pathwürdephp artisan migrateauch die noch im Workspace liegenden Phase-3-, Phase-4- und Offers-Migrationen ausführen, die noch nicht deployreif sind. Einzeln per--pathhält den Deploy auf exakt diese drei Dateien beschränkt.
Auf Live: Ablauf siehe phase-2-live-deploy.md. Der Live-Deploy muss innerhalb eines Wartungsfensters passieren, weil DB und Code atomar zusammengehen müssen.
Ergebnis-Prüfung
SHOW TABLES LIKE 'contacts';
SHOW TABLES LIKE 'inquiries';
SHOW COLUMNS FROM booking LIKE 'inquiry_id';
SELECT * FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_NAME = 'booking' AND COLUMN_NAME = 'inquiry_id';
Oder via Laravel-Tinker:
php artisan tinker --execute="
echo Schema::hasTable('contacts') ? 'contacts OK' : 'contacts FEHLT'; echo PHP_EOL;
echo Schema::hasTable('inquiries') ? 'inquiries OK' : 'inquiries FEHLT'; echo PHP_EOL;
echo Schema::hasColumn('booking', 'inquiry_id') ? 'booking.inquiry_id OK' : 'FEHLT'; echo PHP_EOL;
"
Rollback Phase 2
# In umgekehrter Reihenfolge
php artisan migrate:rollback --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php --force
php artisan migrate:rollback --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php --force
php artisan migrate:rollback --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php --force
Wichtig: Rollback MUSS zusammen mit Code-Revert passieren (Code erwartet sonst die falschen Tabellennamen). Details im phase-2-live-deploy.md.
Phase 3 — Participants konsolidieren
Schritt 1: Neue Tabelle erstellen + Daten migrieren
# Backup erstellen!
php artisan migrate --path=database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php
Ergebnis-Prüfung (vor Schritt 2!)
-- Zeilenzahlen vergleichen
SELECT 'lead_participant' AS src, COUNT(*) AS cnt FROM lead_participant
UNION ALL
SELECT 'participant', COUNT(*) FROM participant
UNION ALL
SELECT 'participants_unified (inquiry)', COUNT(*) FROM participants_unified WHERE inquiry_id IS NOT NULL
UNION ALL
SELECT 'participants_unified (booking)', COUNT(*) FROM participants_unified WHERE booking_id IS NOT NULL
UNION ALL
SELECT 'participants_unified (lead_contact)', COUNT(*) FROM participants_unified WHERE is_lead_contact = 1;
-- Stichprobe: Vergleich einzelner Datensätze
SELECT * FROM lead_participant LIMIT 5;
SELECT * FROM participants_unified WHERE inquiry_id IS NOT NULL LIMIT 5;
Schritt 2: Alte Tabellen droppen (erst nach erfolgreicher Prüfung!)
# Backup erstellen!
php artisan migrate --path=database/migrations/2025_04_15_300002_phase3_drop_old_participant_tables.php
Achtung: Schritt 2 ist irreversibel. Nur ausführen wenn:
- participants_unified seit mindestens 1 Woche stabil läuft
- Alle PDF-Generierungen, Buchungsbestätigungen etc. korrekt funktionieren
- Kein Rollback auf Schritt 2 geplant
Rollback Phase 3
# Nur Schritt 1 rollback-fähig:
php artisan migrate:rollback --path=database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php
# Schritt 2 ist irreversibel → Backup einspielen
Phase 4 — Communications / Notices / Attachments konsolidieren
Voraussetzung: Phase 2 muss abgeschlossen sein (inquiries + contacts Tabellen müssen existieren).
Schritt 1: Neue Tabellen erstellen + Daten migrieren
Die drei Schritt-1-Migrationen sind voneinander nicht unabhängig:
400003_attachmentssetzt400001_communicationsvoraus (wegencommunication_idFK)- Reihenfolge daher: communications → notices → attachments
# Backup erstellen!
php artisan migrate --path=database/migrations/2025_04_15_400001_phase4_create_communications_table.php
php artisan migrate --path=database/migrations/2025_04_15_400002_phase4_create_notices_table.php
php artisan migrate --path=database/migrations/2025_04_15_400003_phase4_create_attachments_table.php
Ergebnis-Prüfung Communications
SELECT 'lead_mails' AS src, COUNT(*) FROM lead_mails
UNION ALL
SELECT 'customer_mails', COUNT(*) FROM customer_mails
UNION ALL
SELECT 'communications (lead)', COUNT(*) FROM communications WHERE legacy_source = 'lead_mail'
UNION ALL
SELECT 'communications (cust)', COUNT(*) FROM communications WHERE legacy_source = 'customer_mail';
-- Reply-Chain korrekt?
SELECT COUNT(*) FROM communications WHERE reply_id IS NOT NULL;
-- Muss gleich sein wie:
SELECT COUNT(*) FROM lead_mails WHERE reply_id IS NOT NULL;
-- + COUNT(*) FROM customer_mails WHERE reply_id IS NOT NULL;
Ergebnis-Prüfung Notices
SELECT 'lead_notices' AS src, COUNT(*) FROM lead_notices
UNION ALL
SELECT 'booking_notices', COUNT(*) FROM booking_notices
UNION ALL
SELECT 'notices (inquiry)', COUNT(*) FROM notices WHERE inquiry_id IS NOT NULL
UNION ALL
SELECT 'notices (booking)', COUNT(*) FROM notices WHERE booking_id IS NOT NULL;
Ergebnis-Prüfung Attachments
SELECT 'lead_files' AS src, COUNT(*) FROM lead_files
UNION ALL
SELECT 'booking_files', COUNT(*) FROM booking_files
UNION ALL
SELECT 'attachments (lead)', COUNT(*) FROM attachments WHERE legacy_source = 'lead_file'
UNION ALL
SELECT 'attachments (booking)', COUNT(*) FROM attachments WHERE legacy_source = 'booking_file';
-- communication_id korrekt verknüpft?
SELECT COUNT(*) FROM lead_files WHERE lead_mail_id IS NOT NULL;
-- Muss gleich sein wie:
SELECT COUNT(*) FROM attachments WHERE legacy_source = 'lead_file' AND communication_id IS NOT NULL;
Schritt 2: Alte Tabellen droppen (erst nach erfolgreicher Prüfung!)
Kann unabhängig pro Tabellenpaar eingespielt werden:
# Backup erstellen!
# Communications droppen (achtet auf FK von lead_files → lead_mails wird intern behandelt):
php artisan migrate --path=database/migrations/2025_04_15_400004_phase4_drop_old_communication_tables.php
# Notices droppen:
php artisan migrate --path=database/migrations/2025_04_15_400005_phase4_drop_old_notice_tables.php
# Attachments droppen:
php artisan migrate --path=database/migrations/2025_04_15_400006_phase4_drop_old_attachment_tables.php
Achtung: Reihenfolge bei Schritt 2:
400004_communicationsmuss vor400006_attachmentslaufen, dalead_files.lead_mail_idFK auflead_mailszeigt und erst in 400004 entfernt wird. (Der FK wird in 400004 automatisch entfernt falls noch vorhanden.)
Rollback Phase 4
# Schritt 1 rollback (umgekehrte Reihenfolge):
php artisan migrate:rollback --path=database/migrations/2025_04_15_400003_phase4_create_attachments_table.php
php artisan migrate:rollback --path=database/migrations/2025_04_15_400002_phase4_create_notices_table.php
php artisan migrate:rollback --path=database/migrations/2025_04_15_400001_phase4_create_communications_table.php
# Schritt 2 ist irreversibel → Backup einspielen
Vollständige Deployment-Sequenz (alle Phasen auf einmal)
Nur empfohlen wenn alle Phasen bereits lokal/staging getestet wurden:
# 1. Backup
mysqldump -u [user] -p stern_crm > backup_pre_migration_$(date +%Y%m%d).sql
# 2. Maintenance Mode aktivieren
php artisan down --render="errors::503"
# 3. Phase 1 — Deduplizierungsfelder
php artisan migrate --path=database/migrations/2025_04_15_100001_phase1_add_merge_fields_to_customer_table.php
# 4. Phase 1 — Duplikate zusammenführen
php artisan contacts:merge-duplicates --confidence=HIGH --force
php artisan contacts:merge-duplicates --confidence=MEDIUM --force
# 5. Phase 2 — Umbenennung
php artisan migrate --path=database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php
php artisan migrate --path=database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php
php artisan migrate --path=database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php
# 6. Phase 3 — Participants
php artisan migrate --path=database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php
# 7. Phase 4 — Communications / Notices / Attachments
php artisan migrate --path=database/migrations/2025_04_15_400001_phase4_create_communications_table.php
php artisan migrate --path=database/migrations/2025_04_15_400002_phase4_create_notices_table.php
php artisan migrate --path=database/migrations/2025_04_15_400003_phase4_create_attachments_table.php
# 8. Maintenance Mode deaktivieren
php artisan up
# 9. Testen — BEVOR die Cleanup-Migrationen laufen!
# → Daten prüfen (SQL-Queries oben), App manuell testen
# Cleanup-Migrationen (300002, 400004-400006) SEPARAT nach Testphase einspielen!
Abhängigkeiten zwischen Phasen
Phase 1 ──────────────────────────────────────► unabhängig
Phase 2 ──────────────────────────────────────► unabhängig (empfohlen nach Phase 1)
Phase 3 ──────────► setzt Phase 2 voraus (FK auf inquiries)
Phase 4a (comm) ──► setzt Phase 2 voraus (FK auf inquiries + contacts)
Phase 4b (not.) ──► setzt Phase 2 voraus (FK auf inquiries)
Phase 4c (att.) ──► setzt Phase 2 + Phase 4a voraus (FK auf communications)
Rollback-Strategie: Zusammenfassung
| Migration | Rollback möglich? | Methode |
|---|---|---|
| Phase 1 (merge fields) | Ja | migrate:rollback |
| Phase 2 (rename) | Ja | migrate:rollback (umgekehrte Reihenfolge) |
| Phase 3 Schritt 1 (create participants_unified) | Ja | migrate:rollback |
| Phase 3 Schritt 2 (drop participant tables) | NEIN | Backup einspielen |
| Phase 4 Schritt 1 (create comm/notices/attach) | Ja | migrate:rollback (umgekehrte Reihenfolge) |
| Phase 4 Schritt 2 (drop old tables) | NEIN | Backup einspielen |
Faustregel: Alle _create_* Migrationen sind rollback-fähig. Alle _drop_* Migrationen sind irreversibel.
Abhängiges Modul: Newsletter (/newsletter)
Das Newsletter-Modul (app/Http/Controllers/NewsletterController.php) ist nicht direkt betroffen von den bisherigen Änderungen, muss aber bei zukünftigen Phasen im Blick behalten werden:
- Eigene Tabelle
newsletter_contactsmitcustomer_idFK →customer.id - Verwendet
App\Models\Customer(altes Model) über diecustomer()-Beziehung - Export (
/newsletter/export) läuft überNewsletterExport→NewsletterContact-Model, nicht direkt übercustomer - Sync-Commands:
contacts:sync-newsletter-kulturreisen,contacts:sync-newsletter-ferienwohnungen
Was bei Phase 2 angepasst werden muss
Wenn customer → contacts umbenannt wird (Phase 2), muss im Newsletter-Modul:
App\Models\Customer→ weiterhin verwenden ODER aufApp\Models\Contactumstellen- FK
newsletter_contacts.customer_idzeigt weiterhin auf dieselbe Tabelle (jetztcontacts) — keine Migration nötig, da FK-Name sich nicht ändert, nur der Tabellenname
Aktueller Status
- ✅ Newsletter-Export funktioniert unverändert (Phase 1 hat nichts daran geändert)
- ✅ Phase-2-Check:
NewsletterContactbelongs-toCustomer::class—Customerverwendet jetzt$table = 'contacts', der FKnewsletter_contacts.customer_idbleibt stabil. Keine Code-Änderung im Newsletter-Modul nötig.
Code-Änderungen nach den Migrationen
Die Migrationen erstellen nur die neuen Tabellen und migrieren die Daten. Der App-Code muss separat angepasst werden (nicht Teil der Migrations selbst):
Nach Phase 2
- ✅
app/Models/Customer.php:protected $table = 'contacts'; - ✅
app/Models/Lead.php:protected $table = 'inquiries'; - ✅
app/Models/Contact.php:protected $table = 'contacts'; - ✅
app/Models/Booking.php:lead_id→inquiry_id($casts,$fillable,@property,lead()-Relation mit expliziter FK-Spalteinquiry_id, neuerinquiry()-Alias) - ✅ Alle Repositories, Controllers, Services, Commands und Views im Booking-Kontext auf
inquiry_idumgestellt - ✅ Raw-SQL (
DB::table('customer')/DB::table('lead'),select('customer.*')/select('lead.*'),whereColumn(..., 'lead.id')) aufcontacts/inquiriesumgestellt - ⬜ Route-URLs:
/lead/→/inquiry/(301 Redirects) — nachgelagert, nicht blockierend für DB-Rename
Nach Phase 3
LeadRepository::createBooking(): Participants nicht mehr kopieren — bestehendeinquiry_id-Einträge werden perbooking_idergänzt- PDF-Generierung: auf
participants_unifiedumstellen stattlead_participant/participant - Nach Cleanup (Schritt 2): alle direkten Queries auf
lead_participant/participantentfernen
Nach Phase 4
LeadMailRepository+CustomerMailRepository→CommunicationRepository(neu erstellen)LeadFileRepository+BookingFileRepository→AttachmentRepository(neu erstellen)LeadNoticeRepository+BookingNoticeRepository→NoticeRepository(neu erstellen)MailDirService: Datenzugriff aufcommunicationsumstellen- Views für Mails/Notizen/Dateien in Lead und Booking können nach Umstellung geteilt werden