# 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: ```diff - '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`): ```bash 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: ```bash 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: ```bash 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: ```sql 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): ```sql 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. ```bash # 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` | ```bash # 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: ```bash 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:** ```bash # 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).