feat(ticket): implement validity time management for tickets and catalog items
- Added ValidityTime model and migration to manage ticket validity periods. - Updated TicketGeneratorService to resolve and assign validity times to tickets. - Refactored ticket generation logic to remove legacy date fields and use validity time. - Introduced timezone support for tenants to handle service dates correctly. - Updated migrations to remove deprecated columns and add foreign keys for validity times. - Modified seeders and tests to accommodate new validity time structure. - Enhanced tests to validate ticket generation and validity time behavior.
This commit is contained in:
@@ -32,14 +32,11 @@ class CatalogItemControllerTest extends TestCase
|
||||
'slug' => 'shirt',
|
||||
'nombre' => 'Shirt',
|
||||
'precio' => 100,
|
||||
'minimum_use_date' => '2026-08-01 09:00:00',
|
||||
'maximum_use_date' => '2026-08-31 18:00:00',
|
||||
'attribute_codes' => [$attribute->codigo],
|
||||
'images' => [$image, $image],
|
||||
'variants' => [
|
||||
[
|
||||
'real_stock' => 5,
|
||||
'maximum_use_date' => '2026-08-15 18:00:00',
|
||||
'values' => ['size' => 'M'],
|
||||
'images' => [$image],
|
||||
],
|
||||
@@ -51,20 +48,7 @@ class CatalogItemControllerTest extends TestCase
|
||||
->assertJsonPath('data.nombre', 'Shirt')
|
||||
->assertJsonCount(2, 'data.images')
|
||||
->assertJsonCount(1, 'data.variants')
|
||||
->assertJsonCount(1, 'data.variants.0.images')
|
||||
->assertJsonPath('data.variants.0.minimum_use_date', null)
|
||||
->assertJsonPath(
|
||||
'data.variants.0.maximum_use_date',
|
||||
fn (string $value): bool => str_starts_with($value, '2026-08-15T18:00:00'),
|
||||
)
|
||||
->assertJsonPath(
|
||||
'data.variants.0.effective_minimum_use_date',
|
||||
fn (string $value): bool => str_starts_with($value, '2026-08-01T09:00:00'),
|
||||
)
|
||||
->assertJsonPath(
|
||||
'data.variants.0.effective_maximum_use_date',
|
||||
fn (string $value): bool => str_starts_with($value, '2026-08-15T18:00:00'),
|
||||
);
|
||||
->assertJsonCount(1, 'data.variants.0.images');
|
||||
|
||||
$item = CatalogItem::query()->where('slug', 'shirt')->firstOrFail();
|
||||
$variant = $item->variants()->firstOrFail();
|
||||
|
||||
@@ -44,8 +44,7 @@ class CatalogSchemaTest extends TestCase
|
||||
'precio',
|
||||
'inventory_policy',
|
||||
'has_tickets',
|
||||
'maximum_use_date',
|
||||
'minimum_use_date',
|
||||
'validity_time_id',
|
||||
], Schema::getColumnListing('catalog_items'));
|
||||
}
|
||||
|
||||
@@ -183,8 +182,6 @@ class CatalogSchemaTest extends TestCase
|
||||
{
|
||||
$this->assertTrue(Schema::hasColumns('variantes', [
|
||||
'event_date_id',
|
||||
'minimum_use_date',
|
||||
'maximum_use_date',
|
||||
]));
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ use App\Domains\Catalog\Services\CatalogService;
|
||||
use App\Domains\Shared\Enums\FieldType;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -94,73 +93,6 @@ class CatalogServiceTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_variant_use_dates_override_or_inherit_catalog_item_dates(): void
|
||||
{
|
||||
$attribute = $this->createAttribute('day');
|
||||
$itemMinimum = Carbon::parse('2026-08-01 09:00:00');
|
||||
$itemMaximum = Carbon::parse('2026-08-31 18:00:00');
|
||||
$variantMaximum = Carbon::parse('2026-08-15 18:00:00');
|
||||
|
||||
$item = $this->service->create([
|
||||
'tenant_code' => $this->tenant->codigo,
|
||||
'slug' => 'dated-variants',
|
||||
'nombre' => 'Dated variants',
|
||||
'precio' => 100,
|
||||
'minimum_use_date' => $itemMinimum,
|
||||
'maximum_use_date' => $itemMaximum,
|
||||
'attribute_codes' => [$attribute->codigo],
|
||||
'variants' => [
|
||||
[
|
||||
'real_stock' => 5,
|
||||
'maximum_use_date' => $variantMaximum,
|
||||
'values' => [$attribute->codigo => 'Saturday'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$variant = $item->variants->firstOrFail();
|
||||
|
||||
$this->assertNull($variant->minimum_use_date);
|
||||
$this->assertTrue($variant->maximum_use_date->equalTo($variantMaximum));
|
||||
$this->assertTrue($variant->getMinimumUseDate()->equalTo($itemMinimum));
|
||||
$this->assertTrue($variant->getMaximumUseDate()->equalTo($variantMaximum));
|
||||
}
|
||||
|
||||
public function test_it_rejects_an_invalid_effective_variant_use_date_range(): void
|
||||
{
|
||||
$attribute = $this->createAttribute('day');
|
||||
|
||||
try {
|
||||
$this->service->create([
|
||||
'tenant_code' => $this->tenant->codigo,
|
||||
'slug' => 'invalid-dated-variant',
|
||||
'nombre' => 'Invalid dated variant',
|
||||
'precio' => 100,
|
||||
'minimum_use_date' => '2026-08-10 09:00:00',
|
||||
'maximum_use_date' => '2026-08-31 18:00:00',
|
||||
'attribute_codes' => [$attribute->codigo],
|
||||
'variants' => [
|
||||
[
|
||||
'real_stock' => 5,
|
||||
'maximum_use_date' => '2026-08-09 18:00:00',
|
||||
'values' => [$attribute->codigo => 'Saturday'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->fail('A validation exception was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertArrayHasKey(
|
||||
'variants.0.maximum_use_date',
|
||||
$exception->errors(),
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertDatabaseMissing('catalog_items', [
|
||||
'slug' => 'invalid-dated-variant',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_rejects_direct_inventory_together_with_variants(): void
|
||||
{
|
||||
$attribute = $this->createAttribute('size');
|
||||
|
||||
@@ -259,7 +259,7 @@ class TelepagosWebhookTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_webhook_confirms_a_purchase_when_its_ticket_use_date_has_ended(): void
|
||||
public function test_webhook_confirms_a_purchase_with_tickets_enabled(): void
|
||||
{
|
||||
$tenant = $this->createTenant('expired-ticket', 'Expired Ticket', 'expired-ticket.com.ar');
|
||||
$this->configureTelepagosIntegration($tenant);
|
||||
@@ -267,7 +267,6 @@ class TelepagosWebhookTest extends TestCase
|
||||
$variant = $this->createVariantForTenant('expired-ticket', 1, '50.00');
|
||||
$variant->catalogItem->update([
|
||||
'has_tickets' => true,
|
||||
'maximum_use_date' => now()->subMinute(),
|
||||
]);
|
||||
$purchase = $this->createPendingTransferPurchase(
|
||||
$tenant,
|
||||
|
||||
@@ -95,22 +95,8 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase
|
||||
->map(fn ($variant) => $variant->eventDate->date->format('Y-m-d'))
|
||||
->all()
|
||||
);
|
||||
$this->assertSame(
|
||||
[
|
||||
['2026-10-09 00:00:00', '2026-10-09 23:59:59'],
|
||||
['2026-10-10 00:00:00', '2026-10-10 23:59:59'],
|
||||
['2026-10-11 00:00:00', '2026-10-11 23:59:59'],
|
||||
['2026-10-12 00:00:00', '2026-10-12 23:59:59'],
|
||||
],
|
||||
$generalAdmission->variants
|
||||
->sortBy(fn ($variant) => $variant->eventDate->date)
|
||||
->map(fn ($variant): array => [
|
||||
$variant->getMinimumUseDate()->format('Y-m-d H:i:s'),
|
||||
$variant->getMaximumUseDate()->format('Y-m-d H:i:s'),
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
);
|
||||
$this->assertSame('2026-10-09 00:00:00', $generalAdmission->validityTime->fixed_starts_at->format('Y-m-d H:i:s'));
|
||||
$this->assertSame('2026-10-12 23:59:59', $generalAdmission->validityTime->fixed_expires_at->format('Y-m-d H:i:s'));
|
||||
|
||||
$standardItems = CatalogItem::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
@@ -122,11 +108,11 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase
|
||||
$this->assertSame($event->id, $standardItem->event_id);
|
||||
$this->assertSame(
|
||||
'2026-10-09 00:00:00',
|
||||
$standardItem->minimum_use_date->format('Y-m-d H:i:s'),
|
||||
$standardItem->validityTime->fixed_starts_at->format('Y-m-d H:i:s'),
|
||||
);
|
||||
$this->assertSame(
|
||||
'2026-10-12 23:59:59',
|
||||
$standardItem->maximum_use_date->format('Y-m-d H:i:s'),
|
||||
$standardItem->validityTime->fixed_expires_at->format('Y-m-d H:i:s'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,7 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
|
||||
public function test_it_generates_tickets_from_a_standard_catalog_item(): void
|
||||
{
|
||||
$item = $this->createTicketableItem(
|
||||
'single-day',
|
||||
now()->subHour(),
|
||||
now()->addDay(),
|
||||
);
|
||||
$item = $this->createTicketableItem('single-day');
|
||||
|
||||
$tickets = $this->service->generate($item, $this->user, 2);
|
||||
|
||||
@@ -68,8 +64,8 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
$this->assertSame($item->descripcion, $ticket->description);
|
||||
$this->assertSame($item->id, $ticket->source_catalog_item_id);
|
||||
$this->assertNull($ticket->source_variant_id);
|
||||
$this->assertTrue($ticket->starts_at->equalTo($item->minimum_use_date));
|
||||
$this->assertTrue($ticket->expires_at->equalTo($item->maximum_use_date));
|
||||
$this->assertNull($ticket->validity_time_id);
|
||||
$this->assertNull($ticket->service_date);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,41 +80,10 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
$this->service->generate($item->fresh(), $this->user);
|
||||
}
|
||||
|
||||
public function test_it_generates_a_ticket_when_its_maximum_use_date_was_reached(): void
|
||||
{
|
||||
$item = $this->createTicketableItem('expired', maximumUseDate: now());
|
||||
|
||||
$tickets = $this->service->generate($item, $this->user);
|
||||
|
||||
$this->assertCount(1, $tickets);
|
||||
$this->assertTrue($tickets->first()->expires_at->equalTo($item->maximum_use_date));
|
||||
}
|
||||
|
||||
public function test_variant_dates_override_and_inherit_catalog_item_dates(): void
|
||||
{
|
||||
$item = $this->createTicketableItem(
|
||||
'variant-dates',
|
||||
now()->subDay(),
|
||||
now()->addMonth(),
|
||||
);
|
||||
$inventory = Inventory::query()->create();
|
||||
$variant = $item->variants()->create([
|
||||
'inventory_id' => $inventory->id,
|
||||
'maximum_use_date' => now()->addWeek(),
|
||||
]);
|
||||
|
||||
$ticket = $this->service
|
||||
->generate($item, $this->user, sourceVariantId: $variant->id)
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertTrue($ticket->starts_at->equalTo($item->minimum_use_date));
|
||||
$this->assertTrue($ticket->expires_at->equalTo($variant->maximum_use_date));
|
||||
}
|
||||
|
||||
public function test_it_generates_tickets_for_every_bundle_component_and_quantity(): void
|
||||
{
|
||||
$first = $this->createTicketableItem('first', maximumUseDate: now()->addDay());
|
||||
$second = $this->createTicketableItem('second', maximumUseDate: now()->addDays(2));
|
||||
$first = $this->createTicketableItem('first');
|
||||
$second = $this->createTicketableItem('second');
|
||||
$bundle = $this->createBundle('bundle');
|
||||
$bundle->bundleComponents()->createMany([
|
||||
['component_catalog_item_id' => $first->id, 'quantity' => 2],
|
||||
@@ -134,18 +99,12 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
$this->assertCount(2, $tickets->where('name', $second->nombre));
|
||||
}
|
||||
|
||||
public function test_bundle_component_uses_its_variant_dates(): void
|
||||
public function test_bundle_component_preserves_its_source_variant(): void
|
||||
{
|
||||
$component = $this->createTicketableItem(
|
||||
'variant-component',
|
||||
now()->subDay(),
|
||||
now()->addMonth(),
|
||||
);
|
||||
$component = $this->createTicketableItem('variant-component');
|
||||
$inventory = Inventory::query()->create();
|
||||
$variant = $component->variants()->create([
|
||||
'inventory_id' => $inventory->id,
|
||||
'minimum_use_date' => now()->addDay(),
|
||||
'maximum_use_date' => now()->addWeek(),
|
||||
]);
|
||||
$bundle = $this->createBundle('variant-bundle');
|
||||
$bundle->bundleComponents()->create([
|
||||
@@ -160,8 +119,6 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
|
||||
$this->assertSame($component->id, $ticket->source_catalog_item_id);
|
||||
$this->assertSame($variant->id, $ticket->source_variant_id);
|
||||
$this->assertTrue($ticket->starts_at->equalTo($variant->minimum_use_date));
|
||||
$this->assertTrue($ticket->expires_at->equalTo($variant->maximum_use_date));
|
||||
}
|
||||
|
||||
public function test_bundle_generation_is_rolled_back_when_a_component_is_invalid(): void
|
||||
@@ -244,9 +201,9 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
->assertJsonPath('data.has_generated_tickets', false);
|
||||
}
|
||||
|
||||
public function test_paid_status_is_confirmed_when_ticket_maximum_use_date_was_reached(): void
|
||||
public function test_paid_status_is_confirmed_when_ticket_is_generated(): void
|
||||
{
|
||||
$item = $this->createTicketableItem('expired-paid-ticket', maximumUseDate: now());
|
||||
$item = $this->createTicketableItem('paid-ticket');
|
||||
$purchase = $this->createPurchase($item, 1);
|
||||
|
||||
$purchase->markAsPaid();
|
||||
@@ -258,11 +215,8 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
private function createTicketableItem(
|
||||
string $slug,
|
||||
mixed $minimumUseDate = null,
|
||||
mixed $maximumUseDate = null,
|
||||
): CatalogItem {
|
||||
private function createTicketableItem(string $slug): CatalogItem
|
||||
{
|
||||
return CatalogItem::query()->create([
|
||||
'tenant_code' => $this->tenant->codigo,
|
||||
'slug' => $slug,
|
||||
@@ -270,8 +224,6 @@ class TicketGeneratorServiceTest extends TestCase
|
||||
'descripcion' => "Descripción de {$slug}",
|
||||
'precio' => 10,
|
||||
'has_tickets' => true,
|
||||
'minimum_use_date' => $minimumUseDate,
|
||||
'maximum_use_date' => $maximumUseDate,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
43
tests/Feature/Ticket/TicketValiditySchemaTest.php
Normal file
43
tests/Feature/Ticket/TicketValiditySchemaTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Ticket;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TicketValiditySchemaTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_ticket_validity_time_schema_is_available(): void
|
||||
{
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'id',
|
||||
'type',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'fixed_starts_at',
|
||||
'fixed_expires_at',
|
||||
'active',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
], Schema::getColumnListing('validity_times'));
|
||||
|
||||
$this->assertTrue(Schema::hasColumns('tickets', [
|
||||
'validity_time_id',
|
||||
'service_date',
|
||||
]));
|
||||
$this->assertFalse(Schema::hasColumn('tickets', 'starts_at'));
|
||||
$this->assertFalse(Schema::hasColumn('tickets', 'expires_at'));
|
||||
|
||||
$this->assertTrue(Schema::hasColumn('catalog_items', 'validity_time_id'));
|
||||
$this->assertFalse(Schema::hasColumn('catalog_items', 'minimum_use_date'));
|
||||
$this->assertFalse(Schema::hasColumn('catalog_items', 'maximum_use_date'));
|
||||
$this->assertTrue(Schema::hasColumn('attribute_options', 'validity_time_id'));
|
||||
$this->assertFalse(Schema::hasColumn('variantes', 'minimum_use_date'));
|
||||
$this->assertFalse(Schema::hasColumn('variantes', 'maximum_use_date'));
|
||||
|
||||
$this->assertTrue(Schema::hasColumn('tenants', 'timezone'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user