release($purchase, Purchase::STATUS_CANCELLED, restoreCart: true); } public function cancelWithoutRestoringCart(Purchase $purchase): Purchase { return $this->release($purchase, Purchase::STATUS_CANCELLED, restoreCart: false); } public function expire(Purchase $purchase): Purchase { return $this->release($purchase, Purchase::STATUS_EXPIRED, restoreCart: true); } public function expireOverdue(): int { $expiredCount = 0; Purchase::query() ->whereIn('status', [Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT]) ->whereNotNull('expires_at') ->where('expires_at', '<=', now()) ->orderBy('id') ->eachById(function (Purchase $purchase) use (&$expiredCount): void { $purchase = $this->expire($purchase); if ($purchase->status === Purchase::STATUS_EXPIRED) { $expiredCount++; } }); return $expiredCount; } private function release(Purchase $purchase, string $targetStatus, bool $restoreCart): Purchase { return DB::transaction(function () use ($purchase, $targetStatus, $restoreCart): Purchase { $purchase = $this->lockPurchase($purchase); if ($purchase->status === Purchase::STATUS_PAID) { if ($targetStatus === Purchase::STATUS_EXPIRED) { return $this->loadPurchase($purchase); } throw ValidationException::withMessages([ 'purchase' => __('api.purchase.paid_cannot_cancel'), ]); } if ($this->isAlreadyReleased($purchase)) { return $this->loadPurchase($purchase); } if ( $targetStatus === Purchase::STATUS_EXPIRED && ($purchase->expires_at === null || $purchase->expires_at->isFuture()) ) { return $this->loadPurchase($purchase); } $items = $purchase->items() ->where('reservation_status', PurchaseItem::RESERVATION_ACTIVE) ->lockForUpdate() ->get(); $reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase); foreach ($items as $item) { if (! $reservationReturnedToCart) { $this->releaseInventory($purchase, $item); } $item->update([ 'reservation_status' => PurchaseItem::RESERVATION_RELEASED, ]); } $purchase->update(['status' => $targetStatus]); return $this->loadPurchase($purchase); }); } private function releaseInventory(Purchase $purchase, PurchaseItem $item): void { $selection = $this->selections->resolvePurchaseItem($purchase->tenant, $item); try { $this->inventory->release($selection, (int) $item->cantidad); } catch (\InvalidArgumentException) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), ]); } } private function isAlreadyReleased(Purchase $purchase): bool { return in_array($purchase->status, [ 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']); } }