From 63f98b5da28191406995869aa4fbd7cd5bd6471b Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 7 Aug 2026 15:05:50 -0300 Subject: [PATCH] feat(merchandise): implement MerchandiseController, MerchandiseService, and UpsertMerchandiseRequest for managing merchandise items and variants --- .../Controllers/MerchandiseController.php | 27 ++ .../Requests/UpsertMerchandiseRequest.php | 46 ++ .../Resources/MerchandiseResource.php | 53 +++ .../Services/MerchandiseService.php | 412 ++++++++++++++++++ .../FiestaFutbolInfantil/routes/api.php | 4 + .../MerchandiseControllerTest.php | 301 +++++++++++++ 6 files changed, 843 insertions(+) create mode 100644 app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php create mode 100644 app/Domains/FiestaFutbolInfantil/Requests/UpsertMerchandiseRequest.php create mode 100644 app/Domains/FiestaFutbolInfantil/Resources/MerchandiseResource.php create mode 100644 app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php create mode 100644 tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php b/app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php new file mode 100644 index 0000000..0ad55d2 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Controllers/MerchandiseController.php @@ -0,0 +1,27 @@ +user()->tenant()->firstOrFail(); + $items = $this->merchandiseService->upsertMany( + $tenant, + $request->validated('items'), + ); + + return MerchandiseResource::collection($items) + ->response() + ->setStatusCode(200); + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Requests/UpsertMerchandiseRequest.php b/app/Domains/FiestaFutbolInfantil/Requests/UpsertMerchandiseRequest.php new file mode 100644 index 0000000..77bfeab --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Requests/UpsertMerchandiseRequest.php @@ -0,0 +1,46 @@ + */ + public function rules(): array + { + $tenantCode = $this->user()?->tenant_codigo; + + return [ + 'items' => ['required', 'array', 'min:1', 'max:100'], + 'items.*' => ['required', 'array:id,title,description,max_units_per_user,variants'], + 'items.*.id' => [ + 'sometimes', + 'nullable', + 'integer', + 'distinct', + Rule::exists('catalog_items', 'id')->where( + fn ($query) => $query + ->where('tenant_code', $tenantCode) + ->where('event_product_type', 'producto') + ), + ], + 'items.*.title' => ['required', 'string', 'max:255'], + 'items.*.description' => ['sometimes', 'nullable', 'string'], + 'items.*.max_units_per_user' => ['required', 'integer', 'min:1'], + 'items.*.variants' => ['required', 'array', 'min:1', 'max:500'], + 'items.*.variants.*' => ['required', 'array:id,color,size,stock,price'], + 'items.*.variants.*.id' => ['sometimes', 'nullable', 'integer', 'distinct'], + 'items.*.variants.*.color' => ['required', 'string', 'max:255'], + 'items.*.variants.*.size' => ['required', 'string', 'max:255'], + 'items.*.variants.*.stock' => ['required', 'integer', 'min:0'], + 'items.*.variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'], + ]; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Resources/MerchandiseResource.php b/app/Domains/FiestaFutbolInfantil/Resources/MerchandiseResource.php new file mode 100644 index 0000000..68f1c9e --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Resources/MerchandiseResource.php @@ -0,0 +1,53 @@ + */ + public function toArray(Request $request): array + { + $itemAttributes = $this->itemAttributes->keyBy( + fn ($itemAttribute) => $itemAttribute->attribute?->codigo + ); + $colorAttribute = $itemAttributes->get('color'); + $sizeAttribute = $itemAttributes->get('talle'); + $colorOptions = $colorAttribute?->attribute?->options?->keyBy('value') ?? collect(); + $sizeOptions = $sizeAttribute?->attribute?->options?->keyBy('value') ?? collect(); + + return [ + 'id' => $this->id, + 'title' => $this->nombre, + 'description' => $this->descripcion, + 'max_units_per_user' => $this->max_units_per_user, + 'variants' => $this->variants->map(function ($variant) use ( + $colorAttribute, + $sizeAttribute, + $colorOptions, + $sizeOptions, + ): array { + $colorValue = $variant->definitions + ->firstWhere('item_attribute_id', $colorAttribute?->id) + ?->value; + $sizeValue = $variant->definitions + ->firstWhere('item_attribute_id', $sizeAttribute?->id) + ?->value; + + return [ + 'id' => $variant->id, + 'color' => $colorOptions->get($colorValue)?->label ?? $colorValue, + 'color_value' => $colorValue, + 'size' => $sizeOptions->get($sizeValue)?->label ?? $sizeValue, + 'size_value' => $sizeValue, + 'stock' => $variant->inventory->real_stock, + 'price' => number_format($variant->getPrice(), 2, '.', ''), + ]; + })->values(), + ]; + } +} diff --git a/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php b/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php new file mode 100644 index 0000000..2a76944 --- /dev/null +++ b/app/Domains/FiestaFutbolInfantil/Services/MerchandiseService.php @@ -0,0 +1,412 @@ +> $items + * @return Collection + */ + public function upsertMany(Tenant $tenant, array $items): Collection + { + return DB::transaction(function () use ($tenant, $items): Collection { + $attributes = $this->attributes($tenant); + $category = Category::query()->firstOrCreate([ + 'tenant_code' => $tenant->codigo, + 'nombre' => 'Merchandising', + ]); + $reservedSlugs = []; + + return collect($items)->map(function (array $data, int $index) use ( + $tenant, + $attributes, + $category, + &$reservedSlugs, + ): CatalogItem { + $item = isset($data['id']) + ? $this->existingItem($tenant, $category, (int) $data['id'], $index) + : $this->createItem($tenant, $category, $data, $reservedSlugs); + + if (! isset($data['id'])) { + $reservedSlugs[] = $item->slug; + } + + $item->update([ + 'nombre' => trim($data['title']), + 'descripcion' => $data['description'] ?? null, + 'category_id' => $category->id, + 'max_units_per_user' => (int) $data['max_units_per_user'], + 'event_product_type' => EventProductType::Product->value, + 'inventory_policy' => InventoryPolicy::Tracked->value, + 'has_tickets' => false, + ]); + + $itemAttributes = $this->itemAttributes($item, $attributes); + $existingVariants = $item->variants() + ->with(['inventory', 'definitions']) + ->lockForUpdate() + ->get(); + $variants = $this->resolveVariants($data['variants'], $attributes, $index); + + $this->validateCombinations($variants, $existingVariants, $itemAttributes, $index); + + foreach ($variants as $variantIndex => $variantData) { + $variant = isset($variantData['id']) + ? $existingVariants->firstWhere('id', (int) $variantData['id']) + : null; + + if (isset($variantData['id']) && $variant === null) { + throw ValidationException::withMessages([ + "items.{$index}.variants.{$variantIndex}.id" => [ + 'La variante no pertenece al artículo de merchandising.', + ], + ]); + } + + if ($variant === null) { + $this->createVariant($item, $itemAttributes, $variantData); + } else { + $this->updateVariant($variant, $itemAttributes, $variantData, $index, $variantIndex); + } + } + + $minimumPrice = $item->variants()->min('precio'); + if ($minimumPrice !== null) { + $item->update(['precio' => $minimumPrice]); + } + + return $item->fresh()->load([ + 'itemAttributes.attribute.options', + 'variants.catalogItem', + 'variants.inventory', + 'variants.definitions', + ]); + })->values(); + }); + } + + /** @return Collection */ + private function attributes(Tenant $tenant): Collection + { + $attributes = Attribute::query() + ->where('tenant_codigo', $tenant->codigo) + ->whereIn('codigo', self::ATTRIBUTE_CODES) + ->with('options') + ->lockForUpdate() + ->get() + ->keyBy('codigo'); + + $missingCodes = collect(self::ATTRIBUTE_CODES)->diff($attributes->keys()); + if ($missingCodes->isNotEmpty()) { + throw ValidationException::withMessages([ + 'items' => [ + 'Faltan atributos requeridos para merchandising: '.$missingCodes->implode(', ').'.', + ], + ]); + } + + return $attributes; + } + + private function existingItem( + Tenant $tenant, + Category $category, + int $itemId, + int $index, + ): CatalogItem { + $item = CatalogItem::query() + ->whereKey($itemId) + ->where('tenant_code', $tenant->codigo) + ->where('category_id', $category->id) + ->where('event_product_type', EventProductType::Product->value) + ->lockForUpdate() + ->first(); + + if ($item === null) { + throw ValidationException::withMessages([ + "items.{$index}.id" => ['El artículo no pertenece al merchandising del tenant.'], + ]); + } + + return $item; + } + + /** + * @param array $data + * @param array $reservedSlugs + */ + private function createItem( + Tenant $tenant, + Category $category, + array $data, + array $reservedSlugs, + ): CatalogItem { + return CatalogItem::query()->create([ + 'tenant_code' => $tenant->codigo, + 'slug' => $this->uniqueSlug($tenant, $data['title'], $reservedSlugs), + 'nombre' => trim($data['title']), + 'descripcion' => $data['description'] ?? null, + 'category_id' => $category->id, + 'precio' => collect($data['variants'])->min('price') ?? 0, + 'max_units_per_user' => (int) $data['max_units_per_user'], + 'event_product_type' => EventProductType::Product->value, + 'inventory_policy' => InventoryPolicy::Tracked->value, + 'has_tickets' => false, + 'inventory_id' => null, + ]); + } + + /** + * @param Collection $attributes + * @return Collection + */ + private function itemAttributes(CatalogItem $item, Collection $attributes): Collection + { + return $attributes->mapWithKeys(function (Attribute $attribute, string $code) use ($item): array { + $itemAttribute = $item->itemAttributes()->firstOrCreate( + ['attribute_id' => $attribute->id], + ['allow_multi_select' => false], + ); + + if ($itemAttribute->allow_multi_select) { + $itemAttribute->update(['allow_multi_select' => false]); + } + + return [$code => $itemAttribute]; + }); + } + + /** + * @param array> $variants + * @param Collection $attributes + * @return array> + */ + private function resolveVariants(array $variants, Collection $attributes, int $itemIndex): array + { + return collect($variants)->map(function (array $variant, int $variantIndex) use ( + $attributes, + $itemIndex, + ): array { + $color = $this->resolveColor($attributes['color'], $variant['color']); + $size = $this->existingOption( + $attributes['talle'], + $variant['size'], + "items.{$itemIndex}.variants.{$variantIndex}.size", + ); + + return [ + ...$variant, + 'color' => $color->value, + 'size' => $size->value, + 'stock' => (int) $variant['stock'], + ]; + })->all(); + } + + private function resolveColor(Attribute $attribute, string $color): AttributeOption + { + $option = $this->findOption($attribute, $color); + if ($option !== null) { + return $option; + } + + $label = trim($color); + $option = $attribute->options()->create([ + 'value' => $this->valueCode($label), + 'label' => $label, + 'sort_order' => ((int) $attribute->options->max('sort_order')) + 1, + ]); + $attribute->options->push($option); + + return $option; + } + + private function existingOption( + Attribute $attribute, + string $value, + string $validationKey, + ): AttributeOption { + $option = $this->findOption($attribute, $value); + + if ($option === null) { + throw ValidationException::withMessages([ + $validationKey => ["El valor seleccionado no es válido para {$attribute->nombre}."], + ]); + } + + return $option; + } + + private function findOption(Attribute $attribute, string $value): ?AttributeOption + { + $key = $this->optionKey($value); + + return $attribute->options->first( + fn (AttributeOption $option): bool => $this->optionKey($option->value) === $key + || $this->optionKey($option->label) === $key + ); + } + + /** + * @param array> $incoming + * @param Collection $existing + * @param Collection $itemAttributes + */ + private function validateCombinations( + array $incoming, + Collection $existing, + Collection $itemAttributes, + int $itemIndex, + ): void { + $incomingIds = collect($incoming)->pluck('id')->filter()->map(fn ($id): int => (int) $id); + $seen = []; + + foreach ($existing->whereNotIn('id', $incomingIds) as $variant) { + $values = $variant->definitions->keyBy('item_attribute_id'); + $color = $values->get($itemAttributes['color']->id)?->value; + $size = $values->get($itemAttributes['talle']->id)?->value; + + if ($color !== null && $size !== null) { + $seen[$this->combinationKey($color, $size)] = true; + } + } + + foreach ($incoming as $variantIndex => $variant) { + $key = $this->combinationKey($variant['color'], $variant['size']); + + if (isset($seen[$key])) { + throw ValidationException::withMessages([ + "items.{$itemIndex}.variants.{$variantIndex}" => [ + 'La combinación de color y talle ya existe para el artículo.', + ], + ]); + } + + $seen[$key] = true; + } + } + + /** + * @param Collection $itemAttributes + * @param array $data + */ + private function createVariant( + CatalogItem $item, + Collection $itemAttributes, + array $data, + ): void { + $inventory = Inventory::query()->create(['real_stock' => $data['stock']]); + $variant = $item->variants()->create([ + 'inventory_id' => $inventory->id, + 'precio' => $data['price'], + ]); + $this->syncDefinitions($variant, $itemAttributes, $data); + } + + /** + * @param Collection $itemAttributes + * @param array $data + */ + private function updateVariant( + Variant $variant, + Collection $itemAttributes, + array $data, + int $itemIndex, + int $variantIndex, + ): void { + $inventory = Inventory::query() + ->whereKey($variant->inventory_id) + ->lockForUpdate() + ->firstOrFail(); + + if ($data['stock'] < $inventory->reserved_stock) { + throw ValidationException::withMessages([ + "items.{$itemIndex}.variants.{$variantIndex}.stock" => [ + 'El stock no puede ser menor que la cantidad actualmente reservada.', + ], + ]); + } + + $variant->update(['precio' => $data['price']]); + $inventory->update(['real_stock' => $data['stock']]); + $this->syncDefinitions($variant, $itemAttributes, $data); + } + + /** + * @param Collection $itemAttributes + * @param array $data + */ + private function syncDefinitions( + Variant $variant, + Collection $itemAttributes, + array $data, + ): void { + $variant->definitions() + ->whereIn('item_attribute_id', $itemAttributes->pluck('id')) + ->delete(); + $variant->definitions()->createMany([ + [ + 'item_attribute_id' => $itemAttributes['color']->id, + 'value' => $data['color'], + ], + [ + 'item_attribute_id' => $itemAttributes['talle']->id, + 'value' => $data['size'], + ], + ]); + } + + /** @param array $reservedSlugs */ + private function uniqueSlug(Tenant $tenant, string $title, array $reservedSlugs): string + { + $baseSlug = Str::slug($title) ?: 'merchandising'; + $slug = $baseSlug; + $suffix = 2; + + while ( + in_array($slug, $reservedSlugs, true) + || CatalogItem::query() + ->where('tenant_code', $tenant->codigo) + ->where('slug', $slug) + ->exists() + ) { + $slug = "{$baseSlug}-{$suffix}"; + $suffix++; + } + + return $slug; + } + + private function combinationKey(string $color, string $size): string + { + return $this->optionKey($color).'|'.$this->optionKey($size); + } + + private function optionKey(string $value): string + { + return Str::ascii(mb_strtolower((string) preg_replace('/[_\s]+/u', ' ', trim($value)))); + } + + private function valueCode(string $value): string + { + return mb_strtolower((string) preg_replace('/\s+/u', '_', trim($value))); + } +} diff --git a/app/Domains/FiestaFutbolInfantil/routes/api.php b/app/Domains/FiestaFutbolInfantil/routes/api.php index 48209ff..8a9c9d6 100644 --- a/app/Domains/FiestaFutbolInfantil/routes/api.php +++ b/app/Domains/FiestaFutbolInfantil/routes/api.php @@ -3,6 +3,7 @@ use App\Domains\FiestaFutbolInfantil\Controllers\AccommodationController; use App\Domains\FiestaFutbolInfantil\Controllers\EntryController; use App\Domains\FiestaFutbolInfantil\Controllers\FoodController; +use App\Domains\FiestaFutbolInfantil\Controllers\MerchandiseController; use Illuminate\Support\Facades\Route; Route::prefix('v1/adminapp/tenant') @@ -17,4 +18,7 @@ Route::prefix('v1/adminapp/tenant') Route::post('accommodations', [AccommodationController::class, 'store']) ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos') ->name('adminapp.fiesta-futbol-infantil.accommodations.store'); + Route::post('merchandise', [MerchandiseController::class, 'store']) + ->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising') + ->name('adminapp.fiesta-futbol-infantil.merchandise.store'); }); diff --git a/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php b/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php new file mode 100644 index 0000000..e33a395 --- /dev/null +++ b/tests/Feature/FiestaFutbolInfantil/MerchandiseControllerTest.php @@ -0,0 +1,301 @@ +seed(AuthorizationSeeder::class); + WebsiteType::query()->create([ + 'codigo' => 'onticket', + 'nombre' => 'OnTicket', + ]); + } + + public function test_authentication_is_required(): void + { + $this->postJson('/api/v1/adminapp/tenant/merchandise', ['items' => []]) + ->assertUnauthorized(); + } + + public function test_it_creates_multiple_items_with_color_and_size_variants(): void + { + [$tenant, $colorAttribute] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [ + $this->itemPayload('Camiseta', 3, [ + $this->variantPayload('Verde', 'S', 1700, 100000), + $this->variantPayload('Verde', 'M', 1600, 95000), + $this->variantPayload('Azul Marino', 'L', 500, 110000), + ]), + $this->itemPayload('Buzo', 2, [ + $this->variantPayload('Blanco', 'XL', 300, 120000), + ]), + ], + ]) + ->assertOk() + ->assertJsonCount(2, 'data') + ->assertJsonPath('data.0.title', 'Camiseta') + ->assertJsonPath('data.0.max_units_per_user', 3) + ->assertJsonCount(3, 'data.0.variants') + ->assertJsonPath('data.0.variants.0.color', 'Verde') + ->assertJsonPath('data.0.variants.0.color_value', 'Verde') + ->assertJsonPath('data.0.variants.0.size', 'S') + ->assertJsonPath('data.0.variants.0.stock', 1700) + ->assertJsonPath('data.0.variants.1.price', '95000.00') + ->assertJsonPath('data.0.variants.2.color', 'Azul Marino') + ->assertJsonPath('data.0.variants.2.color_value', 'azul_marino'); + + $this->assertDatabaseCount('catalog_items', 2); + $this->assertDatabaseCount('variantes', 4); + $this->assertDatabaseCount('inventories', 4); + $this->assertDatabaseCount('item_attributes', 4); + $this->assertDatabaseCount('variant_values', 8); + $this->assertDatabaseHas('attribute_options', [ + 'attribute_id' => $colorAttribute->id, + 'value' => 'azul_marino', + 'label' => 'Azul Marino', + ]); + $this->assertDatabaseHas('catalog_items', [ + 'nombre' => 'Camiseta', + 'precio' => 95000, + 'max_units_per_user' => 3, + 'inventory_policy' => 'tracked', + ]); + } + + public function test_it_updates_items_and_variants_with_ids_and_creates_those_without_ids(): void + { + [$tenant] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $created = $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [ + $this->itemPayload('Camiseta', 3, [ + $this->variantPayload('Verde', 'S', 100, 10000), + ]), + ], + ])->assertOk(); + $itemId = $created->json('data.0.id'); + $variantId = $created->json('data.0.variants.0.id'); + + $updatedVariant = $this->variantPayload('Blanco', 'M', 80, 12500); + $updatedVariant['id'] = $variantId; + $updatedItem = $this->itemPayload('Camiseta oficial', 2, [ + $updatedVariant, + $this->variantPayload('Verde', 'L', 60, 15000), + ]); + $updatedItem['id'] = $itemId; + + $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [$updatedItem], + ]) + ->assertOk() + ->assertJsonPath('data.0.id', $itemId) + ->assertJsonPath('data.0.title', 'Camiseta oficial') + ->assertJsonPath('data.0.max_units_per_user', 2) + ->assertJsonPath('data.0.variants.0.id', $variantId) + ->assertJsonPath('data.0.variants.0.color', 'Blanco') + ->assertJsonPath('data.0.variants.0.size', 'M') + ->assertJsonPath('data.0.variants.0.stock', 80) + ->assertJsonCount(2, 'data.0.variants'); + + $this->assertDatabaseCount('catalog_items', 1); + $this->assertDatabaseCount('variantes', 2); + $this->assertDatabaseHas('catalog_items', [ + 'id' => $itemId, + 'nombre' => 'Camiseta oficial', + 'precio' => 12500, + 'max_units_per_user' => 2, + ]); + } + + public function test_it_rejects_duplicate_color_and_size_combinations(): void + { + [$tenant] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [ + $this->itemPayload('Camiseta', 3, [ + $this->variantPayload('Verde', 'S', 100, 10000), + $this->variantPayload(' verde ', 's', 50, 12000), + ]), + ], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors('items.0.variants.1'); + + $this->assertDatabaseCount('catalog_items', 0); + } + + public function test_it_rejects_sizes_that_are_not_attribute_options(): void + { + [$tenant] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [ + $this->itemPayload('Gorra', 2, [ + $this->variantPayload('Verde', 'Único', 100, 10000), + ]), + ], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors('items.0.variants.0.size'); + + $this->assertDatabaseCount('catalog_items', 0); + } + + public function test_a_variant_id_must_belong_to_its_merchandise_item(): void + { + [$tenant] = $this->configuredTenant(); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $created = $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [ + $this->itemPayload('Camiseta', 3, [ + $this->variantPayload('Verde', 'S', 100, 10000), + ]), + $this->itemPayload('Buzo', 2, [ + $this->variantPayload('Blanco', 'M', 50, 20000), + ]), + ], + ])->assertOk(); + + $firstItemId = $created->json('data.0.id'); + $secondItemVariantId = $created->json('data.1.variants.0.id'); + $foreignVariant = $this->variantPayload('Azul Marino', 'XL', 20, 30000); + $foreignVariant['id'] = $secondItemVariantId; + $firstItem = $this->itemPayload('Camiseta', 3, [$foreignVariant]); + $firstItem['id'] = $firstItemId; + + $this->postJson('/api/v1/adminapp/tenant/merchandise', [ + 'items' => [$firstItem], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors('items.0.variants.0.id'); + + $this->assertDatabaseMissing('attribute_options', ['value' => 'azul_marino']); + } + + /** @return array{Tenant, Attribute, Attribute} */ + private function configuredTenant(): array + { + $headerLogo = $this->attachment('header.png'); + $footerLogo = $this->attachment('footer.png'); + $tenant = Tenant::query()->create([ + 'codigo' => 'fiesta_futbol_infantil', + 'nombre' => 'Fiesta Fútbol Infantil', + 'dominio' => 'fiesta.test', + 'primary_color' => '#112233', + 'secondary_color' => '#445566', + 'danger_color' => '#cc0000', + 'header_bg_color' => '#ffffff', + 'footer_bg_color' => '#111111', + 'header_logo_id' => $headerLogo->id, + 'footer_logo_id' => $footerLogo->id, + 'website_type_code' => 'onticket', + ]); + $colorAttribute = Attribute::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'codigo' => 'color', + 'nombre' => 'Color', + 'type' => FieldType::Select, + 'is_required' => true, + ]); + $colorAttribute->options()->createMany([ + ['value' => 'Verde', 'label' => 'Verde', 'sort_order' => 1], + ['value' => 'Blanco', 'label' => 'Blanco', 'sort_order' => 2], + ]); + $sizeAttribute = Attribute::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'codigo' => 'talle', + 'nombre' => 'Talle', + 'type' => FieldType::Select, + 'is_required' => true, + ]); + $sizeAttribute->options()->createMany([ + ['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], + ]); + $menu = Menu::query()->create([ + 'code' => 'adminapp.fiesta-futbol-infantil.merchandising', + 'label' => 'Merchandising', + 'route' => '/admin/merchandising', + ]); + $tenant->menues()->attach($menu->code); + + return [$tenant, $colorAttribute, $sizeAttribute]; + } + + private function attachment(string $filename): Attachment + { + return Attachment::query()->create([ + 'path' => "test/{$filename}", + 'filename' => $filename, + 'type' => AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + } + + /** + * @param array> $variants + * @return array + */ + private function itemPayload(string $title, int $limit, array $variants): array + { + return [ + 'title' => $title, + 'description' => null, + 'max_units_per_user' => $limit, + 'variants' => $variants, + ]; + } + + /** @return array */ + private function variantPayload( + string $color, + string $size, + int $stock, + float $price, + ): array { + return [ + 'color' => $color, + 'size' => $size, + 'stock' => $stock, + 'price' => $price, + ]; + } + + private function createAdminAppUser(Tenant $tenant): User + { + return User::factory()->create([ + 'rol_codigo' => RoleCode::AdminApp->value, + 'tenant_codigo' => $tenant->codigo, + ]); + } +}