diff --git a/app/Domains/Cart/documentacion/README.md b/app/Domains/Cart/documentacion/README.md index afbe4e4..8dad58e 100644 --- a/app/Domains/Cart/documentacion/README.md +++ b/app/Domains/Cart/documentacion/README.md @@ -34,4 +34,4 @@ Depende de `Catalog` para productos y variantes, de `Tenant` para aislar datos y Un carrito puede pasar a `checkout`. Las compras directas usan un carrito técnico con `origin=direct_checkout`; los carritos normales conservan `origin=user` y pueden restaurarse al cancelar o vencer la compra. -El comando `php artisan carts:expire` procesa reservas activas sin compra cuyo `expires_at` haya vencido. Se ejecuta cada minuto mediante el scheduler, conserva la fila de reserva con estado `expired`, elimina el ítem abandonado y elimina lógicamente el carrito cuando queda vacío. +El comando unificado `php artisan reservations:expire` procesa primero las compras vencidas y luego las reservas activas sin compra cuyo `expires_at` haya vencido. Se ejecuta cada minuto mediante el scheduler, conserva la fila de reserva con estado `expired`, elimina el ítem abandonado y elimina lógicamente el carrito cuando queda vacío. diff --git a/app/Domains/Catalog/Services/ExpireStockReservationsService.php b/app/Domains/Catalog/Services/ExpireStockReservationsService.php new file mode 100644 index 0000000..741f5a1 --- /dev/null +++ b/app/Domains/Catalog/Services/ExpireStockReservationsService.php @@ -0,0 +1,25 @@ + $this->checkout->expireOverduePurchases(), + 'cart_items' => $this->carts->expireOverdue(), + ]; + } +} diff --git a/routes/console.php b/routes/console.php index faa90aa..ffb9927 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,7 +1,6 @@ comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); -Artisan::command('purchases:expire', function (): void { - $expiredCount = app(CheckoutService::class) - ->expireOverduePurchases(); +Artisan::command('reservations:expire', function (): void { + $expired = app(ExpireStockReservationsService::class)->expireOverdue(); - $this->info("Expired purchases: {$expiredCount}"); -})->purpose('Release stock reservations from expired purchases'); + $this->info("Expired purchases: {$expired['purchases']}"); + $this->info("Expired cart items: {$expired['cart_items']}"); +})->purpose('Release expired stock reservations from purchases and abandoned carts'); -Artisan::command('carts:expire', function (): void { - $expiredCount = app(ExpireCartReservationsService::class) - ->expireOverdue(); - - $this->info("Expired cart items: {$expiredCount}"); -})->purpose('Release expired stock reservations from abandoned carts'); - -Schedule::command('purchases:expire') - ->everyMinute() - ->withoutOverlapping(); - -Schedule::command('carts:expire') +Schedule::command('reservations:expire') ->everyMinute() ->withoutOverlapping(); diff --git a/tests/Feature/Cart/CartControllerTest.php b/tests/Feature/Cart/CartControllerTest.php index 8617c75..d69c1b7 100644 --- a/tests/Feature/Cart/CartControllerTest.php +++ b/tests/Feature/Cart/CartControllerTest.php @@ -117,13 +117,15 @@ class CartControllerTest extends TestCase $cartId = $response->json('data.id'); $cartItemId = $response->json('data.items.0.id'); - $this->artisan('carts:expire') + $this->artisan('reservations:expire') + ->expectsOutput('Expired purchases: 0') ->expectsOutput('Expired cart items: 0') ->assertSuccessful(); $this->travel(31)->minutes(); - $this->artisan('carts:expire') + $this->artisan('reservations:expire') + ->expectsOutput('Expired purchases: 0') ->expectsOutput('Expired cart items: 1') ->assertSuccessful(); @@ -146,7 +148,8 @@ class CartControllerTest extends TestCase 'expires_at' => null, ]); - $this->artisan('carts:expire') + $this->artisan('reservations:expire') + ->expectsOutput('Expired purchases: 0') ->expectsOutput('Expired cart items: 0') ->assertSuccessful(); diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 171e01f..af4c3ad 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -850,8 +850,9 @@ class StorePurchaseTest extends TestCase $this->travel(31)->minutes(); - $this->artisan('purchases:expire') + $this->artisan('reservations:expire') ->expectsOutput('Expired purchases: 1') + ->expectsOutput('Expired cart items: 0') ->assertSuccessful(); $this->assertDatabaseHas('compras', [ @@ -877,8 +878,9 @@ class StorePurchaseTest extends TestCase 'deleted_at' => null, ]); - $this->artisan('purchases:expire') + $this->artisan('reservations:expire') ->expectsOutput('Expired purchases: 0') + ->expectsOutput('Expired cart items: 0') ->assertSuccessful(); } diff --git a/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php new file mode 100644 index 0000000..5cc11d2 --- /dev/null +++ b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php @@ -0,0 +1,33 @@ +shouldReceive('expireOverduePurchases') + ->once() + ->ordered() + ->andReturn(2); + + $carts = \Mockery::mock(ExpireCartReservationsService::class); + $carts->shouldReceive('expireOverdue') + ->once() + ->ordered() + ->andReturn(3); + + $result = (new ExpireStockReservationsService($checkout, $carts))->expireOverdue(); + + $this->assertSame([ + 'purchases' => 2, + 'cart_items' => 3, + ], $result); + } +}