assertSame([ 'paginated', 'simple', 'simple_vertical', 'carousel', ], GroupLayout::values()); } public function test_attribute_maps_its_values_and_relations(): void { $attribute = new Attribute; $attribute->setRawAttributes([ 'is_required' => 1, 'metadata_schema' => '{"swatch":true}', 'type' => FieldType::Select->value, ]); $this->assertSame('attribute', $attribute->getTable()); $this->assertTrue($attribute->is_required); $this->assertSame(['swatch' => true], $attribute->metadata_schema); $this->assertSame(FieldType::Select, $attribute->type); $this->assertInstanceOf(Tenant::class, $attribute->tenant()->getRelated()); $this->assertInstanceOf(AttributeOption::class, $attribute->options()->getRelated()); } public function test_event_date_is_an_attribute_type_with_dynamic_options(): void { $attribute = new Attribute; $attribute->type = FieldType::EventDate->value; $this->assertSame(FieldType::EventDate, $attribute->type); $this->assertTrue($attribute->type->supportsOptions()); $this->assertTrue($attribute->type->usesDynamicOptions()); $this->assertContains('event_date', FieldType::values()); $this->assertInstanceOf(EventDate::class, $attribute->eventDates()->getRelated()); } public function test_catalog_item_is_the_catalog_root(): void { $item = new CatalogItem; $item->setRawAttributes([ 'category_id' => '10', 'brand_id' => '20', 'inventory_id' => '30', 'event_product_type' => EventProductType::Entry->value, 'type' => CatalogItemType::Standard->value, 'precio' => '12.50', 'inventory_policy' => InventoryPolicy::Tracked->value, 'has_tickets' => 1, ]); $this->assertSame('catalog_items', $item->getTable()); $this->assertFalse($item->usesTimestamps()); $this->assertSame(10, $item->category_id); $this->assertSame(20, $item->brand_id); $this->assertSame(30, $item->inventory_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(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()); $this->assertInstanceOf(Attachment::class, $item->attachments()->getRelated()); $this->assertSame('catalog_items_attachments', $item->attachments()->getTable()); } public function test_featured_models_map_catalog_relations_and_layout(): void { $group = new FeaturedGroup; $group->setRawAttributes([ 'source_type' => FeaturedGroupSource::Category->value, 'category_id' => '4', 'product_layout' => ProductLayout::ColumnWithImage->value, 'group_layout' => GroupLayout::SimpleVertical->value, 'group_order' => '2', ]); $featuredItem = new FeaturedItem; $featuredItem->setRawAttributes([ 'featured_group_id' => '10', 'catalog_item_id' => '20', 'order' => '3', ]); $this->assertSame('featured_groups', $group->getTable()); $this->assertFalse($group->usesTimestamps()); $this->assertSame(ProductLayout::ColumnWithImage, $group->product_layout); $this->assertSame(FeaturedGroupSource::Category, $group->source_type); $this->assertSame(4, $group->category_id); $this->assertSame(GroupLayout::SimpleVertical, $group->group_layout); $this->assertSame(2, $group->group_order); $this->assertInstanceOf(Tenant::class, $group->tenant()->getRelated()); $this->assertInstanceOf(Category::class, $group->category()->getRelated()); $this->assertInstanceOf(FeaturedItem::class, $group->featuredItems()->getRelated()); $this->assertSame('featured_items', $featuredItem->getTable()); $this->assertFalse($featuredItem->usesTimestamps()); $this->assertSame(10, $featuredItem->featured_group_id); $this->assertSame(20, $featuredItem->catalog_item_id); $this->assertSame(3, $featuredItem->order); $this->assertInstanceOf(FeaturedGroup::class, $featuredItem->featuredGroup()->getRelated()); $this->assertInstanceOf(CatalogItem::class, $featuredItem->catalogItem()->getRelated()); } public function test_variant_has_direct_catalog_and_inventory_relations(): void { $variant = new Variant; $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()); } 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->setRelation('catalogItem', $item); $variant->setRelation('eventDate', $eventDate); $variant->setRelation('definitions', new EloquentCollection); $this->assertSame('Entrada General', $variant->getName()); $this->assertSame('2026-10-09', $variant->eventDate->date->format('Y-m-d')); } public function test_variant_exposes_selection_values_with_api_labels(): void { $attribute = new Attribute; $attribute->codigo = 'size'; $attribute->setRelation('options', new EloquentCollection([ new AttributeOption(['value' => 'M', 'label' => 'Medium']), ])); $itemAttribute = new ItemAttribute; $itemAttribute->allow_multi_select = false; $itemAttribute->setRelation('attribute', $attribute); $definition = new VariantDefinition(['value' => 'M']); $definition->item_attribute_id = 1; $definition->setRelation('itemAttribute', $itemAttribute); $eventDate = new EventDate([ 'date' => '2026-10-09', 'time_start' => '09:00:00', 'time_end' => '18:00:00', ]); $eventDate->id = 20; $variant = new Variant; $variant->setRelation('definitions', new EloquentCollection([$definition])); $variant->setRelation('eventDates', new EloquentCollection([$eventDate])); $this->assertSame( ['value' => 'M', 'label' => 'Medium'], $variant->selectionOptions()->get('size'), ); $this->assertSame( ['value' => '20', 'label' => '09/10/2026'], $variant->selectionOptions()->get('event_date'), ); } public function test_variant_orders_selection_options_by_item_order_and_attribute_label(): void { $definitions = collect([ ['id' => 1, 'code' => 'zeta', 'label' => 'Zeta', 'sort_order' => 2], ['id' => 2, 'code' => 'priority', 'label' => 'Priority', 'sort_order' => 1], ['id' => 3, 'code' => 'alpha', 'label' => 'Alpha', 'sort_order' => 2], ])->map(function (array $data): VariantDefinition { $attribute = new Attribute([ 'codigo' => $data['code'], 'nombre' => $data['label'], ]); $attribute->setRelation('options', new EloquentCollection); $itemAttribute = new ItemAttribute([ 'sort_order' => $data['sort_order'], 'allow_multi_select' => false, ]); $itemAttribute->id = $data['id']; $itemAttribute->setRelation('attribute', $attribute); $definition = new VariantDefinition(['value' => $data['code']]); $definition->item_attribute_id = $itemAttribute->id; $definition->setRelation('itemAttribute', $itemAttribute); return $definition; }); $variant = new Variant; $variant->setRelation('definitions', new EloquentCollection($definitions)); $variant->setRelation('eventDates', new EloquentCollection); $this->assertSame( ['priority', 'alpha', 'zeta'], $variant->selectionOptions()->keys()->all(), ); } public function test_inventory_maps_stock_without_a_polymorphic_owner(): void { $inventory = $this->trackedInventory(realStock: 10, reservedStock: 3); $inventory->sold_units = '2'; $this->assertSame('inventories', $inventory->getTable()); $this->assertFalse($inventory->usesTimestamps()); $this->assertSame(2, $inventory->sold_units); $this->assertSame(7, $inventory->availableStock()); $this->assertInstanceOf(CatalogItem::class, $inventory->catalogItem()->getRelated()); $this->assertInstanceOf(Variant::class, $inventory->variant()->getRelated()); } public function test_catalog_item_aggregates_variant_inventory(): void { $first = (new Variant)->setRelation('inventory', $this->trackedInventory(10, 3)); $second = (new Variant)->setRelation('inventory', $this->trackedInventory(5, 1)); $item = new CatalogItem; $item->inventory_policy = InventoryPolicy::Tracked; $item->setRelation('variants', new EloquentCollection([$first, $second])); $item->setRelation('inventory', null); $this->assertSame(11, $item->availableStock()); $this->assertTrue($item->isAvailable()); } public function test_catalog_item_only_exposes_available_tracked_variants(): void { $unavailable = (new Variant)->setRelation('inventory', $this->trackedInventory(3, 3)); $available = (new Variant)->setRelation('inventory', $this->trackedInventory(5, 2)); $unavailable->id = 10; $available->id = 20; $item = new CatalogItem; $item->inventory_policy = InventoryPolicy::Tracked; $item->setRelation('variants', new EloquentCollection([$unavailable, $available])); $this->assertSame([$available], $item->visibleVariants()->all()); $this->assertSame( [$unavailable, $available], $item->visibleVariants($unavailable->id)->all(), ); $item->inventory_policy = InventoryPolicy::Unlimited; $this->assertSame([$unavailable, $available], $item->visibleVariants()->all()); } public function test_catalog_item_prioritizes_its_inventory_over_variants(): void { $item = new CatalogItem; $item->inventory_policy = InventoryPolicy::Tracked; $item->setRelation('variants', new EloquentCollection([ (new Variant)->setRelation('inventory', $this->trackedInventory(100, 0)), ])); $item->setRelation('inventory', $this->trackedInventory(8, 2)); $this->assertSame(6, $item->availableStock()); $this->assertTrue($item->isAvailable()); } public function test_item_attribute_and_variant_definition_use_catalog_keys(): void { $itemAttribute = new ItemAttribute; $definition = new VariantDefinition; $this->assertSame('item_attributes', $itemAttribute->getTable()); $this->assertInstanceOf(CatalogItem::class, $itemAttribute->catalogItem()->getRelated()); $this->assertInstanceOf(Attribute::class, $itemAttribute->attribute()->getRelated()); $this->assertInstanceOf(VariantDefinition::class, $itemAttribute->variantDefinitions()->getRelated()); $this->assertSame('variant_values', $definition->getTable()); $this->assertInstanceOf(Variant::class, $definition->variant()->getRelated()); $this->assertInstanceOf(ItemAttribute::class, $definition->itemAttribute()->getRelated()); } private function trackedInventory(int $realStock, int $reservedStock): Inventory { $inventory = new Inventory; $inventory->setRawAttributes([ 'real_stock' => $realStock, 'reserved_stock' => $reservedStock, ]); return $inventory; } }