refactor(stock): propagate reservation expiration

This commit is contained in:
2026-08-25 16:09:52 -03:00
parent 3c0b43fea3
commit d69a6210f6
6 changed files with 82 additions and 12 deletions

View File

@@ -38,6 +38,16 @@ class Cart extends Model
protected $table = 'carritos';
public const STATUS_ACTIVE = 'active';
public const STATUS_CHECKOUT = 'checkout';
public const STATUS_CONVERTED = 'converted';
public const STATUS_EXPIRED = 'expired';
public const STATUS_ABANDONED = 'abandoned';
public const ORIGIN_USER = 'user';
public const ORIGIN_DIRECT_CHECKOUT = 'direct_checkout';

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Cart\Services;
use App\Domains\Auth\Models\User;
use App\Domains\Cart\Models\Cart;
use App\Domains\Catalog\Exceptions\StockReservationExpiredException;
use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Services\ExpireStockReservationsService;
use App\Domains\Tenant\Models\Tenant;
@@ -126,7 +127,7 @@ class CartService
}
$cart->update([
'status' => 'abandoned',
'status' => Cart::STATUS_ABANDONED,
'current_purchase_id' => null,
]);
@@ -161,7 +162,7 @@ class CartService
{
$cart = new Cart([
'tenant_codigo' => $tenant->codigo,
'status' => 'active',
'status' => Cart::STATUS_ACTIVE,
]);
$cart->setRelation('items', collect());
@@ -262,12 +263,14 @@ class CartService
{
return Cart::query()
->where('tenant_codigo', $tenant->codigo)
->where('status', 'active')
->where('origin', Cart::ORIGIN_USER)
->whereIn('status', [Cart::STATUS_ACTIVE, Cart::STATUS_EXPIRED])
->when(
$identity['user_id'] !== null,
fn ($query) => $query->where('user_id', $identity['user_id']),
fn ($query) => $query->where('guest_token', $identity['guest_token']),
)
->orderByRaw('CASE WHEN status = ? THEN 0 ELSE 1 END', [Cart::STATUS_ACTIVE])
->first();
}
@@ -290,9 +293,18 @@ class CartService
*/
protected function findOrCreateCart(Tenant $tenant, array $identity): Cart
{
$cart = $this->findCart($tenant, $identity);
if ($cart?->status === Cart::STATUS_EXPIRED) {
throw new StockReservationExpiredException;
}
if ($cart !== null) {
return $cart;
}
$attributes = [
'tenant_codigo' => $tenant->codigo,
'status' => 'active',
'status' => Cart::STATUS_ACTIVE,
'origin' => Cart::ORIGIN_USER,
];
if ($identity['user_id'] !== null) {

View File

@@ -141,7 +141,7 @@ class ExpireStockReservationsService
$cart = Cart::query()
->whereKey($cartId)
->where('current_stock_reservation_id', $reservationId)
->where('status', 'active')
->where('status', Cart::STATUS_ACTIVE)
->lockForUpdate()
->first();
if ($cart === null) {
@@ -155,6 +155,7 @@ class ExpireStockReservationsService
}
$this->reservations->expire($reservation);
$cart->update(['status' => Cart::STATUS_EXPIRED]);
return 'cart_reservations';
});

View File

@@ -129,13 +129,25 @@ class ReleaseCheckoutService
return;
}
if ($cart->status === 'active') {
if ($targetStatus === Purchase::STATUS_EXPIRED
&& in_array($cart->status, [Cart::STATUS_ACTIVE, Cart::STATUS_CHECKOUT], true)) {
Cart::query()
->whereKey($cart->getKey())
->where('current_purchase_id', $purchase->getKey())
->where('current_stock_reservation_id', $purchase->stock_reservation_id)
->update([
'status' => Cart::STATUS_EXPIRED,
'current_purchase_id' => null,
]);
return;
}
if ($cart->status === Cart::STATUS_ACTIVE) {
$cartUpdate = [
'current_purchase_id' => null,
'current_stock_reservation_id' => null,
];
if ($targetStatus !== Purchase::STATUS_EXPIRED) {
$cartUpdate['current_stock_reservation_id'] = null;
}
Cart::query()
->whereKey($cart->getKey())
@@ -145,12 +157,12 @@ class ReleaseCheckoutService
return;
}
if ($cart->status !== 'checkout') {
if ($cart->status !== Cart::STATUS_CHECKOUT) {
return;
}
if (! $cart->trashed()) {
$cart->update(['status' => 'converted']);
$cart->update(['status' => Cart::STATUS_CONVERTED]);
$cart->delete();
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Purchase\Services\Checkout;
use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Exceptions\StockReservationExpiredException;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Catalog\Services\CatalogInventoryService;
@@ -329,7 +330,11 @@ class StartCheckoutService
throw new NotFoundHttpException('Cart not found for tenant.');
}
if ($cart->status !== 'active') {
if ($cart->status === Cart::STATUS_EXPIRED) {
throw new StockReservationExpiredException;
}
if ($cart->status !== Cart::STATUS_ACTIVE) {
throw ValidationException::withMessages([
'cart_id' => __('api.purchase.inactive_cart'),
]);

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$expiredReservationIds = fn ($query) => $query
->select('id')
->from('stock_reservations')
->where('status', 'expired');
DB::table('compras')
->whereIn('status', ['created', 'pending_payment'])
->whereIn('stock_reservation_id', $expiredReservationIds)
->update(['status' => 'expired']);
DB::table('carritos')
->whereIn('status', ['active', 'checkout'])
->whereIn('current_stock_reservation_id', $expiredReservationIds)
->update(['status' => 'expired']);
}
public function down(): void
{
// Terminal business states cannot be reversed without inventing their prior state.
}
};