From f8a329682182a0f6525c2dbafc94d4b1917ceced Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 16 Sep 2026 14:27:35 -0300 Subject: [PATCH] feat(event): add effective date resolution for rescheduled events and update tests --- app/Domains/Event/Models/EventDate.php | 6 +++ .../Services/NotificationMailService.php | 2 +- .../Services/AdminAppTicketRowService.php | 41 +++++++++-------- .../event-date-rescheduled.blade.php | 4 +- .../NotificationMailServiceTest.php | 7 +-- .../Ticket/AdminAppTicketControllerTest.php | 44 +++++++++++++++++++ tests/Unit/Event/EventModelsTest.php | 17 +++++++ 7 files changed, 96 insertions(+), 25 deletions(-) diff --git a/app/Domains/Event/Models/EventDate.php b/app/Domains/Event/Models/EventDate.php index 81b826a..02add11 100644 --- a/app/Domains/Event/Models/EventDate.php +++ b/app/Domains/Event/Models/EventDate.php @@ -4,6 +4,7 @@ namespace App\Domains\Event\Models; use App\Domains\Catalog\Models\Variant; use App\Domains\Event\Enums\EventDateStatus; +use App\Domains\Event\Services\EffectiveEventDateResolver; use App\Domains\Event\Services\EventDateTextFormatter; use App\Domains\Tenant\Models\Tenant; use App\Domains\Ticket\Enums\ValidityTimeType; @@ -80,6 +81,11 @@ class EventDate extends Model return $this->belongsTo(self::class, 'rescheduled_to_event_date_id'); } + public function effectiveDate(): ?self + { + return app(EffectiveEventDateResolver::class)->resolve($this); + } + /** @return HasMany */ public function rescheduledFrom(): HasMany { diff --git a/app/Domains/Notification/Services/NotificationMailService.php b/app/Domains/Notification/Services/NotificationMailService.php index 057810a..32e32d6 100644 --- a/app/Domains/Notification/Services/NotificationMailService.php +++ b/app/Domains/Notification/Services/NotificationMailService.php @@ -297,7 +297,7 @@ class NotificationMailService ->forTenant($tenantCode) ->send( $recipient, - "Tu evento fue reprogramado - Compra #{$purchase->getKey()}", + "Tu evento fue reprogramado - N° de Orden #{$purchase->getKey()}", view('mail.notifications.event-date-rescheduled', compact( 'purchase', 'previousDate', 'newDate', 'tickets' ))->render(), diff --git a/app/Domains/Ticket/Services/AdminAppTicketRowService.php b/app/Domains/Ticket/Services/AdminAppTicketRowService.php index ed19924..cf831cf 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketRowService.php +++ b/app/Domains/Ticket/Services/AdminAppTicketRowService.php @@ -3,6 +3,7 @@ namespace App\Domains\Ticket\Services; use App\Domains\Catalog\Models\ItemAttribute; +use App\Domains\Event\Models\EventDate; use App\Domains\Ticket\Models\Ticket; use Illuminate\Support\Collection; @@ -11,12 +12,12 @@ class AdminAppTicketRowService private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil'; private const CATEGORY_PRESENTATIONS = [ - 'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'date' => null, 'size' => null], - 'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'date' => null, 'size' => null], - 'entradas' => ['category' => null, 'product' => 'product', 'type' => null, 'date' => null, 'size' => null], - 'comidas' => ['category' => 'Comida', 'product' => 'horario', 'type' => 'servicio', 'date' => 'event_date', 'size' => null], - 'comida' => ['category' => null, 'product' => 'horario', 'type' => 'servicio', 'date' => 'event_date', 'size' => null], - 'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color', 'date' => null, 'size' => 'talle'], + 'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null], + 'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null], + 'entradas' => ['category' => null, 'product' => 'product', 'type' => null, 'size' => null], + 'comidas' => ['category' => 'Comida', 'product' => 'horario', 'type' => 'servicio', 'size' => null], + 'comida' => ['category' => null, 'product' => 'horario', 'type' => 'servicio', 'size' => null], + 'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color', 'size' => 'talle'], ]; /** @return array */ @@ -111,13 +112,14 @@ class AdminAppTicketRowService private function presentation(Ticket $ticket, array $details): array { $sourceCategory = trim((string) ($ticket->sourceCatalogItem?->category?->nombre ?? '')) ?: '-'; + $effectiveDates = $this->effectiveEventDateLabels($ticket) ?: '-'; if ($ticket->tenant_code !== self::FIESTA_FUTBOL_INFANTIL) { return [ 'category' => $sourceCategory, 'product' => (string) ($details['product'] ?: $ticket->name ?: '-'), 'type' => $this->allPropertyLabels($details) ?: '-', - 'date' => '-', + 'date' => $effectiveDates, 'size' => '-', ]; } @@ -128,7 +130,7 @@ class AdminAppTicketRowService 'category' => $sourceCategory, 'product' => (string) ($details['product'] ?: $ticket->name ?: '-'), 'type' => $this->allPropertyLabels($details) ?: '-', - 'date' => '-', + 'date' => $effectiveDates, 'size' => '-', ]; } @@ -141,29 +143,30 @@ class AdminAppTicketRowService 'type' => $configuration['type'] === null ? '-' : ($this->propertyLabels($details, $configuration['type']) ?: '-'), - 'date' => $configuration['date'] === null - ? '-' - : ($this->propertyLabels($details, $configuration['date']) ?: '-'), + 'date' => $effectiveDates, 'size' => $configuration['size'] === null ? '-' : ($this->propertyLabels($details, $configuration['size']) ?: '-'), ]; } + private function effectiveEventDateLabels(Ticket $ticket): string + { + return $ticket->sourceVariant?->selectedEventDates() + ->map(fn (EventDate $date): ?EventDate => $date->effectiveDate()) + ->filter() + ->unique(fn (EventDate $date): int => $date->getKey()) + ->sortBy(fn (EventDate $date): string => $date->date->format('Y-m-d')) + ->map(fn (EventDate $date): string => $date->date->format('d/m')) + ->implode(', ') ?? ''; + } + /** @param array $details */ private function propertyLabels(array $details, string $code): string { $property = collect($details['variant_properties'] ?? [])->firstWhere('code', $code); $labels = collect($property['values'] ?? [])->pluck('label')->filter(); - if ($code === 'event_date') { - $labels = $labels->map(function (string $label): string { - [$day, $month] = array_pad(explode('/', $label), 2, null); - - return $day !== null && $month !== null ? "{$day}/{$month}" : $label; - }); - } - return $labels->implode(', '); } diff --git a/resources/views/mail/notifications/event-date-rescheduled.blade.php b/resources/views/mail/notifications/event-date-rescheduled.blade.php index af33d18..bffd003 100644 --- a/resources/views/mail/notifications/event-date-rescheduled.blade.php +++ b/resources/views/mail/notifications/event-date-rescheduled.blade.php @@ -8,7 +8,7 @@

