$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 updateItem( Purchase $purchase, PurchaseItem $purchaseItem, ?int $quantity, ?int $variantId, bool $updateVariant, ): Purchase { return DB::transaction(function () use ( $purchase, $purchaseItem, $quantity, $variantId, $updateVariant, ): 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); $tenant = $purchase->tenant()->firstOrFail(); $finalQuantity = $quantity ?? (int) $purchaseItem->cantidad; if ($quantity !== null && ! $tenant->cart_editing_policy->allowsQuantityChanges()) { throw ValidationException::withMessages([ 'quantity' => __('api.cart.editing_disabled'), ]); } if ($updateVariant && ! $tenant->cart_editing_policy->allowsVariantChanges()) { throw ValidationException::withMessages([ 'variant_id' => __('api.purchase.variant_change_disabled'), ]); } if ($updateVariant && $variantId !== $purchaseItem->source_variant_id) { $this->changeItemVariant( $purchase, $purchaseItem, (int) $variantId, $finalQuantity, ); } else { $difference = $finalQuantity - (int) $purchaseItem->cantidad; if ($difference !== 0) { $this->adjustReservation($purchase, $purchaseItem, $finalQuantity, $difference); $purchaseItem->update([ 'cantidad' => $finalQuantity, 'total' => (float) $purchaseItem->precio_unitario * $finalQuantity, ]); $this->sourceCart->syncItemQuantity($purchase, $purchaseItem, $finalQuantity); } } $purchase->update([ 'total' => $purchase->calculateCurrentTotalAmount(), ]); return $this->loadPurchase($purchase); }); } private function changeItemVariant( Purchase $purchase, PurchaseItem $sourceItem, int $variantId, int $quantity, ): void { $tenant = $purchase->tenant()->firstOrFail(); $currentSelection = $this->selections->resolvePurchaseItem($tenant, $sourceItem); $targetSelection = $this->selections->resolve( $tenant, (int) $sourceItem->source_catalog_item_id, $variantId, 'item', ); if (! $targetSelection instanceof Variant) { throw ValidationException::withMessages([ 'variant_id' => __('api.cart.variant_required'), ]); } /** @var PurchaseItem|null $targetItem */ $targetItem = $purchase->items() ->where('source_catalog_item_id', $sourceItem->source_catalog_item_id) ->where('source_variant_id', $targetSelection->id) ->whereKeyNot($sourceItem->getKey()) ->lockForUpdate() ->first(); if ($targetItem !== null && $targetItem->reservation_status !== PurchaseItem::RESERVATION_ACTIVE) { throw ValidationException::withMessages([ 'variant_id' => __('api.purchase.item_not_editable'), ]); } $otherItemQuantity = (int) $purchase->items() ->where('source_catalog_item_id', $sourceItem->source_catalog_item_id) ->whereKeyNot($sourceItem->getKey()) ->sum('cantidad'); $this->purchaseLimits->assertCanPurchase( $targetSelection->catalogItem, (int) $purchase->user_id, $otherItemQuantity + $quantity, $purchase->getKey(), 'variant_id', ); try { $this->inventory->release($currentSelection, (int) $sourceItem->cantidad); $this->inventory->reserve($targetSelection, $quantity); } catch (\InvalidArgumentException) { throw ValidationException::withMessages([ 'variant_id' => __('api.purchase.insufficient_stock'), ]); } $previousVariantId = $sourceItem->source_variant_id; $finalQuantity = $quantity; if ($targetItem !== null) { $finalQuantity += (int) $targetItem->cantidad; $targetItem->update($this->snapshots->fromVariant($targetSelection, $finalQuantity)); $sourceItem->delete(); } else { $sourceItem->update($this->snapshots->fromVariant($targetSelection, $finalQuantity)); } $this->sourceCart->syncItemSelection( $purchase, (int) $sourceItem->source_catalog_item_id, $previousVariantId, $targetSelection->id, $finalQuantity, ); } public function prepareItemEditing(Purchase $purchase): Purchase { return DB::transaction(function () use ($purchase): Purchase { $purchase = $this->lockPurchase($purchase); $this->assertEditable($purchase); if (! $purchase->tenant()->firstOrFail()->cart_editing_policy->allowsModification()) { throw ValidationException::withMessages([ 'purchase' => __('api.cart.editing_disabled'), ]); } $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 $this->responses->load($purchase); } }