mein-sterntours/dev/customer-bookings/phase-2-live-deploy.md
2026-04-22 16:01:27 +02:00

15 KiB
Raw Blame History

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 2529, 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_idinquiry_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 ~1020 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 2448 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)

# 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:

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:

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

# 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

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

# 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:

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:

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:

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

php artisan config:cache
php artisan route:cache
php artisan view:cache

Schritt 8 — Maintenance-Mode aus

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

php artisan down --render="errors::503"

Schritt B — DB zurückrollen

Option 1 (sauber, per Migration):

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):

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:

# Wenn Live-Server ein Git-Checkout ist:
git checkout <phase-1-commit-id> -- app/ resources/ config/

Schritt D — Caches + Up

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.