feat(event): add effective date resolution for rescheduled events and update tests
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Domains\Event\Models;
|
|||||||
|
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use App\Domains\Event\Enums\EventDateStatus;
|
use App\Domains\Event\Enums\EventDateStatus;
|
||||||
|
use App\Domains\Event\Services\EffectiveEventDateResolver;
|
||||||
use App\Domains\Event\Services\EventDateTextFormatter;
|
use App\Domains\Event\Services\EventDateTextFormatter;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
use App\Domains\Ticket\Enums\ValidityTimeType;
|
||||||
@@ -80,6 +81,11 @@ class EventDate extends Model
|
|||||||
return $this->belongsTo(self::class, 'rescheduled_to_event_date_id');
|
return $this->belongsTo(self::class, 'rescheduled_to_event_date_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function effectiveDate(): ?self
|
||||||
|
{
|
||||||
|
return app(EffectiveEventDateResolver::class)->resolve($this);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return HasMany<EventDate, $this> */
|
/** @return HasMany<EventDate, $this> */
|
||||||
public function rescheduledFrom(): HasMany
|
public function rescheduledFrom(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ class NotificationMailService
|
|||||||
->forTenant($tenantCode)
|
->forTenant($tenantCode)
|
||||||
->send(
|
->send(
|
||||||
$recipient,
|
$recipient,
|
||||||
"Tu evento fue reprogramado - Compra #{$purchase->getKey()}",
|
"Tu evento fue reprogramado - N° de Orden #{$purchase->getKey()}",
|
||||||
view('mail.notifications.event-date-rescheduled', compact(
|
view('mail.notifications.event-date-rescheduled', compact(
|
||||||
'purchase', 'previousDate', 'newDate', 'tickets'
|
'purchase', 'previousDate', 'newDate', 'tickets'
|
||||||
))->render(),
|
))->render(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Domains\Ticket\Services;
|
namespace App\Domains\Ticket\Services;
|
||||||
|
|
||||||
use App\Domains\Catalog\Models\ItemAttribute;
|
use App\Domains\Catalog\Models\ItemAttribute;
|
||||||
|
use App\Domains\Event\Models\EventDate;
|
||||||
use App\Domains\Ticket\Models\Ticket;
|
use App\Domains\Ticket\Models\Ticket;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
@@ -11,12 +12,12 @@ class AdminAppTicketRowService
|
|||||||
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
|
||||||
|
|
||||||
private const CATEGORY_PRESENTATIONS = [
|
private const CATEGORY_PRESENTATIONS = [
|
||||||
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'date' => null, 'size' => null],
|
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null],
|
||||||
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'date' => null, 'size' => null],
|
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null],
|
||||||
'entradas' => ['category' => null, 'product' => 'product', 'type' => null, 'date' => null, 'size' => null],
|
'entradas' => ['category' => null, 'product' => 'product', 'type' => null, 'size' => null],
|
||||||
'comidas' => ['category' => 'Comida', 'product' => 'horario', 'type' => 'servicio', 'date' => 'event_date', 'size' => null],
|
'comidas' => ['category' => 'Comida', 'product' => 'horario', 'type' => 'servicio', 'size' => null],
|
||||||
'comida' => ['category' => null, 'product' => 'horario', 'type' => 'servicio', 'date' => 'event_date', 'size' => null],
|
'comida' => ['category' => null, 'product' => 'horario', 'type' => 'servicio', 'size' => null],
|
||||||
'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color', 'date' => null, 'size' => 'talle'],
|
'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color', 'size' => 'talle'],
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
@@ -111,13 +112,14 @@ class AdminAppTicketRowService
|
|||||||
private function presentation(Ticket $ticket, array $details): array
|
private function presentation(Ticket $ticket, array $details): array
|
||||||
{
|
{
|
||||||
$sourceCategory = trim((string) ($ticket->sourceCatalogItem?->category?->nombre ?? '')) ?: '-';
|
$sourceCategory = trim((string) ($ticket->sourceCatalogItem?->category?->nombre ?? '')) ?: '-';
|
||||||
|
$effectiveDates = $this->effectiveEventDateLabels($ticket) ?: '-';
|
||||||
|
|
||||||
if ($ticket->tenant_code !== self::FIESTA_FUTBOL_INFANTIL) {
|
if ($ticket->tenant_code !== self::FIESTA_FUTBOL_INFANTIL) {
|
||||||
return [
|
return [
|
||||||
'category' => $sourceCategory,
|
'category' => $sourceCategory,
|
||||||
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
|
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
|
||||||
'type' => $this->allPropertyLabels($details) ?: '-',
|
'type' => $this->allPropertyLabels($details) ?: '-',
|
||||||
'date' => '-',
|
'date' => $effectiveDates,
|
||||||
'size' => '-',
|
'size' => '-',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -128,7 +130,7 @@ class AdminAppTicketRowService
|
|||||||
'category' => $sourceCategory,
|
'category' => $sourceCategory,
|
||||||
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
|
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
|
||||||
'type' => $this->allPropertyLabels($details) ?: '-',
|
'type' => $this->allPropertyLabels($details) ?: '-',
|
||||||
'date' => '-',
|
'date' => $effectiveDates,
|
||||||
'size' => '-',
|
'size' => '-',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -141,29 +143,30 @@ class AdminAppTicketRowService
|
|||||||
'type' => $configuration['type'] === null
|
'type' => $configuration['type'] === null
|
||||||
? '-'
|
? '-'
|
||||||
: ($this->propertyLabels($details, $configuration['type']) ?: '-'),
|
: ($this->propertyLabels($details, $configuration['type']) ?: '-'),
|
||||||
'date' => $configuration['date'] === null
|
'date' => $effectiveDates,
|
||||||
? '-'
|
|
||||||
: ($this->propertyLabels($details, $configuration['date']) ?: '-'),
|
|
||||||
'size' => $configuration['size'] === null
|
'size' => $configuration['size'] === null
|
||||||
? '-'
|
? '-'
|
||||||
: ($this->propertyLabels($details, $configuration['size']) ?: '-'),
|
: ($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<string, mixed> $details */
|
/** @param array<string, mixed> $details */
|
||||||
private function propertyLabels(array $details, string $code): string
|
private function propertyLabels(array $details, string $code): string
|
||||||
{
|
{
|
||||||
$property = collect($details['variant_properties'] ?? [])->firstWhere('code', $code);
|
$property = collect($details['variant_properties'] ?? [])->firstWhere('code', $code);
|
||||||
$labels = collect($property['values'] ?? [])->pluck('label')->filter();
|
$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(', ');
|
return $labels->implode(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<p><strong>Tickets afectados</strong></p>
|
<p><strong>Tickets afectados</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
@foreach ($tickets as $ticket)
|
@foreach ($tickets as $ticket)
|
||||||
<li>{{ $ticket->name }} · #{{ $ticket->id }}</li>
|
<li>{{ $ticket->name }} · N° de Ticket #{{ $ticket->id }}</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
<p style="color: #64748b; font-size: 13px;">Compra #{{ $purchase->id }}</p>
|
<p style="color: #64748b; font-size: 13px;">N° de Orden #{{ $purchase->id }}</p>
|
||||||
|
|||||||
@@ -352,11 +352,12 @@ class NotificationMailServiceTest extends TestCase
|
|||||||
Mail::assertSent(Mailable::class, function (Mailable $mail) use ($purchase, $firstTicket, $secondTicket): bool {
|
Mail::assertSent(Mailable::class, function (Mailable $mail) use ($purchase, $firstTicket, $secondTicket): bool {
|
||||||
$mail->assertTo('checkout@example.com');
|
$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(), '09/10/2027')
|
||||||
&& str_contains($mail->render(), '20/10/2027')
|
&& str_contains($mail->render(), '20/10/2027')
|
||||||
&& str_contains($mail->render(), '#'.$firstTicket->id)
|
&& str_contains($mail->render(), 'N° de Ticket #'.$firstTicket->id)
|
||||||
&& str_contains($mail->render(), '#'.$secondTicket->id);
|
&& str_contains($mail->render(), 'N° de Ticket #'.$secondTicket->id)
|
||||||
|
&& str_contains($mail->render(), 'N° de Orden #'.$purchase->id);
|
||||||
});
|
});
|
||||||
$this->assertDatabaseHas('email_deliveries', [
|
$this->assertDatabaseHas('email_deliveries', [
|
||||||
'idempotency_key' => "event-date-rescheduled:10:20:{$purchase->id}",
|
'idempotency_key' => "event-date-rescheduled:10:20:{$purchase->id}",
|
||||||
|
|||||||
@@ -835,6 +835,50 @@ class AdminAppTicketControllerTest extends TestCase
|
|||||||
->assertJsonPath('total_tickets', 1);
|
->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
|
public function test_it_exposes_and_filters_merchandise_color_and_size(): void
|
||||||
{
|
{
|
||||||
$tenant = $this->createTenant('fiesta_futbol_infantil');
|
$tenant = $this->createTenant('fiesta_futbol_infantil');
|
||||||
|
|||||||
@@ -89,4 +89,21 @@ class EventModelsTest extends TestCase
|
|||||||
|
|
||||||
$this->assertInstanceOf(EventDate::class, $tenant->eventDates()->getRelated());
|
$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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user