mein-sterntours/dev/hotfix-2026-04-18/HOTFIX.md

8.5 KiB

Hotfix 2026-04-18 — Fehlende booking.lead_id bei neu importierten Buchungen

Status: Dokumentation des am 2026-04-17/18 aufgetretenen Live-Bugs und der durchgeführten Korrekturen. Zielsystem: Live (mein.sterntours.de, Phase 1) Abhängigkeit: Phase-1-Deploy vom 2026-04-17 — NICHT vermischen mit Phase-2-Deploy.


1. Symptom

Auf Live wurden ab dem 2026-04-17 neu eingehende Anfragen über die API zwar in contact (ehem. customer) und lead sauber angelegt, aber das zugehörige booking blieb in den alten Views „frei schwebend":

  • Die Buchung war im Kontakt-Detail sichtbar (über booking.customer_id).
  • In der Lead-Ansicht tauchte sie NICHT auf (lead -> bookings).
  • In der Booking-Liste wurde die Verknüpfung zum Lead nicht angezeigt.
  • In der DB: booking.lead_id = NULL bei jedem neuen Datensatz seit Phase-1-Deploy.

Zusätzlich trat in Einzelfällen auf, dass ein importiertes Booking keine Programm-Verknüpfung hatte (title, travel_number, travelagenda_id etc. leer).

2. Root Cause

Beim Phase-1-Deploy am 2026-04-17 ist die Phase-2-Version von app/Services/BookingImport.php versehentlich mit auf Live gelandet. Diese Datei enthält die für Phase 2 vorbereitete Zeile:

- 'lead_id' => $lead->id,      // Phase 1 (korrekt für Live)
+ 'inquiry_id' => $lead->id,   // Phase 2 (Workspace-Stand)

Auf Live existiert die Spalte booking.inquiry_id (noch) nicht — sie entsteht erst in Phase 2 durch das Rename booking.lead_id → booking.inquiry_id. Und das Live-Booking-Model hat 'inquiry_id' nicht in $fillable. Folge:

  • Booking::create(['inquiry_id' => 42, ...]) → Eloquent ignoriert das Feld stumm (mass-assignment guard).
  • customer_id wird trotzdem korrekt gesetzt → Kontakt sieht die Buchung.
  • lead_id bleibt NULL → alle leadbasierten Views finden die Buchung nicht.

Warum Programm-Felder in Einzelfällen fehlten, ist davon unabhängig: wenn travel_booking.selected_travel zum Import-Zeitpunkt leer/unvollständig war, übernimmt BookingImport die sechs Programm-Felder (title, travel_number, travel_country_id, travel_category_id, travelagenda_id, travel_company_id) als null.

3. Verifikation des Root Cause

Git-Commit 5a74789 deplay phase 1 vs. aktueller Workspace-Stand (ba48745 phase 2 dev):

cd /workspace/mein.sterntours.de
git diff 5a74789 HEAD -- app/Services/BookingImport.php
# → Zeigt genau die `'lead_id'` ↔ `'inquiry_id'` Zeile.

Auf Live gegengecheckt mit:

grep -n "lead_id\|inquiry_id" app/Services/BookingImport.php
# → Live zeigte 'inquiry_id' (Bug bestätigt)

4. Sofort-Hotfix (erledigt)

4.1 Datei-Austausch

Die saubere Phase-1-Version von BookingImport.php wurde aus Commit 5a74789 exportiert und liegt unter:

dev/hotfix-2026-04-18/BookingImport.php

Upload:

scp dev/hotfix-2026-04-18/BookingImport.php \
    user@live:/var/www/html/app/Services/BookingImport.php

# Auf Live:
php artisan cache:clear
php artisan view:clear
php artisan config:clear
composer dump-autoload

4.2 DB-Reparatur für bereits verwaiste Buchungen

Vorschau-Query:

SELECT COUNT(*) FROM booking WHERE lead_id IS NULL AND created_at >= '2026-04-17';

SELECT b.id AS booking_id, b.customer_id, b.created_at AS booking_created,
       l.id AS lead_candidate_id, l.created_at AS lead_created,
       c.firstname, c.name, c.email
FROM booking b
LEFT JOIN lead l ON l.customer_id = b.customer_id
LEFT JOIN customer c ON c.id = b.customer_id
WHERE b.lead_id IS NULL
  AND b.created_at >= '2026-04-17'
ORDER BY b.id DESC;

-- Sanity-Check: eindeutige 1:1-Zuordnung Customer ↔ Lead?
SELECT customer_id, COUNT(*) AS lead_count
FROM lead
WHERE customer_id IN (
    SELECT customer_id FROM booking WHERE lead_id IS NULL AND created_at >= '2026-04-17'
)
GROUP BY customer_id
HAVING lead_count > 1;

Reparatur (nur wenn Sanity-Check leer):

UPDATE booking b
INNER JOIN lead l ON l.customer_id = b.customer_id
SET b.lead_id = l.id
WHERE b.lead_id IS NULL
  AND b.created_at >= '2026-04-17';

Zwei betroffene Buchungen wurden am 2026-04-18 so manuell nachgetragen (vom User).

