release($purchase, Purchase::STATUS_CANCELLED); } public function cancelFromAdmin(Purchase $purchase): Purchase { return $this->release($purchase, Purchase::STATUS_CANCELLED, allowInReviewCancellation: true); } public function expire(Purchase $purchase): Purchase { return $this->release($purchase, Purchase::STATUS_EXPIRED); } private function release( Purchase $purchase, string $targetStatus, bool $allowInReviewCancellation = false, ): Purchase { return DB::transaction(function () use ( $purchase, $targetStatus, $allowInReviewCancellation, ): Purchase { $purchase = $this->lockPurchase($purchase); if ($purchase->status === Purchase::STATUS_EXPIRED && $targetStatus !== Purchase::STATUS_EXPIRED) { throw new PurchaseExpiredException; } 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 ( $purchase->status === Purchase::STATUS_IN_REVIEW && $targetStatus === Purchase::STATUS_CANCELLED && ! $allowInReviewCancellation ) { return $this->createNewCartWithoutCancelling($purchase); } if ($this->isAlreadyReleased($purchase)) { return $this->loadPurchase($purchase); } $cart = $purchase->cart()->withTrashed()->lockForUpdate()->first(); if ($targetStatus === Purchase::STATUS_CANCELLED && $cart?->status === 'active' && in_array($purchase->status, [ Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT, ], true)) { $this->reservations->returnToCart($purchase, $cart); $purchase->update(['status' => Purchase::STATUS_CANCELLED]); return $this->loadPurchase($purchase); } if ( $targetStatus === Purchase::STATUS_EXPIRED && (! in_array($purchase->status, [ Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT, ], true) || ! $this->hasOverdueActiveReservation($purchase)) ) { return $this->loadPurchase($purchase); } $this->releasePurchaseReservations($purchase, $targetStatus, $cart); $purchase->update(['status' => $targetStatus]); return $this->loadPurchase($purchase); }); } private function releasePurchaseReservations( Purchase $purchase, string $targetStatus, ?Cart $cart, ): void { try { $this->reservations->releaseForPurchase( $purchase, $targetStatus === Purchase::STATUS_EXPIRED ? StockReservation::STATUS_EXPIRED : StockReservation::STATUS_RELEASED, $targetStatus === Purchase::STATUS_CANCELLED ? StockReservationService::REASON_PURCHASE_CANCELLED : ($targetStatus === Purchase::STATUS_REJECTED ? StockReservationService::REASON_PAYMENT_REJECTED : null), ); } catch (\InvalidArgumentException) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), ]); } if ($cart === null) { return; } if ($cart->status === 'active') { $cartUpdate = [ 'current_purchase_id' => null, ]; if ($targetStatus !== Purchase::STATUS_EXPIRED) { $cartUpdate['current_stock_reservation_id'] = null; } Cart::query() ->whereKey($cart->getKey()) ->where('current_purchase_id', $purchase->getKey()) ->update($cartUpdate); return; } if ($cart->status !== 'checkout') { return; } if (! $cart->trashed()) { $cart->update(['status' => Cart::STATUS_CONVERTED]); $cart->delete(); } } private function createNewCartWithoutCancelling(Purchase $purchase): Purchase { return DB::transaction(function () use ($purchase): Purchase { $purchase = $this->lockPurchase($purchase); if ($purchase->status !== Purchase::STATUS_IN_REVIEW || $purchase->user_id === null) { return $this->loadPurchase($purchase); } /** @var Cart|null $sourceCart */ $sourceCart = $purchase->cart()->withTrashed()->lockForUpdate()->first(); if ($sourceCart !== null && ! $sourceCart->trashed() && $sourceCart->status === 'active') { $sourceCart->update(['status' => 'checkout']); } Cart::query()->firstOrCreate([ 'tenant_codigo' => $purchase->tenant_codigo, 'user_id' => $purchase->user_id, 'guest_token' => null, 'status' => 'active', 'origin' => Cart::ORIGIN_USER, ]); return $this->loadPurchase($purchase); }); } private function isAlreadyReleased(Purchase $purchase): bool { return in_array($purchase->status, [ Purchase::STATUS_CANCELLED, Purchase::STATUS_REJECTED, Purchase::STATUS_EXPIRED, Purchase::STATUS_SUPERSEDED, ], 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', 'stockReservation']); } private function hasOverdueActiveReservation(Purchase $purchase): bool { /** @var StockReservation|null $reservation */ $reservation = $purchase->stockReservation()->lockForUpdate()->first(); return $reservation !== null && $reservation->status === StockReservation::STATUS_ACTIVE && $reservation->expires_at !== null && ! $reservation->expires_at->isFuture(); } }