diff --git a/app/Domains/Event/Services/EffectiveEventDateResolver.php b/app/Domains/Event/Services/EffectiveEventDateResolver.php index 7a50244..f74db64 100644 --- a/app/Domains/Event/Services/EffectiveEventDateResolver.php +++ b/app/Domains/Event/Services/EffectiveEventDateResolver.php @@ -7,6 +7,14 @@ use App\Domains\Event\Models\EventDate; class EffectiveEventDateResolver { public function resolve(EventDate $eventDate): ?EventDate + { + $date = $this->resolveLatest($eventDate); + + return $date !== null && $date->suspended_at === null ? $date : null; + } + + /** Sigue las reprogramaciones para presentación, incluso si el destino está suspendido. */ + public function resolveLatest(EventDate $eventDate): ?EventDate { $current = $eventDate; $visited = []; @@ -23,7 +31,7 @@ class EffectiveEventDateResolver $visited[$identity] = true; if ($current->rescheduled_to_event_date_id === null) { - return $current->suspended_at === null ? $current : null; + return $current; } $current->loadMissing('rescheduledTo'); diff --git a/app/Domains/Ticket/Services/TicketPresentationResolver.php b/app/Domains/Ticket/Services/TicketPresentationResolver.php index 19de892..f2e7681 100644 --- a/app/Domains/Ticket/Services/TicketPresentationResolver.php +++ b/app/Domains/Ticket/Services/TicketPresentationResolver.php @@ -2,10 +2,14 @@ namespace App\Domains\Ticket\Services; +use App\Domains\Event\Models\EventDate; +use App\Domains\Event\Services\EffectiveEventDateResolver; use App\Domains\Ticket\Models\Ticket; class TicketPresentationResolver { + public function __construct(private readonly EffectiveEventDateResolver $effectiveEventDateResolver) {} + /** Relaciones necesarias para calcular nombre y descripción sin consultas N+1. */ public const RELATIONS = [ 'sourceCatalogItem', @@ -30,9 +34,15 @@ class TicketPresentationResolver } $itemAttributes = $variant->catalogItem->itemAttributes; + $eventDateLabels = $variant->selectedEventDates() + ->map(fn (EventDate $date): EventDate => $this->effectiveEventDateResolver->resolveLatest($date) ?? $date) + ->unique(fn (EventDate $date): int => $date->getKey()) + ->map(fn (EventDate $date): string => $date->date->format('d/m/Y')) + ->implode(', '); + $properties = $variant->selectionOptions($itemAttributes) - ->map(function (array $option, string $attributeCode) use ($itemAttributes): ?string { - $labels = collect(array_is_list($option) ? $option : [$option]) + ->map(function (array $option, string $attributeCode) use ($itemAttributes, $eventDateLabels): ?string { + $labels = $attributeCode === 'event_date' ? $eventDateLabels : collect(array_is_list($option) ? $option : [$option]) ->pluck('label') ->filter(fn ($label): bool => is_string($label) && $label !== '') ->implode(', '); diff --git a/tests/Feature/Ticket/TicketControllerTest.php b/tests/Feature/Ticket/TicketControllerTest.php index 588746e..dc9e0a0 100644 --- a/tests/Feature/Ticket/TicketControllerTest.php +++ b/tests/Feature/Ticket/TicketControllerTest.php @@ -5,12 +5,17 @@ namespace Tests\Feature\Ticket; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; use App\Domains\Auth\Models\User; +use App\Domains\Catalog\Models\Attribute; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Inventory; +use App\Domains\Event\Events\EventDateRescheduled; +use App\Domains\Event\Events\EventDateSuspended; use App\Domains\Event\Models\EventDate; +use App\Domains\Event\Services\EventService; use App\Domains\Tenant\Models\Tenant; use App\Domains\Ticket\Models\Ticket; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; use Tests\TestCase; @@ -18,6 +23,72 @@ class TicketControllerTest extends TestCase { use RefreshDatabase; + public function test_ticket_names_follow_reschedules_without_changing_variant_selections(): void + { + Event::fake([EventDateRescheduled::class, EventDateSuspended::class]); + $tenant = $this->createTenant('rescheduled'); + $user = User::factory()->create(); + $ticket = $this->createTicket($tenant, $user, 'COMIDA'); + $item = $ticket->sourceCatalogItem; + $attribute = Attribute::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'codigo' => 'service', + 'nombre' => 'Servicio', + 'type' => 'string', + ]); + $itemAttribute = $item->itemAttributes()->create([ + 'attribute_id' => $attribute->id, + 'ticket_label' => 'Servicio', + ]); + $source = $tenant->eventDates()->create([ + 'date' => '2026-10-29', + 'time_start' => '08:00:00', + 'time_end' => '10:00:00', + ]); + $variant = $item->variants()->create([ + 'event_date_id' => $source->id, + 'inventory_id' => Inventory::query()->create()->id, + ]); + $variant->definitions()->create([ + 'item_attribute_id' => $itemAttribute->id, + 'value' => 'COMEDOR', + ]); + $ticket->update(['source_variant_id' => $variant->id]); + + foreach (['29/10/2026', '02/11/2026', '05/11/2026'] as $step => $expectedDate) { + if ($step > 0) { + $previous = $step === 1 ? $source : $tenant->eventDates()->whereDate('date', '2026-11-02')->firstOrFail(); + app(EventService::class)->rescheduleDateForTenant($tenant, $previous, [ + 'date' => $step === 1 ? '2026-11-02' : '2026-11-05', + ]); + } + + $this->actingAs($user, 'sanctum') + ->getJson("/api/tenants/{$tenant->codigo}/tickets") + ->assertOk() + ->assertJsonPath('data.0.name', "COMIDA ({$expectedDate}, Servicio COMEDOR)") + ->assertJsonPath('data.0.ticket', $ticket->ticket); + } + + $destination = $tenant->eventDates()->whereDate('date', '2026-11-05')->firstOrFail(); + $variant->eventDates()->sync([$source->id, $destination->id]); + $this->assertSame('COMIDA (05/11/2026, Servicio COMEDOR)', $ticket->fresh()->name); + $this->assertSame('29/10/2026', $variant->fresh()->selectionOptions()->get('event_date')[0]['label']); + $this->assertSame($source->id, $variant->fresh()->event_date_id); + + app(EventService::class)->suspendDateForTenant($tenant, $destination); + + $this->actingAs($user, 'sanctum') + ->getJson("/api/tenants/{$tenant->codigo}/tickets") + ->assertOk() + ->assertJsonPath('data.0.name', 'COMIDA (05/11/2026, Servicio COMEDOR)') + ->assertJsonPath('data.0.status', 'disabled') + ->assertJsonPath('data.0.is_valid', false) + ->assertJsonPath('data.0.starts_at', null) + ->assertJsonPath('data.0.expires_at', null) + ->assertJsonPath('data.0.ticket', $ticket->ticket); + } + public function test_an_authenticated_user_can_list_their_tickets_for_the_tenant(): void { $tenant = $this->createTenant('current');