# Phase-2-Live-Deploy — Anleitung **Ausgangssituation (nach Phase-1-Live-Deploy am 2026-04-17):** - Live: Phase 1 ✅ (Code + DB-Migrationen 25, 26) - Test: Phase 1 ✅ + Phase 2 ✅ (Code + DB-Migrationen 25–29, verifiziert) - Workspace (lokal): Phase 1 + Phase 2 + Offers-Code ready Dieses Handbuch beschreibt, wie Phase 2 vom Test-Stand auf Live übertragen wird. Phase 2 umfasst zwei atomar zusammengehörige Teile plus eine kleine Ergänzung aus Phase 1: 1. **DB-Schema-Änderungen** (3 Migrationen, breaking): - `RENAME TABLE customer → contacts` - `RENAME TABLE lead → inquiries` - Spalte `booking.lead_id → booking.inquiry_id` (inkl. FK-Index-Neuaufbau) 2. **Application-Code-Änderungen** (23 modifizierte Dateien): - Models `Customer`, `Contact`, `Lead`, `Booking` mit neuen `$table`-Werten bzw. Spaltennamen - Repositories, Controllers, Services, Commands, Views — alle Referenzen auf `booking.lead_id` → `inquiry_id` umgestellt; Raw-SQL auf `contacts`/`inquiries` umgestellt - Smoke-Test-Fixes (2026-04-17): `Lead::bookings()` mit explizitem FK `inquiry_id`; `orderColumn`-SQL in `ContactController`/`ReportController`/`ReportBookingController`/`ReportLeadsController` auf `contacts.*` gezogen. 3. **Phase-1-Hotfixes — nullable Parameter in den Mail-Dir-Services** (3 modifizierte Dateien): - `app/Services/Booking.php`, `app/Services/Lead.php`, `app/Services/MailDirService.php` - `int $mailDirId` → `?int $mailDirId` bei `getCustomerMailName()` / `getCustomerMailEmails()` / `resolveModel()` (behebt Mail-Dialog-Crash wenn `customer_mails.mail_dir_id` / `lead_mails.mail_dir_id` NULL ist). - `string $subdir` → `?string $subdir` bei `setOutputDirs()` / `setOutputDir()` (behebt Mail-Dialog-Crash wenn `$mail_sdir_id` NULL ist, z.B. bei Entwürfen oder Top-Level-Ordnern). - Fachlich eigentlich Phase-1-Bugs (Laravel-10/PHP-8-strict-typing vs. `null`-Werte aus nullable DB-Spalten), die aber erst durch die Test-Durchläufe am 2026-04-17 sichtbar wurden. Wir liefern die Fixes zusammen mit Phase 2 aus, weil ein zweites separates Wartungsfenster für drei Service-Dateien nicht sinnvoll wäre und die Änderungen keinerlei DB-Abhängigkeit haben (also auch außerhalb der DB-Migration risikoarm sind). > **Wichtig:** DB und Phase-2-Code müssen atomar gemeinsam live gehen. Zwischen den beiden gibt es kein kompatibles Fenster, deswegen ist ein **Maintenance-Window von ~10–20 min Pflicht**. Die 3 Phase-1-Hotfix-Services werden im selben rsync-Schritt mit hochgeladen. --- ## Vorbereitung (am Tag vor dem Deploy) 1. **Timing**: Wartungsfenster einplanen, idealerweise außerhalb der Geschäftszeiten. ~20 Minuten reichen erfahrungsgemäß. 2. **Deploy-Freigabe**: Phase 1 auf Live läuft stabil (mindestens 24–48 h), keine Regressions-Meldungen. 3. **Team-Kommunikation**: Mitarbeiter:innen informieren — während des Wartungsfensters keine Mails, keine Buchungen anlegen, keine PDFs drucken. ### Vorab-Kontrolle auf Live (lesend, keine Änderungen) ```bash # Live-Server: aktueller Stand? php artisan --version # → sollte 10.50+ sein (Phase 1 hat Laravel 10 gebracht) php artisan migrate:status | grep phase1 # → sollte beide phase1-Migrationen mit "Ran" zeigen mysql -e "SHOW TABLES LIKE 'customer';" mysql -e "SHOW TABLES LIKE 'lead';" # → beide müssen noch existieren (Phase 2 NICHT ausgeführt) mysql -e "SHOW COLUMNS FROM booking LIKE 'lead_id';" # → lead_id muss existieren ``` Wenn einer dieser Checks nicht das erwartete Ergebnis liefert: **STOP**, klären bevor weitergemacht wird. ### Smoke-Tests auf Test (am Deploy-Tag, vor dem Live-Deploy) Diese Liste einmal komplett auf `https://mein.sterntours.test` durchgehen. Wenn einer fehlschlägt → Fehler fixen, Live-Deploy verschieben. - [ ] Login funktioniert, Dashboard lädt - [ ] **Buchungen**: `/booking` lädt, Filter- und Sortier-Funktionen arbeiten, Anfrage-Nr-Spalte zeigt korrekte Werte - [ ] **Buchungsdetail** öffnen, alle Tabs laden (Buchung, Mails, Notizen, Dokumente) - [ ] **PDF-Generierung** aus einer Buchung: Buchungsauftrag, Reisebestätigung, Voucher (Kunde + Agentur), Storno - [ ] **Anfragen**: `/lead` lädt, einzelne Anfrage öffnen - [ ] **Anfrage → Buchung erzeugen** (legt `booking.inquiry_id` korrekt an) - [ ] **Kontakte**: `/contacts` lädt, einzelner Kontakt öffnet, `leads_count` + `bookings_count` zeigen korrekte Werte (testet die Beziehungen über die neuen Tabellennamen) - [ ] **Kontakt-Duplikate**: `/contact/duplicates` lädt, Zähler stimmen - [ ] **Mail-Versand** aus einer Buchung (Eintrag in `customer_mails.lead_id` muss identisch zur `booking.inquiry_id` sein) - [ ] **Mail-Dialog öffnen** bei Buchung und Anfrage (Modal "Neue Mail schreiben" / "Mail anzeigen") — testet den nullable-`mail_dir_id`-Fix in `Services\Booking::getCustomerMailName()` / `Services\Lead::getCustomerMailName()`. - [ ] **Admin-Reports**: `/admin/report`, `/admin/report-bookings`, `/admin/report-provider`, `/admin/report-leads` laden UND jeweils einmal nach Kundenname sortieren (testet die `orderColumn`-Fixes für `contacts.firstname/name`) + CSV-Export - [ ] **BookingImport über API** (falls testbar — feuert gegen `/api/booking/import`) legt Datensatz mit korrekter `inquiry_id` an - [ ] Newsletter-Sync-Command als Dry-Run: `php artisan newsletter:sync-kulturreisen --dry-run` (falls implementiert) --- ## Deploy-Ablauf Live ### Schritt 1 — Upload-Liste generieren (lokal) Der Phase-2-Delta gegen Phase 1: **3 neue Migrationen + 24 modifizierte Dateien (Phase 2) + 3 Phase-1-Hotfix-Services = 30 Dateien.** Exakte Liste: ```bash cd /workspace/mein.sterntours.de cat > /tmp/phase2-files-to-upload.txt <<'EOF' database/migrations/2025_04_15_200001_phase2_rename_customer_to_contacts.php database/migrations/2025_04_15_200002_phase2_rename_lead_to_inquiries.php database/migrations/2025_04_15_200003_phase2_rename_booking_lead_id_to_inquiry_id.php app/Models/Booking.php app/Models/Customer.php app/Models/Contact.php app/Models/Lead.php app/Repositories/BookingPDFRepository.php app/Repositories/LeadRepository.php app/Repositories/CustomerMailRepository.php app/Http/Controllers/RequestController.php app/Http/Controllers/API/BookingController.php app/Http/Controllers/Admin/ReportController.php app/Http/Controllers/Admin/ReportBookingController.php app/Http/Controllers/Admin/ReportProviderController.php app/Http/Controllers/Admin/ReportLeadsController.php app/Http/Controllers/LeadController.php app/Http/Controllers/CustomerController.php app/Http/Controllers/ContactController.php app/Services/BookingImport.php app/Services/Booking.php app/Services/Lead.php app/Services/MailDirService.php app/Console/Commands/SyncNewsletterKulturreisen.php app/Console/Commands/ContactsFindDuplicates.php app/Console/Commands/ContactsMergeDuplicates.php resources/views/customer/mail/modal-show-mail-inner.blade.php resources/views/contact/index.blade.php resources/views/pdf/components/booking_head.blade.php resources/views/pdf/components/booking_header.blade.php EOF wc -l /tmp/phase2-files-to-upload.txt # → sollte 30 zeigen (3 Migrationen + 24 Phase-2-Code + 3 Phase-1-Hotfix-Services) ``` **Erklärung zu den zusätzlichen Dateien gegenüber einer rein strukturellen Phase-2-Liste:** - `ReportBookingController.php` — Smoke-Test-Fix am 2026-04-17: `orderColumn('customer.firstname/name', …)` → `contacts.*`. War schon vor dem Fix nicht perfekt, fiele aber beim ersten Sortier-Klick nach Phase-2-Rename hart auf. - `resources/views/contact/index.blade.php` — Smoke-Test-Fix am 2026-04-17: DataTables-Column-Definition `name: 'customer.id'` → `name: 'contacts.id'` (+ passender `orderColumn('contacts.id', …)` / `filterColumn('contacts.id', …)` im `ContactController`). Yajra verwendet `name:` als raw SQL-Spalte, wenn kein `orderColumn` matched — deswegen muss der Identifier zwischen Blade und Controller konsistent sein und dem neuen Tabellennamen entsprechen. - `app/Services/Booking.php`, `Lead.php`, `MailDirService.php` — Phase-1-Hotfix: nullable-Parameter (`?int $mailDirId`, `?string $subdir`). Behebt Laravel-10/PHP-8-strict-typing-Fehler bei Mail-Dialogen mit NULL-Werten aus `customer_mails` / `lead_mails`. > **Ausdrücklich NICHT mit hochladen:** Phase-3-/Phase-4-Migrationen (5+6 Stück), Offers-Migrationen (7 Stück), Offer-Models (6 Stück), `config/filesystems.php` (offer-Disk). Diese kommen erst mit späteren Modulen und würden auf Live ungewollt Tabellen anlegen oder Code-Erwartungen brechen. ### Schritt 1b — Upload-Liste gegen den Workspace verifizieren (Sanity-Check) Damit sich hier kein Tippfehler einschleicht, einmal prüfen, dass alle 29 Einträge tatsächlich im Workspace existieren: ```bash cd /workspace/mein.sterntours.de MISSING=0 while read f; do if [[ ! -f "$f" ]]; then echo "FEHLT: $f" MISSING=$((MISSING+1)) fi done < /tmp/phase2-files-to-upload.txt echo "---" echo "Fehlend: $MISSING (sollte 0 sein)" echo "Gesamt: $(wc -l < /tmp/phase2-files-to-upload.txt)" ``` ### Schritt 2 — DB-Backup auf Live ```bash # Auf dem Live-Server, vor dem Deploy TIMESTAMP=$(date +%Y-%m-%d_%H%M) mysqldump --single-transaction --routines --triggers \ --databases DEIN_DB_NAME \ | gzip > /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz # Größe prüfen: ls -lh /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz ``` Falls `mysqldump` nicht zur Hand: das Hoster-Tool nutzen (phpMyAdmin-Export, Plesk/cPanel-Backup-Feature, etc.). ### Schritt 3 — Maintenance-Mode an ```bash cd /pfad/zu/mein.sterntours.de php artisan down --render="errors::503" --secret="phase-2-deploy-$(date +%s)" # → Secret-URL wird ausgegeben, z.B.: # https://domain/phase-2-deploy-1746123456 # Damit kannst du während der Wartung selbst noch als Admin auf die Seite. ``` ### Schritt 4 — Upload per rsync ```bash # Vom lokalen Workspace (oder Test-Server) auf Live rsync -av --files-from=/tmp/phase2-files-to-upload.txt \ /workspace/mein.sterntours.de/ \ user@live-server:/pfad/zu/mein.sterntours.de/ ``` Alternative mit scp, falls rsync nicht verfügbar: ```bash while read f; do scp "/workspace/mein.sterntours.de/$f" "user@live-server:/pfad/zu/mein.sterntours.de/$f" done < /tmp/phase2-files-to-upload.txt ``` ### Schritt 5 — Code-Caches leeren Auf dem Live-Server: ```bash cd /pfad/zu/mein.sterntours.de php artisan cache:clear php artisan config:clear php artisan route:clear php artisan view:clear composer dump-autoload --optimize ``` ### Schritt 6 — DB-Migrationen gezielt ausführen > **Wichtig: KEIN `php artisan migrate` ohne `--path`!** > Wenn man nacktes `migrate` aufruft, werden auch die im Workspace liegenden Phase-3-, Phase-4- und Offer-Migrationen ausgeführt, die noch nicht deployreif sind. Deswegen gezielt per `--path`: ```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 # Verifikation php artisan migrate:status | grep phase2 # → alle drei sollten jetzt "Ran" zeigen ``` Erwartete Dauer: wenige Sekunden. `RENAME TABLE` ist in MySQL eine Metadaten-Operation und läuft unabhängig von der Tabellengröße fast instantan. Der Spaltenrename `booking.lead_id → inquiry_id` ist je nach MySQL-Version ebenfalls schnell (MySQL 8+ mit `ALGORITHM=INSTANT`) oder dauert ein paar Sekunden (mit Fallback). ### Schritt 7 — Production-Caches neu aufbauen ```bash php artisan config:cache php artisan route:cache php artisan view:cache ``` ### Schritt 8 — Maintenance-Mode aus ```bash php artisan up ``` ### Schritt 9 — Live-Smoke-Tests Dieselbe Checkliste wie bei Test (oben). Besonders aufpassen: - PDF-Erzeugung (Voucher einer aktuellen Buchung) - Mail-Versand aus einer Buchung - Anfrage-Erstellung + daraus Buchung ableiten - `/contacts/duplicates` öffnen — zeigt korrekte Counts Bei Problemen sofort zu **Rollback-Plan** (unten) wechseln. --- ## Rollback-Plan (falls Phase 2 auf Live scheitert) Der Rollback muss ebenfalls atomar passieren: **Code und DB zusammen zurück.** ### Schritt A — Maintenance-Mode an ```bash php artisan down --render="errors::503" ``` ### Schritt B — DB zurückrollen Option 1 (sauber, per Migration): ```bash 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 ``` (Die Migrationen haben symmetrische `down()`-Methoden, die die Renames rückgängig machen.) Option 2 (Notfall, falls `migrate:rollback` fehlschlägt): ```bash gunzip < /backup/live-db-before-phase2-${TIMESTAMP}.sql.gz | mysql DEIN_DB_NAME ``` ### Schritt C — Code zurück Entweder die pre-Phase-2-Versionen der 22 Dateien zurückspielen (z.B. aus dem DB-Backup-Zeitpunkt-Dateisystem-Backup), oder mit git Revert-Commit: ```bash # Wenn Live-Server ein Git-Checkout ist: git checkout -- app/ resources/ config/ ``` ### Schritt D — Caches + Up ```bash php artisan cache:clear && php artisan config:clear && php artisan view:clear && php artisan route:clear composer dump-autoload php artisan config:cache && php artisan route:cache && php artisan view:cache php artisan up ``` --- ## Dokumentation nach erfolgreichem Live-Deploy 1. `dev/customer-bookings/umsetzung.md` updaten: Phase 2 auf Live per 2026-MM-DD. 2. Dieses Handbuch als "durchgeführt" markieren, ggf. Learnings ergänzen. 3. Git-Commit auf Live-Server (falls Git-basiert): mit Tag `deploy/phase-2-live-2026-MM-DD`. 4. Nächster Schritt: Entscheidung, wann Phase 3 (Participants-Unification), Phase 4 (Communications-Unification) oder das Offers-Modul an die Reihe kommen. --- ## Hintergrund: warum das Maintenance-Window nötig ist Zwischen DB-Rename und Code-Deploy ist der Zustand inkompatibel: | Zustand | DB hat | Code erwartet | Resultat | |---|---|---|---| | Vor Deploy | `customer`, `lead`, `booking.lead_id` | `customer`, `lead`, `booking.lead_id` | OK | | DB-migriert, Code alt | `contacts`, `inquiries`, `booking.inquiry_id` | `customer`, `lead`, `booking.lead_id` | **Fehler: Table doesn't exist** | | DB alt, Code neu | `customer`, `lead`, `booking.lead_id` | `contacts`, `inquiries`, `booking.inquiry_id` | **Fehler: Table doesn't exist** | | Nach Deploy | `contacts`, `inquiries`, `booking.inquiry_id` | `contacts`, `inquiries`, `booking.inquiry_id` | OK | Deswegen: Maintenance-Mode an → Code + DB gleichzeitig → Maintenance-Mode aus. Alles innerhalb eines Fensters, in dem keine Requests an die App laufen.