refactor(reservations): unify expiration command
This commit is contained in:
@@ -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.
|
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.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Services;
|
||||||
|
|
||||||
|
use App\Domains\Cart\Services\ExpireCartReservationsService;
|
||||||
|
use App\Domains\Purchase\Services\CheckoutService;
|
||||||
|
|
||||||
|
class ExpireStockReservationsService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CheckoutService $checkout,
|
||||||
|
private readonly ExpireCartReservationsService $carts,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{purchases: int, cart_items: int}
|
||||||
|
*/
|
||||||
|
public function expireOverdue(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'purchases' => $this->checkout->expireOverduePurchases(),
|
||||||
|
'cart_items' => $this->carts->expireOverdue(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Domains\Cart\Services\ExpireCartReservationsService;
|
use App\Domains\Catalog\Services\ExpireStockReservationsService;
|
||||||
use App\Domains\Purchase\Services\CheckoutService;
|
|
||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Schedule;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
@@ -10,24 +9,13 @@ Artisan::command('inspire', function () {
|
|||||||
$this->comment(Inspiring::quote());
|
$this->comment(Inspiring::quote());
|
||||||
})->purpose('Display an inspiring quote');
|
})->purpose('Display an inspiring quote');
|
||||||
|
|
||||||
Artisan::command('purchases:expire', function (): void {
|
Artisan::command('reservations:expire', function (): void {
|
||||||
$expiredCount = app(CheckoutService::class)
|
$expired = app(ExpireStockReservationsService::class)->expireOverdue();
|
||||||
->expireOverduePurchases();
|
|
||||||
|
|
||||||
$this->info("Expired purchases: {$expiredCount}");
|
$this->info("Expired purchases: {$expired['purchases']}");
|
||||||
})->purpose('Release stock reservations from 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 {
|
Schedule::command('reservations:expire')
|
||||||
$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')
|
|
||||||
->everyMinute()
|
->everyMinute()
|
||||||
->withoutOverlapping();
|
->withoutOverlapping();
|
||||||
|
|||||||
@@ -117,13 +117,15 @@ class CartControllerTest extends TestCase
|
|||||||
$cartId = $response->json('data.id');
|
$cartId = $response->json('data.id');
|
||||||
$cartItemId = $response->json('data.items.0.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')
|
->expectsOutput('Expired cart items: 0')
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
$this->travel(31)->minutes();
|
$this->travel(31)->minutes();
|
||||||
|
|
||||||
$this->artisan('carts:expire')
|
$this->artisan('reservations:expire')
|
||||||
|
->expectsOutput('Expired purchases: 0')
|
||||||
->expectsOutput('Expired cart items: 1')
|
->expectsOutput('Expired cart items: 1')
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
@@ -146,7 +148,8 @@ class CartControllerTest extends TestCase
|
|||||||
'expires_at' => null,
|
'expires_at' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->artisan('carts:expire')
|
$this->artisan('reservations:expire')
|
||||||
|
->expectsOutput('Expired purchases: 0')
|
||||||
->expectsOutput('Expired cart items: 0')
|
->expectsOutput('Expired cart items: 0')
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
|
|||||||
@@ -850,8 +850,9 @@ class StorePurchaseTest extends TestCase
|
|||||||
|
|
||||||
$this->travel(31)->minutes();
|
$this->travel(31)->minutes();
|
||||||
|
|
||||||
$this->artisan('purchases:expire')
|
$this->artisan('reservations:expire')
|
||||||
->expectsOutput('Expired purchases: 1')
|
->expectsOutput('Expired purchases: 1')
|
||||||
|
->expectsOutput('Expired cart items: 0')
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
$this->assertDatabaseHas('compras', [
|
$this->assertDatabaseHas('compras', [
|
||||||
@@ -877,8 +878,9 @@ class StorePurchaseTest extends TestCase
|
|||||||
'deleted_at' => null,
|
'deleted_at' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->artisan('purchases:expire')
|
$this->artisan('reservations:expire')
|
||||||
->expectsOutput('Expired purchases: 0')
|
->expectsOutput('Expired purchases: 0')
|
||||||
|
->expectsOutput('Expired cart items: 0')
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
tests/Unit/Catalog/ExpireStockReservationsServiceTest.php
Normal file
33
tests/Unit/Catalog/ExpireStockReservationsServiceTest.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Catalog;
|
||||||
|
|
||||||
|
use App\Domains\Cart\Services\ExpireCartReservationsService;
|
||||||
|
use App\Domains\Catalog\Services\ExpireStockReservationsService;
|
||||||
|
use App\Domains\Purchase\Services\CheckoutService;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ExpireStockReservationsServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_expires_purchases_before_abandoned_cart_items(): void
|
||||||
|
{
|
||||||
|
$checkout = \Mockery::mock(CheckoutService::class);
|
||||||
|
$checkout->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user