mein-sterntours/dev/customer-bookings/umsetzung.md
2026-04-22 16:01:27 +02:00

29 KiB
Raw Blame History

Umsetzung: Neustrukturierung Customer / Lead / Booking

Status: Phase 1 auf Live abgeschlossen (2026-04-17); Phase 2 auf Test erfolgreich migriert und verifiziert — bereit für Live-Deploy.
Erstellt: April 2025
Letzte Aktualisierung: 2026-04-17
Konzept: konzept.md
Deploy-Handbücher: phase-1-live-deploy.md · phase-2-live-deploy.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 2729 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)
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: /leadsColumn not found: 'booking.lead_id'.
  • Ursache: $this->hasMany(Booking::class) ohne 2. Argument ⇒ Laravel leitet den FK aus dem Model-Namen ab (Leadlead_id), der nach dem Rename nicht mehr existiert.
  • Fix: app/Models/Lead.php Zeile 249253: $this->hasMany(Booking::class, 'inquiry_id').

2. Hartcodierte Tabellenqualifier in orderColumn-SQL-Strings + Blade-Column-name:

  • Fehler A: /contactsColumn not found: 'customer.id' in 'order clause'. Behoben durch SQL-Umstellung im Controller — blieb aber bestehen, weil Yajra bei fehlendem orderColumn-Match den name:-String aus der Blade-Column-Definition als raw SQL verwendet.
  • Fehler B (gleiche Ursache, 2. Runde): /contacts → derselbe Fehler trotz Controller-Fix. Root Cause: In resources/views/contact/index.blade.php Zeile 188 stand name: 'customer.id'. Yajra matched orderColumn() auf diesen name:-String — mein erster Fix mit orderColumn('id', …) hat also nicht getroffen.
  • Ursache zusammengefasst: Yajra-DataTables-orderColumn() bekommt als 2. Argument eine Raw-SQL-Expression; und der name:-Wert einer DataTable-Column muss exakt zum 1. Argument von orderColumn() / filterColumn() passen — sonst fällt Yajra zurück auf "Name wörtlich als SQL einsetzen".
  • Fix in 5 Dateien:
    • app/Http/Controllers/ContactController.phporderColumn('contacts.id' / 'deleted_at', …), filterColumn('contacts.id', …)
    • resources/views/contact/index.blade.php — Column-Name customer.idcontacts.id
    • app/Http/Controllers/Admin/ReportController.php — 2 Blöcke (customer.firstname $1contacts.firstname $1 etc.)
    • app/Http/Controllers/Admin/ReportBookingController.php — 2 Blöcke
    • app/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 (Model Customer/Lead$table = 'contacts'/'inquiries'), und die 3-teiligen booking.customer.firstname-Strings in ReportController.php und ReportProviderController.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 given in modal-show-mail-inner.blade.php Zeile 92.
  • Fehler B: App\Services\Booking::setOutputDirs(): Argument #2 ($subdir) must be of type string, null given in derselben kompilierten View, Zeile 101.
  • Ursache: Blade-Views übergeben $mail_dir_id bzw. $mail_sdir_id direkt aus DB-Feldern, die nullable sein können (Entwurfs-Mails, Top-Level-Ordner, Mails ohne Subdir). Unter Laravel 8 / PHP 7 wurde null stillschweigend zu 0 / "" gecastet; PHP 8 strict types verweigern das.
  • Fix in drei Dateien:
    • app/Services/Booking.phpgetCustomerMailName(), getCustomerMailEmails(): int $mailDirId?int; setOutputDirs(): string $subdir?string.
    • app/Services/Lead.php — identische Änderungen (symmetrische API).
    • app/Services/MailDirService.phpgetCustomerMailName(), getCustomerMailEmails(), resolveModel() auf ?int; setOutputDir() auf ?string; in resolveModel() zusätzlich ein früher return 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_at auf customer-Tabelle → Duplikat-Tracking
  • deleted_at auf customer-Tabelle → Soft Delete für neue Contacts-UI

