From aa4f3303fc44c7f62752553363348e389e21aa8e Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 27 Jul 2026 14:59:07 -0300 Subject: [PATCH] feat(variants): add minimum and maximum use dates to variants and update related logic --- app/Domains/Catalog/Models/CatalogItem.php | 11 ++ app/Domains/Catalog/Models/Variant.php | 17 +++ .../Requests/StoreCatalogItemRequest.php | 7 ++ .../Resources/CatalogItemDetailResource.php | 6 ++ .../Catalog/Resources/CatalogItemResource.php | 6 ++ .../Catalog/Services/CatalogService.php | 23 ++++ .../Exceptions/TicketGenerationException.php | 9 ++ .../Services/TicketGeneratorService.php | 102 ++++++++++++++---- ...00300_add_use_dates_to_variantes_table.php | 26 +++++ .../FiestaFutbolInfantilProductSeeder.php | 10 +- .../Catalog/CatalogItemControllerTest.php | 18 +++- tests/Feature/Catalog/CatalogSchemaTest.php | 8 ++ tests/Feature/Catalog/CatalogServiceTest.php | 68 ++++++++++++ .../FiestaFutbolInfantilProductSeederTest.php | 33 ++++++ .../Ticket/TicketGeneratorServiceTest.php | 56 +++++++++- tests/Unit/Catalog/CatalogModelsTest.php | 19 ++++ 16 files changed, 391 insertions(+), 28 deletions(-) create mode 100644 database/migrations/2026_07_27_000300_add_use_dates_to_variantes_table.php diff --git a/app/Domains/Catalog/Models/CatalogItem.php b/app/Domains/Catalog/Models/CatalogItem.php index c5c0fa7..71c0627 100644 --- a/app/Domains/Catalog/Models/CatalogItem.php +++ b/app/Domains/Catalog/Models/CatalogItem.php @@ -7,6 +7,7 @@ use App\Domains\Catalog\Enums\CatalogItemType; use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Services\CatalogInventoryService; use App\Domains\Tenant\Models\Tenant; +use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -163,6 +164,16 @@ class CatalogItem extends Model return $this->nombre; } + public function getMinimumUseDate(): ?CarbonInterface + { + return $this->minimum_use_date; + } + + public function getMaximumUseDate(): ?CarbonInterface + { + return $this->maximum_use_date; + } + public function isBundle(): bool { return $this->type === CatalogItemType::Bundle; diff --git a/app/Domains/Catalog/Models/Variant.php b/app/Domains/Catalog/Models/Variant.php index 44e5920..fd29de2 100644 --- a/app/Domains/Catalog/Models/Variant.php +++ b/app/Domains/Catalog/Models/Variant.php @@ -3,6 +3,7 @@ namespace App\Domains\Catalog\Models; use App\Domains\Attachable\Models\Attachment; +use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -13,6 +14,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany; #[Fillable([ 'catalog_item_id', 'inventory_id', + 'minimum_use_date', + 'maximum_use_date', ])] class Variant extends Model { @@ -27,6 +30,8 @@ class Variant extends Model return [ 'catalog_item_id' => 'integer', 'inventory_id' => 'integer', + 'minimum_use_date' => 'datetime', + 'maximum_use_date' => 'datetime', ]; } @@ -89,4 +94,16 @@ class Variant extends Model return $definitions === '' ? $name : "{$name} ({$definitions})"; } + + public function getMinimumUseDate(): ?CarbonInterface + { + return $this->minimum_use_date + ?? $this->catalogItem->getMinimumUseDate(); + } + + public function getMaximumUseDate(): ?CarbonInterface + { + return $this->maximum_use_date + ?? $this->catalogItem->getMaximumUseDate(); + } } diff --git a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php b/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php index d0b763a..6e7b4d4 100644 --- a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php +++ b/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php @@ -76,6 +76,13 @@ class StoreCatalogItemRequest extends FormRequest 'variants.*.inventory_id' => ['prohibited'], 'variants.*.reserved_stock' => ['prohibited'], 'variants.*.sold_units' => ['prohibited'], + 'variants.*.minimum_use_date' => ['sometimes', 'nullable', 'date'], + 'variants.*.maximum_use_date' => [ + 'sometimes', + 'nullable', + 'date', + 'after_or_equal:variants.*.minimum_use_date', + ], 'variants.*.values' => ['sometimes', 'array'], 'variants.*.values.*' => ['nullable', 'string'], 'variants.*.images' => ['sometimes', 'array'], diff --git a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php index 1c6e1da..4b71c87 100644 --- a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php @@ -111,6 +111,12 @@ class CatalogItemDetailResource extends JsonResource return [ 'id' => $variant->id, 'stock_tecnico' => $this->variantStock($variant), + 'minimum_use_date' => $variant->minimum_use_date, + 'maximum_use_date' => $variant->maximum_use_date, + 'effective_minimum_use_date' => $variant->minimum_use_date + ?? $this->minimum_use_date, + 'effective_maximum_use_date' => $variant->maximum_use_date + ?? $this->maximum_use_date, 'values' => $variant->definitions ->mapWithKeys(fn ($definition) => [ $definition->itemAttribute?->attribute?->codigo => $definition->value, diff --git a/app/Domains/Catalog/Resources/CatalogItemResource.php b/app/Domains/Catalog/Resources/CatalogItemResource.php index 9aa3a37..ee17c81 100644 --- a/app/Domains/Catalog/Resources/CatalogItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemResource.php @@ -33,6 +33,12 @@ class CatalogItemResource extends JsonResource ->map(fn ($variant) => [ 'id' => $variant->id, 'real_stock' => $variant->inventory?->real_stock, + 'minimum_use_date' => $variant->minimum_use_date, + 'maximum_use_date' => $variant->maximum_use_date, + 'effective_minimum_use_date' => $variant->minimum_use_date + ?? $this->minimum_use_date, + 'effective_maximum_use_date' => $variant->maximum_use_date + ?? $this->maximum_use_date, 'values' => $variant->definitions ->mapWithKeys(fn ($definition) => [ $definition->itemAttribute?->attribute?->codigo => $definition->value, diff --git a/app/Domains/Catalog/Services/CatalogService.php b/app/Domains/Catalog/Services/CatalogService.php index 36570f9..e7a592a 100644 --- a/app/Domains/Catalog/Services/CatalogService.php +++ b/app/Domains/Catalog/Services/CatalogService.php @@ -450,7 +450,12 @@ class CatalogService $inventory = $this->createInventory((int) ($data['real_stock'] ?? 0)); $variant = $catalogItem->variants()->create([ 'inventory_id' => $inventory->id, + 'minimum_use_date' => $data['minimum_use_date'] ?? null, + 'maximum_use_date' => $data['maximum_use_date'] ?? null, ]); + $variant->setRelation('catalogItem', $catalogItem); + + $this->validateVariantUseDates($variant, $index); foreach ($data['values'] ?? [] as $attributeCode => $value) { $itemAttribute = $itemAttributes[$attributeCode] ?? null; @@ -472,6 +477,24 @@ class CatalogService return $variant; } + private function validateVariantUseDates(Variant $variant, int $index): void + { + $minimumUseDate = $variant->getMinimumUseDate(); + $maximumUseDate = $variant->getMaximumUseDate(); + + if ( + $minimumUseDate !== null + && $maximumUseDate !== null + && $maximumUseDate->lessThan($minimumUseDate) + ) { + throw ValidationException::withMessages([ + "variants.{$index}.maximum_use_date" => [ + 'La fecha maxima efectiva debe ser posterior o igual a la fecha minima efectiva.', + ], + ]); + } + } + /** * @param array $data * @param array> $variants diff --git a/app/Domains/Ticket/Exceptions/TicketGenerationException.php b/app/Domains/Ticket/Exceptions/TicketGenerationException.php index 0e86f1f..94c44a0 100644 --- a/app/Domains/Ticket/Exceptions/TicketGenerationException.php +++ b/app/Domains/Ticket/Exceptions/TicketGenerationException.php @@ -29,6 +29,15 @@ class TicketGenerationException extends RuntimeException return new self("El producto {$catalogItem->id} alcanzó su fecha máxima de uso."); } + public static function variantNotFound( + CatalogItem $catalogItem, + int $variantId, + ): self { + return new self( + "La variante {$variantId} no pertenece al producto {$catalogItem->id}.", + ); + } + public static function purchaseWithoutUser(Purchase $purchase): self { return new self("La compra {$purchase->id} no tiene un usuario asociado."); diff --git a/app/Domains/Ticket/Services/TicketGeneratorService.php b/app/Domains/Ticket/Services/TicketGeneratorService.php index 90f920b..4af0808 100644 --- a/app/Domains/Ticket/Services/TicketGeneratorService.php +++ b/app/Domains/Ticket/Services/TicketGeneratorService.php @@ -4,6 +4,7 @@ namespace App\Domains\Ticket\Services; use App\Domains\Auth\Models\User; use App\Domains\Catalog\Models\CatalogItem; +use App\Domains\Catalog\Models\Variant; use App\Domains\Ticket\Exceptions\TicketGenerationException; use App\Domains\Ticket\Models\Ticket; use Carbon\CarbonInterface; @@ -29,38 +30,60 @@ class TicketGeneratorService $now = now(); return DB::transaction(function () use ($catalogItem, $user, $quantity, $sourceVariantId, $now): Collection { - $catalogItems = $this->resolveCatalogItems($catalogItem, $quantity, $now); + $targets = $this->resolveTargets( + $catalogItem, + $quantity, + $sourceVariantId, + $now, + ); - return $catalogItems->map(fn (CatalogItem $item): Ticket => Ticket::query()->create([ - 'tenant_code' => $item->tenant_code, - 'ticket' => (string) Str::uuid(), - 'name' => $item->nombre, - 'description' => (string) ($item->descripcion ?? ''), - 'source_catalog_item_id' => $catalogItem->getKey(), - 'source_variant_id' => $sourceVariantId, - 'starts_at' => $item->minimum_use_date, - 'expires_at' => $item->maximum_use_date, - 'used_at' => null, - 'user_id' => $user->getKey(), - ])); + return $targets->map(function (array $target) use ( + $catalogItem, + $sourceVariantId, + $user, + ): Ticket { + $item = $target['catalog_item']; + $selectedItem = $target['variant'] ?? $item; + + return Ticket::query()->create([ + 'tenant_code' => $item->tenant_code, + 'ticket' => (string) Str::uuid(), + 'name' => $item->nombre, + 'description' => (string) ($item->descripcion ?? ''), + 'source_catalog_item_id' => $catalogItem->getKey(), + 'source_variant_id' => $sourceVariantId, + 'starts_at' => $selectedItem->getMinimumUseDate(), + 'expires_at' => $selectedItem->getMaximumUseDate(), + 'used_at' => null, + 'user_id' => $user->getKey(), + ]); + }); }); } /** - * @return Collection + * @return Collection */ - private function resolveCatalogItems( + private function resolveTargets( CatalogItem $catalogItem, int $quantity, + ?int $sourceVariantId, CarbonInterface $now, ): Collection { if (! $catalogItem->isBundle()) { - $this->validateCatalogItem($catalogItem, $now); + $variant = $this->resolveVariant($catalogItem, $sourceVariantId); + $this->validateTarget($catalogItem, $variant, $now); - return Collection::times($quantity, fn (): CatalogItem => $catalogItem); + return Collection::times($quantity, fn (): array => [ + 'catalog_item' => $catalogItem, + 'variant' => $variant, + ]); } - $catalogItem->loadMissing('bundleComponents.catalogItem'); + $catalogItem->loadMissing([ + 'bundleComponents.catalogItem', + 'bundleComponents.variant.catalogItem', + ]); if ($catalogItem->bundleComponents->isEmpty()) { throw TicketGenerationException::emptyBundle($catalogItem); @@ -69,23 +92,56 @@ class TicketGeneratorService return $catalogItem->bundleComponents ->flatMap(function ($component) use ($quantity, $now): Collection { $componentItem = $component->catalogItem; - $this->validateCatalogItem($componentItem, $now); + $variant = $component->variant; + $this->validateTarget($componentItem, $variant, $now); return Collection::times( $quantity * $component->quantity, - fn (): CatalogItem => $componentItem, + fn (): array => [ + 'catalog_item' => $componentItem, + 'variant' => $variant, + ], ); }) ->values(); } - private function validateCatalogItem(CatalogItem $catalogItem, CarbonInterface $now): void - { + private function resolveVariant( + CatalogItem $catalogItem, + ?int $sourceVariantId, + ): ?Variant { + if ($sourceVariantId === null) { + return null; + } + + $variant = $catalogItem->variants() + ->whereKey($sourceVariantId) + ->first(); + + if ($variant === null) { + throw TicketGenerationException::variantNotFound( + $catalogItem, + $sourceVariantId, + ); + } + + $variant->setRelation('catalogItem', $catalogItem); + + return $variant; + } + + private function validateTarget( + CatalogItem $catalogItem, + ?Variant $variant, + CarbonInterface $now, + ): void { if (! $catalogItem->has_tickets) { throw TicketGenerationException::ticketsDisabled($catalogItem); } - if ($catalogItem->maximum_use_date?->lessThanOrEqualTo($now)) { + $selectedItem = $variant ?? $catalogItem; + + if ($selectedItem->getMaximumUseDate()?->lessThanOrEqualTo($now)) { throw TicketGenerationException::maximumUseDateReached($catalogItem); } } diff --git a/database/migrations/2026_07_27_000300_add_use_dates_to_variantes_table.php b/database/migrations/2026_07_27_000300_add_use_dates_to_variantes_table.php new file mode 100644 index 0000000..3dc9398 --- /dev/null +++ b/database/migrations/2026_07_27_000300_add_use_dates_to_variantes_table.php @@ -0,0 +1,26 @@ +dateTime('minimum_use_date')->nullable()->after('inventory_id'); + $table->dateTime('maximum_use_date')->nullable()->after('minimum_use_date'); + }); + } + + public function down(): void + { + Schema::table('variantes', function (Blueprint $table): void { + $table->dropColumn([ + 'minimum_use_date', + 'maximum_use_date', + ]); + }); + } +}; diff --git a/database/seeders/FiestaFutbolInfantilProductSeeder.php b/database/seeders/FiestaFutbolInfantilProductSeeder.php index 0b70647..240615f 100644 --- a/database/seeders/FiestaFutbolInfantilProductSeeder.php +++ b/database/seeders/FiestaFutbolInfantilProductSeeder.php @@ -38,6 +38,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder ]); $dates = ['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12']; + $minimumUseDate = $dates[0].' 00:00:00'; + $maximumUseDate = $dates[array_key_last($dates)].' 23:59:59'; $generalAdmission = $this->catalogService->create([ 'tenant_code' => $tenant->codigo, @@ -48,12 +50,14 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'precio' => 10000, 'inventory_policy' => InventoryPolicy::Unlimited->value, 'has_tickets' => true, - 'minimum_use_date' => $dates[0].' 00:00:00', - 'maximum_use_date' => $dates[array_key_last($dates)].' 23:59:59', + 'minimum_use_date' => $minimumUseDate, + 'maximum_use_date' => $maximumUseDate, 'attribute_codes' => ['fecha'], 'variants' => array_map( fn (string $date): array => [ 'real_stock' => 0, + 'minimum_use_date' => $date.' 00:00:00', + 'maximum_use_date' => $date.' 23:59:59', 'values' => ['fecha' => $date], ], $dates, @@ -76,6 +80,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'descripcion' => $item['descripcion'] ?? $item['nombre'], 'inventory_policy' => InventoryPolicy::Unlimited->value, 'real_stock' => 0, + 'minimum_use_date' => $minimumUseDate, + 'maximum_use_date' => $maximumUseDate, ...$item, ]); } diff --git a/tests/Feature/Catalog/CatalogItemControllerTest.php b/tests/Feature/Catalog/CatalogItemControllerTest.php index 48eb7d1..76d5190 100644 --- a/tests/Feature/Catalog/CatalogItemControllerTest.php +++ b/tests/Feature/Catalog/CatalogItemControllerTest.php @@ -32,11 +32,14 @@ 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], ], @@ -48,7 +51,20 @@ class CatalogItemControllerTest extends TestCase ->assertJsonPath('data.nombre', 'Shirt') ->assertJsonCount(2, 'data.images') ->assertJsonCount(1, 'data.variants') - ->assertJsonCount(1, 'data.variants.0.images'); + ->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'), + ); $item = CatalogItem::query()->where('slug', 'shirt')->firstOrFail(); $variant = $item->variants()->firstOrFail(); diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index 081bb58..0f539a4 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -174,4 +174,12 @@ class CatalogSchemaTest extends TestCase 'orden' => 3, ]); } + + public function test_variants_can_override_catalog_item_use_dates(): void + { + $this->assertTrue(Schema::hasColumns('variantes', [ + 'minimum_use_date', + 'maximum_use_date', + ])); + } } diff --git a/tests/Feature/Catalog/CatalogServiceTest.php b/tests/Feature/Catalog/CatalogServiceTest.php index 5215b8f..bc1c211 100644 --- a/tests/Feature/Catalog/CatalogServiceTest.php +++ b/tests/Feature/Catalog/CatalogServiceTest.php @@ -13,6 +13,7 @@ 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; @@ -93,6 +94,73 @@ 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'); diff --git a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php index 374e009..9acf98d 100644 --- a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php +++ b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php @@ -80,6 +80,39 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase ['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12'], $generalAdmission->variants->map(fn ($variant) => $variant->definitions->sole()->value)->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->definitions->sole()->value) + ->map(fn ($variant): array => [ + $variant->minimum_use_date->format('Y-m-d H:i:s'), + $variant->maximum_use_date->format('Y-m-d H:i:s'), + ]) + ->values() + ->all() + ); + + $standardItems = CatalogItem::query() + ->where('tenant_code', $tenant->codigo) + ->where('type', CatalogItemType::Standard->value) + ->get(); + + $this->assertCount(7, $standardItems); + foreach ($standardItems as $standardItem) { + $this->assertSame( + '2026-10-09 00:00:00', + $standardItem->minimum_use_date->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'), + ); + } $allDaysItem = CatalogItem::query() ->where('tenant_code', $tenant->codigo) diff --git a/tests/Feature/Ticket/TicketGeneratorServiceTest.php b/tests/Feature/Ticket/TicketGeneratorServiceTest.php index 2d795c4..f8636ec 100644 --- a/tests/Feature/Ticket/TicketGeneratorServiceTest.php +++ b/tests/Feature/Ticket/TicketGeneratorServiceTest.php @@ -7,6 +7,7 @@ use App\Domains\Attachable\Models\Attachment; use App\Domains\Auth\Models\User; use App\Domains\Catalog\Enums\CatalogItemType; use App\Domains\Catalog\Models\CatalogItem; +use App\Domains\Catalog\Models\Inventory; use App\Domains\Notification\Events\TicketsAvailable; use App\Domains\Purchase\Models\Purchase; use App\Domains\Tenant\Models\Tenant; @@ -93,6 +94,27 @@ class TicketGeneratorServiceTest extends TestCase $this->service->generate($item, $this->user); } + 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()); @@ -111,6 +133,34 @@ class TicketGeneratorServiceTest extends TestCase $this->assertCount(2, $tickets->where('name', $second->nombre)); } + public function test_bundle_component_uses_its_variant_dates(): void + { + $component = $this->createTicketableItem( + 'variant-component', + now()->subDay(), + now()->addMonth(), + ); + $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([ + 'component_catalog_item_id' => $component->id, + 'component_variant_id' => $variant->id, + 'quantity' => 1, + ]); + + $ticket = $this->service + ->generate($bundle, $this->user) + ->firstOrFail(); + + $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 { $valid = $this->createTicketableItem('valid'); @@ -152,13 +202,15 @@ class TicketGeneratorServiceTest extends TestCase public function test_a_ticket_generated_from_a_purchase_keeps_its_source_ids(): void { $item = $this->createTicketableItem('sourced-ticket'); - $purchase = $this->createPurchase($item, 1, 1234); + $inventory = Inventory::query()->create(); + $variant = $item->variants()->create(['inventory_id' => $inventory->id]); + $purchase = $this->createPurchase($item, 1, $variant->id); $purchase->markAsPaid(); $this->assertDatabaseHas('tickets', [ 'source_catalog_item_id' => $item->id, - 'source_variant_id' => 1234, + 'source_variant_id' => $variant->id, ]); } diff --git a/tests/Unit/Catalog/CatalogModelsTest.php b/tests/Unit/Catalog/CatalogModelsTest.php index f2d5ed1..34486fa 100644 --- a/tests/Unit/Catalog/CatalogModelsTest.php +++ b/tests/Unit/Catalog/CatalogModelsTest.php @@ -22,6 +22,7 @@ use App\Domains\Catalog\Models\VariantDefinition; use App\Domains\Shared\Enums\FieldType; use App\Domains\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Collection as EloquentCollection; +use Illuminate\Support\Carbon; use Tests\TestCase; class CatalogModelsTest extends TestCase @@ -136,6 +137,24 @@ class CatalogModelsTest extends TestCase $this->assertSame('catalog_items_attachments', $variant->attachments()->getTable()); } + public function test_variant_use_dates_override_or_inherit_catalog_item_dates(): void + { + $item = new CatalogItem; + $item->minimum_use_date = Carbon::parse('2026-08-01 09:00:00'); + $item->maximum_use_date = Carbon::parse('2026-08-31 18:00:00'); + + $variant = new Variant; + $variant->maximum_use_date = Carbon::parse('2026-08-15 18:00:00'); + $variant->setRelation('catalogItem', $item); + + $this->assertTrue( + $variant->getMinimumUseDate()->equalTo($item->minimum_use_date), + ); + $this->assertTrue( + $variant->getMaximumUseDate()->equalTo($variant->maximum_use_date), + ); + } + public function test_inventory_maps_stock_without_a_polymorphic_owner(): void { $inventory = $this->trackedInventory(realStock: 10, reservedStock: 3);