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

537 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](konzept.md)
**Deploy-Handbücher:** [phase-1-live-deploy.md](phase-1-live-deploy.md) · [phase-2-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: `/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.php` Zeile 249253: `$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 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.php``orderColumn('contacts.id' / 'deleted_at', …)`, `filterColumn('contacts.id', …)`
- `resources/views/contact/index.blade.php` — Column-Name `customer.id``contacts.id`
- `app/Http/Controllers/Admin/ReportController.php` — 2 Blöcke (`customer.firstname $1``contacts.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.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`; 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 | `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
```bash
# 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
```bash
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)
```bash
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
```bash
# 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
```sql
-- 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
```bash
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):**
```bash
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](phase-2-live-deploy.md). Der Live-Deploy muss innerhalb eines Wartungsfensters passieren, weil DB und Code atomar zusammengehen müssen.
### Ergebnis-Prüfung
```sql
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:
```bash
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
```bash
# 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-2-live-deploy.md#rollback-plan-falls-phase-2-auf-live-scheitert).
---
## Phase 3 — Participants konsolidieren
### Schritt 1: Neue Tabelle erstellen + Daten migrieren
```bash
# Backup erstellen!
php artisan migrate --path=database/migrations/2025_04_15_300001_phase3_create_participants_unified_table.php
```
### Ergebnis-Prüfung (vor Schritt 2!)
```sql
-- 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!)
```bash
# 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
```bash
# 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
```bash
# 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
```sql
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
```sql
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
```sql
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:
```bash
# 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
```bash
# 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:
```bash
# 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 `NewsletterExport``NewsletterContact`-Model, nicht direkt über `customer`
- 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 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::class``Customer` 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_id``inquiry_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` + `CustomerMailRepository``CommunicationRepository` (neu erstellen)
- `LeadFileRepository` + `BookingFileRepository``AttachmentRepository` (neu erstellen)
- `LeadNoticeRepository` + `BookingNoticeRepository``NoticeRepository` (neu erstellen)
- `MailDirService`: Datenzugriff auf `communications` umstellen
- Views für Mails/Notizen/Dateien in Lead und Booking können nach Umstellung geteilt werden