From 9d870be2942cd66eb156322616711bd8dd104760 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 20 Aug 2026 16:28:43 -0300 Subject: [PATCH] feat(checkout): restore source cart on cancellation --- .../Checkout/ReleaseCheckoutService.php | 21 +++-- .../Services/Checkout/SourceCartService.php | 81 +++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php index 77870d6..4c3eaff 100644 --- a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php @@ -16,22 +16,21 @@ class ReleaseCheckoutService public function __construct( private readonly StockReservationService $reservations, private readonly SourceCartService $sourceCart, - private readonly PurchaseStateGuard $purchaseState, ) {} public function cancel(Purchase $purchase): Purchase { - return $this->release($purchase, Purchase::STATUS_CANCELLED); + return $this->release($purchase, Purchase::STATUS_CANCELLED, restoreCart: true); } public function cancelWithoutRestoringCart(Purchase $purchase): Purchase { - return $this->release($purchase, Purchase::STATUS_CANCELLED); + return $this->release($purchase, Purchase::STATUS_CANCELLED, restoreCart: false); } public function expire(Purchase $purchase): Purchase { - return $this->release($purchase, Purchase::STATUS_EXPIRED); + return $this->release($purchase, Purchase::STATUS_EXPIRED, restoreCart: false); } public function expireOverdue(): int @@ -68,9 +67,9 @@ class ReleaseCheckoutService return $expiredCount; } - private function release(Purchase $purchase, string $targetStatus): Purchase + private function release(Purchase $purchase, string $targetStatus, bool $restoreCart): Purchase { - return DB::transaction(function () use ($purchase, $targetStatus): Purchase { + return DB::transaction(function () use ($purchase, $targetStatus, $restoreCart): Purchase { $purchase = $this->lockPurchase($purchase); if ($targetStatus !== Purchase::STATUS_EXPIRED) { @@ -104,7 +103,8 @@ class ReleaseCheckoutService ]); } - $this->releaseCartReservations($purchase, $targetStatus); + $reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase); + $this->releaseCartReservations($purchase, $reservationReturnedToCart, $targetStatus); $purchase->update(['status' => $targetStatus]); @@ -114,8 +114,15 @@ class ReleaseCheckoutService private function releaseCartReservations( Purchase $purchase, + bool $reservationReturnedToCart, string $targetStatus, ): void { + if ($reservationReturnedToCart) { + $this->reservations->detachFromPurchase($purchase); + + return; + } + $cart = $purchase->cart()->withTrashed()->lockForUpdate()->first(); if ($cart === null) { return; diff --git a/app/Domains/Purchase/Services/Checkout/SourceCartService.php b/app/Domains/Purchase/Services/Checkout/SourceCartService.php index 06a427b..d70df8d 100644 --- a/app/Domains/Purchase/Services/Checkout/SourceCartService.php +++ b/app/Domains/Purchase/Services/Checkout/SourceCartService.php @@ -3,10 +3,65 @@ namespace App\Domains\Purchase\Services\Checkout; use App\Domains\Cart\Models\Cart; +use App\Domains\Cart\Models\CartItem; +use App\Domains\Catalog\Services\StockReservationService; use App\Domains\Purchase\Models\Purchase; class SourceCartService { + public function __construct( + private readonly StockReservationService $reservations, + ) {} + + public function restore(Purchase $purchase): bool + { + $sourceCart = $this->findSourceCart($purchase); + + if ($sourceCart === null) { + return false; + } + + if ($sourceCart->origin === Cart::ORIGIN_DIRECT_CHECKOUT) { + return false; + } + + /** @var Cart|null $activeCart */ + $activeCart = Cart::query() + ->where('tenant_codigo', $purchase->tenant_codigo) + ->where('user_id', $purchase->user_id) + ->where('status', 'active') + ->where('id', '!=', $sourceCart->getKey()) + ->lockForUpdate() + ->first(); + + if ($activeCart !== null) { + $this->mergeIntoActiveCart($sourceCart, $activeCart); + + $sourceCart->update([ + 'status' => 'converted', + 'guest_token' => null, + ]); + + if (! $sourceCart->trashed()) { + $sourceCart->delete(); + } + + return true; + } + + if ($sourceCart->trashed()) { + $sourceCart->restore(); + } + + $sourceCart->update([ + 'status' => 'active', + 'user_id' => $purchase->user_id, + 'guest_token' => null, + ]); + + return true; + } + public function syncItemQuantity( Purchase $purchase, PurchaseItem $purchaseItem, @@ -116,4 +171,30 @@ class SourceCartService ->lockForUpdate() ->first(); } + + private function mergeIntoActiveCart(Cart $sourceCart, Cart $activeCart): void + { + $sourceItems = $sourceCart->items()->lockForUpdate()->get(); + + foreach ($sourceItems as $sourceItem) { + /** @var CartItem|null $activeItem */ + $activeItem = $activeCart->items() + ->where('catalog_item_id', $sourceItem->catalog_item_id) + ->where('variant_id', $sourceItem->variant_id) + ->lockForUpdate() + ->first(); + + if ($activeItem === null) { + $activeItem = $activeCart->items()->create([ + 'catalog_item_id' => $sourceItem->catalog_item_id, + 'variant_id' => $sourceItem->variant_id, + 'cantidad' => $sourceItem->cantidad, + ]); + } else { + $activeItem->increment('cantidad', (int) $sourceItem->cantidad); + } + + $this->reservations->transfer($sourceItem, $activeItem); + } + } }