From 16bc657a31b30e99ddfa51c65e85b57d38156697 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 19 Aug 2026 11:22:46 -0300 Subject: [PATCH] refactor(catalog): add ticket_label to item attributes and update related logic --- app/Domains/Catalog/Models/ItemAttribute.php | 1 + .../Resources/CatalogItemDetailResource.php | 1 + .../Catalog/Services/CatalogService.php | 14 ++++++++ .../Services/TicketPresentationResolver.php | 26 +++++++++----- ...00_add_ticket_label_to_item_attributes.php | 36 +++++++++++++++++++ .../seeders/DesfilePuraTendenciaSeeder.php | 5 +++ lang/en/api.php | 1 + lang/es/api.php | 1 + tests/Feature/Catalog/CatalogSchemaTest.php | 1 + tests/Feature/Catalog/CatalogServiceTest.php | 20 +++++++++++ .../DesfilePuraTendenciaSeederTest.php | 14 ++++++++ .../Ticket/TicketGeneratorServiceTest.php | 5 +-- 12 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 database/migrations/2026_08_19_010000_add_ticket_label_to_item_attributes.php diff --git a/app/Domains/Catalog/Models/ItemAttribute.php b/app/Domains/Catalog/Models/ItemAttribute.php index a2b6c74..6c7ff25 100644 --- a/app/Domains/Catalog/Models/ItemAttribute.php +++ b/app/Domains/Catalog/Models/ItemAttribute.php @@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'allow_multi_select', 'sort_order', 'show_in_selector', + 'ticket_label', ])] class ItemAttribute extends Model { diff --git a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php index 6f74c8a..f89422f 100644 --- a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php @@ -85,6 +85,7 @@ class CatalogItemDetailResource extends JsonResource 'nombre' => $attribute->nombre, 'sort_order' => $itemAttribute->sort_order, 'show_in_selector' => $itemAttribute->show_in_selector, + 'ticket_label' => $itemAttribute->ticket_label, 'is_required' => $attribute->is_required, 'allow_multi_select' => $itemAttribute->allow_multi_select, 'metadata_schema' => $attribute->metadata_schema, diff --git a/app/Domains/Catalog/Services/CatalogService.php b/app/Domains/Catalog/Services/CatalogService.php index 7c30cbe..5d858bf 100644 --- a/app/Domains/Catalog/Services/CatalogService.php +++ b/app/Domains/Catalog/Services/CatalogService.php @@ -38,6 +38,7 @@ class CatalogService $attributeCodes = $data['attribute_codes'] ?? []; $multiSelectAttributeCodes = $data['multi_select_attribute_codes'] ?? []; $hiddenAttributeCodes = $data['hidden_attribute_codes'] ?? []; + $ticketAttributeLabels = $data['ticket_attribute_labels'] ?? []; $components = $data['components'] ?? []; $hasDirectStock = array_key_exists('real_stock', $data); $realStock = (int) ($data['real_stock'] ?? 0); @@ -73,6 +74,14 @@ class CatalogService ]); } + if (array_diff(array_keys($ticketAttributeLabels), $attributeCodes) !== []) { + throw ValidationException::withMessages([ + 'ticket_attribute_labels' => [ + __('api.catalog.ticket_label_attribute_not_on_item'), + ], + ]); + } + $this->validateUniqueVariantCombinations($variants, $attributeCodes); if ($type === CatalogItemType::Bundle) { @@ -98,6 +107,7 @@ class CatalogService $data['attribute_codes'], $data['multi_select_attribute_codes'], $data['hidden_attribute_codes'], + $data['ticket_attribute_labels'], $data['components'], $data['real_stock'], $data['reserved_stock'], @@ -124,6 +134,7 @@ class CatalogService $attributeCodes, $multiSelectAttributeCodes, $hiddenAttributeCodes, + $ticketAttributeLabels, ) : []; @@ -461,6 +472,7 @@ class CatalogService * @param array $attributeCodes * @param array $multiSelectAttributeCodes * @param array $hiddenAttributeCodes + * @param array $ticketAttributeLabels * @return array */ private function createItemAttributes( @@ -468,6 +480,7 @@ class CatalogService array $attributeCodes, array $multiSelectAttributeCodes = [], array $hiddenAttributeCodes = [], + array $ticketAttributeLabels = [], ): array { $itemAttributes = []; $attributeCodes = array_values(array_unique($attributeCodes)); @@ -492,6 +505,7 @@ class CatalogService 'attribute_id' => $attribute->id, 'allow_multi_select' => in_array($attributeCode, $multiSelectAttributeCodes, true), 'show_in_selector' => ! in_array($attributeCode, $hiddenAttributeCodes, true), + 'ticket_label' => $ticketAttributeLabels[$attributeCode] ?? null, ]); $itemAttributes[$attributeCode] = $itemAttribute; diff --git a/app/Domains/Ticket/Services/TicketPresentationResolver.php b/app/Domains/Ticket/Services/TicketPresentationResolver.php index ec6c85e..19de892 100644 --- a/app/Domains/Ticket/Services/TicketPresentationResolver.php +++ b/app/Domains/Ticket/Services/TicketPresentationResolver.php @@ -29,19 +29,27 @@ class TicketPresentationResolver return $catalogItem->nombre; } - $properties = $variant->selectionOptions() - ->flatMap(function (array $option): array { - if (array_is_list($option)) { - return collect($option) - ->pluck('label') - ->filter(fn ($label): bool => is_string($label) && $label !== '') - ->all(); + $itemAttributes = $variant->catalogItem->itemAttributes; + $properties = $variant->selectionOptions($itemAttributes) + ->map(function (array $option, string $attributeCode) use ($itemAttributes): ?string { + $labels = collect(array_is_list($option) ? $option : [$option]) + ->pluck('label') + ->filter(fn ($label): bool => is_string($label) && $label !== '') + ->implode(', '); + + if ($labels === '') { + return null; } - $label = $option['label'] ?? null; + $ticketLabel = $itemAttributes->first( + fn ($itemAttribute): bool => $itemAttribute->attribute?->codigo === $attributeCode, + )?->ticket_label; - return is_string($label) && $label !== '' ? [$label] : []; + return is_string($ticketLabel) && trim($ticketLabel) !== '' + ? trim($ticketLabel).' '.$labels + : $labels; }) + ->filter() ->values(); return $properties->isEmpty() diff --git a/database/migrations/2026_08_19_010000_add_ticket_label_to_item_attributes.php b/database/migrations/2026_08_19_010000_add_ticket_label_to_item_attributes.php new file mode 100644 index 0000000..68fdba3 --- /dev/null +++ b/database/migrations/2026_08_19_010000_add_ticket_label_to_item_attributes.php @@ -0,0 +1,36 @@ +string('ticket_label', 100)->nullable()->after('show_in_selector'); + }); + + foreach (['sector' => 'Lado', 'fila' => 'Fila', 'asiento' => 'Asiento'] as $code => $label) { + $itemAttributeIds = DB::table('item_attributes') + ->join('catalog_items', 'catalog_items.id', '=', 'item_attributes.catalog_item_id') + ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id') + ->where('catalog_items.tenant_code', 'desfile_pura_tendencia') + ->where('attribute.codigo', $code) + ->pluck('item_attributes.id'); + + DB::table('item_attributes') + ->whereIn('id', $itemAttributeIds) + ->update(['ticket_label' => $label]); + } + } + + public function down(): void + { + Schema::table('item_attributes', function (Blueprint $table): void { + $table->dropColumn('ticket_label'); + }); + } +}; diff --git a/database/seeders/DesfilePuraTendenciaSeeder.php b/database/seeders/DesfilePuraTendenciaSeeder.php index cd1dac1..0357067 100644 --- a/database/seeders/DesfilePuraTendenciaSeeder.php +++ b/database/seeders/DesfilePuraTendenciaSeeder.php @@ -170,6 +170,11 @@ class DesfilePuraTendenciaSeeder extends Seeder 'inventory_subject' => InventorySubject::Seat->value, 'has_tickets' => true, 'attribute_codes' => array_keys($attributes), + 'ticket_attribute_labels' => [ + 'sector' => 'Lado', + 'fila' => 'Fila', + 'asiento' => 'Asiento', + ], 'variants' => $this->entryVariants(), 'images' => [ $this->uploadedImage( diff --git a/lang/en/api.php b/lang/en/api.php index e632e90..fb1dbcf 100644 --- a/lang/en/api.php +++ b/lang/en/api.php @@ -112,6 +112,7 @@ return [ 'event_date_attribute_required' => 'The event_date attribute is required for event date variants.', 'multi_select_attribute_not_on_item' => 'Multi-select attributes must also be present in attribute_codes.', 'hidden_attribute_not_on_item' => 'Hidden attributes must also be present in attribute_codes.', + 'ticket_label_attribute_not_on_item' => 'Ticket-label attributes must also be present in attribute_codes.', 'event_date_selection_required' => 'At least one event date must be selected.', 'single_event_date_required' => 'Exactly one event date must be selected.', 'event_date_wrong_tenant' => 'Every event date must belong to the catalog item tenant.', diff --git a/lang/es/api.php b/lang/es/api.php index c968c0d..f6ecbe7 100644 --- a/lang/es/api.php +++ b/lang/es/api.php @@ -112,6 +112,7 @@ return [ 'event_date_attribute_required' => 'El atributo event_date es obligatorio para las variantes con fecha de evento.', 'multi_select_attribute_not_on_item' => 'Los atributos multiselección también deben estar incluidos en attribute_codes.', 'hidden_attribute_not_on_item' => 'Los atributos ocultos también deben estar incluidos en attribute_codes.', + 'ticket_label_attribute_not_on_item' => 'Los atributos con etiqueta de ticket también deben estar incluidos en attribute_codes.', 'event_date_selection_required' => 'Debe seleccionar al menos una fecha de evento.', 'single_event_date_required' => 'Debe seleccionar exactamente una fecha de evento.', 'event_date_wrong_tenant' => 'Todas las fechas del evento deben pertenecer al tenant del ítem de catálogo.', diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index ad89e39..705bfc7 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -26,6 +26,7 @@ class CatalogSchemaTest extends TestCase $this->assertTrue(Schema::hasTable('item_attributes')); $this->assertTrue(Schema::hasColumn('item_attributes', 'sort_order')); $this->assertTrue(Schema::hasColumn('item_attributes', 'show_in_selector')); + $this->assertTrue(Schema::hasColumn('item_attributes', 'ticket_label')); $this->assertTrue(Schema::hasTable('variant_values')); } diff --git a/tests/Feature/Catalog/CatalogServiceTest.php b/tests/Feature/Catalog/CatalogServiceTest.php index cb4e8ec..89a8b50 100644 --- a/tests/Feature/Catalog/CatalogServiceTest.php +++ b/tests/Feature/Catalog/CatalogServiceTest.php @@ -196,6 +196,26 @@ class CatalogServiceTest extends TestCase $this->assertFalse($item->itemAttributes->sole()->show_in_selector); } + public function test_it_can_configure_an_attribute_label_for_ticket_names(): void + { + $attribute = $this->createAttribute('sector'); + + $item = $this->service->create([ + 'tenant_code' => $this->tenant->codigo, + 'slug' => 'ticket-labeled-attribute-item', + 'nombre' => 'Ticket labeled attribute item', + 'precio' => 100, + 'attribute_codes' => [$attribute->codigo], + 'ticket_attribute_labels' => [$attribute->codigo => 'Lado'], + 'variants' => [[ + 'real_stock' => 5, + 'values' => [$attribute->codigo => 'A'], + ]], + ]); + + $this->assertSame('Lado', $item->itemAttributes->sole()->ticket_label); + } + public function test_it_allows_the_same_event_date_with_different_attribute_values(): void { $sector = $this->createAttribute('sector'); diff --git a/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php b/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php index e92b85c..64159d1 100644 --- a/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php +++ b/tests/Feature/Seeders/DesfilePuraTendenciaSeederTest.php @@ -191,6 +191,20 @@ class DesfilePuraTendenciaSeederTest extends TestCase ->pluck('attribute.codigo') ->all(), ); + $this->assertSame( + [ + 'tipo' => null, + 'sector' => 'Lado', + 'fila' => 'Fila', + 'asiento' => 'Asiento', + ], + DB::table('item_attributes') + ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id') + ->where('item_attributes.catalog_item_id', $catalogItem->id) + ->orderBy('item_attributes.sort_order') + ->pluck('item_attributes.ticket_label', 'attribute.codigo') + ->all(), + ); $variants = DB::table('variantes')->where('catalog_item_id', $catalogItem->id); diff --git a/tests/Feature/Ticket/TicketGeneratorServiceTest.php b/tests/Feature/Ticket/TicketGeneratorServiceTest.php index db34009..a8c937e 100644 --- a/tests/Feature/Ticket/TicketGeneratorServiceTest.php +++ b/tests/Feature/Ticket/TicketGeneratorServiceTest.php @@ -125,6 +125,7 @@ class TicketGeneratorServiceTest extends TestCase $itemSize = $item->itemAttributes()->create([ 'attribute_id' => $size->id, 'sort_order' => 2, + 'ticket_label' => 'Talle', ]); $variant = $item->variants()->create([ 'inventory_id' => Inventory::query()->create()->id, @@ -142,7 +143,7 @@ class TicketGeneratorServiceTest extends TestCase ->generate($item, $this->user, 1, $variant->id) ->sole(); - $this->assertSame('Shirt (Negro, XL)', $ticket->name); + $this->assertSame('Shirt (Negro, Talle XL)', $ticket->name); $this->assertSame('Descripción de la variante', $ticket->description); $item->update([ @@ -153,7 +154,7 @@ class TicketGeneratorServiceTest extends TestCase $variant->update(['descripcion' => 'Descripción actualizada de la variante']); $ticket = $ticket->fresh(); - $this->assertSame('Remera (Azul, XL)', $ticket->name); + $this->assertSame('Remera (Azul, Talle XL)', $ticket->name); $this->assertSame('Descripción actualizada de la variante', $ticket->description); }