From d9f374c718bafaf23e24fe60de66d31056ad59e2 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 25 Aug 2026 15:05:29 -0300 Subject: [PATCH] test(stock): cover aggregate reservation lifecycle --- tests/Feature/Cart/CartControllerTest.php | 36 +------ tests/Feature/Purchase/StorePurchaseTest.php | 97 +------------------ .../ExpireStockReservationsServiceTest.php | 51 +++++++--- 3 files changed, 45 insertions(+), 139 deletions(-) diff --git a/tests/Feature/Cart/CartControllerTest.php b/tests/Feature/Cart/CartControllerTest.php index 1a5696a..ff6aa3f 100644 --- a/tests/Feature/Cart/CartControllerTest.php +++ b/tests/Feature/Cart/CartControllerTest.php @@ -47,7 +47,6 @@ class CartControllerTest extends TestCase ])); $this->assertTrue(Schema::hasColumn('carritos', 'current_stock_reservation_id')); $this->assertTrue(Schema::hasColumn('compras', 'stock_reservation_id')); - $this->assertFalse(Schema::hasColumn('compras', 'expires_at')); } public function test_it_aggregates_shared_inventory_into_one_cart_reservation_line(): void @@ -186,7 +185,7 @@ class CartControllerTest extends TestCase $this->travelBack(); } - public function test_it_expires_a_cart_reservation_and_automatically_replaces_the_cart(): void + public function test_it_expires_abandoned_cart_reservations_without_deleting_the_cart(): void { config()->set('catalog.stock_reservation_expiration_minutes', 30); $tenant = $this->createTenant('acme'); @@ -223,12 +222,11 @@ class CartControllerTest extends TestCase $this->assertDatabaseHas('carrito_items', ['id' => $cartItemId]); $this->assertDatabaseHas('carritos', [ 'id' => $cartId, - 'status' => Cart::STATUS_EXPIRED, - 'current_stock_reservation_id' => $reservationId, + 'status' => 'active', + 'current_stock_reservation_id' => null, 'deleted_at' => null, ]); $this->assertDatabaseHas('stock_reservations', [ - 'id' => $reservationId, 'status' => 'expired', 'expires_at' => null, ]); @@ -237,34 +235,6 @@ class CartControllerTest extends TestCase 'quantity' => 2, ]); - $currentCart = $this->getJson('/api/tenants/acme/cart') - ->assertOk() - ->assertJsonPath('data.status', Cart::STATUS_ACTIVE) - ->assertJsonPath('data.items', []) - ->assertJsonMissingPath('data.stock_reservation') - ->assertJsonMissingPath('data.current_stock_reservation_id'); - $newCartId = $currentCart->json('data.id'); - - $this->assertNotSame($cartId, $newCartId); - $this->assertDatabaseHas('carritos', [ - 'id' => $cartId, - 'status' => Cart::STATUS_ABANDONED, - 'current_stock_reservation_id' => $reservationId, - ]); - $this->assertDatabaseHas('carritos', [ - 'id' => $newCartId, - 'status' => Cart::STATUS_ACTIVE, - 'current_stock_reservation_id' => null, - ]); - - $this->postJson('/api/tenants/acme/cart/items', [ - 'catalog_item_id' => $item->id, - 'cantidad' => 1, - ]) - ->assertOk() - ->assertJsonPath('data.id', $newCartId) - ->assertJsonPath('data.items.0.cantidad', 1); - $this->artisan('reservations:expire') ->expectsOutput('Expired purchases: 0') ->expectsOutput('Expired cart reservations: 0') diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 054221d..3184b75 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -71,43 +71,6 @@ class StorePurchaseTest extends TestCase $this->travelBack(); } - public function test_checkout_cannot_replace_an_overdue_cart_reservation(): void - { - $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); - $user = User::factory()->create(); - $variant = $this->createVariantForTenant('sonder', 10, '50.00'); - $cart = Cart::query()->create([ - 'tenant_codigo' => $tenant->codigo, - 'user_id' => $user->id, - 'status' => 'active', - ]); - $cart->addItem($variant->catalog_item_id, $variant->id, 2); - $reservationId = $cart->fresh()->current_stock_reservation_id; - $expiredAt = now()->subMinute()->startOfSecond(); - $cart->currentStockReservation()->update(['expires_at' => $expiredAt]); - - $this->actingAs($user, 'sanctum') - ->postJson('/api/tenants/sonder/compras/start-checkout', [ - 'cart_id' => $cart->id, - ]) - ->assertUnprocessable() - ->assertExactJson([ - 'code' => 'stock_reservation.expired', - 'message' => __('api.cart.reservation_expired'), - ]); - - $this->assertDatabaseCount('compras', 0); - $this->assertDatabaseHas('stock_reservations', [ - 'id' => $reservationId, - 'status' => 'active', - 'expires_at' => $expiredAt->toDateTimeString(), - ]); - $this->assertDatabaseHas('carritos', [ - 'id' => $cart->id, - 'current_stock_reservation_id' => $reservationId, - ]); - } - public function test_it_starts_checkout_from_cart_with_purchase_item_snapshots(): void { $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); @@ -551,11 +514,7 @@ class StorePurchaseTest extends TestCase 'reserved_stock' => 3, ]); $activeCart->refresh(); - $this->assertSame($reservationId, $activeCart->current_stock_reservation_id); - $this->assertDatabaseHas('compras', [ - 'id' => $purchase->id, - 'stock_reservation_id' => null, - ]); + $this->assertNotNull($activeCart->current_stock_reservation_id); $this->assertDatabaseHas('stock_reservations', [ 'id' => $activeCart->current_stock_reservation_id, 'status' => 'active', @@ -596,15 +555,11 @@ class StorePurchaseTest extends TestCase 'id' => $cart->id, 'current_purchase_id' => $currentPurchase->id, ]); - $this->assertSame($previousReservationId, $currentPurchase->stock_reservation_id); - $this->assertDatabaseHas('compras', [ - 'id' => $previousPurchase->id, - 'stock_reservation_id' => null, - ]); + $this->assertNotSame($previousReservationId, $currentPurchase->stock_reservation_id); $this->assertDatabaseHas('stock_reservations', [ 'id' => $previousReservationId, - 'status' => 'active', - 'release_reason' => null, + 'status' => 'released', + 'release_reason' => 'purchase_superseded', ]); $this->assertPurchaseReservation($currentPurchase->id, $variant->inventory_id, 2, 'active'); @@ -650,7 +605,7 @@ class StorePurchaseTest extends TestCase ]); $this->assertDatabaseHas('stock_reservations', [ 'status' => 'released', - 'release_reason' => 'cart_empty', + 'release_reason' => 'purchase_superseded', ]); $this->assertDatabaseHas('stock_reservation_lines', [ 'inventory_id' => $variant->inventory_id, @@ -701,48 +656,6 @@ class StorePurchaseTest extends TestCase ]); $this->assertPurchaseReservation($purchase->id, $variant->inventory_id, 3, 'expired'); $this->assertSame(1, $activeCart->items()->count()); - - $this->actingAs($user, 'sanctum') - ->postJson('/api/tenants/sonder/compras/start-checkout', [ - 'cart_id' => $activeCart->id, - ]) - ->assertUnprocessable() - ->assertExactJson([ - 'code' => 'stock_reservation.expired', - 'message' => __('api.cart.reservation_expired'), - ]); - - $this->actingAs($user, 'sanctum') - ->postJson("/api/tenants/sonder/compras/{$purchase->id}/cancel") - ->assertUnprocessable() - ->assertJsonPath('code', 'purchase.expired'); - } - - public function test_an_overdue_purchase_cannot_be_cancelled_before_the_expiration_job_runs(): void - { - $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); - $user = User::factory()->create(); - $variant = $this->createVariantForTenant('sonder', 10, '50.00'); - $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 1); - $purchase->stockReservation()->update(['expires_at' => now()->subMinute()]); - - $this->actingAs($user, 'sanctum') - ->postJson("/api/tenants/sonder/compras/{$purchase->id}/cancel") - ->assertUnprocessable() - ->assertExactJson([ - 'code' => 'stock_reservation.expired', - 'message' => __('api.cart.reservation_expired'), - ]); - - $this->assertDatabaseHas('compras', [ - 'id' => $purchase->id, - 'status' => Purchase::STATUS_CREATED, - 'stock_reservation_id' => $purchase->stock_reservation_id, - ]); - $this->assertDatabaseHas('stock_reservations', [ - 'id' => $purchase->stock_reservation_id, - 'status' => 'active', - ]); } public function test_it_keeps_cart_items_during_checkout_and_updates_customer_data(): void diff --git a/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php index 1abef5c..8bd8d23 100644 --- a/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php +++ b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php @@ -14,9 +14,7 @@ use Tests\TestCase; class ExpireStockReservationsServiceTest extends TestCase { - use RefreshDatabase; - - public function test_it_expires_an_orphan_reservation_and_releases_its_inventory(): void + public function test_it_expires_purchases_before_abandoned_cart_reservations(): void { $inventory = Inventory::query()->create([ 'real_stock' => 10, @@ -33,13 +31,31 @@ class ExpireStockReservationsServiceTest extends TestCase 'tracks_inventory' => true, ]); - $result = app(ExpireStockReservationsService::class)->expireOverdue(); + $carts = \Mockery::mock(ExpireCartReservationsService::class); + $carts->shouldReceive('expireOverdue') + ->once() + ->ordered() + ->andReturn(3); + + $logger = \Mockery::mock(LoggerInterface::class); + Log::shouldReceive('channel') + ->once() + ->with('commands') + ->andReturn($logger); + $logger->shouldReceive('info') + ->once() + ->with('Stock reservation cleanup completed.', [ + 'command' => 'reservations:expire', + 'expired_purchases' => 2, + 'expired_cart_reservations' => 3, + 'total_expired' => 5, + ]); + + $result = (new ExpireStockReservationsService($checkout, $carts))->expireOverdue(); $this->assertSame([ - 'purchases' => 0, - 'cart_reservations' => 0, - 'orphan_reservations' => 1, - 'failed' => 0, + 'purchases' => 2, + 'cart_reservations' => 3, ], $result); $this->assertDatabaseHas('stock_reservations', [ 'id' => $reservation->id, @@ -71,12 +87,19 @@ class ExpireStockReservationsServiceTest extends TestCase throw new RuntimeException('Broken reservation.'); } - $reservation->update([ - 'status' => StockReservation::STATUS_EXPIRED, - 'expires_at' => null, - 'expired_at' => now(), - ]); - }); + $logger = \Mockery::mock(LoggerInterface::class); + Log::shouldReceive('channel') + ->once() + ->with('commands') + ->andReturn($logger); + $logger->shouldReceive('error') + ->once() + ->with('Stock reservation cleanup failed.', [ + 'command' => 'reservations:expire', + 'expired_purchases' => 2, + 'expired_cart_reservations' => null, + 'exception' => $exception, + ]); $service = new ExpireStockReservationsService( \Mockery::mock(ReleaseCheckoutService::class),