mein-sterntours/app/Console/Commands/BookingsResyncFromTravelBookings.php

172 lines
6.9 KiB
PHP

<?php
namespace App\Console\Commands;
use Throwable;
use Carbon\Carbon;
use App\Models\Booking;
use App\Models\TravelBooking;
use App\Services\BookingImport;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Abgleich: travel_booking (DB mysql_stern, Web-Frontend-Backup)
* ↔ booking (DB mysql, CRM-Hauptsystem)
*
* Hintergrund:
* Das Web-Frontend schreibt jede Buchung zuerst in die Tabelle `travel_booking`
* (Connection `mysql_stern`) und ruft dann via API den Import-Endpunkt des CRM
* auf. Dieser legt Customer + Lead + Booking (+ Participants/Arrangements/Drafts)
* an und trägt zuletzt die neue `booking.id` in `travel_booking.crm_booking_id`
* zurück. Bricht der Import irgendwo vorher ab, fehlt entweder das CRM-Booking
* ganz oder es ist nur teilweise angelegt und `crm_booking_id` bleibt NULL.
*
* Was der Command macht:
* • Listet `travel_booking`-Einträge der letzten N Tage, bei denen
* `crm_booking_id IS NULL` (primärer Fehlerfall).
* • Optional: listet Orphans (crm_booking_id gesetzt, aber Booking existiert
* nicht mehr — z. B. gelöscht) mit --include-orphans.
* • Default ist Dry-Run — nur mit --apply wird wirklich importiert.
* • Jeder Import läuft in einer eigenen CRM-DB-Transaktion.
*
* Beispiele:
* php artisan bookings:resync-from-travel-bookings
* php artisan bookings:resync-from-travel-bookings --days=30
* php artisan bookings:resync-from-travel-bookings --apply
* php artisan bookings:resync-from-travel-bookings --id=12345 --id=12346 --apply
* php artisan bookings:resync-from-travel-bookings --include-orphans
*
* Einzelfall neu importieren (wenn Datensatz im CRM unvollständig ist,
* z. B. fehlendes Reiseprogramm):
* 1) Im CRM das unvollständige Booking löschen (oder gezielt merken).
* 2) In mysql_stern.travel_booking `crm_booking_id` für diesen Datensatz
* auf NULL zurücksetzen.
* 3) `php artisan bookings:resync-from-travel-bookings --id=<travel_booking.id> --apply`
*/
class BookingsResyncFromTravelBookings extends Command
{
protected $signature = 'bookings:resync-from-travel-bookings
{--days=14 : Zeitfenster in Tagen (auf travel_booking.created); ignoriert bei --id}
{--id=* : Konkrete travel_booking.id(s) prüfen/importieren (überschreibt --days)}
{--apply : Fehlende Imports wirklich ausführen (ohne Flag: nur Dry-Run)}
{--include-orphans : Zusätzlich prüfen, ob das verknüpfte CRM-Booking noch existiert}';
protected $description = 'Gleicht travel_booking (mysql_stern) mit booking (CRM) ab und zieht fehlende API-Imports nach.';
public function handle(): int
{
$apply = (bool) $this->option('apply');
$ids = (array) $this->option('id');
$days = max(1, (int) $this->option('days'));
$includeOrphans = (bool) $this->option('include-orphans');
$query = TravelBooking::query()->orderBy('id', 'desc');
if (!empty($ids)) {
$query->whereIn('id', $ids);
$scopeLabel = 'per --id=' . implode(',', $ids);
} else {
$query->where('created', '>=', Carbon::now()->subDays($days));
$scopeLabel = "letzte {$days} Tage";
}
$total = (clone $query)->count();
$this->line(sprintf('Prüfe %d travel_booking-Eintrag/e (%s)…', $total, $scopeLabel));
if ($total === 0) {
$this->info('Keine Einträge im Scope gefunden.');
return self::SUCCESS;
}
$missing = [];
$orphans = [];
$query->chunkById(200, function ($chunk) use (&$missing, &$orphans, $includeOrphans) {
foreach ($chunk as $tb) {
if (empty($tb->crm_booking_id)) {
$missing[] = $tb;
continue;
}
if ($includeOrphans && !Booking::whereKey($tb->crm_booking_id)->exists()) {
$orphans[] = $tb;
}
}
});
$this->newLine();
$this->info('Ergebnis:');
$this->line(sprintf(' • Nicht importiert (crm_booking_id IS NULL): %d', count($missing)));
if ($includeOrphans) {
$this->line(sprintf(' • Orphan (CRM-Booking fehlt): %d', count($orphans)));
}
if (empty($missing) && empty($orphans)) {
$this->newLine();
$this->info('Alles synchron — nichts zu tun.');
return self::SUCCESS;
}
if (!empty($missing)) {
$this->newLine();
$this->line('Fehlende Imports:');
foreach ($missing as $tb) {
$this->line(sprintf(
' MISSING tb#%d %s %s <%s> %s → %s "%s"',
$tb->id,
$tb->first_name ?: '—',
$tb->last_name ?: '—',
$tb->email ?: '—',
optional($tb->selected_start_date)->format('Y-m-d') ?? '?',
optional($tb->selected_end_date)->format('Y-m-d') ?? '?',
$tb->program_name ?: ($tb->selected_travel['travel_title'] ?? '—')
));
}
}
if (!empty($orphans)) {
$this->newLine();
$this->line('Orphans (CRM-Booking gelöscht/fehlt):');
foreach ($orphans as $tb) {
$this->line(sprintf(' ORPHAN tb#%d → crm_booking_id=%d nicht vorhanden', $tb->id, $tb->crm_booking_id));
}
$this->warn('Orphans werden NICHT automatisch neu importiert — bitte manuell prüfen (Daten evtl. bewusst gelöscht).');
}
if (!$apply) {
$this->newLine();
$this->warn('Dry-Run — keine Änderungen. Mit --apply ausführen, um fehlende zu importieren.');
return self::SUCCESS;
}
if (empty($missing)) {
return self::SUCCESS;
}
$this->newLine();
$this->info('Starte Import der fehlenden Einträge…');
$ok = 0;
$fail = 0;
foreach ($missing as $tb) {
try {
DB::connection('mysql')->beginTransaction();
$booking = BookingImport::importFrom($tb);
DB::connection('mysql')->commit();
$ok++;
$this->info(sprintf(' ✓ tb#%d → booking #%d', $tb->id, $booking->id));
} catch (Throwable $e) {
if (DB::connection('mysql')->transactionLevel() > 0) {
DB::connection('mysql')->rollBack();
}
$fail++;
$this->error(sprintf(' ✗ tb#%d FAILED: %s', $tb->id, $e->getMessage()));
}
}
$this->newLine();
$this->info(sprintf('Fertig. Erfolgreich: %d Fehlgeschlagen: %d', $ok, $fail));
return $fail > 0 ? self::FAILURE : self::SUCCESS;
}
}