lockPurchase($purchase); $this->purchaseState->assertNotExpired($purchase); if ($purchase->payment_method === null) { throw ValidationException::withMessages([ 'payment_method' => __('api.purchase.payment_method_required'), ]); } if ($this->isTerminal($purchase)) { return $this->loadPurchase($purchase); } $purchase->update([ 'status' => Purchase::STATUS_PENDING_PAYMENT, 'total' => $purchase->calculateCurrentTotalAmount(), ]); return $this->loadPurchase($purchase); }); } public function submitForReview(Purchase $purchase): Purchase { return DB::transaction(function () use ($purchase): Purchase { $purchase = $this->lockPurchase($purchase); $this->purchaseState->assertNotExpired($purchase); if ($purchase->status === Purchase::STATUS_PAID) { return $this->loadPurchase($purchase); } if ( $purchase->status !== Purchase::STATUS_PENDING_PAYMENT || ($purchase->expires_at !== null && $purchase->expires_at->isPast()) ) { throw ValidationException::withMessages([ 'purchase' => __('api.purchase.not_available_for_review'), ]); } $purchase->update(['expires_at' => null]); $this->reservations->syncPurchaseExpiration($purchase); return $this->loadPurchase($purchase); }); } public function confirm(Purchase $purchase): void { DB::transaction(function () use ($purchase): void { $purchase = $this->lockPurchase($purchase); $this->purchaseState->assertNotExpired($purchase); if ($purchase->status === Purchase::STATUS_PAID) { return; } if (in_array($purchase->status, [ Purchase::STATUS_CANCELLED, Purchase::STATUS_REJECTED, Purchase::STATUS_EXPIRED, ], true)) { throw ValidationException::withMessages([ 'purchase' => __('api.purchase.cannot_confirm'), ]); } if (! $purchase->items()->exists()) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), ]); } $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'), ]); } $cartItems = $cart->items()->orderBy('id')->lockForUpdate()->get(); if ($cartItems->isEmpty()) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), ]); } $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); foreach ($cartItems as $cartItem) { $selection = $cartItem->selectedItem(); if ($selection === null) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), ]); } try { $this->reservations->commit($cartItem, $selection); } catch (\InvalidArgumentException) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), ]); } } $this->sourceCart->finalize($purchase); }); } private function isTerminal(Purchase $purchase): bool { return in_array($purchase->status, [ Purchase::STATUS_PAID, Purchase::STATUS_CANCELLED, Purchase::STATUS_REJECTED, Purchase::STATUS_EXPIRED, ], true); } private function lockPurchase(Purchase $purchase): Purchase { /** @var Purchase */ return Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey()); } private function loadPurchase(Purchase $purchase): Purchase { return $purchase->load(['items.imageAttachment']); } private function itemKey(int $catalogItemId, ?int $variantId): string { return $catalogItemId.':'.($variantId ?? 'none'); } /** @param Collection $cartItems */ private function loadCartItems(Collection $cartItems): void { $cartItems->load([ 'catalogItem.inventory', 'catalogItem.attachments', 'variant.inventory', 'variant.attachments', 'variant.catalogItem', 'variant.definitions.itemAttribute.attribute', 'variant.eventDates', 'variant.eventDate', ]); } }