172 lines
6.9 KiB
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;
|
|
}
|
|
}
|