254 lines
8.5 KiB
Markdown
254 lines
8.5 KiB
Markdown
# 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).
|