feat(event): dispatch date change notifications

This commit is contained in:
2026-09-14 08:53:45 -03:00
parent 4f7ede1072
commit 205d77bc0b
5 changed files with 158 additions and 2 deletions

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Domains\Event\Events;
use Illuminate\Foundation\Events\Dispatchable;
class EventDateRescheduled
{
use Dispatchable;
/**
* @param list<array{purchase_id: int, ticket_ids: list<int>}> $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,
) {}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Domains\Event\Events;
use Illuminate\Foundation\Events\Dispatchable;
class EventDateSuspended
{
use Dispatchable;
/**
* @param list<array{purchase_id: int, ticket_ids: list<int>}> $purchaseTickets
*/
public function __construct(
public readonly string $tenantCode,
public readonly int $eventDateId,
public readonly string $date,
public readonly array $purchaseTickets,
) {}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Domains\Event\Services;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket;
use App\Domains\Ticket\Services\TicketValidityResolver;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class AffectedEventDatePurchaseResolver
{
/**
* Finds active tickets belonging to paid purchases before an event-date mutation.
*
* @param Collection<int, int>|list<int> $eventDateIds
* @return list<array{purchase_id: int, ticket_ids: list<int>}>
*/
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<int, Ticket> $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();
}
}

View File

@@ -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<int, int> */
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)

View File

@@ -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);