Artisan Commands:

  • contacts:find-duplicates — findet Duplikate nach E-Mail / Name+Geburtsdatum / Name+PLZ
  • contacts: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 aus
  • app/Repositories/ContactRepository.php
  • app/Http/Controllers/ContactController.php — index, detail, store, destroy, getContacts
  • resources/views/contact/ — index, detail, partials
  • Neue Routes unter /contacts und /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.php mit $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 customercontacts umbenennen
2025_04_15_200002_phase2_rename_lead_to_inquiries.php 2 leadinquiries umbenennen
2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php 2 booking.lead_idbooking.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'] + $fillable enthalten '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. In booking_documents.lead_id (Shadow-Feld, Spaltenname bleibt) wird jetzt $this->model->inquiry_id geschrieben.
  • 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-Queries where('lead_id', ...)where('inquiry_id', ...); Datatables-SQL-Sort auf inquiry_id.
  • API/BookingController.php: API-Feldname lead_id bleibt (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 Subquery whereColumn('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: alle DB::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_id sind 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 nach RENAME TABLE automatisch auf inquiries.id.
  • API-Feld lead_id in API/BookingController::import-Response — Abwärtskompatibilität für API-Konsumenten.
  • Metadaten-Keys lead_id in Newsletter-Payload.
  • HTML-Data-Attribute data-lead_id + DataTables-Spaltennamen lead_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 2729):

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 — --path verwenden! Ohne --path würde php artisan migrate auch die noch im Workspace liegenden Phase-3-, Phase-4- und Offers-Migrationen ausführen, die noch nicht deployreif sind. Einzeln per --path hä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_attachments setzt 400001_communications voraus (wegen communication_id FK)
  • 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_communications muss vor 400006_attachments laufen, da lead_files.lead_mail_id FK auf lead_mails zeigt 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_contacts mit customer_id FK → customer.id
  • Verwendet App\Models\Customer (altes Model) über die customer()-Beziehung
  • Export (/newsletter/export) läuft über NewsletterExportNewsletterContact-Model, nicht direkt über customer
  • Sync-Commands: contacts:sync-newsletter-kulturreisen, contacts:sync-newsletter-ferienwohnungen

Was bei Phase 2 angepasst werden muss

Wenn customercontacts umbenannt wird (Phase 2), muss im Newsletter-Modul:

  • App\Models\Customer → weiterhin verwenden ODER auf App\Models\Contact umstellen
  • FK newsletter_contacts.customer_id zeigt weiterhin auf dieselbe Tabelle (jetzt contacts) — 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: NewsletterContact belongs-to Customer::classCustomer verwendet jetzt $table = 'contacts', der FK newsletter_contacts.customer_id bleibt 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_idinquiry_id ($casts, $fillable, @property, lead()-Relation mit expliziter FK-Spalte inquiry_id, neuer inquiry()-Alias)
  • Alle Repositories, Controllers, Services, Commands und Views im Booking-Kontext auf inquiry_id umgestellt
  • Raw-SQL (DB::table('customer') / DB::table('lead'), select('customer.*') / select('lead.*'), whereColumn(..., 'lead.id')) auf contacts / inquiries umgestellt
  • Route-URLs: /lead//inquiry/ (301 Redirects) — nachgelagert, nicht blockierend für DB-Rename

Nach Phase 3

  • LeadRepository::createBooking(): Participants nicht mehr kopieren — bestehende inquiry_id-Einträge werden per booking_id ergänzt
  • PDF-Generierung: auf participants_unified umstellen statt lead_participant/participant
  • Nach Cleanup (Schritt 2): alle direkten Queries auf lead_participant/participant entfernen

Nach Phase 4

  • LeadMailRepository + CustomerMailRepositoryCommunicationRepository (neu erstellen)
  • LeadFileRepository + BookingFileRepositoryAttachmentRepository (neu erstellen)
  • LeadNoticeRepository + BookingNoticeRepositoryNoticeRepository (neu erstellen)
  • MailDirService: Datenzugriff auf communications umstellen
  • Views für Mails/Notizen/Dateien in Lead und Booking können nach Umstellung geteilt werden