inventory->reserve($selection, $quantity); $this->recordIncrease($cartItem, $selection, $quantity); }); } public function release( CartItem $cartItem, CatalogItem|Variant $selection, int $quantity, string $releasedStatus = StockReservation::STATUS_RELEASED, ): void { DB::transaction(function () use ($cartItem, $selection, $quantity, $releasedStatus): void { $this->ensure($cartItem, $selection); $this->inventory->release($selection, $quantity); $this->recordDecrease($cartItem, $selection, $quantity, $releasedStatus); }); } public function commit(CartItem $cartItem, CatalogItem|Variant $selection): void { DB::transaction(function () use ($cartItem, $selection): void { $this->ensure($cartItem, $selection); $this->inventory->commit($selection, (int) $cartItem->cantidad); $requirements = $this->inventory->requirementsFor($selection, (int) $cartItem->cantidad); foreach ($requirements as $inventoryId => $quantity) { $reservation = $this->lockReservation($cartItem, $inventoryId); if ($reservation === null || $reservation->status !== StockReservation::STATUS_ACTIVE || $reservation->quantity !== $quantity) { throw new \InvalidArgumentException('La reserva de stock no coincide con el item del carrito.'); } $reservation->update([ 'status' => StockReservation::STATUS_COMMITTED, 'committed_at' => now(), 'expires_at' => null, ]); } }); } public function ensure(CartItem $cartItem, CatalogItem|Variant $selection): void { $requirements = $this->inventory->requirementsFor($selection, (int) $cartItem->cantidad); foreach ($requirements as $inventoryId => $quantity) { $reservation = $this->lockReservation($cartItem, $inventoryId); if ($reservation === null) { StockReservation::query()->create([ 'inventory_id' => $inventoryId, 'cart_item_id' => $cartItem->getKey(), 'quantity' => $quantity, 'status' => StockReservation::STATUS_ACTIVE, 'expires_at' => $this->expiration(), ]); continue; } if ($reservation->status !== StockReservation::STATUS_ACTIVE || $reservation->quantity !== $quantity) { $reservation->update([ 'quantity' => $quantity, 'status' => StockReservation::STATUS_ACTIVE, 'committed_at' => null, 'released_at' => null, 'expires_at' => $this->expiration(), ]); } } } public function attachToPurchase( CartItem $cartItem, CatalogItem|Variant $selection, Purchase $purchase, ): void { DB::transaction(function () use ($cartItem, $selection, $purchase): void { $this->ensure($cartItem, $selection); StockReservation::query() ->where('cart_item_id', $cartItem->getKey()) ->where('status', StockReservation::STATUS_ACTIVE) ->update([ 'purchase_id' => $purchase->getKey(), 'expires_at' => $purchase->expires_at, ]); }); } public function detachFromPurchase(Purchase $purchase): void { StockReservation::query() ->where('purchase_id', $purchase->getKey()) ->where('status', StockReservation::STATUS_ACTIVE) ->update([ 'purchase_id' => null, 'expires_at' => $this->expiration(), ]); } public function syncPurchaseExpiration(Purchase $purchase): void { StockReservation::query() ->where('purchase_id', $purchase->getKey()) ->where('status', StockReservation::STATUS_ACTIVE) ->update(['expires_at' => $purchase->expires_at]); } public function transfer(CartItem $source, CartItem $target): void { DB::transaction(function () use ($source, $target): void { $sourceReservations = StockReservation::query() ->where('cart_item_id', $source->getKey()) ->where('status', StockReservation::STATUS_ACTIVE) ->orderBy('inventory_id') ->lockForUpdate() ->get(); foreach ($sourceReservations as $sourceReservation) { $targetReservation = $this->lockReservation($target, (int) $sourceReservation->inventory_id); if ($targetReservation === null) { $sourceItemQuantity = (int) $source->cantidad; $targetItemQuantity = (int) $target->fresh()->cantidad; $perItemQuantity = intdiv((int) $sourceReservation->quantity, $sourceItemQuantity); $sourceReservation->update([ 'cart_item_id' => $target->getKey(), 'purchase_id' => null, 'quantity' => $perItemQuantity * $targetItemQuantity, 'expires_at' => $this->expiration(), ]); continue; } $targetReservation->update([ 'quantity' => $targetReservation->quantity + $sourceReservation->quantity, 'status' => StockReservation::STATUS_ACTIVE, 'expires_at' => $this->expiration(), ]); $sourceReservation->delete(); } }); } private function recordIncrease(CartItem $cartItem, CatalogItem|Variant $selection, int $quantity): void { foreach ($this->inventory->requirementsFor($selection, $quantity) as $inventoryId => $requiredQuantity) { $reservation = $this->lockReservation($cartItem, $inventoryId); if ($reservation === null) { StockReservation::query()->create([ 'inventory_id' => $inventoryId, 'cart_item_id' => $cartItem->getKey(), 'quantity' => $requiredQuantity, 'status' => StockReservation::STATUS_ACTIVE, 'expires_at' => $this->expiration(), ]); continue; } $reservation->update([ 'quantity' => ($reservation->status === StockReservation::STATUS_ACTIVE ? $reservation->quantity : 0) + $requiredQuantity, 'status' => StockReservation::STATUS_ACTIVE, 'committed_at' => null, 'released_at' => null, 'expires_at' => $this->expiration(), ]); } } private function recordDecrease( CartItem $cartItem, CatalogItem|Variant $selection, int $quantity, string $releasedStatus, ): void { foreach ($this->inventory->requirementsFor($selection, $quantity) as $inventoryId => $requiredQuantity) { $reservation = $this->lockReservation($cartItem, $inventoryId); if ($reservation === null || $reservation->status !== StockReservation::STATUS_ACTIVE || $reservation->quantity < $requiredQuantity) { throw new \InvalidArgumentException('La reserva de stock no alcanza para liberar la cantidad solicitada.'); } $remaining = $reservation->quantity - $requiredQuantity; $reservation->update([ 'quantity' => $remaining, 'status' => $remaining === 0 ? $releasedStatus : StockReservation::STATUS_ACTIVE, 'released_at' => $remaining === 0 ? now() : null, 'expires_at' => $remaining === 0 ? null : $reservation->expires_at, ]); } } private function lockReservation(CartItem $cartItem, int $inventoryId): ?StockReservation { return StockReservation::query() ->where('cart_item_id', $cartItem->getKey()) ->where('inventory_id', $inventoryId) ->lockForUpdate() ->first(); } private function expiration(): Carbon { return now()->addMinutes( max(1, (int) config('catalog.stock_reservation_expiration_minutes', 30)), ); } }