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 fa9e9359f..bde1b489c 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,7 @@ public function __construct */ public function addSponsor(Summit $summit, array $payload): Sponsor { - $sponsor = $this->tx_service->transaction(function () use ($summit, $payload) { + [$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; @@ -140,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']); @@ -150,6 +152,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,17 +165,38 @@ public function addSponsor(Summit $summit, array $payload): Sponsor $sponsorship = new SummitSponsorship(); $sponsorship->setType($summit_sponsorship_type); $sponsor->addSponsorship($sponsorship); + $new_sponsorships[] = $sponsorship; } } $summit->addSummitSponsor($sponsor); - return $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( + "SummitSponsorService::addSponsor dispatching SponsorshipCreated for sponsorship %s type %s sponsor %s", + $sponsorship->getId(), + $sponsorship->getType()->getId(), + $sponsor->getId() + )); + 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; } @@ -187,7 +211,7 @@ 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) { + [$sponsor, $added_sponsorships, $removed_sponsorship_ids, $updated_sponsorships] = $this->tx_service->transaction(function () use ($summit, $sponsor_id, $payload) { Log::debug ( sprintf @@ -233,6 +257,10 @@ public function updateSponsor(Summit $summit, int $sponsor_id, array $payload): } } + $added_sponsorships = []; + $removed_sponsorship_ids = []; + $updated_sponsorships = []; + if(isset($payload['sponsorship_id'])) { $type_id = intval($payload['sponsorship_id']); @@ -253,7 +281,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 @@ -270,6 +300,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'])) { @@ -307,6 +338,7 @@ 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); } } @@ -317,6 +349,7 @@ function (array $acc, SummitSponsorship $sp): array { $sponsorship = new SummitSponsorship(); $sponsorship->setType($type); $summit_sponsor->addSponsorship($sponsorship); + $added_sponsorships[] = $sponsorship; } } @@ -332,12 +365,65 @@ 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())); + } + + foreach ($removed_sponsorship_ids as $sponsorship_id) { + Log::debug(sprintf( + "SummitSponsorService::updateSponsor dispatching SponsorshipRemoved for sponsorship %s sponsor %s", + $sponsorship_id, + $sponsor_id + )); + 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) { + Log::debug(sprintf( + "SummitSponsorService::updateSponsor dispatching SponsorshipCreated for sponsorship %s type %s sponsor %s", + $sponsorship->getId(), + $sponsorship->getType()->getId(), + $sponsor_id + )); + 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) { + Log::debug(sprintf( + "SummitSponsorService::updateSponsor dispatching SponsorshipUpdated for sponsorship %s type %s sponsor %s", + $sponsorship->getId(), + $sponsorship->getType()->getId(), + $sponsor_id + )); + 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; - }); + 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'); + } +}