diff --git a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php index 81ebcab..bdf17a4 100644 --- a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php @@ -14,22 +14,21 @@ class ReleaseCheckoutService { public function __construct( private readonly StockReservationService $reservations, - private readonly SourceCartService $sourceCart, ) {} public function cancel(Purchase $purchase): Purchase { - return $this->release($purchase, Purchase::STATUS_CANCELLED, restoreCart: true); + return $this->release($purchase, Purchase::STATUS_CANCELLED); } public function cancelWithoutRestoringCart(Purchase $purchase): Purchase { - return $this->release($purchase, Purchase::STATUS_CANCELLED, restoreCart: false); + return $this->release($purchase, Purchase::STATUS_CANCELLED); } public function expire(Purchase $purchase): Purchase { - return $this->release($purchase, Purchase::STATUS_EXPIRED, restoreCart: true); + return $this->release($purchase, Purchase::STATUS_EXPIRED); } public function expireOverdue(): int @@ -66,9 +65,9 @@ class ReleaseCheckoutService return $expiredCount; } - private function release(Purchase $purchase, string $targetStatus, bool $restoreCart): Purchase + private function release(Purchase $purchase, string $targetStatus): Purchase { - return DB::transaction(function () use ($purchase, $targetStatus, $restoreCart): Purchase { + return DB::transaction(function () use ($purchase, $targetStatus): Purchase { $purchase = $this->lockPurchase($purchase); if ($purchase->status === Purchase::STATUS_PAID) { @@ -98,8 +97,7 @@ class ReleaseCheckoutService ]); } - $reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase); - $this->releaseCartReservations($purchase, $reservationReturnedToCart, $targetStatus); + $this->releaseCartReservations($purchase, $targetStatus); $purchase->update(['status' => $targetStatus]); @@ -109,15 +107,8 @@ 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 d70df8d..06a427b 100644 --- a/app/Domains/Purchase/Services/Checkout/SourceCartService.php +++ b/app/Domains/Purchase/Services/Checkout/SourceCartService.php @@ -3,65 +3,10 @@ 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, @@ -171,30 +116,4 @@ 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); - } - } } diff --git a/app/Domains/Purchase/documentacion/README.md b/app/Domains/Purchase/documentacion/README.md index a159fbf..8b7299c 100644 --- a/app/Domains/Purchase/documentacion/README.md +++ b/app/Domains/Purchase/documentacion/README.md @@ -19,7 +19,7 @@ Implementa el ciclo de compra y checkout: crea la cabecera de compra desde un ca - `EditCheckoutService`: modifica los datos del comprador antes del cierre. - `CompleteCheckoutService`: completa, envía a revisión o materializa los `PurchaseItem` al confirmar el pago. - `ReleaseCheckoutService`: cancela, vence y procesa vencimientos pendientes. -- `SourceCartService`: restaura o finaliza el carrito fuente. +- `SourceCartService`: sincroniza o finaliza el carrito de checkout asociado a la compra. - `CatalogSelectionResolver` y `PurchaseItemSnapshotFactory`: resuelven selecciones y generan snapshots. Durante `created` y `pending_payment`, `PurchaseResource` publica las líneas del carrito con `items_source=cart`; una compra materializada publica `items_source=purchase`. Los datos descriptivos y económicos del checkout se resuelven siempre desde el catálogo vigente. diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 49060e2..ac61311 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -412,7 +412,7 @@ class StorePurchaseTest extends TestCase ]); } - public function test_it_restores_the_source_cart_when_checkout_is_cancelled(): void + public function test_it_releases_and_closes_the_checkout_cart_when_purchase_is_cancelled(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); @@ -430,11 +430,10 @@ class StorePurchaseTest extends TestCase ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_CANCELLED); - $this->assertDatabaseHas('carritos', [ + $this->assertSoftDeleted('carritos', [ 'id' => $purchase->cart_id, 'user_id' => $user->id, - 'status' => 'active', - 'deleted_at' => null, + 'status' => 'converted', ]); $this->assertDatabaseHas('carrito_items', [ 'cart_id' => $purchase->cart_id, @@ -444,11 +443,16 @@ class StorePurchaseTest extends TestCase ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, - 'reserved_stock' => 3, + 'reserved_stock' => 0, + ]); + $this->assertDatabaseHas('stock_reservations', [ + 'purchase_id' => $purchase->id, + 'quantity' => 0, + 'status' => 'released', ]); } - public function test_it_merges_the_checkout_cart_when_the_user_created_another_active_cart(): void + public function test_it_keeps_a_new_active_cart_separate_when_checkout_is_cancelled(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); @@ -480,11 +484,15 @@ class StorePurchaseTest extends TestCase 'cart_id' => $activeCartId, 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, - 'cantidad' => 3, + 'cantidad' => 1, + ]); + $this->assertSoftDeleted('carritos', [ + 'id' => $purchase->cart_id, + 'status' => 'converted', ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, - 'reserved_stock' => 3, + 'reserved_stock' => 1, ]); } @@ -1069,7 +1077,7 @@ class StorePurchaseTest extends TestCase ]); } - public function test_it_expires_an_abandoned_purchase_and_restores_its_cart(): void + public function test_it_expires_an_abandoned_purchase_and_releases_its_checkout_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); @@ -1095,21 +1103,20 @@ class StorePurchaseTest extends TestCase ]); $this->assertDatabaseMissing('compra_items', ['compra_id' => $purchase->id]); $this->assertDatabaseHas('stock_reservations', [ - 'purchase_id' => null, - 'quantity' => 3, - 'status' => 'active', + 'purchase_id' => $purchase->id, + 'quantity' => 0, + 'status' => 'expired', ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'real_stock' => 10, - 'reserved_stock' => 3, + 'reserved_stock' => 0, 'sold_units' => 0, ]); - $this->assertDatabaseHas('carritos', [ + $this->assertSoftDeleted('carritos', [ 'id' => $purchase->cart_id, 'user_id' => $user->id, - 'status' => 'active', - 'deleted_at' => null, + 'status' => 'converted', ]); $this->artisan('reservations:expire')