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

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