refactor(checkout): materialize purchase item snapshots at start
This commit is contained in:
@@ -15,7 +15,6 @@ class CompleteCheckoutService
|
||||
public function __construct(
|
||||
private readonly StockReservationService $reservations,
|
||||
private readonly SourceCartService $sourceCart,
|
||||
private readonly PurchaseItemSnapshotFactory $snapshots,
|
||||
private readonly PurchaseStateGuard $purchaseState,
|
||||
) {}
|
||||
|
||||
@@ -90,18 +89,17 @@ class CompleteCheckoutService
|
||||
]);
|
||||
}
|
||||
|
||||
if ($purchase->items()->exists()) {
|
||||
$cart = $purchase->cart()->withTrashed()->lockForUpdate()->first();
|
||||
if ($cart?->status === 'converted' && $cart->trashed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $purchase->items()->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => __('api.purchase.inconsistent_reservation'),
|
||||
]);
|
||||
}
|
||||
|
||||
$cart = $purchase->cart()->lockForUpdate()->first();
|
||||
$cart = $purchase->cart()->withTrashed()->lockForUpdate()->first();
|
||||
if ($cart?->status === 'converted' && $cart->trashed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($cart === null || $cart->status !== 'checkout') {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => __('api.purchase.inconsistent_reservation'),
|
||||
@@ -115,10 +113,32 @@ class CompleteCheckoutService
|
||||
]);
|
||||
}
|
||||
|
||||
$snapshotQuantities = $purchase->items()
|
||||
->lockForUpdate()
|
||||
->get(['source_catalog_item_id', 'source_variant_id', 'cantidad'])
|
||||
->groupBy(fn ($item): string => $this->itemKey(
|
||||
(int) $item->source_catalog_item_id,
|
||||
$item->source_variant_id === null ? null : (int) $item->source_variant_id,
|
||||
))
|
||||
->map(fn (Collection $items): int => (int) $items->sum('cantidad'))
|
||||
->sortKeys()
|
||||
->all();
|
||||
$cartQuantities = $cartItems
|
||||
->groupBy(fn (CartItem $item): string => $this->itemKey(
|
||||
(int) $item->catalog_item_id,
|
||||
$item->variant_id === null ? null : (int) $item->variant_id,
|
||||
))
|
||||
->map(fn (Collection $items): int => (int) $items->sum('cantidad'))
|
||||
->sortKeys()
|
||||
->all();
|
||||
|
||||
if ($snapshotQuantities !== $cartQuantities) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => __('api.purchase.inconsistent_reservation'),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->loadCartItems($cartItems);
|
||||
$purchase->items()->createMany(
|
||||
$this->snapshots->fromCartItems($cartItems),
|
||||
);
|
||||
|
||||
foreach ($cartItems as $cartItem) {
|
||||
$selection = $cartItem->selectedItem();
|
||||
@@ -159,16 +179,12 @@ class CompleteCheckoutService
|
||||
|
||||
private function loadPurchase(Purchase $purchase): Purchase
|
||||
{
|
||||
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',
|
||||
]);
|
||||
return $purchase->load(['items.imageAttachment']);
|
||||
}
|
||||
|
||||
private function itemKey(int $catalogItemId, ?int $variantId): string
|
||||
{
|
||||
return $catalogItemId.':'.($variantId ?? 'none');
|
||||
}
|
||||
|
||||
/** @param Collection<int, CartItem> $cartItems */
|
||||
|
||||
@@ -8,51 +8,6 @@ class PurchaseResponseLoader
|
||||
{
|
||||
public function load(Purchase $purchase): Purchase
|
||||
{
|
||||
$purchase->load(['tenant', 'items.imageAttachment']);
|
||||
|
||||
if (
|
||||
$purchase->cart_id !== null
|
||||
&& (
|
||||
in_array($purchase->status, [
|
||||
Purchase::STATUS_CREATED,
|
||||
Purchase::STATUS_PENDING_PAYMENT,
|
||||
], true)
|
||||
|| $purchase->items->isEmpty()
|
||||
)
|
||||
) {
|
||||
$purchase->load([
|
||||
'cart.items.catalogItem.inventory',
|
||||
'cart.items.catalogItem.attachments',
|
||||
'cart.items.variant.inventory',
|
||||
'cart.items.variant.attachments',
|
||||
'cart.items.variant.catalogItem',
|
||||
'cart.items.variant.definitions.itemAttribute.attribute',
|
||||
'cart.items.variant.eventDates',
|
||||
'cart.items.variant.eventDate',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $purchase->tenant->checkout_editing_policy->allowsVariantChanges()) {
|
||||
return $purchase;
|
||||
}
|
||||
|
||||
$purchase->load([
|
||||
'items.sourceCatalogItem.itemAttributes.attribute',
|
||||
'items.sourceCatalogItem.variants' => fn ($query) => $query->orderBy('id'),
|
||||
'items.sourceCatalogItem.variants.inventory',
|
||||
'items.sourceCatalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'),
|
||||
'items.sourceCatalogItem.variants.definitions.itemAttribute.attribute.options',
|
||||
'items.sourceCatalogItem.variants.eventDates',
|
||||
'items.sourceCatalogItem.variants.eventDate',
|
||||
'cart.items.catalogItem.itemAttributes.attribute',
|
||||
'cart.items.catalogItem.variants' => fn ($query) => $query->orderBy('id'),
|
||||
'cart.items.catalogItem.variants.inventory',
|
||||
'cart.items.catalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'),
|
||||
'cart.items.catalogItem.variants.definitions.itemAttribute.attribute.options',
|
||||
'cart.items.catalogItem.variants.eventDates',
|
||||
'cart.items.catalogItem.variants.eventDate',
|
||||
]);
|
||||
|
||||
return $purchase;
|
||||
return $purchase->load(['tenant', 'items.imageAttachment']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +96,6 @@ class ReleaseCheckoutService
|
||||
return $this->loadPurchase($purchase);
|
||||
}
|
||||
|
||||
if ($purchase->items()->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => __('api.purchase.inconsistent_reservation'),
|
||||
]);
|
||||
}
|
||||
|
||||
$reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase);
|
||||
$this->releaseCartReservations($purchase, $reservationReturnedToCart, $targetStatus);
|
||||
|
||||
@@ -181,15 +175,6 @@ class ReleaseCheckoutService
|
||||
|
||||
private function loadPurchase(Purchase $purchase): Purchase
|
||||
{
|
||||
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',
|
||||
]);
|
||||
return $purchase->load(['items.imageAttachment']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class StartCheckoutService
|
||||
private readonly CatalogSelectionResolver $selections,
|
||||
private readonly InsufficientStockMessageBuilder $stockMessages,
|
||||
private readonly PurchaseResponseLoader $responses,
|
||||
private readonly PurchaseItemSnapshotFactory $snapshots,
|
||||
) {}
|
||||
|
||||
/** @param array<string, mixed> $purchaseData */
|
||||
@@ -193,6 +194,10 @@ class StartCheckoutService
|
||||
$cart->getKey(),
|
||||
);
|
||||
|
||||
$cartItems = $cart->items()->orderBy('id')->lockForUpdate()->get();
|
||||
$this->loadCartItems($cartItems);
|
||||
$purchase->items()->createMany($this->snapshots->fromCartItems($cartItems));
|
||||
|
||||
foreach ($cartItems as $index => $cartItem) {
|
||||
$this->reservations->attachToPurchase(
|
||||
$cartItem,
|
||||
@@ -259,6 +264,8 @@ class StartCheckoutService
|
||||
$cart->getTotalAmount(),
|
||||
$cart->getKey(),
|
||||
);
|
||||
$purchase->items()->createMany($this->snapshots->fromCartItems($cartItems));
|
||||
|
||||
foreach ($cartItems as $cartItem) {
|
||||
$this->reservations->attachToPurchase(
|
||||
$cartItem,
|
||||
|
||||
Reference in New Issue
Block a user