From 9f3f02ba1d68e16ad77bc78c959be877b234cd67 Mon Sep 17 00:00:00 2001 From: romanetar Date: Tue, 19 May 2026 21:07:18 +0200 Subject: [PATCH 1/5] fix(sponsors): dispatch SponsorshipCreated/Removed when sponsorships are attached inline via addSponsor or updateSponsor Co-Authored-By: Claude Sonnet 4.6 --- .../Model/Imp/SummitSponsorService.php | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index fa9e9359f..999e55420 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -14,6 +14,7 @@ use App\Events\SponsorServices\SponsorDomainEvents; use App\Events\SponsorServices\SummitSponsorCreatedEventDTO; +use App\Events\SponsorServices\SummitSponsorshipCreatedEventDTO; use App\Events\SponsorServices\DeletedEventDTO; use App\Http\Utils\IFileUploader; use App\Jobs\SponsorServices\PublishSponsorServiceDomainEventsJob; @@ -114,7 +115,9 @@ public function __construct */ public function addSponsor(Summit $summit, array $payload): Sponsor { - $sponsor = $this->tx_service->transaction(function () use ($summit, $payload) { + $new_sponsorships = []; + + $sponsor = $this->tx_service->transaction(function () use ($summit, $payload, &$new_sponsorships) { $company_id = intval($payload['company_id']); $featured_event_id = isset($payload['featured_event_id']) ? intval($payload['featured_event_id']) : 0; @@ -150,6 +153,7 @@ public function addSponsor(Summit $summit, array $payload): Sponsor $sponsorship = new SummitSponsorship(); $sponsorship->setType($summit_sponsorship_type); $sponsor->addSponsorship($sponsorship); + $new_sponsorships[] = $sponsorship; } else if(isset($payload['sponsorships'])) { foreach ($payload['sponsorships'] as $sponsorship_payload) { $type_id = isset($sponsorship_payload['type_id']) ? @@ -162,6 +166,7 @@ public function addSponsor(Summit $summit, array $payload): Sponsor $sponsorship = new SummitSponsorship(); $sponsorship->setType($summit_sponsorship_type); $sponsor->addSponsorship($sponsorship); + $new_sponsorships[] = $sponsorship; } } @@ -174,6 +179,12 @@ public function addSponsor(Summit $summit, array $payload): Sponsor SummitSponsorCreatedEventDTO::fromSummitSponsor($sponsor)->serialize(), SponsorDomainEvents::SponsorCreated); + foreach ($new_sponsorships as $sponsorship) { + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipCreated); + } + return $sponsor; } @@ -187,7 +198,10 @@ public function addSponsor(Summit $summit, array $payload): Sponsor */ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): Sponsor { - return $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload) { + $added_sponsorships = []; + $removed_sponsorships = []; + + $sponsor = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload, &$added_sponsorships, &$removed_sponsorships) { Log::debug ( sprintf @@ -270,6 +284,7 @@ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): $sponsorship = new SummitSponsorship(); $sponsorship->setType($summit_sponsorship_type); $summit_sponsor->addSponsorship($sponsorship); + $added_sponsorships[] = $sponsorship; } } else if(isset($payload['sponsorships'])) { @@ -308,6 +323,7 @@ function (array $acc, SummitSponsorship $sp): array { foreach ($current_sponsorships as $type_id => $sponsorship) { if (!$new_sponsorship_types->contains($sponsorship->getType())) { $summit_sponsor->removeSponsorship($sponsorship); + $removed_sponsorships[] = $sponsorship; } } @@ -317,6 +333,7 @@ function (array $acc, SummitSponsorship $sp): array { $sponsorship = new SummitSponsorship(); $sponsorship->setType($type); $summit_sponsor->addSponsorship($sponsorship); + $added_sponsorships[] = $sponsorship; } } @@ -338,6 +355,20 @@ function (array $acc, SummitSponsorship $sp): array { return $sponsor; }); + + foreach ($added_sponsorships as $sponsorship) { + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipCreated); + } + + foreach ($removed_sponsorships as $sponsorship) { + PublishSponsorServiceDomainEventsJob::dispatch( + DeletedEventDTO::fromEntity($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipRemoved); + } + + return $sponsor; } /** From 3ec6aa33155d8fb42ff82f95547e3c3ddde6fb4d Mon Sep 17 00:00:00 2001 From: romanetar Date: Tue, 19 May 2026 21:16:02 +0200 Subject: [PATCH 2/5] fix(sponsors): add debug logs and fix dispatch order (removed before created) for inline sponsorship events Co-Authored-By: Claude Sonnet 4.6 --- .../Model/Imp/SummitSponsorService.php | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index 999e55420..4ee8e9d01 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -180,6 +180,12 @@ public function addSponsor(Summit $summit, array $payload): Sponsor SponsorDomainEvents::SponsorCreated); foreach ($new_sponsorships as $sponsorship) { + Log::debug(sprintf( + "SummitSponsorService::addSponsor dispatching SponsorshipCreated for sponsorship %s type %s sponsor %s", + $sponsorship->getId(), + $sponsorship->getType()->getId(), + $sponsor->getId() + )); PublishSponsorServiceDomainEventsJob::dispatch( SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), SponsorDomainEvents::SponsorshipCreated); @@ -356,18 +362,30 @@ function (array $acc, SummitSponsorship $sp): array { return $sponsor; }); - foreach ($added_sponsorships as $sponsorship) { - PublishSponsorServiceDomainEventsJob::dispatch( - SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), - SponsorDomainEvents::SponsorshipCreated); - } - foreach ($removed_sponsorships as $sponsorship) { + Log::debug(sprintf( + "SummitSponsorService::updateSponsor dispatching SponsorshipRemoved for sponsorship %s type %s sponsor %s", + $sponsorship->getId(), + $sponsorship->getType()->getId(), + $sponsor_id + )); PublishSponsorServiceDomainEventsJob::dispatch( DeletedEventDTO::fromEntity($sponsorship)->serialize(), SponsorDomainEvents::SponsorshipRemoved); } + foreach ($added_sponsorships as $sponsorship) { + Log::debug(sprintf( + "SummitSponsorService::updateSponsor dispatching SponsorshipCreated for sponsorship %s type %s sponsor %s", + $sponsorship->getId(), + $sponsorship->getType()->getId(), + $sponsor_id + )); + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipCreated); + } + return $sponsor; } From 79913e1c8bb51c8cdfae38d4cacd313386816d38 Mon Sep 17 00:00:00 2001 From: romanetar Date: Wed, 20 May 2026 15:17:30 +0200 Subject: [PATCH 3/5] fix(sponsors): avoid by-reference capture in transaction callbacks to prevent duplicate events on retry Co-Authored-By: Claude Sonnet 4.6 --- app/Services/Model/Imp/SummitSponsorService.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index 4ee8e9d01..074ab02ef 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -115,9 +115,7 @@ public function __construct */ public function addSponsor(Summit $summit, array $payload): Sponsor { - $new_sponsorships = []; - - $sponsor = $this->tx_service->transaction(function () use ($summit, $payload, &$new_sponsorships) { + [$sponsor, $new_sponsorships] = $this->tx_service->transaction(function () use ($summit, $payload) { $company_id = intval($payload['company_id']); $featured_event_id = isset($payload['featured_event_id']) ? intval($payload['featured_event_id']) : 0; @@ -143,6 +141,7 @@ public function addSponsor(Summit $summit, array $payload): Sponsor } $sponsor = SponsorFactory::build($payload); + $new_sponsorships = []; if(isset($payload['sponsorship_id'])) { $type_id = intval($payload['sponsorship_id']); @@ -172,7 +171,7 @@ public function addSponsor(Summit $summit, array $payload): Sponsor $summit->addSummitSponsor($sponsor); - return $sponsor; + return [$sponsor, $new_sponsorships]; }); PublishSponsorServiceDomainEventsJob::dispatch( @@ -204,10 +203,7 @@ public function addSponsor(Summit $summit, array $payload): Sponsor */ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): Sponsor { - $added_sponsorships = []; - $removed_sponsorships = []; - - $sponsor = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload, &$added_sponsorships, &$removed_sponsorships) { + [$sponsor, $added_sponsorships, $removed_sponsorships] = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload) { Log::debug ( sprintf @@ -253,6 +249,9 @@ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): } } + $added_sponsorships = []; + $removed_sponsorships = []; + if(isset($payload['sponsorship_id'])) { $type_id = intval($payload['sponsorship_id']); @@ -359,7 +358,7 @@ function (array $acc, SummitSponsorship $sp): array { SummitSponsorCreatedEventDTO::fromSummitSponsor($sponsor)->serialize(), SponsorDomainEvents::SponsorUpdated); - return $sponsor; + return [$sponsor, $added_sponsorships, $removed_sponsorships]; }); foreach ($removed_sponsorships as $sponsorship) { From 012b1cba7c1b9264602cceb08fd724544744c644 Mon Sep 17 00:00:00 2001 From: romanetar Date: Wed, 20 May 2026 15:24:03 +0200 Subject: [PATCH 4/5] fix(sponsors): dispatch SponsorshipUpdated when sponsorship_id path mutates an existing sponsorship type Co-Authored-By: Claude Sonnet 4.6 --- .../Model/Imp/SummitSponsorService.php | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index 074ab02ef..a1dede04b 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -203,7 +203,7 @@ public function addSponsor(Summit $summit, array $payload): Sponsor */ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): Sponsor { - [$sponsor, $added_sponsorships, $removed_sponsorships] = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload) { + [$sponsor, $added_sponsorships, $removed_sponsorships, $updated_sponsorships] = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload) { Log::debug ( sprintf @@ -251,6 +251,7 @@ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): $added_sponsorships = []; $removed_sponsorships = []; + $updated_sponsorships = []; if(isset($payload['sponsorship_id'])) { $type_id = intval($payload['sponsorship_id']); @@ -272,7 +273,9 @@ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): ) ); // update the first - $summit_sponsor->getSponsorships()->first()->setType($summit_sponsorship_type); + $sponsorship = $summit_sponsor->getSponsorships()->first(); + $sponsorship->setType($summit_sponsorship_type); + $updated_sponsorships[] = $sponsorship; } else { Log::debug @@ -358,7 +361,7 @@ function (array $acc, SummitSponsorship $sp): array { SummitSponsorCreatedEventDTO::fromSummitSponsor($sponsor)->serialize(), SponsorDomainEvents::SponsorUpdated); - return [$sponsor, $added_sponsorships, $removed_sponsorships]; + return [$sponsor, $added_sponsorships, $removed_sponsorships, $updated_sponsorships]; }); foreach ($removed_sponsorships as $sponsorship) { @@ -385,6 +388,18 @@ function (array $acc, SummitSponsorship $sp): array { SponsorDomainEvents::SponsorshipCreated); } + foreach ($updated_sponsorships as $sponsorship) { + Log::debug(sprintf( + "SummitSponsorService::updateSponsor dispatching SponsorshipUpdated for sponsorship %s type %s sponsor %s", + $sponsorship->getId(), + $sponsorship->getType()->getId(), + $sponsor_id + )); + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipUpdated); + } + return $sponsor; } From 3bde93d5894e276a5f382c04f2673dd08eebd42b Mon Sep 17 00:00:00 2001 From: romanetar Date: Wed, 20 May 2026 16:15:02 +0200 Subject: [PATCH 5/5] fix: review feedback Signed-off-by: romanetar --- .../PublishSponsorServiceDomainEventsJob.php | 12 +- .../Model/Imp/SummitSponsorService.php | 75 ++++--- .../SummitSponsorServiceEventDispatchTest.php | 208 ++++++++++++++++++ 3 files changed, 268 insertions(+), 27 deletions(-) create mode 100644 tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php diff --git a/app/Jobs/SponsorServices/PublishSponsorServiceDomainEventsJob.php b/app/Jobs/SponsorServices/PublishSponsorServiceDomainEventsJob.php index 8e16154b7..3d2c856b8 100644 --- a/app/Jobs/SponsorServices/PublishSponsorServiceDomainEventsJob.php +++ b/app/Jobs/SponsorServices/PublishSponsorServiceDomainEventsJob.php @@ -35,11 +35,21 @@ final class PublishSponsorServiceDomainEventsJob implements ShouldQueue private $event_type; public function __construct(array $payload, string $event_type){ - $this->payload = $payload; + $this->payload = $payload; $this->event_type = $event_type; Log::debug(sprintf("PublishSponsorServiceDomainEventsJob::__construct payload %s event_type %s ", json_encode($payload), $event_type)); } + public function getEventType(): string + { + return $this->event_type; + } + + public function getPayload(): array + { + return $this->payload; + } + public function handle(): void { try { diff --git a/app/Services/Model/Imp/SummitSponsorService.php b/app/Services/Model/Imp/SummitSponsorService.php index a1dede04b..bde1b489c 100644 --- a/app/Services/Model/Imp/SummitSponsorService.php +++ b/app/Services/Model/Imp/SummitSponsorService.php @@ -174,9 +174,13 @@ public function addSponsor(Summit $summit, array $payload): Sponsor return [$sponsor, $new_sponsorships]; }); - PublishSponsorServiceDomainEventsJob::dispatch( - SummitSponsorCreatedEventDTO::fromSummitSponsor($sponsor)->serialize(), - SponsorDomainEvents::SponsorCreated); + try { + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorCreatedEventDTO::fromSummitSponsor($sponsor)->serialize(), + SponsorDomainEvents::SponsorCreated); + } catch (\Exception $e) { + Log::error(sprintf("SummitSponsorService::addSponsor failed to dispatch SponsorCreated for sponsor %s: %s", $sponsor->getId(), $e->getMessage())); + } foreach ($new_sponsorships as $sponsorship) { Log::debug(sprintf( @@ -185,9 +189,13 @@ public function addSponsor(Summit $summit, array $payload): Sponsor $sponsorship->getType()->getId(), $sponsor->getId() )); - PublishSponsorServiceDomainEventsJob::dispatch( - SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), - SponsorDomainEvents::SponsorshipCreated); + try { + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipCreated); + } catch (\Exception $e) { + Log::error(sprintf("SummitSponsorService::addSponsor failed to dispatch SponsorshipCreated for sponsorship %s: %s", $sponsorship->getId(), $e->getMessage())); + } } return $sponsor; @@ -203,7 +211,7 @@ public function addSponsor(Summit $summit, array $payload): Sponsor */ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): Sponsor { - [$sponsor, $added_sponsorships, $removed_sponsorships, $updated_sponsorships] = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload) { + [$sponsor, $added_sponsorships, $removed_sponsorship_ids, $updated_sponsorships] = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload) { Log::debug ( sprintf @@ -250,7 +258,7 @@ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): } $added_sponsorships = []; - $removed_sponsorships = []; + $removed_sponsorship_ids = []; $updated_sponsorships = []; if(isset($payload['sponsorship_id'])) { @@ -330,8 +338,8 @@ function (array $acc, SummitSponsorship $sp): array { // Remove types not in the new list foreach ($current_sponsorships as $type_id => $sponsorship) { if (!$new_sponsorship_types->contains($sponsorship->getType())) { + $removed_sponsorship_ids[] = $sponsorship->getId(); // capture id before Doctrine clears it on DELETE $summit_sponsor->removeSponsorship($sponsorship); - $removed_sponsorships[] = $sponsorship; } } @@ -357,23 +365,30 @@ function (array $acc, SummitSponsorship $sp): array { $summit->recalculateSummitSponsorOrder($sponsor, $payload['order']); } + return [$sponsor, $added_sponsorships, $removed_sponsorship_ids, $updated_sponsorships]; + }); + + try { PublishSponsorServiceDomainEventsJob::dispatch( - SummitSponsorCreatedEventDTO::fromSummitSponsor($sponsor)->serialize(), + SummitSponsorCreatedEventDTO::fromSummitSponsor($sponsor)->serialize(), SponsorDomainEvents::SponsorUpdated); + } catch (\Exception $e) { + Log::error(sprintf("SummitSponsorService::updateSponsor failed to dispatch SponsorUpdated for sponsor %s: %s", $sponsor_id, $e->getMessage())); + } - return [$sponsor, $added_sponsorships, $removed_sponsorships, $updated_sponsorships]; - }); - - foreach ($removed_sponsorships as $sponsorship) { + foreach ($removed_sponsorship_ids as $sponsorship_id) { Log::debug(sprintf( - "SummitSponsorService::updateSponsor dispatching SponsorshipRemoved for sponsorship %s type %s sponsor %s", - $sponsorship->getId(), - $sponsorship->getType()->getId(), + "SummitSponsorService::updateSponsor dispatching SponsorshipRemoved for sponsorship %s sponsor %s", + $sponsorship_id, $sponsor_id )); - PublishSponsorServiceDomainEventsJob::dispatch( - DeletedEventDTO::fromEntity($sponsorship)->serialize(), - SponsorDomainEvents::SponsorshipRemoved); + try { + PublishSponsorServiceDomainEventsJob::dispatch( + ['id' => $sponsorship_id], + SponsorDomainEvents::SponsorshipRemoved); + } catch (\Exception $e) { + Log::error(sprintf("SummitSponsorService::updateSponsor failed to dispatch SponsorshipRemoved for sponsorship %s: %s", $sponsorship_id, $e->getMessage())); + } } foreach ($added_sponsorships as $sponsorship) { @@ -383,9 +398,13 @@ function (array $acc, SummitSponsorship $sp): array { $sponsorship->getType()->getId(), $sponsor_id )); - PublishSponsorServiceDomainEventsJob::dispatch( - SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), - SponsorDomainEvents::SponsorshipCreated); + try { + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipCreated); + } catch (\Exception $e) { + Log::error(sprintf("SummitSponsorService::updateSponsor failed to dispatch SponsorshipCreated for sponsorship %s: %s", $sponsorship->getId(), $e->getMessage())); + } } foreach ($updated_sponsorships as $sponsorship) { @@ -395,9 +414,13 @@ function (array $acc, SummitSponsorship $sp): array { $sponsorship->getType()->getId(), $sponsor_id )); - PublishSponsorServiceDomainEventsJob::dispatch( - SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), - SponsorDomainEvents::SponsorshipUpdated); + try { + PublishSponsorServiceDomainEventsJob::dispatch( + SummitSponsorshipCreatedEventDTO::fromSponsorship($sponsorship)->serialize(), + SponsorDomainEvents::SponsorshipUpdated); + } catch (\Exception $e) { + Log::error(sprintf("SummitSponsorService::updateSponsor failed to dispatch SponsorshipUpdated for sponsorship %s: %s", $sponsorship->getId(), $e->getMessage())); + } } return $sponsor; diff --git a/tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php b/tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php new file mode 100644 index 000000000..df286f98d --- /dev/null +++ b/tests/Unit/Services/SummitSponsorServiceEventDispatchTest.php @@ -0,0 +1,208 @@ +getEventType() === $event_type; + })->all(); + + foreach ($jobs as $job) { + $payload = $job->getPayload(); + $this->assertArrayHasKey('id', $payload, "Payload for '$event_type' is missing 'id'"); + $this->assertGreaterThan(0, $payload['id'], "Payload 'id' for '$event_type' must be > 0"); + } + + return $jobs; + } + + // ------------------------------------------------------------------------- + // addSponsor — sponsorship_id path + // ------------------------------------------------------------------------- + + public function testAddSponsorWithSponsorshipIdDispatchesSponsorCreatedAndSponsorshipCreated(): void + { + Queue::fake(); + + $this->getService()->addSponsor(self::$summit, [ + 'company_id' => self::$companies_without_sponsor[0]->getId(), + 'sponsorship_id' => self::$default_summit_sponsor_type->getId(), + ]); + + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorCreated), 'Expected 1 SponsorCreated'); + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorshipCreated), 'Expected 1 SponsorshipCreated'); + } + + // ------------------------------------------------------------------------- + // addSponsor — sponsorships[] path + // ------------------------------------------------------------------------- + + public function testAddSponsorWithSponsorshipsArrayDispatchesSponsorshipCreatedForEach(): void + { + Queue::fake(); + + $this->getService()->addSponsor(self::$summit, [ + 'company_id' => self::$companies_without_sponsor[1]->getId(), + 'sponsorships' => [ + self::$default_summit_sponsor_type->getId(), + self::$default_summit_sponsor_type2->getId(), + ], + ]); + + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorCreated), 'Expected 1 SponsorCreated'); + $this->assertCount(2, $this->jobsFor(SponsorDomainEvents::SponsorshipCreated), 'Expected 1 SponsorshipCreated per type'); + } + + // ------------------------------------------------------------------------- + // updateSponsor — sponsorship_id path, no existing sponsorship → creates new + // ------------------------------------------------------------------------- + + public function testUpdateSponsorWithSponsorshipIdCreatesNewDispatchesSponsorshipCreated(): void + { + $sponsor = self::$sponsors[0]; + foreach ($sponsor->getSponsorships()->toArray() as $sp) { + $sponsor->removeSponsorship($sp); + } + self::$em->flush(); + + Queue::fake(); + + $this->getService()->updateSponsor(self::$summit, $sponsor->getId(), [ + 'sponsorship_id' => self::$default_summit_sponsor_type->getId(), + ]); + + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorUpdated), 'Expected 1 SponsorUpdated'); + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorshipCreated), 'Expected 1 SponsorshipCreated'); + } + + // ------------------------------------------------------------------------- + // updateSponsor — sponsorship_id path, hasSponsorships = true → mutates type + // ------------------------------------------------------------------------- + + public function testUpdateSponsorWithSponsorshipIdMutatesTypeDispatchesSponsorshipUpdated(): void + { + $sponsor = self::$sponsors[0]; + $this->assertTrue($sponsor->hasSponsorships(), 'Pre-condition: sponsor must have at least one sponsorship'); + + Queue::fake(); + + $this->getService()->updateSponsor(self::$summit, $sponsor->getId(), [ + 'sponsorship_id' => self::$default_summit_sponsor_type2->getId(), + ]); + + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorUpdated), 'Expected 1 SponsorUpdated'); + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorshipUpdated), 'Expected 1 SponsorshipUpdated'); + $this->assertCount(0, $this->jobsFor(SponsorDomainEvents::SponsorshipCreated), 'Expected no SponsorshipCreated for a type-change'); + $this->assertCount(0, $this->jobsFor(SponsorDomainEvents::SponsorshipRemoved), 'Expected no SponsorshipRemoved for a type-change'); + } + + // ------------------------------------------------------------------------- + // updateSponsor — sponsorships[] path, remove all + // ------------------------------------------------------------------------- + + public function testUpdateSponsorRemovesAllSponsorshipsDispatchesSponsorshipRemoved(): void + { + $sponsor = self::$sponsors[0]; + $this->assertTrue($sponsor->hasSponsorships(), 'Pre-condition: sponsor must have at least one sponsorship'); + + Queue::fake(); + + $this->getService()->updateSponsor(self::$summit, $sponsor->getId(), [ + 'sponsorships' => [], + ]); + + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorUpdated), 'Expected 1 SponsorUpdated'); + $this->assertNotEmpty($this->jobsFor(SponsorDomainEvents::SponsorshipRemoved), 'Expected at least 1 SponsorshipRemoved'); + $this->assertCount(0, $this->jobsFor(SponsorDomainEvents::SponsorshipCreated), 'Expected no SponsorshipCreated'); + } + + // ------------------------------------------------------------------------- + // updateSponsor — sponsorships[] path, replace → SponsorshipRemoved before SponsorshipCreated + // ------------------------------------------------------------------------- + + public function testUpdateSponsorReplacesSponsorshipDispatchesRemovedBeforeCreated(): void + { + // sponsors[0] starts with default_summit_sponsor_type (type1); switch to type2. + $sponsor = self::$sponsors[0]; + + Queue::fake(); + + $this->getService()->updateSponsor(self::$summit, $sponsor->getId(), [ + 'sponsorships' => [self::$default_summit_sponsor_type2->getId()], + ]); + + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorUpdated), 'Expected 1 SponsorUpdated'); + $this->assertNotEmpty($this->jobsFor(SponsorDomainEvents::SponsorshipRemoved), 'Expected at least 1 SponsorshipRemoved'); + $this->assertCount(1, $this->jobsFor(SponsorDomainEvents::SponsorshipCreated), 'Expected 1 SponsorshipCreated'); + + // SponsorshipRemoved must appear before SponsorshipCreated in the queue. + $all = Queue::pushed(PublishSponsorServiceDomainEventsJob::class)->values(); + $types = $all->map(fn($job) => $job->getEventType())->all(); + + $removed_idx = array_search(SponsorDomainEvents::SponsorshipRemoved, $types); + $created_idx = array_search(SponsorDomainEvents::SponsorshipCreated, $types); + + $this->assertLessThan($created_idx, $removed_idx, 'SponsorshipRemoved must be dispatched before SponsorshipCreated'); + } +}