diff --git a/tests/Feature/Event/AdminAppEventControllerTest.php b/tests/Feature/Event/AdminAppEventControllerTest.php index f4116fc..2ce68c7 100644 --- a/tests/Feature/Event/AdminAppEventControllerTest.php +++ b/tests/Feature/Event/AdminAppEventControllerTest.php @@ -11,6 +11,7 @@ use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\Variant; use App\Domains\Event\Events\EventDateRescheduled; use App\Domains\Event\Events\EventDateSuspended; +use App\Domains\Purchase\Services\Checkout\CatalogSelectionResolver; use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\WebsiteType; use App\Domains\Ticket\Enums\ValidityTimeType; @@ -20,6 +21,7 @@ use Database\Seeders\SocialMediaSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Laravel\Sanctum\Sanctum; use Tests\TestCase; @@ -247,6 +249,7 @@ class AdminAppEventControllerTest extends TestCase 'time_end' => '20:00', ]); $variant = $this->createVariant($tenant, $original->id); + $variant->inventory()->update(['real_stock' => 5]); $ticket = $this->createTicket($tenant, $admin, $variant); Sanctum::actingAs($admin); @@ -267,6 +270,31 @@ class AdminAppEventControllerTest extends TestCase }); $this->assertDatabaseCount('event_dates', 2); + $variant->refresh(); + $replacement = $variant->replacement()->firstOrFail(); + $this->assertSame($original->id, $variant->event_date_id); + $this->assertSame($destination->id, $replacement->event_date_id); + $this->assertSame($variant->inventory_id, $replacement->inventory_id); + $this->assertNotNull($variant->sales_disabled_at); + $this->assertSame($replacement->id, $variant->replaced_by_variant_id); + $this->assertSame( + [$replacement->id], + $variant->catalogItem->fresh(['variants.inventory'])->visibleVariants()->modelKeys(), + ); + $this->assertSame($variant->id, $ticket->fresh()->source_variant_id); + + try { + app(CatalogSelectionResolver::class)->resolve( + $tenant, + $variant->catalog_item_id, + $variant->id, + 'direct_items.0', + ); + $this->fail('The historical variant should not be sellable.'); + } catch (ValidationException $exception) { + $this->assertArrayHasKey('direct_items.0.variant_id', $exception->errors()); + } + $this->assertSame('20 de Octubre 2027', $tenant->fresh()->event_date_text); $this->getJson('/api/tenants/bootstrap?dominio=acme.test&path=%2F') ->assertOk() @@ -287,6 +315,12 @@ class AdminAppEventControllerTest extends TestCase ])->assertOk()->assertJsonPath('data.status', 'rescheduled'); $this->assertDatabaseCount('event_dates', 3); + $replacement->refresh(); + $latestReplacement = $replacement->replacement()->firstOrFail(); + $this->assertSame($replacement->inventory_id, $latestReplacement->inventory_id); + $this->assertSame('2027-10-25', $latestReplacement->eventDate->date->format('Y-m-d')); + $this->assertFalse($replacement->isSellable()); + $this->assertTrue($latestReplacement->isSellable()); $this->assertSame('25 de Octubre 2027', $tenant->fresh()->event_date_text); $this->getJson('/api/tenants/bootstrap?dominio=acme.test&path=%2F') ->assertOk() @@ -299,6 +333,41 @@ class AdminAppEventControllerTest extends TestCase ); } + public function test_rescheduling_reuses_an_equivalent_destination_variant(): void + { + Event::fake([EventDateRescheduled::class]); + $tenant = $this->createActiveEvent($this->createTenant('acme'), 'Festival Acme'); + $original = $tenant->eventDates()->create([ + 'date' => '2027-10-09', + 'time_start' => '09:00', + 'time_end' => '18:30', + ]); + $destination = $tenant->eventDates()->create([ + 'date' => '2027-10-20', + 'time_start' => '09:00', + 'time_end' => '18:30', + ]); + $historicalVariant = $this->createVariant($tenant, $original->id); + $destinationVariant = Variant::query()->create([ + 'catalog_item_id' => $historicalVariant->catalog_item_id, + 'inventory_id' => Inventory::query()->create(['real_stock' => 5])->id, + 'event_date_id' => $destination->id, + ]); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->postJson("/api/v1/adminapp/tenant/event-dates/{$original->id}/reschedule", [ + 'date' => '2027-10-20', + ])->assertOk(); + + $this->assertSame(2, Variant::query()->count()); + $this->assertSame( + $destinationVariant->id, + $historicalVariant->fresh()->replaced_by_variant_id, + ); + $this->assertSame($original->id, $historicalVariant->fresh()->event_date_id); + $this->assertTrue($destinationVariant->fresh()->isSellable()); + } + public function test_suspending_disables_only_tickets_without_another_usable_date(): void { Event::fake([EventDateSuspended::class]); @@ -335,6 +404,8 @@ class AdminAppEventControllerTest extends TestCase $this->assertNotNull($singleDateTicket->fresh()->disabled_at); $this->assertNull($multipleDateTicket->fresh()->disabled_at); + $this->assertNotNull($singleDateVariant->fresh()->sales_disabled_at); + $this->assertNotNull($multipleDateVariant->fresh()->sales_disabled_at); $this->assertSame('10 de Octubre 2027', $tenant->fresh()->event_date_text); $this->getJson('/api/tenants/bootstrap?dominio=acme.test&path=%2F') ->assertOk() diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index e7fd951..9014f75 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -72,6 +72,79 @@ class StorePurchaseTest extends TestCase $this->travelBack(); } + public function test_checkout_replaces_a_historical_cart_variant_without_duplicating_stock(): void + { + $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); + $user = User::factory()->create(); + $historicalVariant = $this->createVariantForTenant('sonder', 10, '50.00'); + $cart = Cart::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'user_id' => $user->id, + 'status' => Cart::STATUS_ACTIVE, + ]); + $cart->addItem($historicalVariant->catalog_item_id, $historicalVariant->id, 2); + + $replacement = Variant::query()->create([ + 'catalog_item_id' => $historicalVariant->catalog_item_id, + 'inventory_id' => $historicalVariant->inventory_id, + 'precio' => '50.00', + ]); + $historicalVariant->update([ + 'replaced_by_variant_id' => $replacement->id, + 'sales_disabled_at' => now(), + ]); + + $purchase = app(CheckoutService::class)->startCheckout($tenant, $user->id, [ + 'cart_id' => $cart->id, + ]); + + $this->assertDatabaseHas('carrito_items', [ + 'cart_id' => $cart->id, + 'variant_id' => $replacement->id, + 'cantidad' => 2, + ]); + $this->assertDatabaseHas('compra_items', [ + 'compra_id' => $purchase->id, + 'source_variant_id' => $replacement->id, + 'cantidad' => 2, + ]); + $this->assertDatabaseHas('inventories', [ + 'id' => $historicalVariant->inventory_id, + 'real_stock' => 10, + 'reserved_stock' => 2, + ]); + $this->assertSame(8, $historicalVariant->catalogItem->fresh()->availableStock()); + } + + public function test_direct_checkout_rejects_a_historical_variant(): void + { + $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); + $user = User::factory()->create(); + $historicalVariant = $this->createVariantForTenant('sonder', 10, '50.00'); + $replacement = Variant::query()->create([ + 'catalog_item_id' => $historicalVariant->catalog_item_id, + 'inventory_id' => $historicalVariant->inventory_id, + 'precio' => '50.00', + ]); + $historicalVariant->update([ + 'replaced_by_variant_id' => $replacement->id, + 'sales_disabled_at' => now(), + ]); + + $this->actingAs($user, 'sanctum') + ->postJson('/api/tenants/sonder/compras/start-checkout', [ + 'direct_items' => [[ + 'catalog_item_id' => $historicalVariant->catalog_item_id, + 'variant_id' => $historicalVariant->id, + 'cantidad' => 1, + ]], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors('direct_items.0.variant_id'); + + $this->assertDatabaseCount('compras', 0); + } + public function test_checkout_cannot_replace_an_overdue_cart_reservation(): void { $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');