From 653efdf9269b442f9167af51f92c376a04c16737 Mon Sep 17 00:00:00 2001 From: stevan Date: Mon, 20 Oct 2025 15:52:01 +0200 Subject: [PATCH] Enhance clean events --- app/Console/Commands/CleanDuplicateEvents.php | 113 +++++++++++------- 1 file changed, 72 insertions(+), 41 deletions(-) diff --git a/app/Console/Commands/CleanDuplicateEvents.php b/app/Console/Commands/CleanDuplicateEvents.php index 672910af5..8c41971dd 100644 --- a/app/Console/Commands/CleanDuplicateEvents.php +++ b/app/Console/Commands/CleanDuplicateEvents.php @@ -6,8 +6,8 @@ use Illuminate\Support\Facades\DB; /** - * Detect and optionally delete duplicate events (same title, start_date, end_date) - * that have consecutive IDs and were created within <2 seconds. + * Detect and optionally delete duplicate event *chains* in 2025 + * (same title, start_date, end_date) created within <2s of each other. * * Related pivot tables: * - event_tag @@ -27,74 +27,109 @@ class CleanDuplicateEvents extends Command protected $signature = 'events:clean-duplicates {--delete : Permanently delete detected duplicates and related records from DB}'; /** @var string */ - protected $description = 'Detect duplicate events (same title, start_date, end_date, consecutive IDs, created <2s apart) and optionally delete them with related pivot data.'; + protected $description = 'Detect duplicate event groups in 2025 (same title/start/end, created <2s apart) and optionally delete them with related pivot data.'; public function handle(): int { $shouldDelete = $this->option('delete'); $this->info(sprintf( - 'Scanning for possible duplicate events... (mode: %s)', + 'Scanning for possible duplicate events in 2025... (mode: %s)', $shouldDelete ? 'DELETE' : 'DRY-RUN' )); - // --- Detect duplicate event pairs --- + // --- Detect clusters of events with same title/start/end within 2 seconds --- $duplicates = DB::select(" SELECT - e1.id AS id1, - e2.id AS id2, - e1.title, - e1.start_date, - e1.end_date, - TIMESTAMPDIFF(SECOND, e1.created_at, e2.created_at) AS diff_seconds - FROM events e1 - JOIN events e2 ON e2.id = e1.id + 1 - WHERE e1.title = e2.title - AND e1.start_date = e2.start_date - AND e1.end_date = e2.end_date - AND ABS(TIMESTAMPDIFF(SECOND, e1.created_at, e2.created_at)) < 2 - ORDER BY e1.id + e.id, + e.title, + e.start_date, + e.end_date, + e.created_at, + LAG(e.created_at) OVER (PARTITION BY e.title, e.start_date, e.end_date ORDER BY e.created_at) AS prev_created + FROM events e + WHERE YEAR(e.created_at) = 2025 + AND e.title <> '' + AND e.start_date IS NOT NULL + AND e.end_date IS NOT NULL + ORDER BY e.title, e.start_date, e.created_at "); - if (empty($duplicates)) { - $this->info('No duplicate events found.'); + $toDelete = []; + $groups = []; + $prevTitle = $prevStart = $prevEnd = null; + $group = []; + + foreach ($duplicates as $row) { + $diff = null; + if ($row->prev_created) { + $diff = abs(strtotime($row->created_at) - strtotime($row->prev_created)); + } + + if ( + $row->title === $prevTitle && + $row->start_date === $prevStart && + $row->end_date === $prevEnd && + $diff !== null && $diff < 2 + ) { + $group[] = $row->id; + } else { + if (count($group) > 1) { + $groups[] = $group; + } + $group = [$row->id]; + } + + $prevTitle = $row->title; + $prevStart = $row->start_date; + $prevEnd = $row->end_date; + } + + if (count($group) > 1) { + $groups[] = $group; + } + + if (empty($groups)) { + $this->info('No duplicate event groups found in 2025.'); return self::SUCCESS; } - $this->info(sprintf('Found %d duplicate pair(s):', count($duplicates))); + $this->info(sprintf('Found %d duplicate group(s):', count($groups))); $rows = []; - foreach ($duplicates as $row) { + foreach ($groups as $g) { + $firstEvent = collect($duplicates)->firstWhere('id', $g[0]); + $rows[] = [ - $row->id1, - $row->id2, - $row->title, - $row->start_date, - $row->end_date, - $row->diff_seconds . 's', + implode(', ', $g), + count($g) . ' items', + $firstEvent?->title ?? '—', + $firstEvent?->start_date ?? '—', + $firstEvent?->end_date ?? '—', ]; + + $toDelete = array_merge($toDelete, array_slice($g, 1)); } - $this->table(['Event ID A', 'Event ID B', 'Title', 'Start', 'End', 'At'], $rows); + $this->table( + ['Event IDs (Group)', 'Count', 'Title', 'Start date', 'End date'], + $rows + ); if (!$shouldDelete) { $this->newLine(); - $this->info('💡 Use --delete to remove duplicates permanently after review.'); + $this->info('Use --delete to remove duplicates permanently after review.'); return self::SUCCESS; } $confirm = $this->confirm( - sprintf('Are you sure you want to delete %d duplicate entries (including pivot records)?', count($duplicates)) + sprintf('Delete %d duplicate event(s) across %d groups (including pivot records)?', count($toDelete), count($groups)) ); if (!$confirm) { $this->warn('Aborted. No records deleted.'); return self::SUCCESS; } - // --- Proceed deletion safely --- - $toDelete = array_column($duplicates, 'id2'); - $deleted = 0; - $bar = $this->output->createProgressBar(count($toDelete)); $bar->start(); @@ -104,24 +139,20 @@ public function handle(): int DB::table('event_tag')->where('event_id', $eventId)->delete(); DB::table('event_theme')->where('event_id', $eventId)->delete(); DB::table('audience_event')->where('event_id', $eventId)->delete(); - DB::table('events')->where('id', $eventId)->delete(); - - $deleted++; $bar->advance(); } - DB::commit(); } catch (\Throwable $e) { DB::rollBack(); $bar->finish(); - $this->error("\nError occurred: " . $e->getMessage()); + $this->error("\nError: " . $e->getMessage()); return self::FAILURE; } $bar->finish(); $this->newLine(2); - $this->info(sprintf('Successfully deleted %d duplicate event(s) and related pivot records.', $deleted)); + $this->info(sprintf('Deleted %d duplicate event(s) and related pivot records.', count($toDelete))); return self::SUCCESS; }