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, ]); } }