--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; } }