seed(AuthorizationSeeder::class); WebsiteType::query()->create([ 'codigo' => 'onticket', 'nombre' => 'OnTicket', ]); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_authentication_is_required(): void { $this->postJson('/api/v1/adminapp/tenant/foods', ['variants' => []]) ->assertUnauthorized(); } public function test_it_creates_one_food_item_with_multiple_variants(): void { [$tenant, $firstDate, $secondDate] = $this->configuredTenant(); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->postJson('/api/v1/adminapp/tenant/foods', [ 'variants' => [ $this->variantPayload($firstDate->id, 'Almuerzo', 'Comedor', 1700, 10000), $this->variantPayload($secondDate->id, 'Cena', 'Vianda', 900, 8000), ], ]) ->assertOk() ->assertJsonPath('data.name', 'Comida') ->assertJsonCount(2, 'data.variants') ->assertJsonPath('data.variants.0.schedule', 'Almuerzo') ->assertJsonPath('data.variants.0.service', 'Comedor') ->assertJsonPath('data.variants.0.stock', 1700) ->assertJsonPath('data.variants.0.price', '10000.00') ->assertJsonPath('data.variants.1.price', '8000.00'); $this->getJson('/api/v1/adminapp/tenant/foods') ->assertOk() ->assertJsonPath('data.name', 'Comida') ->assertJsonCount(2, 'data.variants') ->assertJsonPath('data.variants.0.schedule', 'Almuerzo') ->assertJsonPath('data.variants.1.service', 'Vianda'); $this->assertDatabaseCount('catalog_items', 1); $this->assertDatabaseCount('variantes', 2); $this->assertDatabaseCount('inventories', 2); $this->assertDatabaseCount('variant_event_dates', 2); $this->assertDatabaseCount('variant_values', 4); $food = CatalogItem::query()->where('slug', 'comida')->sole(); $this->assertDatabaseHas('item_attributes', [ 'catalog_item_id' => $food->id, 'attribute_id' => Attribute::query()->where('codigo', 'event_date')->sole()->id, 'sort_order' => 1, ]); $this->assertDatabaseHas('item_attributes', [ 'catalog_item_id' => $food->id, 'attribute_id' => Attribute::query()->where('codigo', 'horario')->sole()->id, 'sort_order' => 2, ]); $this->assertDatabaseHas('item_attributes', [ 'catalog_item_id' => $food->id, 'attribute_id' => Attribute::query()->where('codigo', 'servicio')->sole()->id, 'sort_order' => 3, ]); $this->assertDatabaseHas('catalog_items', [ 'tenant_code' => $tenant->codigo, 'slug' => 'comida', 'nombre' => 'Comida', 'has_tickets' => true, 'category_id' => Category::query() ->where('tenant_code', $tenant->codigo) ->where('nombre', 'Comidas') ->sole() ->id, 'precio' => 8000, ]); } public function test_it_updates_variants_with_an_id_and_creates_variants_without_one(): void { [$tenant, $firstDate, $secondDate] = $this->configuredTenant(); Sanctum::actingAs($this->createAdminAppUser($tenant)); $created = $this->postJson('/api/v1/adminapp/tenant/foods', [ 'variants' => [ $this->variantPayload($firstDate->id, 'Almuerzo', 'Comedor', 100, 10000), ], ])->assertOk(); $variantId = $created->json('data.variants.0.id'); $updated = $this->variantPayload($secondDate->id, 'Cena', 'Vianda', 80, 8500); $updated['id'] = $variantId; $updated['description'] = 'Cena para llevar'; $this->postJson('/api/v1/adminapp/tenant/foods', [ 'variants' => [ $updated, $this->variantPayload($firstDate->id, 'Almuerzo', 'Vianda', 50, 9000), ], ]) ->assertOk() ->assertJsonCount(2, 'data.variants') ->assertJsonPath('data.variants.0.id', $variantId) ->assertJsonPath('data.variants.0.event_date_id', $secondDate->id) ->assertJsonPath('data.variants.0.schedule', 'Cena') ->assertJsonPath('data.variants.0.service', 'Vianda') ->assertJsonPath('data.variants.0.description', 'Cena para llevar') ->assertJsonPath('data.variants.0.stock', 80) ->assertJsonPath('data.variants.0.price', '8500.00'); $this->assertDatabaseCount('catalog_items', 1); $this->assertDatabaseCount('variantes', 2); $this->assertDatabaseHas('variantes', [ 'id' => $variantId, 'event_date_id' => $secondDate->id, 'descripcion' => 'Cena para llevar', 'precio' => 8500, ]); } public function test_it_separates_current_and_historical_food_variants(): void { Carbon::setTestNow('2026-09-01 12:00:00'); [$tenant, $rescheduledDate, $suspendedDate] = $this->configuredTenant(); $completedDate = $tenant->eventDates()->create([ 'date' => '2026-10-11', 'time_start' => '00:00', 'time_end' => '23:59', ]); $activeDate = $tenant->eventDates()->create([ 'date' => '2026-10-21', 'time_start' => '00:00', 'time_end' => '23:59', ]); $admin = $this->createAdminAppUser($tenant); Sanctum::actingAs($admin); $this->postJson('/api/v1/adminapp/tenant/foods', [ 'variants' => [ $this->variantPayload($rescheduledDate->id, 'Almuerzo', 'Comedor', 100, 10000), $this->variantPayload($suspendedDate->id, 'Cena', 'Vianda', 90, 9000), $this->variantPayload($completedDate->id, 'Cena', 'Comedor', 80, 8000), $this->variantPayload($activeDate->id, 'Almuerzo', 'Vianda', 70, 7000), ], ])->assertOk(); $eventService = app(EventService::class); $eventService->rescheduleDateForTenant( $tenant, $rescheduledDate, ['date' => '2026-10-20'], $admin, ); $eventService->suspendDateForTenant($tenant, $suspendedDate, $admin); Carbon::setTestNow('2026-10-15 12:00:00'); $response = $this->getJson('/api/v1/adminapp/tenant/foods')->assertOk(); $response->assertJsonCount(2, 'data.variants')->assertJsonCount(3, 'data.history'); $history = collect($response->json('data.history')); $rescheduled = $history->firstWhere('status', 'rescheduled'); $this->assertSame('REPROGRAMADA', $rescheduled['status_text']); $this->assertSame('2026-10-09', $rescheduled['event_date']); $this->assertSame('2026-10-20', $rescheduled['replacement_event_date']); $this->assertCount(1, $rescheduled['variants']); $suspended = $history->firstWhere('status', 'suspended'); $this->assertSame('CANCELADA', $suspended['status_text']); $this->assertNull($suspended['replacement_event_date']); $completed = $history->firstWhere('status', 'completed'); $this->assertSame('FINALIZADA', $completed['status_text']); $this->assertSame('2026-10-11', $completed['event_date']); $this->assertNull($completed['replacement_event_date']); } public function test_it_updates_only_the_stock_of_historical_food_variants(): void { [$tenant, $historicalDate, $activeDate] = $this->configuredTenant(); $admin = $this->createAdminAppUser($tenant); Sanctum::actingAs($admin); $created = $this->postJson('/api/v1/adminapp/tenant/foods', [ 'variants' => [ $this->variantPayload($historicalDate->id, 'Almuerzo', 'Comedor', 100, 10000), $this->variantPayload($activeDate->id, 'Cena', 'Vianda', 80, 8000), ], ])->assertOk(); $historicalVariantId = $created->json('data.variants.0.id'); $activeVariantId = $created->json('data.variants.1.id'); app(EventService::class)->rescheduleDateForTenant( $tenant, $historicalDate, ['date' => $activeDate->date->format('Y-m-d')], $admin, ); $historicalVariant = Variant::query()->findOrFail($historicalVariantId); $replacementVariant = $historicalVariant->replacement()->firstOrFail(); $replacementInventoryId = $replacementVariant->inventory_id; // Simula variantes creadas antes de que la reprogramación separara sus inventarios. $replacementVariant->update(['inventory_id' => $historicalVariant->inventory_id]); Inventory::query()->whereKey($replacementInventoryId)->delete(); $updated = $this->patchJson('/api/v1/adminapp/tenant/foods/history-stock', [ 'variants' => [['id' => $historicalVariantId, 'stock' => 45]], ]) ->assertOk() ->assertJsonPath('data.history.0.variants.0.id', $historicalVariantId) ->assertJsonPath('data.history.0.variants.0.stock', 45); $replacement = collect($updated->json('data.variants')) ->firstWhere('schedule', 'Almuerzo'); $this->assertSame(100, $replacement['stock']); $historicalInventoryId = Variant::query()->findOrFail($historicalVariantId)->inventory_id; $currentInventoryId = $replacementVariant->fresh()->inventory_id; $this->assertNotSame($historicalInventoryId, $currentInventoryId); $this->assertDatabaseHas('inventories', [ 'id' => $historicalInventoryId, 'real_stock' => 45, ]); $this->patchJson('/api/v1/adminapp/tenant/foods/history-stock', [ 'variants' => [['id' => $activeVariantId, 'stock' => 20]], ]) ->assertUnprocessable() ->assertJsonValidationErrors(['variants.0.id']); } public function test_it_deletes_food_records_and_removes_the_empty_product(): void { [$tenant, $firstDate, $secondDate] = $this->configuredTenant(); Sanctum::actingAs($this->createAdminAppUser($tenant)); $created = $this->postJson('/api/v1/adminapp/tenant/foods', [ 'variants' => [ $this->variantPayload($firstDate->id, 'Almuerzo', 'Comedor', 100, 10000), $this->variantPayload($secondDate->id, 'Cena', 'Vianda', 50, 8000), ], ])->assertOk(); $firstId = $created->json('data.variants.0.id'); $secondId = $created->json('data.variants.1.id'); $this->deleteJson("/api/v1/adminapp/tenant/foods/{$secondId}") ->assertNoContent(); $this->assertDatabaseHas('catalog_items', [ 'tenant_code' => $tenant->codigo, 'slug' => 'comida', 'precio' => 10000, ]); $this->assertSoftDeleted('variantes', ['id' => $secondId]); $this->assertSame(1, Variant::query()->count()); $this->assertDatabaseCount('inventories', 2); $this->deleteJson("/api/v1/adminapp/tenant/foods/{$firstId}") ->assertNoContent(); $this->assertSoftDeleted('variantes', ['id' => $firstId]); $this->assertSame(0, CatalogItem::query()->count()); $this->assertSame(0, Variant::query()->count()); $this->assertDatabaseCount('inventories', 2); } public function test_it_rejects_duplicate_combinations(): void { [$tenant, $firstDate] = $this->configuredTenant(); Sanctum::actingAs($this->createAdminAppUser($tenant)); $this->postJson('/api/v1/adminapp/tenant/foods', [ 'variants' => [ $this->variantPayload($firstDate->id, 'Almuerzo', 'Comedor', 100, 10000), $this->variantPayload($firstDate->id, ' almuerzo ', ' comedor ', 100, 10000), ], ]) ->assertUnprocessable() ->assertJsonValidationErrors(['variants.1']); $this->assertDatabaseCount('catalog_items', 0); } /** @return array{Tenant, mixed, mixed} */ private function configuredTenant(): array { $tenant = Tenant::query()->create([ 'codigo' => 'fiesta_futbol_infantil', 'nombre' => 'Fiesta Fútbol Infantil', 'dominio' => 'fiesta.test', 'website_type_code' => 'onticket', ]); $eventDateAttribute = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, 'codigo' => 'event_date', 'nombre' => 'Fecha', 'type' => FieldType::EventDate, 'is_required' => true, ]); $scheduleAttribute = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, 'codigo' => 'horario', 'nombre' => 'Horario', 'type' => FieldType::Select, 'is_required' => true, ]); $scheduleAttribute->options()->createMany([ ['value' => 'Almuerzo', 'label' => 'Almuerzo', 'sort_order' => 1], ['value' => 'Cena', 'label' => 'Cena', 'sort_order' => 2], ]); $serviceAttribute = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, 'codigo' => 'servicio', 'nombre' => 'Servicio', 'type' => FieldType::Select, 'is_required' => true, ]); $serviceAttribute->options()->createMany([ ['value' => 'Comedor', 'label' => 'Comedor', 'sort_order' => 1], ['value' => 'Vianda', 'label' => 'Vianda', 'sort_order' => 2], ]); $firstDate = $tenant->eventDates()->create([ 'date' => '2026-10-09', 'time_start' => '00:00', 'time_end' => '23:59', ]); $secondDate = $tenant->eventDates()->create([ 'date' => '2026-10-10', 'time_start' => '00:00', 'time_end' => '23:59', ]); $menu = Menu::query()->create([ 'code' => 'adminapp.fiesta-futbol-infantil.comida', 'label' => 'Comida', 'route' => '/admin/comidas', ]); $tenant->menues()->attach($menu->code); $this->assertNotNull($eventDateAttribute); return [$tenant, $firstDate, $secondDate]; } /** @return array */ private function variantPayload( int $eventDateId, string $schedule, string $service, int $stock, float $price, ): array { return [ 'event_date_id' => $eventDateId, 'schedule' => $schedule, 'service' => $service, 'description' => null, 'stock' => $stock, 'price' => $price, ]; } private function createAdminAppUser(Tenant $tenant): User { return User::factory()->create([ 'rol_codigo' => RoleCode::AdminApp->value, 'tenant_codigo' => $tenant->codigo, ]); } }