diff --git a/.env.example b/.env.example index defb4fe..1782db8 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,8 @@ LOG_LEVEL=debug LOG_DAILY_DAYS=14 TELEPAGOS_LOG_LEVEL=info TELEPAGOS_LOG_DAYS=30 +COMMANDS_LOG_LEVEL=info +COMMANDS_LOG_DAYS=30 DB_CONNECTION=mysql DB_HOST=127.0.0.1 diff --git a/app/Domains/Cart/documentacion/README.md b/app/Domains/Cart/documentacion/README.md index 8dad58e..e9c8e3b 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 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. +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. Cada intento registra sus resultados o su error en el log diario `storage/logs/commands/commands-AAAA-MM-DD.log`. diff --git a/app/Domains/Catalog/Services/ExpireStockReservationsService.php b/app/Domains/Catalog/Services/ExpireStockReservationsService.php index 741f5a1..ab29513 100644 --- a/app/Domains/Catalog/Services/ExpireStockReservationsService.php +++ b/app/Domains/Catalog/Services/ExpireStockReservationsService.php @@ -4,6 +4,8 @@ namespace App\Domains\Catalog\Services; use App\Domains\Cart\Services\ExpireCartReservationsService; use App\Domains\Purchase\Services\CheckoutService; +use Illuminate\Support\Facades\Log; +use Throwable; class ExpireStockReservationsService { @@ -17,9 +19,33 @@ class ExpireStockReservationsService */ public function expireOverdue(): array { - return [ - 'purchases' => $this->checkout->expireOverduePurchases(), - 'cart_items' => $this->carts->expireOverdue(), - ]; + $expiredPurchases = null; + $expiredCartItems = null; + + try { + $expiredPurchases = $this->checkout->expireOverduePurchases(); + $expiredCartItems = $this->carts->expireOverdue(); + + Log::channel('commands')->info('Stock reservation cleanup completed.', [ + 'command' => 'reservations:expire', + 'expired_purchases' => $expiredPurchases, + 'expired_cart_items' => $expiredCartItems, + 'total_expired' => $expiredPurchases + $expiredCartItems, + ]); + + return [ + 'purchases' => $expiredPurchases, + 'cart_items' => $expiredCartItems, + ]; + } catch (Throwable $exception) { + Log::channel('commands')->error('Stock reservation cleanup failed.', [ + 'command' => 'reservations:expire', + 'expired_purchases' => $expiredPurchases, + 'expired_cart_items' => $expiredCartItems, + 'exception' => $exception, + ]); + + throw $exception; + } } } diff --git a/config/logging.php b/config/logging.php index aecd361..61a000b 100644 --- a/config/logging.php +++ b/config/logging.php @@ -81,6 +81,14 @@ return [ 'replace_placeholders' => true, ], + 'commands' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/commands/commands.log'), + 'level' => env('COMMANDS_LOG_LEVEL', 'info'), + 'days' => env('COMMANDS_LOG_DAYS', 30), + 'replace_placeholders' => true, + ], + 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), diff --git a/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php index 5cc11d2..4fa57b9 100644 --- a/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php +++ b/tests/Unit/Catalog/ExpireStockReservationsServiceTest.php @@ -5,6 +5,9 @@ namespace Tests\Unit\Catalog; use App\Domains\Cart\Services\ExpireCartReservationsService; use App\Domains\Catalog\Services\ExpireStockReservationsService; use App\Domains\Purchase\Services\CheckoutService; +use Illuminate\Support\Facades\Log; +use Psr\Log\LoggerInterface; +use RuntimeException; use Tests\TestCase; class ExpireStockReservationsServiceTest extends TestCase @@ -23,6 +26,20 @@ class ExpireStockReservationsServiceTest extends TestCase ->ordered() ->andReturn(3); + $logger = \Mockery::mock(LoggerInterface::class); + Log::shouldReceive('channel') + ->once() + ->with('commands') + ->andReturn($logger); + $logger->shouldReceive('info') + ->once() + ->with('Stock reservation cleanup completed.', [ + 'command' => 'reservations:expire', + 'expired_purchases' => 2, + 'expired_cart_items' => 3, + 'total_expired' => 5, + ]); + $result = (new ExpireStockReservationsService($checkout, $carts))->expireOverdue(); $this->assertSame([ @@ -30,4 +47,32 @@ class ExpireStockReservationsServiceTest extends TestCase 'cart_items' => 3, ], $result); } + + public function test_it_logs_failed_cleanup_attempts_and_rethrows_the_error(): void + { + $exception = new RuntimeException('Unable to clean carts.'); + $checkout = \Mockery::mock(CheckoutService::class); + $checkout->shouldReceive('expireOverduePurchases')->once()->andReturn(2); + + $carts = \Mockery::mock(ExpireCartReservationsService::class); + $carts->shouldReceive('expireOverdue')->once()->andThrow($exception); + + $logger = \Mockery::mock(LoggerInterface::class); + Log::shouldReceive('channel') + ->once() + ->with('commands') + ->andReturn($logger); + $logger->shouldReceive('error') + ->once() + ->with('Stock reservation cleanup failed.', [ + 'command' => 'reservations:expire', + 'expired_purchases' => 2, + 'expired_cart_items' => null, + 'exception' => $exception, + ]); + + $this->expectExceptionObject($exception); + + (new ExpireStockReservationsService($checkout, $carts))->expireOverdue(); + } }