$customerData */ public function updateCustomer(Purchase $purchase, array $customerData): Purchase { return DB::transaction(function () use ($purchase, $customerData): Purchase { $purchase = $this->lockPurchase($purchase); $this->assertEditable($purchase); $purchase->update($customerData); return $this->loadPurchase($purchase); }); } public function updateItemQuantity( Purchase $purchase, PurchaseItem $purchaseItem, int $quantity, ): Purchase { return DB::transaction(function () use ($purchase, $purchaseItem, $quantity): Purchase { $purchase = $this->lockPurchase($purchase); if ($purchase->status !== Purchase::STATUS_CREATED || $this->hasExpired($purchase)) { throw ValidationException::withMessages([ 'purchase' => __('api.purchase.not_editable'), ]); } $purchaseItem = $this->lockPurchaseItem($purchase, $purchaseItem); $difference = $quantity - (int) $purchaseItem->cantidad; if ($difference !== 0) { $this->adjustReservation($purchase, $purchaseItem, $quantity, $difference); $purchaseItem->update([ 'cantidad' => $quantity, 'total' => (float) $purchaseItem->precio_unitario * $quantity, ]); $this->sourceCart->syncItemQuantity($purchase, $purchaseItem, $quantity); } $purchase->update([ 'total' => $purchase->calculateCurrentTotalAmount(), ]); return $this->loadPurchase($purchase); }); } public function prepareItemEditing(Purchase $purchase): Purchase { return DB::transaction(function () use ($purchase): Purchase { $purchase = $this->lockPurchase($purchase); $this->assertEditable($purchase); $purchase->telepagosQr()->delete(); $purchase->update([ 'status' => Purchase::STATUS_CREATED, 'payment_method' => null, 'transfer_payer_dni' => null, 'expires_at' => now()->addMinutes( max(1, (int) config('purchase.checkout_expiration_minutes', 30)), ), ]); return $this->loadPurchase($purchase); }); } private function adjustReservation( Purchase $purchase, PurchaseItem $purchaseItem, int $quantity, int $difference, ): void { $selection = $this->selections->resolvePurchaseItem($purchase->tenant, $purchaseItem); try { if ($difference > 0) { $otherItemQuantity = (int) $purchase->items() ->where('source_catalog_item_id', $purchaseItem->source_catalog_item_id) ->whereKeyNot($purchaseItem->getKey()) ->sum('cantidad'); $catalogItem = $selection instanceof Variant ? $selection->catalogItem : $selection; $this->purchaseLimits->assertCanPurchase( $catalogItem, (int) $purchase->user_id, $otherItemQuantity + $quantity, $purchase->getKey(), ); $this->inventory->reserve($selection, $difference); } else { $this->inventory->release($selection, abs($difference)); } } catch (\InvalidArgumentException) { throw ValidationException::withMessages([ 'quantity' => __('api.purchase.insufficient_stock'), ]); } } private function lockPurchaseItem(Purchase $purchase, PurchaseItem $item): PurchaseItem { /** @var PurchaseItem|null $lockedItem */ $lockedItem = $purchase->items() ->whereKey($item->getKey()) ->lockForUpdate() ->first(); if ($lockedItem === null) { throw new NotFoundHttpException('Purchase item not found.'); } if ($lockedItem->reservation_status !== PurchaseItem::RESERVATION_ACTIVE) { throw ValidationException::withMessages([ 'item' => __('api.purchase.item_not_editable'), ]); } return $lockedItem; } private function assertEditable(Purchase $purchase): void { if ( ! in_array($purchase->status, [ Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT, ], true) || $this->hasExpired($purchase) ) { throw ValidationException::withMessages([ 'purchase' => __('api.purchase.not_editable'), ]); } } private function hasExpired(Purchase $purchase): bool { return $purchase->expires_at !== null && $purchase->expires_at->isPast(); } 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']); } }