refactor(stock): make expired reservations terminal

This commit is contained in:
2026-08-25 15:58:41 -03:00
parent d612b7a118
commit f6f138e180
9 changed files with 164 additions and 33 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Catalog\Services;
use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Exceptions\StockReservationExpiredException;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Models\StockReservationLine;
@@ -43,13 +44,8 @@ class StockReservationService
? null
: StockReservation::query()->lockForUpdate()->find($lockedCart->current_stock_reservation_id);
if ($reservation !== null
&& $reservation->status === StockReservation::STATUS_ACTIVE
&& $reservation->expires_at !== null
&& $reservation->expires_at->isPast()) {
$this->finalizeLocked($reservation, StockReservation::STATUS_EXPIRED, null);
$lockedCart->update(['current_stock_reservation_id' => null]);
$reservation = null;
if ($reservation !== null) {
$this->assertUsableCartReservation($reservation);
}
if ($requirements === []) {
@@ -67,7 +63,7 @@ class StockReservationService
return null;
}
if ($reservation === null || $reservation->status !== StockReservation::STATUS_ACTIVE) {
if ($reservation === null) {
$reservation = StockReservation::query()->create([
'status' => StockReservation::STATUS_ACTIVE,
'expires_at' => $this->expiration(),
@@ -175,9 +171,7 @@ class StockReservationService
/** @var StockReservation $reservation */
$reservation = StockReservation::query()->lockForUpdate()->findOrFail($lockedCart->current_stock_reservation_id);
if ($reservation->status !== StockReservation::STATUS_ACTIVE) {
throw new \InvalidArgumentException('La reserva de stock no está activa.');
}
$this->assertUsableCartReservation($reservation);
$linkedPurchase = Purchase::query()
->where('stock_reservation_id', $reservation->getKey())
@@ -213,6 +207,9 @@ class StockReservationService
if ($reservation->status !== StockReservation::STATUS_ACTIVE) {
throw new \InvalidArgumentException('La reserva de stock no está activa.');
}
if ($reservation->expires_at !== null && ! $reservation->expires_at->isFuture()) {
throw new StockReservationExpiredException;
}
$lines = $this->lockLines($reservation);
if ($lines->isEmpty()) {
@@ -267,6 +264,50 @@ class StockReservationService
});
}
public function returnToCart(Purchase $purchase, Cart $cart): StockReservation
{
return DB::transaction(function () use ($purchase, $cart): StockReservation {
/** @var Purchase $purchase */
$purchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());
/** @var Cart $cart */
$cart = Cart::query()->lockForUpdate()->findOrFail($cart->getKey());
if ($purchase->stock_reservation_id === null
|| $cart->current_stock_reservation_id !== $purchase->stock_reservation_id) {
throw new \InvalidArgumentException('La compra y el carrito no comparten la reserva activa.');
}
/** @var StockReservation $reservation */
$reservation = StockReservation::query()
->lockForUpdate()
->findOrFail($purchase->stock_reservation_id);
$this->assertUsableCartReservation($reservation);
$purchase->update(['stock_reservation_id' => null]);
$cart->update(['current_purchase_id' => null]);
$reservation->update(['expires_at' => $this->expiration()]);
return $reservation->fresh('lines');
});
}
public function assertCartReservationUsable(Cart $cart): void
{
DB::transaction(function () use ($cart): void {
/** @var Cart $cart */
$cart = Cart::query()->lockForUpdate()->findOrFail($cart->getKey());
if ($cart->current_stock_reservation_id === null) {
return;
}
/** @var StockReservation $reservation */
$reservation = StockReservation::query()
->lockForUpdate()
->findOrFail($cart->current_stock_reservation_id);
$this->assertUsableCartReservation($reservation);
});
}
public function releaseCurrentCartReservation(
Cart $cart,
string $reason = self::REASON_CART_CHANGED,
@@ -318,6 +359,9 @@ class StockReservationService
if ($reservation->status !== StockReservation::STATUS_ACTIVE) {
throw new \InvalidArgumentException('La reserva de stock no está activa.');
}
if ($reservation->expires_at !== null && ! $reservation->expires_at->isFuture()) {
throw new StockReservationExpiredException;
}
$reservation->update(['expires_at' => $expiresAt]);
});
@@ -420,9 +464,24 @@ class StockReservationService
'release_reason' => $status === StockReservation::STATUS_RELEASED ? $reason : null,
]);
Cart::query()
->where('current_stock_reservation_id', $reservation->getKey())
->update(['current_stock_reservation_id' => null]);
if ($status === StockReservation::STATUS_RELEASED) {
Cart::query()
->where('current_stock_reservation_id', $reservation->getKey())
->update(['current_stock_reservation_id' => null]);
}
}
private function assertUsableCartReservation(StockReservation $reservation): void
{
if ($reservation->status === StockReservation::STATUS_EXPIRED
|| ($reservation->expires_at !== null && ! $reservation->expires_at->isFuture())) {
throw new StockReservationExpiredException;
}
if ($reservation->status !== StockReservation::STATUS_ACTIVE
|| $reservation->expires_at === null) {
throw new \InvalidArgumentException('La reserva de stock no está disponible para operar el carrito.');
}
}
private function expiration(): Carbon