From 14682450e3388a8178665727c9bf198477dae7a5 Mon Sep 17 00:00:00 2001 From: bernardhanna Date: Wed, 1 Jul 2026 12:47:05 +0100 Subject: [PATCH] Require matching address or coordinates for bulk upload duplicates. Prevents distinct events with the same title, date, country, and organiser from being merged when they differ by location. Co-authored-by: Cursor --- app/Imports/GenericEventsImport.php | 9 +- app/Services/BulkEventDuplicateFinder.php | 40 +++++++++ tests/Unit/BulkEventDuplicateFinderTest.php | 92 +++++++++++++++++++++ 3 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 app/Services/BulkEventDuplicateFinder.php create mode 100644 tests/Unit/BulkEventDuplicateFinderTest.php diff --git a/app/Imports/GenericEventsImport.php b/app/Imports/GenericEventsImport.php index bed9bed5e..f9a5b4f2e 100644 --- a/app/Imports/GenericEventsImport.php +++ b/app/Imports/GenericEventsImport.php @@ -5,6 +5,7 @@ use App\Event; use App\Helpers\UserHelper; use App\User; +use App\Services\BulkEventDuplicateFinder; use App\Services\BulkEventImportResult; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; @@ -352,12 +353,8 @@ protected function processRow(int $rowIndex, array $row): ?Model return null; } - // 8) duplicate check: find existing by title + start_date + country_iso + organizer; if found, update instead of create - $existing = Event::where('title', $attrs['title']) - ->where('start_date', $attrs['start_date']) - ->where('country_iso', $attrs['country_iso']) - ->where('organizer', $attrs['organizer']) - ->first(); + // 8) duplicate check: same title/date/country/organiser + same address or coordinates → update + $existing = app(BulkEventDuplicateFinder::class)->find($attrs); try { if ($existing) { diff --git a/app/Services/BulkEventDuplicateFinder.php b/app/Services/BulkEventDuplicateFinder.php new file mode 100644 index 000000000..dc42a4ad0 --- /dev/null +++ b/app/Services/BulkEventDuplicateFinder.php @@ -0,0 +1,40 @@ + $attrs + */ + public function find(array $attrs): ?Event + { + $query = Event::query() + ->where('title', $attrs['title']) + ->where('start_date', $attrs['start_date']) + ->where('country_iso', $attrs['country_iso']) + ->where('organizer', $attrs['organizer']); + + $location = trim((string) ($attrs['location'] ?? '')); + $latitude = (float) ($attrs['latitude'] ?? 0); + $longitude = (float) ($attrs['longitude'] ?? 0); + $hasCoordinates = abs($latitude) > self::COORDINATE_EPSILON + || abs($longitude) > self::COORDINATE_EPSILON; + + if ($location !== '') { + $query->where('location', $location); + } elseif ($hasCoordinates) { + $query->whereBetween('latitude', [$latitude - self::COORDINATE_EPSILON, $latitude + self::COORDINATE_EPSILON]) + ->whereBetween('longitude', [$longitude - self::COORDINATE_EPSILON, $longitude + self::COORDINATE_EPSILON]); + } + + return $query->first(); + } +} diff --git a/tests/Unit/BulkEventDuplicateFinderTest.php b/tests/Unit/BulkEventDuplicateFinderTest.php new file mode 100644 index 000000000..a552f7427 --- /dev/null +++ b/tests/Unit/BulkEventDuplicateFinderTest.php @@ -0,0 +1,92 @@ +create([ + 'title' => 'Coding workshop', + 'organizer' => 'Example School', + 'location' => '1 Main Street', + 'country_iso' => 'IE', + 'start_date' => '2026-10-10 09:00:00', + 'end_date' => '2026-10-10 12:00:00', + 'latitude' => 53.3, + 'longitude' => -6.2, + ]); + + $match = app(BulkEventDuplicateFinder::class)->find([ + 'title' => 'Coding workshop', + 'start_date' => '2026-10-10 09:00:00', + 'country_iso' => 'IE', + 'organizer' => 'Example School', + 'location' => '2 Other Road', + 'latitude' => 53.31, + 'longitude' => -6.21, + ]); + + $this->assertNull($match); + } + + public function test_matches_when_core_fields_and_address_are_the_same(): void + { + $event = Event::factory()->create([ + 'title' => 'Coding workshop', + 'organizer' => 'Example School', + 'location' => '1 Main Street', + 'country_iso' => 'IE', + 'start_date' => '2026-10-10 09:00:00', + 'end_date' => '2026-10-10 12:00:00', + 'latitude' => 53.3, + 'longitude' => -6.2, + ]); + + $match = app(BulkEventDuplicateFinder::class)->find([ + 'title' => 'Coding workshop', + 'start_date' => '2026-10-10 09:00:00', + 'country_iso' => 'IE', + 'organizer' => 'Example School', + 'location' => '1 Main Street', + 'latitude' => 53.30001, + 'longitude' => -6.20001, + ]); + + $this->assertNotNull($match); + $this->assertSame($event->id, $match->id); + } + + public function test_does_not_match_when_coordinates_differ_and_address_is_empty(): void + { + Event::factory()->create([ + 'title' => 'Robotics day', + 'organizer' => 'STEM Hub', + 'location' => '', + 'country_iso' => 'DE', + 'start_date' => '2026-11-01 10:00:00', + 'end_date' => '2026-11-01 14:00:00', + 'latitude' => 52.52, + 'longitude' => 13.405, + ]); + + $match = app(BulkEventDuplicateFinder::class)->find([ + 'title' => 'Robotics day', + 'start_date' => '2026-11-01 10:00:00', + 'country_iso' => 'DE', + 'organizer' => 'STEM Hub', + 'location' => '', + 'latitude' => 48.13, + 'longitude' => 11.58, + ]); + + $this->assertNull($match); + } +}