Tickets afectados

    @foreach ($tickets as $ticket) -
  • {{ $ticket->name }} · #{{ $ticket->id }}
  • +
  • {{ $ticket->name }} · N° de Ticket #{{ $ticket->id }}
  • @endforeach
-

Compra #{{ $purchase->id }}

+

N° de Orden #{{ $purchase->id }}

diff --git a/tests/Feature/Notification/NotificationMailServiceTest.php b/tests/Feature/Notification/NotificationMailServiceTest.php index a642f03..22c1ef0 100644 --- a/tests/Feature/Notification/NotificationMailServiceTest.php +++ b/tests/Feature/Notification/NotificationMailServiceTest.php @@ -352,11 +352,12 @@ class NotificationMailServiceTest extends TestCase Mail::assertSent(Mailable::class, function (Mailable $mail) use ($purchase, $firstTicket, $secondTicket): bool { $mail->assertTo('checkout@example.com'); - return $mail->subject === "Tu evento fue reprogramado - Compra #{$purchase->id}" + return $mail->subject === "Tu evento fue reprogramado - N° de Orden #{$purchase->id}" && str_contains($mail->render(), '09/10/2027') && str_contains($mail->render(), '20/10/2027') - && str_contains($mail->render(), '#'.$firstTicket->id) - && str_contains($mail->render(), '#'.$secondTicket->id); + && str_contains($mail->render(), 'N° de Ticket #'.$firstTicket->id) + && str_contains($mail->render(), 'N° de Ticket #'.$secondTicket->id) + && str_contains($mail->render(), 'N° de Orden #'.$purchase->id); }); $this->assertDatabaseHas('email_deliveries', [ 'idempotency_key' => "event-date-rescheduled:10:20:{$purchase->id}", diff --git a/tests/Feature/Ticket/AdminAppTicketControllerTest.php b/tests/Feature/Ticket/AdminAppTicketControllerTest.php index a9ea96e..b0ae3db 100644 --- a/tests/Feature/Ticket/AdminAppTicketControllerTest.php +++ b/tests/Feature/Ticket/AdminAppTicketControllerTest.php @@ -835,6 +835,50 @@ class AdminAppTicketControllerTest extends TestCase ->assertJsonPath('total_tickets', 1); } + public function test_it_shows_the_effective_date_after_multiple_reschedules_and_suspension(): void + { + $tenant = $this->createTenant('fiesta_futbol_infantil'); + $admin = $this->createAdminAppUser($tenant); + $this->grantTicketsMenu($tenant); + $this->seed([AttributeSeeder::class, FiestaFutbolInfantilProductSeeder::class]); + Sanctum::actingAs($admin); + + $entry = CatalogItem::query()->where('tenant_code', $tenant->codigo)->where('slug', 'abono')->firstOrFail(); + $variant = $entry->variants()->whereHas('eventDates')->firstOrFail(); + $original = $variant->selectedEventDates()->firstOrFail(); + $middle = $tenant->eventDates()->create([ + 'date' => '2026-11-01', 'time_start' => '09:00', 'time_end' => '18:00', + ]); + $latest = $tenant->eventDates()->create([ + 'date' => '2026-11-02', 'time_start' => '09:00', 'time_end' => '18:00', + ]); + $original->update(['rescheduled_to_event_date_id' => $middle->id]); + $middle->update(['rescheduled_to_event_date_id' => $latest->id]); + $ticket = $this->createTicket($tenant, $admin, [ + 'source_catalog_item_id' => $entry->id, + 'source_variant_id' => $variant->id, + ]); + + $this->getJson('/api/v1/adminapp/tenant/tickets') + ->assertOk() + ->assertJsonPath('data.0.id', $ticket->id) + ->assertJsonPath('data.0.values.date', '10/10, 11/10, 12/10, 02/11'); + + $latest->update(['suspended_at' => now()]); + + $this->getJson('/api/v1/adminapp/tenant/tickets') + ->assertOk() + ->assertJsonPath('data.0.values.date', '10/10, 11/10, 12/10'); + + foreach ($variant->selectedEventDates()->skip(1) as $date) { + $date->update(['suspended_at' => now()]); + } + + $this->getJson('/api/v1/adminapp/tenant/tickets') + ->assertOk() + ->assertJsonPath('data.0.values.date', '-'); + } + public function test_it_exposes_and_filters_merchandise_color_and_size(): void { $tenant = $this->createTenant('fiesta_futbol_infantil'); diff --git a/tests/Unit/Event/EventModelsTest.php b/tests/Unit/Event/EventModelsTest.php index cb3673c..7dc90d2 100644 --- a/tests/Unit/Event/EventModelsTest.php +++ b/tests/Unit/Event/EventModelsTest.php @@ -89,4 +89,21 @@ class EventModelsTest extends TestCase $this->assertInstanceOf(EventDate::class, $tenant->eventDates()->getRelated()); } + + public function test_effective_date_follows_all_replacements_and_returns_null_when_suspended(): void + { + $original = new EventDate; + $original->setRawAttributes(['id' => 1, 'rescheduled_to_event_date_id' => 2]); + $middle = new EventDate; + $middle->setRawAttributes(['id' => 2, 'rescheduled_to_event_date_id' => 3]); + $latest = new EventDate; + $latest->setRawAttributes(['id' => 3, 'date' => '2026-11-02']); + $original->setRelation('rescheduledTo', $middle); + $middle->setRelation('rescheduledTo', $latest); + + $this->assertSame($latest, $original->effectiveDate()); + + $latest->suspended_at = '2026-11-01 12:00:00'; + $this->assertNull($original->effectiveDate()); + } }