5. Reparatur-Toolkit für zukünftige Ausfälle

Zwei Artisan-Commands wurden gebaut, um ähnliche Import-Probleme gezielt abhandeln zu können (siehe app/Console/Commands/). Beide sind nicht Teil von Phase 2 — reine Live-Werkzeuge, die sowohl auf Phase 1 als auch auf Phase 2 funktionieren (sie nutzen das bestehende BookingImport).

5.1 bookings:resync-from-travel-bookings

Use Case: travel_booking.crm_booking_id IS NULL — Import ist nie durchgelaufen oder mittendrin abgebrochen. Zieht das komplette Booking (Customer + Lead + Booking + Participants + Arrangements + ServiceItems + Drafts) frisch nach.

# Dry-Run (immer zuerst):
php artisan bookings:resync-from-travel-bookings --days=14

# Mit Anwenden:
php artisan bookings:resync-from-travel-bookings --days=14 --apply

# Einzelfall:
php artisan bookings:resync-from-travel-bookings --id=12345 --apply

# Plus Orphan-Check:
php artisan bookings:resync-from-travel-bookings --days=30 --include-orphans

Jeder Import in eigener Transaktion — Fehler in einem Datensatz stoppt die anderen nicht.

5.2 bookings:repair-from-travel-bookings

Use Case: crm_booking_id ist gesetzt, aber einzelne Programm-Felder im Booking fehlen (z. B. title, travel_number, travelagenda_id). Macht nur ein Partial-Update der sechs Programm-Felder.

booking-Spalte Quelle in travel_booking.selected_travel
title travel_title
travel_number travel_number
travel_country_id travel_country_id[0]
travel_category_id travel_category_id
travelagenda_id travelagenda_id
travel_company_id travel_company_id
# Dry-Run:
php artisan bookings:repair-from-travel-bookings --days=14

# Anwenden:
php artisan bookings:repair-from-travel-bookings --days=14 --apply

# Gezielt auf CRM-Booking-ID:
php artisan bookings:repair-from-travel-bookings --booking-id=9876 --apply

# Auch bereits gesetzte Felder überschreiben:
php artisan bookings:repair-from-travel-bookings --booking-id=9876 --apply --force

Default-Policy: nur leere/NULL-Felder füllen. Manuell gesetzte Werte bleiben unangetastet. Mit --force werden auch bestehende Werte überschrieben.

5.3 Entscheidungshilfe

Situation Tool
travel_booking.crm_booking_id IS NULL bookings:resync-from-travel-bookings
Buchung existiert, nur Programm-Felder leer bookings:repair-from-travel-bookings
Buchung existiert, aber viele Felder / Teilnehmer fehlen CRM-Booking löschen → travel_booking.crm_booking_id auf NULL → bookings:resync-…

6. Deploy dieser Hotfix-Tools auf Live

Beide Commands sind selbst-enthaltene PHP-Dateien, keine DB-Änderung nötig:

scp app/Console/Commands/BookingsResyncFromTravelBookings.php \
    user@live:/var/www/html/app/Console/Commands/BookingsResyncFromTravelBookings.php

scp app/Console/Commands/BookingsRepairFromTravelBookings.php \
    user@live:/var/www/html/app/Console/Commands/BookingsRepairFromTravelBookings.php

# Auf Live:
php artisan list | grep "bookings:"
# → muss beide Commands zeigen

7. Konsequenzen für Phase-2-Deploy

Dieser Vorfall hat gezeigt, dass beim Phase-1-Deploy am 2026-04-17 mindestens eine Phase-2-Datei mit rübergerutscht ist. Vor Phase-2-Deploy MUSS daher zusätzlich zum bestehenden UPLOAD-LIST-Verfahren ein expliziter Scan stattfinden — siehe Ergänzung im Phase-2-Deploy-Handbuch.

Sanity-Check vor jedem zukünftigen Phase-Deploy:

# Auf Live, nach dem Upload:
grep -rn "inquiry_id\|'contacts'\b\|'inquiries'\b" app/ 2>/dev/null | head -40

Wenn nach Phase-2-Deploy alle Treffer auf die erwarteten Stellen zeigen (und nicht auf nicht-gelistete Dateien), ist der Deploy sauber. Ein analoger Gegencheck vor Phase 2 (dort dürfen keine Phase-2-Tokens existieren außer in BookingImport.php, falls dieser Hotfix-Stand noch nicht ersetzt wurde).


Anhang: Inhalt dieses Ordners

dev/hotfix-2026-04-18/
├── HOTFIX.md              ← Dieses Dokument
└── BookingImport.php      ← Phase-1-Version aus Commit 5a74789 (Deploy-Ready)

Die beiden neuen Artisan-Commands liegen im normalen Code-Baum:

app/Console/Commands/BookingsResyncFromTravelBookings.php
app/Console/Commands/BookingsRepairFromTravelBookings.php

Sie gehören dauerhaft zum Projekt und sollen auch nach Phase-2-Deploy bestehen bleiben (sie funktionieren auf beiden Schemas).