seed(AuthorizationSeeder::class); WebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); } public function test_it_returns_row_configs_and_expands_them_into_seat_variants(): void { [$tenant, $entry] = $this->configuredEntry(); $existing = $this->createVariant($entry, 'NORMAL', 'A', '1', '1', 100000); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->getJson('/api/v1/adminapp/tenant/desfile/entries') ->assertOk() ->assertJsonPath('data.id', $entry->id) ->assertJsonPath('data.rows.0.type', 'NORMAL') ->assertJsonPath('data.rows.0.row', '1') ->assertJsonPath('data.rows.0.max_seat', 1) ->assertJsonPath('data.rows.0.price', '100000.00'); $response = $this->putJson('/api/v1/adminapp/tenant/desfile/entries', [ 'rows' => [ [ 'type' => 'NORMAL', 'sector' => 'A', 'row' => '1', 'max_seat' => 2, 'price' => 120000, ], [ 'type' => 'VIP + LUNCH', 'sector' => 'B', 'row' => '2', 'max_seat' => 3, 'price' => 250000, ], ], ]); $response ->assertOk() ->assertJsonCount(2, 'data.rows') ->assertJsonPath('data.rows.0.max_seat', 2) ->assertJsonPath('data.rows.0.price', '120000.00') ->assertJsonPath('data.rows.1.max_seat', 3) ->assertJsonPath('data.rows.1.type', 'VIP + LUNCH'); $this->assertDatabaseCount('variantes', 5); $this->assertDatabaseHas('catalog_items', [ 'id' => $entry->id, 'precio' => 120000, ]); $this->assertSame('120000.00', $existing->fresh()->precio); $this->assertSame(2, $entry->variants()->where('precio', 120000)->count()); $this->assertSame(3, $entry->variants()->where('precio', 250000)->count()); $this->assertSame(5, Inventory::query()->where('real_stock', 1)->count()); } public function test_it_replaces_and_toggles_the_entry_image(): void { Storage::fake('s3'); [$tenant, $entry] = $this->configuredEntry(); $oldImage = Attachment::query()->create([ 'path' => 'catalog-items/old.png', 'filename' => 'old.png', 'type' => AttachmentType::Image, 'mime_type' => 'image/png', 'extension' => 'png', 'size' => 10, ]); Storage::disk('s3')->put($oldImage->path, 'old'); $entry->allAttachments()->attach($oldImage->id, ['orden' => 0]); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->post('/api/v1/adminapp/tenant/desfile/entries/image', [ 'image' => UploadedFile::fake()->image('plano.png'), 'is_enabled' => false, ], ['Accept' => 'application/json']) ->assertOk() ->assertJsonPath('data.image.filename', 'plano.png') ->assertJsonPath('data.image.is_enabled', false); $this->assertDatabaseMissing('attachments', ['id' => $oldImage->id]); $this->assertCount(0, $entry->fresh()->attachments); $this->assertCount(1, $entry->fresh()->allAttachments); $this->patchJson('/api/v1/adminapp/tenant/desfile/entries/image', [ 'is_enabled' => true, ]) ->assertOk() ->assertJsonPath('data.image.is_enabled', true); $this->assertCount(1, $entry->fresh()->attachments); } public function test_it_rejects_duplicate_rows(): void { [$tenant] = $this->configuredEntry(); Sanctum::actingAs($this->createAdminAppUser($tenant)); $payload = [ 'rows' => [ $this->rowPayload('NORMAL', 'A', '1', 10, 100), $this->rowPayload('normal', 'A', '1', 20, 200), ], ]; $this->putJson('/api/v1/adminapp/tenant/desfile/entries', $payload) ->assertUnprocessable() ->assertJsonValidationErrors('rows.1'); } public function test_it_rejects_rows_and_seat_quantities_outside_the_configured_ranges(): void { [$tenant] = $this->configuredEntry(); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->putJson('/api/v1/adminapp/tenant/desfile/entries', [ 'rows' => [ $this->rowPayload('NORMAL', 'A', '6', 1, 100), $this->rowPayload('NORMAL', 'A', '1', 101, 100), ], ]) ->assertUnprocessable() ->assertJsonValidationErrors(['rows.0.row', 'rows.1.max_seat']); } public function test_it_cannot_reduce_the_maximum_below_a_reserved_seat(): void { [$tenant, $entry] = $this->configuredEntry(); $reserved = $this->createVariant($entry, 'NORMAL', 'A', '1', '3', 100); $reserved->inventory()->update(['reserved_stock' => 1]); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->putJson('/api/v1/adminapp/tenant/desfile/entries', [ 'rows' => [$this->rowPayload('NORMAL', 'A', '1', 2, 100)], ]) ->assertUnprocessable() ->assertJsonValidationErrors('rows'); $this->assertDatabaseHas('variantes', ['id' => $reserved->id, 'deleted_at' => null]); } /** @return array{Tenant, CatalogItem} */ private function configuredEntry(string $tenantCode = 'desfile_pura_tendencia'): array { $tenant = Tenant::query()->create([ 'codigo' => $tenantCode, 'nombre' => 'Desfile', 'dominio' => "{$tenantCode}.test", 'website_type_code' => 'onticket', ]); $menu = Menu::query()->firstOrCreate( ['code' => 'adminapp.desfile.entradas'], ['label' => 'Entradas', 'route' => '/admin/desfile/entradas'], ); $tenant->menues()->attach($menu->code); $entry = CatalogItem::query()->create([ 'tenant_code' => $tenant->codigo, 'slug' => 'entrada', 'nombre' => 'Entrada', 'precio' => 0, 'inventory_policy' => InventoryPolicy::Tracked, 'inventory_subject' => InventorySubject::Seat, 'has_tickets' => true, ]); foreach ([ 'tipo' => ['VIP + LUNCH', 'NORMAL'], 'sector' => ['A', 'B', 'C', 'D'], 'fila' => array_map('strval', range(1, 5)), 'asiento' => array_map('strval', range(1, 100)), ] as $code => $options) { $attribute = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, 'codigo' => $code, 'nombre' => ucfirst($code), 'type' => FieldType::Select, 'is_required' => true, ]); foreach ($options as $order => $option) { $attribute->options()->create([ 'value' => $option, 'label' => $option, 'sort_order' => $order + 1, ]); } $entry->itemAttributes()->create([ 'attribute_id' => $attribute->id, 'sort_order' => $entry->itemAttributes()->count() + 1, ]); } return [$tenant, $entry]; } private function createVariant( CatalogItem $entry, string $type, string $sector, string $row, string $seat, int $price, ): Variant { $variant = $entry->variants()->create([ 'inventory_id' => Inventory::query()->create(['real_stock' => 1])->id, 'precio' => $price, ]); $itemAttributes = $entry->itemAttributes()->with('attribute')->get()->keyBy( fn (ItemAttribute $itemAttribute): string => $itemAttribute->attribute->codigo, ); foreach (compact('type', 'sector', 'row', 'seat') as $input => $value) { $code = ['type' => 'tipo', 'sector' => 'sector', 'row' => 'fila', 'seat' => 'asiento'][$input]; $variant->definitions()->create([ 'item_attribute_id' => $itemAttributes[$code]->id, 'value' => $value, ]); } return $variant; } /** @return array */ private function rowPayload( string $type, string $sector, string $row, int $maxSeat, int $price, ): array { return [ ...compact('type', 'sector', 'row', 'price'), 'max_seat' => $maxSeat, ]; } private function createAdminAppUser(Tenant $tenant): User { return User::factory()->create([ 'rol_codigo' => RoleCode::AdminApp->value, 'tenant_codigo' => $tenant->codigo, ]); } }