diff --git a/app/Domains/Catalog/Controllers/CatalogController.php b/app/Domains/Catalog/Controllers/CatalogController.php index 60ee8ea..07dbd06 100644 --- a/app/Domains/Catalog/Controllers/CatalogController.php +++ b/app/Domains/Catalog/Controllers/CatalogController.php @@ -178,6 +178,7 @@ class CatalogController extends Controller 'catalogItem.attachments', 'catalogItem.variants.inventory', 'catalogItem.variants.attachments', + 'catalogItem.variants.eventDate', 'catalogItem.variants.definitions.itemAttribute.attribute', 'catalogItem.bundleComponents.catalogItem', 'catalogItem.bundleComponents.variant.catalogItem', diff --git a/app/Domains/Catalog/Enums/EventProductType.php b/app/Domains/Catalog/Enums/EventProductType.php new file mode 100644 index 0000000..56157ad --- /dev/null +++ b/app/Domains/Catalog/Enums/EventProductType.php @@ -0,0 +1,15 @@ + */ + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} diff --git a/app/Domains/Catalog/Models/CatalogItem.php b/app/Domains/Catalog/Models/CatalogItem.php index 71c0627..2fd62f0 100644 --- a/app/Domains/Catalog/Models/CatalogItem.php +++ b/app/Domains/Catalog/Models/CatalogItem.php @@ -4,9 +4,12 @@ namespace App\Domains\Catalog\Models; use App\Domains\Attachable\Models\Attachment; use App\Domains\Catalog\Enums\CatalogItemType; +use App\Domains\Catalog\Enums\EventProductType; use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Services\CatalogInventoryService; +use App\Domains\Event\Models\Event; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticket\Models\Ticket; use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -17,6 +20,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany; #[Fillable([ 'tenant_code', + 'event_id', + 'event_product_type', 'category_id', 'brand_id', 'inventory_id', @@ -50,6 +55,8 @@ class CatalogItem extends Model 'category_id' => 'integer', 'brand_id' => 'integer', 'inventory_id' => 'integer', + 'event_id' => 'integer', + 'event_product_type' => EventProductType::class, 'type' => CatalogItemType::class, 'precio' => 'decimal:2', 'inventory_policy' => InventoryPolicy::class, @@ -65,6 +72,12 @@ class CatalogItem extends Model return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); } + /** @return BelongsTo */ + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + /** @return BelongsTo */ public function category(): BelongsTo { @@ -101,6 +114,12 @@ class CatalogItem extends Model return $this->hasMany(Variant::class); } + /** @return HasMany */ + public function sourceTickets(): HasMany + { + return $this->hasMany(Ticket::class, 'source_catalog_item_id'); + } + /** @return BelongsToMany */ public function attributes(): BelongsToMany { diff --git a/app/Domains/Catalog/Models/Variant.php b/app/Domains/Catalog/Models/Variant.php index fd29de2..3fb716c 100644 --- a/app/Domains/Catalog/Models/Variant.php +++ b/app/Domains/Catalog/Models/Variant.php @@ -3,6 +3,8 @@ namespace App\Domains\Catalog\Models; use App\Domains\Attachable\Models\Attachment; +use App\Domains\Event\Models\EventDate; +use App\Domains\Ticket\Models\Ticket; use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -13,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; #[Fillable([ 'catalog_item_id', + 'event_date_id', 'inventory_id', 'minimum_use_date', 'maximum_use_date', @@ -29,6 +32,7 @@ class Variant extends Model { return [ 'catalog_item_id' => 'integer', + 'event_date_id' => 'integer', 'inventory_id' => 'integer', 'minimum_use_date' => 'datetime', 'maximum_use_date' => 'datetime', @@ -41,6 +45,18 @@ class Variant extends Model return $this->belongsTo(CatalogItem::class); } + /** @return BelongsTo */ + public function eventDate(): BelongsTo + { + return $this->belongsTo(EventDate::class); + } + + /** @return HasMany */ + public function sourceTickets(): HasMany + { + return $this->hasMany(Ticket::class, 'source_variant_id'); + } + /** @return BelongsTo */ public function inventory(): BelongsTo { @@ -80,7 +96,7 @@ class Variant extends Model public function getName(): string { $name = $this->catalogItem->nombre; - $this->loadMissing('definitions.itemAttribute.attribute'); + $this->loadMissing(['definitions.itemAttribute.attribute', 'eventDate']); $definitions = $this->definitions ->map(function (VariantDefinition $definition): ?string { $attributeName = $definition->itemAttribute?->attribute?->nombre; @@ -89,21 +105,28 @@ class Variant extends Model ? "{$attributeName}: {$definition->value}" : $definition->value; }) - ->filter() - ->implode(', '); + ->filter(); - return $definitions === '' ? $name : "{$name} ({$definitions})"; + if ($this->eventDate !== null) { + $definitions->push('Fecha: '.$this->eventDate->date->format('Y-m-d')); + } + + $description = $definitions->implode(', '); + + return $description === '' ? $name : "{$name} ({$description})"; } public function getMinimumUseDate(): ?CarbonInterface { - return $this->minimum_use_date + return $this->eventDate?->startsAt() + ?? $this->minimum_use_date ?? $this->catalogItem->getMinimumUseDate(); } public function getMaximumUseDate(): ?CarbonInterface { - return $this->maximum_use_date + return $this->eventDate?->endsAt() + ?? $this->maximum_use_date ?? $this->catalogItem->getMaximumUseDate(); } } diff --git a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php b/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php index 6e7b4d4..4ccb4c0 100644 --- a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php +++ b/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php @@ -3,6 +3,7 @@ namespace App\Domains\Catalog\Requests; use App\Domains\Catalog\Enums\CatalogItemType; +use App\Domains\Catalog\Enums\EventProductType; use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Shared\Rules\ImageOrBase64Rule; use Illuminate\Foundation\Http\FormRequest; @@ -25,6 +26,20 @@ class StoreCatalogItemRequest extends FormRequest return [ 'tenant_code' => ['prohibited'], 'type' => ['sometimes', Rule::enum(CatalogItemType::class)], + 'event_id' => [ + 'sometimes', + 'nullable', + 'required_with:event_product_type', + Rule::exists('events', 'id')->where( + fn ($query) => $query->where('tenant_code', $tenantCode) + ), + ], + 'event_product_type' => [ + 'sometimes', + 'nullable', + 'required_with:event_id', + Rule::enum(EventProductType::class), + ], 'category_id' => [ 'sometimes', 'nullable', @@ -73,6 +88,14 @@ class StoreCatalogItemRequest extends FormRequest 'images.*' => ['required', new ImageOrBase64Rule], 'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], 'variants.*.real_stock' => ['sometimes', 'integer', 'min:0'], + 'variants.*.event_date_id' => [ + 'sometimes', + 'nullable', + 'integer', + Rule::exists('event_dates', 'id')->where( + fn ($query) => $query->where('event_id', $this->input('event_id')) + ), + ], 'variants.*.inventory_id' => ['prohibited'], 'variants.*.reserved_stock' => ['prohibited'], 'variants.*.sold_units' => ['prohibited'], diff --git a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php index 86ff7c8..b94122a 100644 --- a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php @@ -32,6 +32,8 @@ class CatalogFeaturedItemResource extends JsonResource 'variants' => $catalogItem->variants ->map(fn (Variant $variant): array => [ 'id' => $variant->id, + 'event_date_id' => $variant->event_date_id, + 'event_date' => $variant->eventDate?->date?->format('Y-m-d'), 'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited ? null : $variant->inventory->availableStock(), diff --git a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php index 4b71c87..00fefe7 100644 --- a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php @@ -22,6 +22,8 @@ class CatalogItemDetailResource extends JsonResource return [ 'id' => $this->id, 'type' => $this->type->value, + 'event_id' => $this->event_id, + 'event_product_type' => $this->event_product_type?->value, 'category_id' => $this->category_id, 'brand_id' => $this->brand_id, 'slug' => $this->slug, @@ -110,12 +112,16 @@ class CatalogItemDetailResource extends JsonResource { return [ 'id' => $variant->id, + 'event_date_id' => $variant->event_date_id, + 'event_date' => $variant->eventDate?->date?->format('Y-m-d'), '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 + 'effective_minimum_use_date' => $variant->eventDate?->startsAt() + ?? $variant->minimum_use_date ?? $this->minimum_use_date, - 'effective_maximum_use_date' => $variant->maximum_use_date + 'effective_maximum_use_date' => $variant->eventDate?->endsAt() + ?? $variant->maximum_use_date ?? $this->maximum_use_date, 'values' => $variant->definitions ->mapWithKeys(fn ($definition) => [ diff --git a/app/Domains/Catalog/Resources/CatalogItemResource.php b/app/Domains/Catalog/Resources/CatalogItemResource.php index ee17c81..363a73c 100644 --- a/app/Domains/Catalog/Resources/CatalogItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemResource.php @@ -15,6 +15,8 @@ class CatalogItemResource extends JsonResource return [ 'id' => $this->id, 'type' => $this->type->value, + 'event_id' => $this->event_id, + 'event_product_type' => $this->event_product_type?->value, 'category_id' => $this->category_id, 'brand_id' => $this->brand_id, 'slug' => $this->slug, @@ -32,12 +34,16 @@ class CatalogItemResource extends JsonResource 'variants' => $this->whenLoaded('variants', fn () => $this->variants ->map(fn ($variant) => [ 'id' => $variant->id, + 'event_date_id' => $variant->event_date_id, + 'event_date' => $variant->eventDate?->date?->format('Y-m-d'), '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 + 'effective_minimum_use_date' => $variant->eventDate?->startsAt() + ?? $variant->minimum_use_date ?? $this->minimum_use_date, - 'effective_maximum_use_date' => $variant->maximum_use_date + 'effective_maximum_use_date' => $variant->eventDate?->endsAt() + ?? $variant->maximum_use_date ?? $this->maximum_use_date, 'values' => $variant->definitions ->mapWithKeys(fn ($definition) => [ diff --git a/app/Domains/Catalog/Resources/CatalogSearchItemResource.php b/app/Domains/Catalog/Resources/CatalogSearchItemResource.php index 6cbca53..3c83e7a 100644 --- a/app/Domains/Catalog/Resources/CatalogSearchItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogSearchItemResource.php @@ -30,6 +30,8 @@ class CatalogSearchItemResource extends JsonResource 'variants' => $this->variants ->map(fn (Variant $variant): array => [ 'id' => $variant->id, + 'event_date_id' => $variant->event_date_id, + 'event_date' => $variant->eventDate?->date?->format('Y-m-d'), 'stock_tecnico' => $this->inventory_policy === InventoryPolicy::Unlimited ? null : $variant->inventory?->availableStock(), diff --git a/app/Domains/Catalog/Services/CatalogService.php b/app/Domains/Catalog/Services/CatalogService.php index 0745324..3896ca8 100644 --- a/app/Domains/Catalog/Services/CatalogService.php +++ b/app/Domains/Catalog/Services/CatalogService.php @@ -5,12 +5,15 @@ namespace App\Domains\Catalog\Services; use App\Domains\Attachable\Models\Attachment; use App\Domains\Attachable\Services\AttachmentService; use App\Domains\Catalog\Enums\CatalogItemType; +use App\Domains\Catalog\Enums\EventProductType; use App\Domains\Catalog\Models\Attribute; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\ItemAttribute; use App\Domains\Catalog\Models\Variant; +use App\Domains\Event\Models\Event; +use App\Domains\Event\Models\EventDate; use App\Domains\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Builder; use Illuminate\Pagination\LengthAwarePaginator; @@ -39,7 +42,12 @@ class CatalogService $hasDirectStock = array_key_exists('real_stock', $data); $realStock = (int) ($data['real_stock'] ?? 0); - $hasVariants = $attributeCodes !== []; + $hasEventDateVariants = $variants !== [] && collect($variants)->every( + fn (array $variant): bool => ! empty($variant['event_date_id']) + ); + $hasVariants = $attributeCodes !== [] || $hasEventDateVariants; + + $this->validateEventData($data); if ($type === CatalogItemType::Bundle) { $this->validateBundleData($data, $components); @@ -121,9 +129,11 @@ class CatalogService 'inventory', 'category', 'brand', + 'event', 'itemAttributes.attribute', 'variants.inventory', 'variants.attachments', + 'variants.eventDate', 'variants.definitions.itemAttribute.attribute', 'bundleComponents.catalogItem', 'bundleComponents.variant.catalogItem', @@ -138,10 +148,12 @@ class CatalogService 'inventory', 'category', 'brand', + 'event', 'itemAttributes.attribute.options', 'variants' => fn ($query) => $query->orderBy('id'), 'variants.inventory', 'variants.attachments', + 'variants.eventDate', 'variants.definitions' => fn ($query) => $query->orderBy('id'), 'variants.definitions.itemAttribute.attribute', 'bundleComponents.catalogItem.inventory', @@ -195,6 +207,7 @@ class CatalogService 'inventory', 'variants.inventory', 'variants.attachments', + 'variants.eventDate', 'variants.definitions.itemAttribute.attribute', 'bundleComponents.catalogItem', 'bundleComponents.variant.catalogItem', @@ -226,6 +239,7 @@ class CatalogService 'inventory', 'variants.inventory', 'variants.attachments', + 'variants.eventDate', 'variants.definitions.itemAttribute.attribute', 'bundleComponents.catalogItem', 'bundleComponents.variant.catalogItem', @@ -472,8 +486,28 @@ class CatalogService } $inventory = $this->createInventory((int) ($data['real_stock'] ?? 0)); + $eventDateId = $data['event_date_id'] ?? null; + + if ( + $eventDateId !== null + && ( + $catalogItem->event_id === null + || ! EventDate::query() + ->whereKey($eventDateId) + ->where('event_id', $catalogItem->event_id) + ->exists() + ) + ) { + throw ValidationException::withMessages([ + "variants.{$index}.event_date_id" => [ + 'The event date must belong to the catalog item event.', + ], + ]); + } + $variant = $catalogItem->variants()->create([ 'inventory_id' => $inventory->id, + 'event_date_id' => $eventDateId, 'minimum_use_date' => $data['minimum_use_date'] ?? null, 'maximum_use_date' => $data['maximum_use_date'] ?? null, ]); @@ -501,6 +535,38 @@ class CatalogService return $variant; } + /** @param array $data */ + private function validateEventData(array $data): void + { + $eventId = $data['event_id'] ?? null; + $eventProductType = $data['event_product_type'] ?? null; + + if (($eventId === null) !== ($eventProductType === null)) { + throw ValidationException::withMessages([ + 'event_id' => ['Event and event product type must be provided together.'], + ]); + } + + if ($eventId === null) { + return; + } + + if (! Event::query() + ->whereKey($eventId) + ->where('tenant_code', $data['tenant_code'] ?? null) + ->exists()) { + throw ValidationException::withMessages([ + 'event_id' => ['The event must belong to the catalog item tenant.'], + ]); + } + + if (! in_array($eventProductType, EventProductType::values(), true)) { + throw ValidationException::withMessages([ + 'event_product_type' => ['The event product type is invalid.'], + ]); + } + } + private function validateVariantUseDates(Variant $variant, int $index): void { $minimumUseDate = $variant->getMinimumUseDate(); diff --git a/app/Domains/Event/Models/Event.php b/app/Domains/Event/Models/Event.php new file mode 100644 index 0000000..52ac62e --- /dev/null +++ b/app/Domains/Event/Models/Event.php @@ -0,0 +1,41 @@ + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); + } + + /** @return HasMany */ + public function dates(): HasMany + { + return $this->hasMany(EventDate::class)->orderBy('date')->orderBy('time_start'); + } + + /** @return HasMany */ + public function catalogItems(): HasMany + { + return $this->hasMany(CatalogItem::class); + } +} diff --git a/app/Domains/Event/Models/EventDate.php b/app/Domains/Event/Models/EventDate.php new file mode 100644 index 0000000..48714e6 --- /dev/null +++ b/app/Domains/Event/Models/EventDate.php @@ -0,0 +1,55 @@ + 'integer', + 'date' => 'date:Y-m-d', + ]; + } + + /** @return BelongsTo */ + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + /** @return HasMany */ + public function variants(): HasMany + { + return $this->hasMany(Variant::class); + } + + public function startsAt(): CarbonInterface + { + return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_start); + } + + public function endsAt(): CarbonInterface + { + return Carbon::parse($this->date->format('Y-m-d').' '.$this->time_end); + } +} diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Tenant/Models/Tenant.php index 555c740..cd0c4d8 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Tenant/Models/Tenant.php @@ -7,6 +7,7 @@ use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\ProductLayout; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; +use App\Domains\Event\Models\Event; use App\Domains\Menu\Models\Menu; use App\Domains\Menu\Models\TenantMenu; use Illuminate\Database\Eloquent\Attributes\Fillable; @@ -32,6 +33,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'search_product_layout', 'search_group_layout', 'search_items_per_page', + 'active_event_id', ])] class Tenant extends Model { @@ -59,6 +61,7 @@ class Tenant extends Model 'search_product_layout' => ProductLayout::class, 'search_group_layout' => GroupLayout::class, 'search_items_per_page' => 'integer', + 'active_event_id' => 'integer', ]; } @@ -91,6 +94,18 @@ class Tenant extends Model return $this->hasMany(CatalogItem::class, 'tenant_code', 'codigo'); } + /** @return HasMany */ + public function events(): HasMany + { + return $this->hasMany(Event::class, 'tenant_code', 'codigo'); + } + + /** @return BelongsTo */ + public function activeEvent(): BelongsTo + { + return $this->belongsTo(Event::class, 'active_event_id'); + } + /** * @return HasMany */ diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index abdff89..354b398 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -32,6 +32,20 @@ class TenantResource extends JsonResource 'header_bg_color' => $this->header_bg_color, 'footer_bg_color' => $this->footer_bg_color, 'website_type_code' => $this->website_type_code, + 'active_event_id' => $this->active_event_id, + 'active_event' => $this->whenLoaded('activeEvent', fn () => $this->activeEvent === null + ? null + : [ + 'id' => $this->activeEvent->id, + 'name' => $this->activeEvent->name, + 'address' => $this->activeEvent->address, + 'dates' => $this->activeEvent->dates->map(fn ($eventDate): array => [ + 'id' => $eventDate->id, + 'date' => $eventDate->date->format('Y-m-d'), + 'time_start' => $eventDate->time_start, + 'time_end' => $eventDate->time_end, + ])->values(), + ]), 'extras' => $this->whenLoaded( 'websiteExtras', fn () => $this->websiteExtras diff --git a/app/Domains/Tenant/Services/TenantInformationService.php b/app/Domains/Tenant/Services/TenantInformationService.php index 10fdf18..0f47a1d 100644 --- a/app/Domains/Tenant/Services/TenantInformationService.php +++ b/app/Domains/Tenant/Services/TenantInformationService.php @@ -15,6 +15,7 @@ class TenantInformationService 'footerLogo', 'socialMedia', 'websiteExtras.websiteTypeExtra', + 'activeEvent.dates', ]; /** diff --git a/app/Domains/Ticket/Controllers/TicketController.php b/app/Domains/Ticket/Controllers/TicketController.php index 7562b47..7c8fc2a 100644 --- a/app/Domains/Ticket/Controllers/TicketController.php +++ b/app/Domains/Ticket/Controllers/TicketController.php @@ -22,6 +22,7 @@ class TicketController extends Controller $tickets = Ticket::query() ->where('tenant_code', $tenant->codigo) ->where('user_id', $request->user()->getKey()) + ->with('sourceVariant.eventDate', 'sourceVariant.catalogItem') ->orderByDesc('id') ->get(); @@ -35,6 +36,7 @@ class TicketController extends Controller ->where('tenant_code', $tenant->codigo) ->where('user_id', $request->user()->getKey()) ->whereIn('id', $ticketIds) + ->with('sourceVariant.eventDate', 'sourceVariant.catalogItem') ->orderByDesc('id') ->get(); diff --git a/app/Domains/Ticket/Models/Ticket.php b/app/Domains/Ticket/Models/Ticket.php index 17319da..7d5e242 100644 --- a/app/Domains/Ticket/Models/Ticket.php +++ b/app/Domains/Ticket/Models/Ticket.php @@ -3,8 +3,11 @@ namespace App\Domains\Ticket\Models; use App\Domains\Auth\Models\User; +use App\Domains\Catalog\Models\CatalogItem; +use App\Domains\Catalog\Models\Variant; use App\Domains\Purchase\Models\Purchase; 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; @@ -66,13 +69,27 @@ class Ticket extends Model return $this->belongsTo(Purchase::class, 'source_purchase_id'); } + /** @return BelongsTo */ + public function sourceCatalogItem(): BelongsTo + { + return $this->belongsTo(CatalogItem::class, 'source_catalog_item_id'); + } + + /** @return BelongsTo */ + public function sourceVariant(): BelongsTo + { + return $this->belongsTo(Variant::class, 'source_variant_id'); + } + public function isValid(): bool { $now = now(); + $startsAt = $this->getEffectiveStartsAt(); + $expiresAt = $this->getEffectiveExpiresAt(); return $this->used_at === null - && ($this->starts_at === null || $this->starts_at->lessThanOrEqualTo($now)) - && ($this->expires_at === null || $this->expires_at->greaterThan($now)); + && ($startsAt === null || $startsAt->lessThanOrEqualTo($now)) + && ($expiresAt === null || $expiresAt->greaterThan($now)); } public function getIsValidAttribute(): bool @@ -82,13 +99,27 @@ class Ticket extends Model public function getIsExpiredAttribute(): bool { + $expiresAt = $this->getEffectiveExpiresAt(); + return $this->used_at === null - && $this->expires_at !== null - && $this->expires_at->lessThanOrEqualTo(now()); + && $expiresAt !== null + && $expiresAt->lessThanOrEqualTo(now()); } public function getIsUsedAttribute(): bool { return $this->used_at !== null; } + + public function getEffectiveStartsAt(): ?CarbonInterface + { + return $this->sourceVariant?->getMinimumUseDate() + ?? $this->starts_at; + } + + public function getEffectiveExpiresAt(): ?CarbonInterface + { + return $this->sourceVariant?->getMaximumUseDate() + ?? $this->expires_at; + } } diff --git a/app/Domains/Ticket/Resources/TicketResource.php b/app/Domains/Ticket/Resources/TicketResource.php index cafac90..d19aa4b 100644 --- a/app/Domains/Ticket/Resources/TicketResource.php +++ b/app/Domains/Ticket/Resources/TicketResource.php @@ -20,8 +20,8 @@ class TicketResource extends JsonResource 'description' => $this->description, 'source_catalog_item_id' => $this->source_catalog_item_id, 'source_variant_id' => $this->source_variant_id, - 'starts_at' => $this->starts_at, - 'expires_at' => $this->expires_at, + 'starts_at' => $this->getEffectiveStartsAt(), + 'expires_at' => $this->getEffectiveExpiresAt(), 'used_at' => $this->used_at, 'is_valid' => $this->is_valid, 'is_expired' => $this->is_expired, diff --git a/app/Domains/Ticket/Services/TicketGeneratorService.php b/app/Domains/Ticket/Services/TicketGeneratorService.php index c65b1e1..652c89d 100644 --- a/app/Domains/Ticket/Services/TicketGeneratorService.php +++ b/app/Domains/Ticket/Services/TicketGeneratorService.php @@ -35,8 +35,6 @@ class TicketGeneratorService ); return $targets->map(function (array $target) use ( - $catalogItem, - $sourceVariantId, $sourcePurchaseId, $user, ): Ticket { @@ -49,8 +47,8 @@ class TicketGeneratorService 'name' => $item->nombre, 'description' => (string) ($item->descripcion ?? ''), 'source_purchase_id' => $sourcePurchaseId, - 'source_catalog_item_id' => $catalogItem->getKey(), - 'source_variant_id' => $sourceVariantId, + 'source_catalog_item_id' => $item->getKey(), + 'source_variant_id' => $target['variant']?->getKey(), 'starts_at' => $selectedItem->getMinimumUseDate(), 'expires_at' => $selectedItem->getMaximumUseDate(), 'used_at' => null, diff --git a/database/migrations/2026_08_03_000000_create_events_and_link_catalog.php b/database/migrations/2026_08_03_000000_create_events_and_link_catalog.php new file mode 100644 index 0000000..e8f84c3 --- /dev/null +++ b/database/migrations/2026_08_03_000000_create_events_and_link_catalog.php @@ -0,0 +1,80 @@ +id(); + $table->string('tenant_code'); + $table->string('name'); + $table->string('address'); + + $table->foreign('tenant_code') + ->references('codigo') + ->on('tenants') + ->cascadeOnUpdate() + ->cascadeOnDelete(); + }); + + Schema::create('event_dates', function (Blueprint $table): void { + $table->id(); + $table->foreignId('event_id')->constrained('events')->cascadeOnDelete(); + $table->date('date'); + $table->time('time_start'); + $table->time('time_end'); + }); + + Schema::table('tenants', function (Blueprint $table): void { + $table->foreignId('active_event_id') + ->nullable() + ->constrained('events') + ->nullOnDelete(); + }); + + Schema::table('catalog_items', function (Blueprint $table): void { + $table->foreignId('event_id') + ->nullable() + ->after('tenant_code') + ->constrained('events') + ->nullOnDelete(); + $table->enum('event_product_type', EventProductType::values()) + ->nullable() + ->after('event_id'); + }); + + Schema::table('variantes', function (Blueprint $table): void { + $table->foreignId('event_date_id') + ->nullable() + ->after('catalog_item_id') + ->constrained('event_dates') + ->nullOnDelete(); + $table->unique(['catalog_item_id', 'event_date_id']); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table): void { + $table->dropConstrainedForeignId('active_event_id'); + }); + + Schema::table('variantes', function (Blueprint $table): void { + $table->dropUnique(['catalog_item_id', 'event_date_id']); + $table->dropConstrainedForeignId('event_date_id'); + }); + + Schema::table('catalog_items', function (Blueprint $table): void { + $table->dropConstrainedForeignId('event_id'); + $table->dropColumn('event_product_type'); + }); + + Schema::dropIfExists('event_dates'); + Schema::dropIfExists('events'); + } +}; diff --git a/database/migrations/2026_08_03_000100_add_catalog_foreign_keys_to_tickets.php b/database/migrations/2026_08_03_000100_add_catalog_foreign_keys_to_tickets.php new file mode 100644 index 0000000..3d89946 --- /dev/null +++ b/database/migrations/2026_08_03_000100_add_catalog_foreign_keys_to_tickets.php @@ -0,0 +1,43 @@ +whereNotNull('source_variant_id') + ->whereNotIn('source_variant_id', DB::table('variantes')->select('id')) + ->update(['source_variant_id' => null]); + + DB::table('tickets') + ->whereNotNull('source_catalog_item_id') + ->whereNotIn('source_catalog_item_id', DB::table('catalog_items')->select('id')) + ->update(['source_catalog_item_id' => null]); + + Schema::table('tickets', function (Blueprint $table): void { + $table->foreign('source_catalog_item_id') + ->references('id') + ->on('catalog_items') + ->cascadeOnUpdate() + ->nullOnDelete(); + $table->foreign('source_variant_id') + ->references('id') + ->on('variantes') + ->cascadeOnUpdate() + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('tickets', function (Blueprint $table): void { + $table->dropForeign(['source_catalog_item_id']); + $table->dropForeign(['source_variant_id']); + }); + } +}; diff --git a/database/seeders/AttributeSeeder.php b/database/seeders/AttributeSeeder.php index dba732d..f4b4cc0 100644 --- a/database/seeders/AttributeSeeder.php +++ b/database/seeders/AttributeSeeder.php @@ -17,83 +17,79 @@ class AttributeSeeder extends Seeder $tenants = Tenant::all(); foreach ($tenants as $tenant) { - // Fiesta Futbol Infantil only uses the Fecha attribute. + // Event dates replace catalog attributes for Fiesta Futbol Infantil. if ($tenant->codigo === 'fiesta_futbol_infantil') { Attribute::query() ->where('tenant_codigo', $tenant->codigo) - ->whereIn('codigo', ['color', 'talle', 'talle_numerico']) + ->whereIn('codigo', ['color', 'talle', 'talle_numerico', 'fecha']) ->delete(); + + continue; } - if ($tenant->codigo !== 'fiesta_futbol_infantil') { - $this->seedAttribute($tenant, [ - 'codigo' => 'color', - 'nombre' => 'Color', - 'type' => FieldType::Select->value, - 'is_required' => true, - 'metadata_schema' => [ - 'hex' => ['type' => 'string'], + $this->seedAttribute($tenant, [ + 'codigo' => 'color', + 'nombre' => 'Color', + 'type' => FieldType::Select->value, + 'is_required' => true, + 'metadata_schema' => [ + 'hex' => ['type' => 'string'], + ], + 'options' => [ + [ + 'value' => 'Negro', + 'label' => 'Negro', + 'sort_order' => 1, + 'metadata' => ['hex' => '#000000'], ], - 'options' => [ - [ - 'value' => 'Negro', - 'label' => 'Negro', - 'sort_order' => 1, - 'metadata' => ['hex' => '#000000'], - ], - [ - 'value' => 'Gris', - 'label' => 'Gris', - 'sort_order' => 2, - 'metadata' => ['hex' => '#808080'], - ], - [ - 'value' => 'Blanco', - 'label' => 'Blanco', - 'sort_order' => 3, - 'metadata' => ['hex' => '#FFFFFF'], - ], - [ - 'value' => 'Azul', - 'label' => 'Azul', - 'sort_order' => 4, - 'metadata' => ['hex' => '#0000FF'], - ], + [ + 'value' => 'Gris', + 'label' => 'Gris', + 'sort_order' => 2, + 'metadata' => ['hex' => '#808080'], ], - ]); - } + [ + 'value' => 'Blanco', + 'label' => 'Blanco', + 'sort_order' => 3, + 'metadata' => ['hex' => '#FFFFFF'], + ], + [ + 'value' => 'Azul', + 'label' => 'Azul', + 'sort_order' => 4, + 'metadata' => ['hex' => '#0000FF'], + ], + ], + ]); // Seed Talle (Size - Text options) attribute - if ($tenant->codigo !== 'fiesta_futbol_infantil') { - $this->seedAttribute($tenant, [ - 'codigo' => 'talle', - 'nombre' => 'Talle', - 'type' => FieldType::Select->value, - 'is_required' => true, - 'options' => [ - ['value' => 'S', 'label' => 'S', 'sort_order' => 1], - ['value' => 'M', 'label' => 'M', 'sort_order' => 2], - ['value' => 'L', 'label' => 'L', 'sort_order' => 3], - ['value' => 'XL', 'label' => 'XL', 'sort_order' => 4], - ], - ]); - } + $this->seedAttribute($tenant, [ + 'codigo' => 'talle', + 'nombre' => 'Talle', + 'type' => FieldType::Select->value, + 'is_required' => true, + 'options' => [ + ['value' => 'S', 'label' => 'S', 'sort_order' => 1], + ['value' => 'M', 'label' => 'M', 'sort_order' => 2], + ['value' => 'L', 'label' => 'L', 'sort_order' => 3], + ['value' => 'XL', 'label' => 'XL', 'sort_order' => 4], + ], + ]); // Seed Talle Numérico (Numeric Size options) attribute - if ($tenant->codigo !== 'fiesta_futbol_infantil') { - $this->seedAttribute($tenant, [ - 'codigo' => 'talle_numerico', - 'nombre' => 'Talle Numérico', - 'type' => FieldType::Select->value, - 'is_required' => true, - 'options' => [ - ['value' => '38', 'label' => '38', 'sort_order' => 1], - ['value' => '40', 'label' => '40', 'sort_order' => 2], - ['value' => '42', 'label' => '42', 'sort_order' => 3], - ['value' => '44', 'label' => '44', 'sort_order' => 4], - ], - ]); - } + $this->seedAttribute($tenant, [ + 'codigo' => 'talle_numerico', + 'nombre' => 'Talle Numérico', + 'type' => FieldType::Select->value, + 'is_required' => true, + 'options' => [ + ['value' => '38', 'label' => '38', 'sort_order' => 1], + ['value' => '40', 'label' => '40', 'sort_order' => 2], + ['value' => '42', 'label' => '42', 'sort_order' => 3], + ['value' => '44', 'label' => '44', 'sort_order' => 4], + ], + ]); // Seed Fecha attribute $this->seedAttribute($tenant, [ diff --git a/database/seeders/FiestaFutbolInfantilProductSeeder.php b/database/seeders/FiestaFutbolInfantilProductSeeder.php index b27fed6..c8174a2 100644 --- a/database/seeders/FiestaFutbolInfantilProductSeeder.php +++ b/database/seeders/FiestaFutbolInfantilProductSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Domains\Catalog\Enums\CatalogItemType; +use App\Domains\Catalog\Enums\EventProductType; use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Enums\ProductLayout; @@ -10,6 +11,7 @@ use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\FeaturedGroup; use App\Domains\Catalog\Services\CatalogService; +use App\Domains\Event\Models\Event; use App\Domains\Tenant\Models\Tenant; use Illuminate\Database\Seeder; use RuntimeException; @@ -28,6 +30,29 @@ class FiestaFutbolInfantilProductSeeder extends Seeder $this->deleteExistingCatalog($tenant); + $event = Event::query()->updateOrCreate( + [ + 'tenant_code' => $tenant->codigo, + 'name' => 'Fiesta Nacional del Fútbol Infantil', + ], + ['address' => 'Sunchales, Santa Fe'], + ); + $event->dates()->delete(); + + $eventDates = collect(['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12']) + ->mapWithKeys(function (string $date) use ($event): array { + $eventDate = $event->dates()->create([ + 'date' => $date, + 'time_start' => '00:00:00', + 'time_end' => '23:59:59', + ]); + + return [$date => $eventDate]; + }); + + $tenant->active_event_id = $event->id; + $tenant->save(); + $ticketCategory = Category::query()->firstOrCreate([ 'nombre' => 'Entradas', 'tenant_code' => $tenant->codigo, @@ -41,12 +66,14 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'tenant_code' => $tenant->codigo, ]); - $dates = ['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12']; + $dates = $eventDates->keys()->all(); $minimumUseDate = $dates[0].' 00:00:00'; $maximumUseDate = $dates[array_key_last($dates)].' 23:59:59'; $generalAdmission = $this->catalogService->create([ 'tenant_code' => $tenant->codigo, + 'event_id' => $event->id, + 'event_product_type' => EventProductType::Entry->value, 'category_id' => $ticketCategory->id, 'slug' => 'entrada-general', 'nombre' => 'Entrada General', @@ -56,13 +83,10 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'has_tickets' => true, '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], + 'event_date_id' => $eventDates->get($date)->id, ], $dates, ), @@ -81,6 +105,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder foreach ($items as $item) { $createdItems[$item['slug']] = $this->catalogService->create([ 'tenant_code' => $tenant->codigo, + 'event_id' => $event->id, + 'event_product_type' => EventProductType::Product->value, 'descripcion' => $item['descripcion'] ?? $item['nombre'], 'inventory_policy' => InventoryPolicy::Unlimited->value, 'real_stock' => 0, @@ -92,6 +118,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder $this->catalogService->create([ 'tenant_code' => $tenant->codigo, + 'event_id' => $event->id, + 'event_product_type' => EventProductType::Entry->value, 'type' => CatalogItemType::Bundle->value, 'slug' => 'entrada-general-todos-los-dias', 'nombre' => 'Entrada General - Todos los días', @@ -109,6 +137,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder $this->catalogService->create([ 'tenant_code' => $tenant->codigo, + 'event_id' => $event->id, + 'event_product_type' => EventProductType::Product->value, 'type' => CatalogItemType::Bundle->value, 'slug' => 'combo-2-panchos-2-hamburguesas', 'nombre' => 'Combo 2 Panchos + 2 Hamburguesas', diff --git a/resources/views/pdf/tickets.blade.php b/resources/views/pdf/tickets.blade.php index d1ec952..ff66e69 100644 --- a/resources/views/pdf/tickets.blade.php +++ b/resources/views/pdf/tickets.blade.php @@ -176,11 +176,11 @@
Desde
-
{{ $ticket->starts_at?->format('d/m/Y') ?? 'Sin fecha inicial' }}
+
{{ $ticket->getEffectiveStartsAt()?->format('d/m/Y') ?? 'Sin fecha inicial' }}
Hasta
-
{{ $ticket->expires_at?->format('d/m/Y') ?? 'Sin vencimiento' }}
+
{{ $ticket->getEffectiveExpiresAt()?->format('d/m/Y') ?? 'Sin vencimiento' }}
diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index 0f539a4..0c1a252 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -32,6 +32,8 @@ class CatalogSchemaTest extends TestCase $this->assertEqualsCanonicalizing([ 'id', 'tenant_code', + 'event_id', + 'event_product_type', 'category_id', 'brand_id', 'inventory_id', @@ -178,8 +180,27 @@ class CatalogSchemaTest extends TestCase public function test_variants_can_override_catalog_item_use_dates(): void { $this->assertTrue(Schema::hasColumns('variantes', [ + 'event_date_id', 'minimum_use_date', 'maximum_use_date', ])); } + + public function test_events_and_event_dates_are_linked_to_the_catalog(): void + { + $this->assertEqualsCanonicalizing([ + 'id', + 'tenant_code', + 'name', + 'address', + ], Schema::getColumnListing('events')); + $this->assertEqualsCanonicalizing([ + 'id', + 'event_id', + 'date', + 'time_start', + 'time_end', + ], Schema::getColumnListing('event_dates')); + $this->assertTrue(Schema::hasColumn('tenants', 'active_event_id')); + } } diff --git a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php index 90b7429..6f9ce42 100644 --- a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php +++ b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Seeders; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; use App\Domains\Catalog\Enums\CatalogItemType; +use App\Domains\Catalog\Enums\EventProductType; use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\ProductLayout; use App\Domains\Catalog\Models\Attribute; @@ -12,6 +13,8 @@ use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\FeaturedGroup; use App\Domains\Catalog\Models\Inventory; +use App\Domains\Event\Models\Event; +use App\Domains\Event\Models\EventDate; use App\Domains\Tenant\Models\Tenant; use Database\Seeders\AttributeSeeder; use Database\Seeders\FiestaFutbolInfantilProductSeeder; @@ -62,24 +65,34 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase $this->assertFalse(Attribute::query() ->where('tenant_codigo', $tenant->codigo) - ->whereIn('codigo', ['color', 'talle', 'talle_numerico']) ->exists()); - $this->assertSame( - ['fecha'], - Attribute::query()->where('tenant_codigo', $tenant->codigo)->pluck('codigo')->all() - ); + + $event = Event::query() + ->where('tenant_code', $tenant->codigo) + ->with('dates') + ->sole(); + + $this->assertSame($event->id, $tenant->fresh()->active_event_id); + $this->assertCount(4, $event->dates); + $this->assertSame(1, Event::query()->where('tenant_code', $tenant->codigo)->count()); + $this->assertSame(4, EventDate::query()->where('event_id', $event->id)->count()); $generalAdmission = CatalogItem::query() ->where('tenant_code', $tenant->codigo) ->where('slug', 'entrada-general') - ->with('variants.definitions') + ->with('variants.eventDate', 'itemAttributes') ->sole(); $this->assertNull($generalAdmission->inventory_id); + $this->assertTrue($generalAdmission->event->is($event)); + $this->assertSame(EventProductType::Entry, $generalAdmission->event_product_type); + $this->assertCount(0, $generalAdmission->itemAttributes); $this->assertCount(4, $generalAdmission->variants); $this->assertEqualsCanonicalizing( ['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12'], - $generalAdmission->variants->map(fn ($variant) => $variant->definitions->sole()->value)->all() + $generalAdmission->variants + ->map(fn ($variant) => $variant->eventDate->date->format('Y-m-d')) + ->all() ); $this->assertSame( [ @@ -89,10 +102,10 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase ['2026-10-12 00:00:00', '2026-10-12 23:59:59'], ], $generalAdmission->variants - ->sortBy(fn ($variant) => $variant->definitions->sole()->value) + ->sortBy(fn ($variant) => $variant->eventDate->date) ->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'), + $variant->getMinimumUseDate()->format('Y-m-d H:i:s'), + $variant->getMaximumUseDate()->format('Y-m-d H:i:s'), ]) ->values() ->all() @@ -105,6 +118,7 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase $this->assertCount(7, $standardItems); foreach ($standardItems as $standardItem) { + $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'), @@ -118,18 +132,20 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase $allDaysItem = CatalogItem::query() ->where('tenant_code', $tenant->codigo) ->where('nombre', 'Entrada General - Todos los días') - ->with('bundleComponents.variant.definitions') + ->with('bundleComponents.variant.eventDate') ->sole(); $this->assertSame(CatalogItemType::Bundle, $allDaysItem->type); $this->assertSame('40000.00', $allDaysItem->precio); $this->assertNull($allDaysItem->inventory_id); $this->assertFalse($allDaysItem->has_tickets); + $this->assertSame($event->id, $allDaysItem->event_id); + $this->assertSame(EventProductType::Entry, $allDaysItem->event_product_type); $this->assertCount(4, $allDaysItem->bundleComponents); $this->assertEqualsCanonicalizing( ['2026-10-09', '2026-10-10', '2026-10-11', '2026-10-12'], $allDaysItem->bundleComponents - ->map(fn ($component) => $component->variant->definitions->sole()->value) + ->map(fn ($component) => $component->variant->eventDate->date->format('Y-m-d')) ->all() ); @@ -142,6 +158,7 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase $this->assertSame(CatalogItemType::Bundle, $foodCombo->type); $this->assertSame('24000.00', $foodCombo->precio); $this->assertNull($foodCombo->inventory_id); + $this->assertSame(EventProductType::Product, $foodCombo->event_product_type); $this->assertSame( [ 'hamburguesa-papa-frita' => 2, diff --git a/tests/Feature/Ticket/TicketGeneratorServiceTest.php b/tests/Feature/Ticket/TicketGeneratorServiceTest.php index 0dc7d89..af886ed 100644 --- a/tests/Feature/Ticket/TicketGeneratorServiceTest.php +++ b/tests/Feature/Ticket/TicketGeneratorServiceTest.php @@ -128,7 +128,8 @@ class TicketGeneratorServiceTest extends TestCase $tickets = $this->service->generate($bundle, $this->user, 2); $this->assertCount(6, $tickets); - $this->assertCount(6, $tickets->where('source_catalog_item_id', $bundle->id)); + $this->assertCount(4, $tickets->where('source_catalog_item_id', $first->id)); + $this->assertCount(2, $tickets->where('source_catalog_item_id', $second->id)); $this->assertCount(4, $tickets->where('name', $first->nombre)); $this->assertCount(2, $tickets->where('name', $second->nombre)); } @@ -157,6 +158,8 @@ class TicketGeneratorServiceTest extends TestCase ->generate($bundle, $this->user) ->firstOrFail(); + $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)); } diff --git a/tests/Unit/Catalog/CatalogModelsTest.php b/tests/Unit/Catalog/CatalogModelsTest.php index 34486fa..10f5719 100644 --- a/tests/Unit/Catalog/CatalogModelsTest.php +++ b/tests/Unit/Catalog/CatalogModelsTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\Catalog; use App\Domains\Attachable\Models\Attachment; use App\Domains\Catalog\Enums\CatalogItemType; +use App\Domains\Catalog\Enums\EventProductType; use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Enums\ProductLayout; @@ -19,8 +20,11 @@ use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\ItemAttribute; use App\Domains\Catalog\Models\Variant; use App\Domains\Catalog\Models\VariantDefinition; +use App\Domains\Event\Models\Event; +use App\Domains\Event\Models\EventDate; use App\Domains\Shared\Enums\FieldType; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Ticket\Models\Ticket; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Carbon; use Tests\TestCase; @@ -61,6 +65,8 @@ class CatalogModelsTest extends TestCase 'category_id' => '10', 'brand_id' => '20', 'inventory_id' => '30', + 'event_id' => '40', + 'event_product_type' => EventProductType::Entry->value, 'type' => CatalogItemType::Standard->value, 'precio' => '12.50', 'inventory_policy' => InventoryPolicy::Tracked->value, @@ -72,17 +78,21 @@ class CatalogModelsTest extends TestCase $this->assertSame(10, $item->category_id); $this->assertSame(20, $item->brand_id); $this->assertSame(30, $item->inventory_id); + $this->assertSame(40, $item->event_id); + $this->assertSame(EventProductType::Entry, $item->event_product_type); $this->assertSame(CatalogItemType::Standard, $item->type); $this->assertSame('12.50', $item->precio); $this->assertSame(InventoryPolicy::Tracked, $item->inventory_policy); $this->assertTrue($item->has_tickets); $this->assertInstanceOf(Tenant::class, $item->tenant()->getRelated()); + $this->assertInstanceOf(Event::class, $item->event()->getRelated()); $this->assertInstanceOf(Category::class, $item->category()->getRelated()); $this->assertInstanceOf(Brand::class, $item->brand()->getRelated()); $this->assertInstanceOf(Inventory::class, $item->inventory()->getRelated()); $this->assertInstanceOf(BundleComponent::class, $item->bundleComponents()->getRelated()); $this->assertInstanceOf(BundleComponent::class, $item->bundleComponentUsages()->getRelated()); $this->assertInstanceOf(Variant::class, $item->variants()->getRelated()); + $this->assertInstanceOf(Ticket::class, $item->sourceTickets()->getRelated()); $this->assertInstanceOf(Attribute::class, $item->attributes()->getRelated()); $this->assertInstanceOf(ItemAttribute::class, $item->itemAttributes()->getRelated()); $this->assertInstanceOf(FeaturedItem::class, $item->featuredItems()->getRelated()); @@ -125,13 +135,20 @@ class CatalogModelsTest extends TestCase public function test_variant_has_direct_catalog_and_inventory_relations(): void { $variant = new Variant; - $variant->setRawAttributes(['catalog_item_id' => '10', 'inventory_id' => '20']); + $variant->setRawAttributes([ + 'catalog_item_id' => '10', + 'event_date_id' => '15', + 'inventory_id' => '20', + ]); $this->assertSame('variantes', $variant->getTable()); $this->assertSame(10, $variant->catalog_item_id); $this->assertSame(20, $variant->inventory_id); + $this->assertSame(15, $variant->event_date_id); $this->assertInstanceOf(CatalogItem::class, $variant->catalogItem()->getRelated()); $this->assertInstanceOf(Inventory::class, $variant->inventory()->getRelated()); + $this->assertInstanceOf(EventDate::class, $variant->eventDate()->getRelated()); + $this->assertInstanceOf(Ticket::class, $variant->sourceTickets()->getRelated()); $this->assertInstanceOf(VariantDefinition::class, $variant->definitions()->getRelated()); $this->assertInstanceOf(Attachment::class, $variant->attachments()->getRelated()); $this->assertSame('catalog_items_attachments', $variant->attachments()->getTable()); @@ -155,6 +172,28 @@ class CatalogModelsTest extends TestCase ); } + public function test_event_date_identifies_a_variant_without_catalog_attributes(): void + { + $item = new CatalogItem; + $item->nombre = 'Entrada General'; + + $eventDate = new EventDate; + $eventDate->date = '2026-10-09'; + $eventDate->time_start = '09:00:00'; + $eventDate->time_end = '18:00:00'; + + $variant = new Variant; + $variant->minimum_use_date = Carbon::parse('2026-10-09 08:00:00'); + $variant->maximum_use_date = Carbon::parse('2026-10-09 20:00:00'); + $variant->setRelation('catalogItem', $item); + $variant->setRelation('eventDate', $eventDate); + $variant->setRelation('definitions', new EloquentCollection); + + $this->assertSame('Entrada General (Fecha: 2026-10-09)', $variant->getName()); + $this->assertSame('2026-10-09 09:00:00', $variant->getMinimumUseDate()->format('Y-m-d H:i:s')); + $this->assertSame('2026-10-09 18:00:00', $variant->getMaximumUseDate()->format('Y-m-d H:i:s')); + } + public function test_inventory_maps_stock_without_a_polymorphic_owner(): void { $inventory = $this->trackedInventory(realStock: 10, reservedStock: 3); diff --git a/tests/Unit/Event/EventModelsTest.php b/tests/Unit/Event/EventModelsTest.php new file mode 100644 index 0000000..070f880 --- /dev/null +++ b/tests/Unit/Event/EventModelsTest.php @@ -0,0 +1,49 @@ +assertFalse($event->usesTimestamps()); + $this->assertInstanceOf(Tenant::class, $event->tenant()->getRelated()); + $this->assertInstanceOf(EventDate::class, $event->dates()->getRelated()); + $this->assertInstanceOf(CatalogItem::class, $event->catalogItems()->getRelated()); + } + + public function test_event_date_maps_schedule_and_variants(): void + { + $eventDate = new EventDate; + $eventDate->setRawAttributes([ + 'event_id' => '10', + 'date' => '2026-10-09', + 'time_start' => '09:00:00', + 'time_end' => '18:30:00', + ]); + + $this->assertFalse($eventDate->usesTimestamps()); + $this->assertSame(10, $eventDate->event_id); + $this->assertSame('2026-10-09 09:00:00', $eventDate->startsAt()->format('Y-m-d H:i:s')); + $this->assertSame('2026-10-09 18:30:00', $eventDate->endsAt()->format('Y-m-d H:i:s')); + $this->assertInstanceOf(Event::class, $eventDate->event()->getRelated()); + $this->assertInstanceOf(Variant::class, $eventDate->variants()->getRelated()); + } + + public function test_tenant_has_many_events_and_one_active_event(): void + { + $tenant = new Tenant; + + $this->assertInstanceOf(Event::class, $tenant->events()->getRelated()); + $this->assertInstanceOf(Event::class, $tenant->activeEvent()->getRelated()); + } +} diff --git a/tests/Unit/Ticket/TicketTest.php b/tests/Unit/Ticket/TicketTest.php index 1f4f526..763e360 100644 --- a/tests/Unit/Ticket/TicketTest.php +++ b/tests/Unit/Ticket/TicketTest.php @@ -3,6 +3,9 @@ namespace Tests\Unit\Ticket; use App\Domains\Auth\Models\User; +use App\Domains\Catalog\Models\CatalogItem; +use App\Domains\Catalog\Models\Variant; +use App\Domains\Event\Models\EventDate; use App\Domains\Tenant\Models\Tenant; use App\Domains\Ticket\Models\Ticket; use Illuminate\Support\Carbon; @@ -39,6 +42,8 @@ class TicketTest extends TestCase $this->assertSame(10, $ticket->user_id); $this->assertInstanceOf(Tenant::class, $ticket->tenant()->getRelated()); $this->assertInstanceOf(User::class, $ticket->user()->getRelated()); + $this->assertInstanceOf(CatalogItem::class, $ticket->sourceCatalogItem()->getRelated()); + $this->assertInstanceOf(Variant::class, $ticket->sourceVariant()->getRelated()); } public function test_unused_ticket_without_date_restrictions_is_valid(): void @@ -114,4 +119,30 @@ class TicketTest extends TestCase $this->assertFalse($ticket->is_expired); $this->assertTrue($ticket->is_used); } + + public function test_event_date_has_priority_over_ticket_and_variant_dates(): void + { + Carbon::setTestNow('2026-10-09 19:00:00'); + + $eventDate = new EventDate; + $eventDate->date = '2026-10-09'; + $eventDate->time_start = '09:00:00'; + $eventDate->time_end = '18:00:00'; + + $variant = new Variant; + $variant->minimum_use_date = Carbon::parse('2026-10-09 08:00:00'); + $variant->maximum_use_date = Carbon::parse('2026-10-09 22:00:00'); + $variant->setRelation('eventDate', $eventDate); + + $ticket = new Ticket([ + 'starts_at' => Carbon::parse('2026-10-09 07:00:00'), + 'expires_at' => Carbon::parse('2026-10-10 23:59:59'), + ]); + $ticket->setRelation('sourceVariant', $variant); + + $this->assertSame('2026-10-09 09:00:00', $ticket->getEffectiveStartsAt()->format('Y-m-d H:i:s')); + $this->assertSame('2026-10-09 18:00:00', $ticket->getEffectiveExpiresAt()->format('Y-m-d H:i:s')); + $this->assertFalse($ticket->isValid()); + $this->assertTrue($ticket->is_expired); + } }