Neustrukturierung Customer / Lead / Booking Phase 2
This commit is contained in:
parent
313f0dbf4e
commit
6df9c401af
69 changed files with 3809 additions and 374 deletions
172
dev/hotfix-2026-04-18/BookingImport.php
Normal file
172
dev/hotfix-2026-04-18/BookingImport.php
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Lead;
|
||||
use App\Models\Booking;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Arrangement;
|
||||
use App\Models\Participant;
|
||||
use App\Models\TravelBooking;
|
||||
use App\Models\BookingServiceItem;
|
||||
use App\Repositories\DraftRepository;
|
||||
|
||||
class BookingImport
|
||||
{
|
||||
|
||||
public static function importFrom(TravelBooking $travel_booking){
|
||||
|
||||
// ---- createCustomer
|
||||
$data = [
|
||||
'salutation_id' => $travel_booking->salutation_id,
|
||||
'name' => $travel_booking->last_name,
|
||||
'firstname' => $travel_booking->first_name,
|
||||
'street' => $travel_booking->street,
|
||||
'zip' => $travel_booking->zipcode,
|
||||
'city' => $travel_booking->city,
|
||||
'country_id' => $travel_booking->country_id,
|
||||
'phone' => $travel_booking->phone,
|
||||
'phonemobile' => $travel_booking->mobile,
|
||||
'email' => $travel_booking->email
|
||||
];
|
||||
$customer = Customer::create($data);
|
||||
|
||||
// ---- createLead
|
||||
$data = [
|
||||
'customer_id' => $customer->id,
|
||||
'request_date' => $travel_booking->created,
|
||||
'travelperiod_start' => $travel_booking->selected_start_date,
|
||||
'travelperiod_end' => $travel_booking->selected_end_date,
|
||||
'remarks' => $travel_booking->comments,
|
||||
'sf_guard_user_id' => 15,
|
||||
'is_closed' => true,
|
||||
'initialcontacttype_id' => 14,
|
||||
'status_id' => 7,
|
||||
'website_id' => 1,
|
||||
];
|
||||
$lead = Lead::create($data);
|
||||
$lead->updateNextDueDate();
|
||||
|
||||
$comfort = false;
|
||||
if(isset($travel_booking->drafts['comfort']) && $travel_booking->drafts['comfort']){
|
||||
$comfort = true;
|
||||
}
|
||||
$data = [
|
||||
'booking_date' => $travel_booking->created->format('Y-m-d'),
|
||||
'customer_id' => $customer->id,
|
||||
'lead_id' => $lead->id,
|
||||
'new_drafts' => $travel_booking->drafts === null ? 0 : 1,
|
||||
'sf_guard_user_id' => 15,
|
||||
'branch_id' => 4,
|
||||
'pax' => $travel_booking->selected_adults,
|
||||
'title' => isset($travel_booking->selected_travel['travel_title']) ? $travel_booking->selected_travel['travel_title'] : "",
|
||||
'comfort' => $comfort,
|
||||
'start_date' => $travel_booking->selected_start_date->format('Y-m-d'),
|
||||
'end_date' => $travel_booking->selected_end_date->format('Y-m-d'),
|
||||
'website_id' => 1,
|
||||
'travel_number' => isset($travel_booking->selected_travel['travel_number']) ? $travel_booking->selected_travel['travel_number'] : null,
|
||||
/*'participant_name' => isset($travel_booking->participants[0]['last_name']) ? $travel_booking->participants[0]['last_name'] : null,
|
||||
'participant_firstname' => isset($travel_booking->participants[0]['first_name']) ? $travel_booking->participants[0]['first_name'] : null,
|
||||
'participant_birthdate' => isset($travel_booking->participants[0]['birthday']) ? date( "Y-m-d", strtotime($travel_booking->participants[0]['birthday'])) : null,
|
||||
'participant_salutation_id' => isset($travel_booking->participants[0]['gender']) ? $travel_booking->participants[0]['gender'] : null,
|
||||
'nationality_id' => isset($travel_booking->participants[0]['nationality']) ? $travel_booking->participants[0]['nationality'] : null,*/
|
||||
'participant_name' => null,
|
||||
'participant_firstname' => null,
|
||||
'participant_birthdate' => null,
|
||||
'participant_salutation_id' => null,
|
||||
'nationality_id' => null,
|
||||
'price' => $travel_booking->price,
|
||||
'price_total' => $travel_booking->price_total,
|
||||
'deposit_total' => $travel_booking->deposit_total,
|
||||
'final_payment' => $travel_booking->final_payment,
|
||||
'final_payment_date' => $travel_booking->final_payment_date->format('Y-m-d'),
|
||||
'travel_country_id' => isset($travel_booking->selected_travel['travel_country_id'][0]) ? $travel_booking->selected_travel['travel_country_id'][0] : null,
|
||||
'travel_category_id' => isset($travel_booking->selected_travel['travel_category_id']) ? $travel_booking->selected_travel['travel_category_id'] : null,
|
||||
'travelagenda_id' => isset($travel_booking->selected_travel['travelagenda_id']) ? $travel_booking->selected_travel['travelagenda_id'] : null,
|
||||
'travel_company_id' => isset($travel_booking->selected_travel['travel_company_id']) ? $travel_booking->selected_travel['travel_company_id'] : 4,
|
||||
'insurance_offer' => $travel_booking->insurance_offer,
|
||||
];
|
||||
|
||||
//createBooking
|
||||
$booking = Booking::create($data);
|
||||
|
||||
//createTraveler
|
||||
if($travel_booking->participants){
|
||||
foreach ($travel_booking->participants as $key => $participant){
|
||||
Participant::create([
|
||||
'booking_id' => $booking->id,
|
||||
'participant_name' => $participant['last_name'],
|
||||
'participant_firstname' => $participant['first_name'],
|
||||
'participant_birthdate' => date( "Y-m-d", strtotime($participant['birthday'])),
|
||||
'participant_salutation_id' => $participant['gender'],
|
||||
'participant_child' => $participant['child'],
|
||||
'nationality_id' =>$participant['nationality'],
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//createServiceItem //service_items
|
||||
if($travel_booking->service_items){
|
||||
foreach ($travel_booking->service_items as $key => $service_item){
|
||||
BookingServiceItem::create([
|
||||
'booking_id' => $booking->id,
|
||||
'travel_company_id' => $service_item['travel_company_id'],
|
||||
'service_price' => $service_item['service_price'],
|
||||
'service_price_refund' => 0,
|
||||
'commission' => $service_item['commission'],
|
||||
'travel_date' => $service_item['travel_date'],
|
||||
'name' => $service_item['name'],
|
||||
'is_commission_locked' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//createServiceItem //insurances
|
||||
if($travel_booking->insurances){
|
||||
foreach ($travel_booking->insurances as $service_item){
|
||||
BookingServiceItem::create([
|
||||
'booking_id' => $booking->id,
|
||||
'travel_company_id' => $service_item['travel_company_id'],
|
||||
'service_price' => $service_item['price'],
|
||||
'service_price_refund' => 0,
|
||||
'commission' => $service_item['commission'],
|
||||
'travel_date' => $service_item['travel_date'],
|
||||
'name' => $service_item['name'],
|
||||
'is_commission_locked' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//createArrangement
|
||||
if($travel_booking->arrangements) {
|
||||
foreach ($travel_booking->arrangements as $key => $arrangement){
|
||||
Arrangement::create([
|
||||
'state' => isset($arrangement['state']) ? $arrangement['state'] : null,
|
||||
'begin' => isset($arrangement['end']) ? $arrangement['end'] : null,
|
||||
'end' => isset($arrangement['end']) ? $arrangement['end'] : null,
|
||||
'type_s' => isset($arrangement['type_s']) ? $arrangement['type_s'] : null,
|
||||
'data_s' => isset($arrangement['data_s']) ? $arrangement['data_s'] : null,
|
||||
'view_position' => isset($arrangement['view_position']) ? $arrangement['view_position'] : null,
|
||||
'booking_id' => $booking->id,
|
||||
'type_id' => $arrangement['type_id'],
|
||||
'in_pdf' => isset($arrangement['in_pdf']) ? $arrangement['in_pdf'] : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//createDrafts
|
||||
|
||||
if($travel_booking->drafts) {
|
||||
$draftRepo = new DraftRepository($booking);
|
||||
$draftRepo->create_drafts_from_booking($booking->id, $travel_booking->drafts);
|
||||
}
|
||||
|
||||
$travel_booking->crm_booking_id = $booking->id;
|
||||
$travel_booking->save();
|
||||
|
||||
return $booking;
|
||||
|
||||
}
|
||||
}
|
||||
254
dev/hotfix-2026-04-18/HOTFIX.md
Normal file
254
dev/hotfix-2026-04-18/HOTFIX.md
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
# 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue