fix(purchase): handle cancellation of expired purchases and update test cases

This commit is contained in:
ncoronel 2026-09-02 15:52:18 -03:00
parent 836aa1afd7
commit a19ace3115
2 changed files with 21 additions and 10 deletions

View File

@ -43,6 +43,11 @@ class ReleaseCheckoutService
): Purchase {
$purchase = $this->lockPurchase($purchase);
if ($purchase->status === Purchase::STATUS_EXPIRED
&& $targetStatus === Purchase::STATUS_CANCELLED) {
return $this->loadPurchase($purchase);
}
if ($purchase->status === Purchase::STATUS_EXPIRED
&& $targetStatus !== Purchase::STATUS_EXPIRED) {
throw new PurchaseExpiredException;
@ -72,6 +77,14 @@ class ReleaseCheckoutService
$cart = $purchase->cart()->withTrashed()->lockForUpdate()->first();
// Leaving checkout is allowed at the exact instant the reservation
// expires. Finish the expiration while holding the purchase lock so
// the request is idempotent with the scheduled expiration job.
if ($targetStatus === Purchase::STATUS_CANCELLED
&& $this->hasOverdueActiveReservation($purchase)) {
$targetStatus = Purchase::STATUS_EXPIRED;
}
if ($targetStatus === Purchase::STATUS_CANCELLED
&& $cart?->status === 'active'
&& in_array($purchase->status, [

View File

@ -10,6 +10,7 @@ use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Category;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Services\CheckoutService;
@ -714,11 +715,11 @@ class StorePurchaseTest extends TestCase
$this->actingAs($user, 'sanctum')
->postJson("/api/tenants/sonder/compras/{$purchase->id}/cancel")
->assertUnprocessable()
->assertJsonPath('code', 'purchase.expired');
->assertOk()
->assertJsonPath('data.status', Purchase::STATUS_EXPIRED);
}
public function test_an_overdue_purchase_cannot_be_cancelled_before_the_expiration_job_runs(): void
public function test_leaving_an_overdue_purchase_expires_it_before_the_expiration_job_runs(): void
{
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
$user = User::factory()->create();
@ -728,20 +729,17 @@ class StorePurchaseTest extends TestCase
$this->actingAs($user, 'sanctum')
->postJson("/api/tenants/sonder/compras/{$purchase->id}/cancel")
->assertUnprocessable()
->assertExactJson([
'code' => 'stock_reservation.expired',
'message' => __('api.cart.reservation_expired'),
]);
->assertOk()
->assertJsonPath('data.status', Purchase::STATUS_EXPIRED);
$this->assertDatabaseHas('compras', [
'id' => $purchase->id,
'status' => Purchase::STATUS_CREATED,
'status' => Purchase::STATUS_EXPIRED,
'stock_reservation_id' => $purchase->stock_reservation_id,
]);
$this->assertDatabaseHas('stock_reservations', [
'id' => $purchase->stock_reservation_id,
'status' => 'active',
'status' => StockReservation::STATUS_EXPIRED,
]);
}