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 = NULLbei 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_idwird trotzdem korrekt gesetzt → Kontakt sieht die Buchung.lead_idbleibtNULL→ 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).