From 205d77bc0b0635f7a962d7f632aea45a38b77884 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 14 Sep 2026 08:53:45 -0300 Subject: [PATCH] feat(event): dispatch date change notifications --- .../Event/Events/EventDateRescheduled.php | 22 +++++++ .../Event/Events/EventDateSuspended.php | 20 +++++++ .../AffectedEventDatePurchaseResolver.php | 57 +++++++++++++++++++ app/Domains/Event/Services/EventService.php | 40 ++++++++++++- .../Event/AdminAppEventControllerTest.php | 21 +++++++ 5 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 app/Domains/Event/Events/EventDateRescheduled.php create mode 100644 app/Domains/Event/Events/EventDateSuspended.php create mode 100644 app/Domains/Event/Services/AffectedEventDatePurchaseResolver.php diff --git a/app/Domains/Event/Events/EventDateRescheduled.php b/app/Domains/Event/Events/EventDateRescheduled.php new file mode 100644 index 0000000..d0c5271 --- /dev/null +++ b/app/Domains/Event/Events/EventDateRescheduled.php @@ -0,0 +1,22 @@ +}> $purchaseTickets + */ + public function __construct( + public readonly string $tenantCode, + public readonly int $sourceEventDateId, + public readonly int $destinationEventDateId, + public readonly string $previousDate, + public readonly string $newDate, + public readonly array $purchaseTickets, + ) {} +} diff --git a/app/Domains/Event/Events/EventDateSuspended.php b/app/Domains/Event/Events/EventDateSuspended.php new file mode 100644 index 0000000..fe99365 --- /dev/null +++ b/app/Domains/Event/Events/EventDateSuspended.php @@ -0,0 +1,20 @@ +}> $purchaseTickets + */ + public function __construct( + public readonly string $tenantCode, + public readonly int $eventDateId, + public readonly string $date, + public readonly array $purchaseTickets, + ) {} +} diff --git a/app/Domains/Event/Services/AffectedEventDatePurchaseResolver.php b/app/Domains/Event/Services/AffectedEventDatePurchaseResolver.php new file mode 100644 index 0000000..23b7e04 --- /dev/null +++ b/app/Domains/Event/Services/AffectedEventDatePurchaseResolver.php @@ -0,0 +1,57 @@ +|list $eventDateIds + * @return list}> + */ + public function resolve(Tenant $tenant, Collection|array $eventDateIds): array + { + $eventDateIds = collect($eventDateIds)->map(fn (mixed $id): int => (int) $id)->unique()->values(); + + if ($eventDateIds->isEmpty()) { + return []; + } + + /** @var Collection $tickets */ + $tickets = Ticket::query() + ->where('tenant_code', $tenant->codigo) + ->whereHas('sourcePurchaseItem.purchase', fn (Builder $query) => $query + ->where('status', Purchase::STATUS_PAID)) + ->whereHas('sourceVariant', function (Builder $query) use ($eventDateIds): void { + $query->whereIn('event_date_id', $eventDateIds) + ->orWhereHas('eventDates', fn (Builder $eventDates) => $eventDates + ->whereIn('event_dates.id', $eventDateIds)); + }) + ->with([ + ...TicketValidityResolver::RELATIONS, + 'sourcePurchaseItem.purchase', + ]) + ->get() + ->filter(fn (Ticket $ticket): bool => $ticket->is_active()) + ->values(); + + return $tickets + ->groupBy(fn (Ticket $ticket): int => (int) $ticket->sourcePurchaseItem->purchase->getKey()) + ->map(function (Collection $purchaseTickets): array { + return [ + 'purchase_id' => (int) $purchaseTickets->first()->sourcePurchaseItem->purchase->getKey(), + 'ticket_ids' => $purchaseTickets->modelKeys(), + ]; + }) + ->values() + ->all(); + } +} diff --git a/app/Domains/Event/Services/EventService.php b/app/Domains/Event/Services/EventService.php index c3b19c8..1cafb91 100644 --- a/app/Domains/Event/Services/EventService.php +++ b/app/Domains/Event/Services/EventService.php @@ -3,9 +3,12 @@ namespace App\Domains\Event\Services; use App\Domains\Catalog\Models\Variant; +use App\Domains\Event\Events\EventDateRescheduled; +use App\Domains\Event\Events\EventDateSuspended; use App\Domains\Event\Models\EventDate; use App\Domains\Tenant\Models\Tenant; use App\Domains\Ticket\Models\Ticket; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; @@ -19,6 +22,7 @@ class EventService public function __construct( private readonly EffectiveEventDateResolver $effectiveEventDateResolver, + private readonly AffectedEventDatePurchaseResolver $affectedPurchaseResolver, ) {} public function forTenant(Tenant $tenant): Tenant @@ -111,8 +115,21 @@ class EventService ]); } + $purchaseTickets = $this->affectedPurchaseResolver->resolve( + $tenant, + $this->affectedDateIds($tenant, $source), + ); $source->update(['rescheduled_to_event_date_id' => $destination->getKey()]); + EventDateRescheduled::dispatch( + $tenant->codigo, + $source->getKey(), + $destination->getKey(), + $source->date->format('d/m/Y'), + $destination->date->format('d/m/Y'), + $purchaseTickets, + ); + return $source->fresh(['validityTime', 'rescheduledTo.validityTime']); }); } @@ -132,9 +149,20 @@ class EventService return $date->load('validityTime'); } + $purchaseTickets = $this->affectedPurchaseResolver->resolve( + $tenant, + $this->affectedDateIds($tenant, $date), + ); $date->update(['suspended_at' => now()]); $this->disableTicketsWithoutUsableDates($tenant, $date); + EventDateSuspended::dispatch( + $tenant->codigo, + $date->getKey(), + $date->date->format('d/m/Y'), + $purchaseTickets, + ); + return $date->fresh('validityTime'); }); } @@ -172,9 +200,10 @@ class EventService return $current->is($expected); } - private function disableTicketsWithoutUsableDates(Tenant $tenant, EventDate $suspendedDate): void + /** @return Collection */ + private function affectedDateIds(Tenant $tenant, EventDate $eventDate): Collection { - $affectedDateIds = collect([$suspendedDate->getKey()]); + $affectedDateIds = collect([$eventDate->getKey()]); $frontier = $affectedDateIds; while ($frontier->isNotEmpty()) { @@ -187,6 +216,13 @@ class EventService $frontier = $predecessors; } + return $affectedDateIds; + } + + private function disableTicketsWithoutUsableDates(Tenant $tenant, EventDate $suspendedDate): void + { + $affectedDateIds = $this->affectedDateIds($tenant, $suspendedDate); + $variants = Variant::withTrashed() ->where(function ($query) use ($affectedDateIds): void { $query->whereIn('event_date_id', $affectedDateIds) diff --git a/tests/Feature/Event/AdminAppEventControllerTest.php b/tests/Feature/Event/AdminAppEventControllerTest.php index ebf2591..f4116fc 100644 --- a/tests/Feature/Event/AdminAppEventControllerTest.php +++ b/tests/Feature/Event/AdminAppEventControllerTest.php @@ -9,6 +9,8 @@ use App\Domains\Authorization\Enums\RoleCode; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\Variant; +use App\Domains\Event\Events\EventDateRescheduled; +use App\Domains\Event\Events\EventDateSuspended; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\WebsiteType; use App\Domains\Ticket\Enums\ValidityTimeType; @@ -16,6 +18,7 @@ use App\Domains\Ticket\Models\Ticket; use Database\Seeders\AuthorizationSeeder; use Database\Seeders\SocialMediaSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; use Laravel\Sanctum\Sanctum; use Tests\TestCase; @@ -230,6 +233,7 @@ class AdminAppEventControllerTest extends TestCase public function test_rescheduling_reuses_an_existing_date_and_tickets_resolve_its_validity(): void { + Event::fake([EventDateRescheduled::class]); $tenant = $this->createActiveEvent($this->createTenant('acme'), 'Festival Acme'); $admin = $this->createAdminAppUser($tenant); $original = $tenant->eventDates()->create([ @@ -253,6 +257,15 @@ class AdminAppEventControllerTest extends TestCase ->assertJsonPath('data.status', 'rescheduled') ->assertJsonPath('data.rescheduled_to_event_date_id', $destination->id); + Event::assertDispatched(EventDateRescheduled::class, function (EventDateRescheduled $event) use ($tenant, $original, $destination): bool { + return $event->tenantCode === $tenant->codigo + && $event->sourceEventDateId === $original->id + && $event->destinationEventDateId === $destination->id + && $event->previousDate === '09/10/2027' + && $event->newDate === '20/10/2027' + && $event->purchaseTickets === []; + }); + $this->assertDatabaseCount('event_dates', 2); $this->assertSame('20 de Octubre 2027', $tenant->fresh()->event_date_text); $this->getJson('/api/tenants/bootstrap?dominio=acme.test&path=%2F') @@ -288,6 +301,7 @@ class AdminAppEventControllerTest extends TestCase public function test_suspending_disables_only_tickets_without_another_usable_date(): void { + Event::fake([EventDateSuspended::class]); $tenant = $this->createActiveEvent($this->createTenant('acme'), 'Festival Acme'); $admin = $this->createAdminAppUser($tenant); $suspendedDate = $tenant->eventDates()->create([ @@ -312,6 +326,13 @@ class AdminAppEventControllerTest extends TestCase ->assertJsonPath('data.status', 'suspended') ->assertJsonPath('data.suspended_at', fn ($value) => is_string($value)); + Event::assertDispatched(EventDateSuspended::class, function (EventDateSuspended $event) use ($tenant, $suspendedDate): bool { + return $event->tenantCode === $tenant->codigo + && $event->eventDateId === $suspendedDate->id + && $event->date === '09/10/2027' + && $event->purchaseTickets === []; + }); + $this->assertNotNull($singleDateTicket->fresh()->disabled_at); $this->assertNull($multipleDateTicket->fresh()->disabled_at); $this->assertSame('10 de Octubre 2027', $tenant->fresh()->event_date_text);