diff --git a/tests/Feature/Cart/CartControllerTest.php b/tests/Feature/Cart/CartControllerTest.php index 382f96d..38a75af 100644 --- a/tests/Feature/Cart/CartControllerTest.php +++ b/tests/Feature/Cart/CartControllerTest.php @@ -190,20 +190,28 @@ class CartControllerTest extends TestCase { config()->set('catalog.stock_reservation_expiration_minutes', 30); $tenant = $this->createTenant('acme'); + $user = User::factory()->create(); $item = $this->createDirectItem($tenant, 10, '49.90'); - $response = $this->postJson('/api/tenants/acme/cart/items', [ - 'catalog_item_id' => $item->id, - 'cantidad' => 2, - ])->assertOk(); + $response = $this->actingAs($user, 'sanctum') + ->postJson('/api/tenants/acme/cart/items', [ + 'catalog_item_id' => $item->id, + 'cantidad' => 2, + ])->assertOk(); $cartId = $response->json('data.id'); $cartItemId = $response->json('data.items.0.id'); + $cart = Cart::query()->findOrFail($cartId); + $reservationId = $cart->current_stock_reservation_id; $this->artisan('reservations:expire') ->expectsOutput('Expired purchases: 0') ->expectsOutput('Expired cart reservations: 0') ->assertSuccessful(); + $this->postJson('/api/tenants/acme/cart/restart') + ->assertUnprocessable() + ->assertJsonValidationErrors('cart'); + $this->travel(31)->minutes(); $this->artisan('reservations:expire') @@ -220,10 +228,11 @@ class CartControllerTest extends TestCase $this->assertDatabaseHas('carritos', [ 'id' => $cartId, 'status' => 'active', - 'current_stock_reservation_id' => null, + 'current_stock_reservation_id' => $reservationId, 'deleted_at' => null, ]); $this->assertDatabaseHas('stock_reservations', [ + 'id' => $reservationId, 'status' => 'expired', 'expires_at' => null, ]); @@ -232,6 +241,36 @@ class CartControllerTest extends TestCase 'quantity' => 2, ]); + $this->postJson('/api/tenants/acme/cart/items', [ + 'catalog_item_id' => $item->id, + 'cantidad' => 1, + ]) + ->assertUnprocessable() + ->assertExactJson([ + 'code' => 'stock_reservation.expired', + 'message' => __('api.cart.reservation_expired'), + ]); + + $restart = $this->postJson('/api/tenants/acme/cart/restart') + ->assertOk() + ->assertJsonPath('code', 'cart.restarted') + ->assertJsonPath('data.items', []) + ->assertJsonMissingPath('data.stock_reservation') + ->assertJsonMissingPath('data.current_stock_reservation_id'); + $newCartId = $restart->json('data.id'); + + $this->assertNotSame($cartId, $newCartId); + $this->assertDatabaseHas('carritos', [ + 'id' => $cartId, + 'status' => 'abandoned', + 'current_stock_reservation_id' => $reservationId, + ]); + $this->assertDatabaseHas('carritos', [ + 'id' => $newCartId, + 'status' => 'active', + 'current_stock_reservation_id' => null, + ]); + $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 e882f57..5ea8732 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -71,6 +71,43 @@ 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'); @@ -480,6 +517,7 @@ class StorePurchaseTest extends TestCase $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 3); + $reservationId = $purchase->stock_reservation_id; $activeCart = Cart::query() ->where('user_id', $user->id) ->where('status', 'active') @@ -513,7 +551,11 @@ class StorePurchaseTest extends TestCase 'reserved_stock' => 3, ]); $activeCart->refresh(); - $this->assertNotNull($activeCart->current_stock_reservation_id); + $this->assertSame($reservationId, $activeCart->current_stock_reservation_id); + $this->assertDatabaseHas('compras', [ + 'id' => $purchase->id, + 'stock_reservation_id' => null, + ]); $this->assertDatabaseHas('stock_reservations', [ 'id' => $activeCart->current_stock_reservation_id, 'status' => 'active', @@ -526,7 +568,7 @@ class StorePurchaseTest extends TestCase $this->assertSame(1, $activeCart->items()->count()); } - public function test_it_reassigns_a_terminal_purchase_reservation_and_rejects_a_late_confirmation(): void + public function test_it_reuses_the_cart_reservation_for_a_new_checkout_and_rejects_a_late_confirmation(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); @@ -554,11 +596,15 @@ class StorePurchaseTest extends TestCase 'id' => $cart->id, 'current_purchase_id' => $currentPurchase->id, ]); - $this->assertNotSame($previousReservationId, $currentPurchase->stock_reservation_id); + $this->assertSame($previousReservationId, $currentPurchase->stock_reservation_id); + $this->assertDatabaseHas('compras', [ + 'id' => $previousPurchase->id, + 'stock_reservation_id' => null, + ]); $this->assertDatabaseHas('stock_reservations', [ 'id' => $previousReservationId, - 'status' => 'released', - 'release_reason' => 'purchase_superseded', + 'status' => 'active', + 'release_reason' => null, ]); $this->assertPurchaseReservation($currentPurchase->id, $variant->inventory_id, 2, 'active'); @@ -604,7 +650,7 @@ class StorePurchaseTest extends TestCase ]); $this->assertDatabaseHas('stock_reservations', [ 'status' => 'released', - 'release_reason' => 'purchase_superseded', + 'release_reason' => 'cart_empty', ]); $this->assertDatabaseHas('stock_reservation_lines', [ 'inventory_id' => $variant->inventory_id, @@ -654,6 +700,38 @@ 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/{$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