Squashed commit of the following:
commit1dc4e29c69Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 13:58:03 2026 -0300 refactor(reservations): unify expiration command commit093e894cc3Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 13:48:09 2026 -0300 feat(cart): expire abandoned stock reservations commitfdf0f3328fAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:53:12 2026 -0300 refactor(stock): implement expiration for stock reservations and add configuration commit8d6bcdcc43Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:38:21 2026 -0300 refactor(cart): invalidate payment on actual changes commit3206e293ebAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:24:48 2026 -0300 refactor(cart): own checkout item editing commitaed99bd05eAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:14:57 2026 -0300 refactor(checkout): remove legacy purchase item reservations commitf1649e0e4bAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:06:29 2026 -0300 refactor(checkout): materialize purchase items on confirmation commite6c4b40a37Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:06:19 2026 -0300 feat(inventory): add traceable cart stock reservations
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Domains\Cart\Models\CartItem;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Catalog\Models\Variant;
|
||||
use App\Domains\Catalog\Services\CatalogInventoryService;
|
||||
use App\Domains\Catalog\Services\StockReservationService;
|
||||
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||
use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Purchase\Services\UserPurchaseLimitService;
|
||||
@@ -20,9 +21,9 @@ class StartCheckoutService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CatalogInventoryService $inventory,
|
||||
private readonly StockReservationService $reservations,
|
||||
private readonly UserPurchaseLimitService $purchaseLimits,
|
||||
private readonly CatalogSelectionResolver $selections,
|
||||
private readonly PurchaseItemSnapshotFactory $snapshots,
|
||||
private readonly InsufficientStockMessageBuilder $stockMessages,
|
||||
) {}
|
||||
|
||||
@@ -144,9 +145,24 @@ class StartCheckoutService
|
||||
throw new InsufficientStockException($unavailableItems);
|
||||
}
|
||||
|
||||
$cart = Cart::query()->create([
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'user_id' => $userId,
|
||||
'guest_token' => null,
|
||||
'status' => 'checkout',
|
||||
'origin' => Cart::ORIGIN_DIRECT_CHECKOUT,
|
||||
]);
|
||||
|
||||
$cartItems = collect();
|
||||
foreach ($resolvedLines as $line) {
|
||||
$cartItem = $cart->items()->create([
|
||||
'catalog_item_id' => $line['catalog_item_id'],
|
||||
'variant_id' => $line['variant_id'],
|
||||
'cantidad' => $line['quantity'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->inventory->reserve($line['selection'], $line['quantity']);
|
||||
$this->reservations->reserve($cartItem, $line['selection'], $line['quantity']);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$availableQuantity = $this->inventory->availableQuantity($line['selection']) ?? 0;
|
||||
|
||||
@@ -154,6 +170,10 @@ class StartCheckoutService
|
||||
$this->unavailableItem($line, $availableQuantity),
|
||||
]);
|
||||
}
|
||||
|
||||
$cartItem->setRelation('catalogItem', $line['catalog_item']);
|
||||
$cartItem->setRelation('variant', $line['selection'] instanceof Variant ? $line['selection'] : null);
|
||||
$cartItems->push($cartItem);
|
||||
}
|
||||
|
||||
$purchase = $this->createPurchase(
|
||||
@@ -163,19 +183,16 @@ class StartCheckoutService
|
||||
(float) $resolvedLines->sum(
|
||||
fn (array $line): float => $line['selection']->getPrice() * $line['quantity'],
|
||||
),
|
||||
null,
|
||||
$cart->getKey(),
|
||||
);
|
||||
|
||||
$directCartItems = $resolvedLines->map(fn (array $line): CartItem => $this->makeDirectCartItem(
|
||||
$line['selection'],
|
||||
$line['catalog_item_id'],
|
||||
$line['variant_id'],
|
||||
$line['quantity'],
|
||||
));
|
||||
|
||||
$purchase->items()->createMany(
|
||||
$this->snapshots->fromCartItems($directCartItems),
|
||||
);
|
||||
foreach ($cartItems as $index => $cartItem) {
|
||||
$this->reservations->attachToPurchase(
|
||||
$cartItem,
|
||||
$resolvedLines->get($index)['selection'],
|
||||
$purchase,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->loadPurchase($purchase);
|
||||
}
|
||||
@@ -235,7 +252,13 @@ class StartCheckoutService
|
||||
$cart->getTotalAmount(),
|
||||
$cart->getKey(),
|
||||
);
|
||||
$purchase->items()->createMany($this->snapshots->fromCartItems($cartItems));
|
||||
foreach ($cartItems as $cartItem) {
|
||||
$this->reservations->attachToPurchase(
|
||||
$cartItem,
|
||||
$cartItem->selectedItem(),
|
||||
$purchase,
|
||||
);
|
||||
}
|
||||
|
||||
// The purchase owns the reservation until checkout finishes. The cart is
|
||||
// retained so it can be restored if the purchase is cancelled or expires.
|
||||
@@ -336,35 +359,6 @@ class StartCheckoutService
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeDirectCartItem(
|
||||
CatalogItem|Variant $selection,
|
||||
int $catalogItemId,
|
||||
?int $variantId,
|
||||
int $quantity,
|
||||
): CartItem {
|
||||
$catalogItem = $selection instanceof Variant ? $selection->catalogItem : $selection;
|
||||
$catalogItem->loadMissing(['inventory', 'attachments']);
|
||||
|
||||
if ($selection instanceof Variant) {
|
||||
$selection->loadMissing([
|
||||
'inventory',
|
||||
'attachments',
|
||||
'catalogItem',
|
||||
'definitions.itemAttribute.attribute',
|
||||
]);
|
||||
}
|
||||
|
||||
$item = new CartItem([
|
||||
'catalog_item_id' => $catalogItemId,
|
||||
'variant_id' => $variantId,
|
||||
'cantidad' => $quantity,
|
||||
]);
|
||||
$item->setRelation('catalogItem', $catalogItem);
|
||||
$item->setRelation('variant', $selection instanceof Variant ? $selection : null);
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/** @param Collection<int, CartItem> $cartItems */
|
||||
private function loadCartItems(Collection $cartItems): void
|
||||
{
|
||||
@@ -382,6 +376,15 @@ class StartCheckoutService
|
||||
|
||||
private function loadPurchase(Purchase $purchase): Purchase
|
||||
{
|
||||
return $purchase->load(['items.imageAttachment']);
|
||||
return $purchase->load([
|
||||
'items.imageAttachment',
|
||||
'cart.items.catalogItem.inventory',
|
||||
'cart.items.catalogItem.attachments',
|
||||
'cart.items.variant.inventory',
|
||||
'cart.items.variant.attachments',
|
||||
'cart.items.variant.definitions.itemAttribute.attribute',
|
||||
'cart.items.variant.eventDates',
|
||||
'cart.items.variant.eventDate',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user