539 lines
30 KiB
Markdown
539 lines
30 KiB
Markdown
# 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](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)
|
||
**Hotfix-Dokumentation:** [../hotfix-2026-04-18/HOTFIX.md](../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.php` Zeile 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 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 27–29):**
|
||
```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
|