refactor(stock): implement expiration for stock reservations and add configuration

This commit is contained in:
2026-08-19 12:53:12 -03:00
parent 8d6bcdcc43
commit fdf0f3328f
4 changed files with 45 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ PURCHASE_CHECKOUT_EXPIRATION_MINUTES=30
PURCHASE_QR_EXPIRATION_MINUTES=15 PURCHASE_QR_EXPIRATION_MINUTES=15
PURCHASE_TELEPAGOS_EXPIRATION_MINUTES=30 PURCHASE_TELEPAGOS_EXPIRATION_MINUTES=30
PURCHASE_TRANSFER_EXPIRATION_MINUTES=1440 PURCHASE_TRANSFER_EXPIRATION_MINUTES=1440
STOCK_RESERVATION_EXPIRATION_MINUTES=30
FRONTEND_URLS=http://localhost:4200 FRONTEND_URLS=http://localhost:4200
APP_LOCALE=es APP_LOCALE=es

View File

@@ -7,6 +7,7 @@ use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\StockReservation; use App\Domains\Catalog\Models\StockReservation;
use App\Domains\Catalog\Models\Variant; use App\Domains\Catalog\Models\Variant;
use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\Purchase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class StockReservationService class StockReservationService
@@ -71,6 +72,7 @@ class StockReservationService
'cart_item_id' => $cartItem->getKey(), 'cart_item_id' => $cartItem->getKey(),
'quantity' => $quantity, 'quantity' => $quantity,
'status' => StockReservation::STATUS_ACTIVE, 'status' => StockReservation::STATUS_ACTIVE,
'expires_at' => $this->expiration(),
]); ]);
continue; continue;
@@ -82,6 +84,7 @@ class StockReservationService
'status' => StockReservation::STATUS_ACTIVE, 'status' => StockReservation::STATUS_ACTIVE,
'committed_at' => null, 'committed_at' => null,
'released_at' => null, 'released_at' => null,
'expires_at' => $this->expiration(),
]); ]);
} }
} }
@@ -111,7 +114,7 @@ class StockReservationService
->where('status', StockReservation::STATUS_ACTIVE) ->where('status', StockReservation::STATUS_ACTIVE)
->update([ ->update([
'purchase_id' => null, 'purchase_id' => null,
'expires_at' => null, 'expires_at' => $this->expiration(),
]); ]);
} }
@@ -144,7 +147,7 @@ class StockReservationService
'cart_item_id' => $target->getKey(), 'cart_item_id' => $target->getKey(),
'purchase_id' => null, 'purchase_id' => null,
'quantity' => $perItemQuantity * $targetItemQuantity, 'quantity' => $perItemQuantity * $targetItemQuantity,
'expires_at' => null, 'expires_at' => $this->expiration(),
]); ]);
continue; continue;
@@ -153,6 +156,7 @@ class StockReservationService
$targetReservation->update([ $targetReservation->update([
'quantity' => $targetReservation->quantity + $sourceReservation->quantity, 'quantity' => $targetReservation->quantity + $sourceReservation->quantity,
'status' => StockReservation::STATUS_ACTIVE, 'status' => StockReservation::STATUS_ACTIVE,
'expires_at' => $this->expiration(),
]); ]);
$sourceReservation->delete(); $sourceReservation->delete();
} }
@@ -170,6 +174,7 @@ class StockReservationService
'cart_item_id' => $cartItem->getKey(), 'cart_item_id' => $cartItem->getKey(),
'quantity' => $requiredQuantity, 'quantity' => $requiredQuantity,
'status' => StockReservation::STATUS_ACTIVE, 'status' => StockReservation::STATUS_ACTIVE,
'expires_at' => $this->expiration(),
]); ]);
continue; continue;
@@ -180,6 +185,7 @@ class StockReservationService
'status' => StockReservation::STATUS_ACTIVE, 'status' => StockReservation::STATUS_ACTIVE,
'committed_at' => null, 'committed_at' => null,
'released_at' => null, 'released_at' => null,
'expires_at' => $this->expiration(),
]); ]);
} }
} }
@@ -214,4 +220,11 @@ class StockReservationService
->lockForUpdate() ->lockForUpdate()
->first(); ->first();
} }
private function expiration(): Carbon
{
return now()->addMinutes(
max(1, (int) config('catalog.stock_reservation_expiration_minutes', 30)),
);
}
} }

5
config/catalog.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
return [
'stock_reservation_expiration_minutes' => (int) env('STOCK_RESERVATION_EXPIRATION_MINUTES', 30),
];

View File

@@ -80,6 +80,30 @@ class CartControllerTest extends TestCase
]); ]);
} }
public function test_it_sets_the_configured_expiration_when_creating_a_stock_reservation(): void
{
config()->set('catalog.stock_reservation_expiration_minutes', 45);
$now = now()->startOfSecond();
$this->travelTo($now);
$tenant = $this->createTenant('acme');
$item = $this->createDirectItem($tenant, 10, '49.90');
$this->postJson('/api/tenants/acme/cart/items', [
'catalog_item_id' => $item->id,
'cantidad' => 2,
])->assertOk();
$this->assertDatabaseHas('stock_reservations', [
'inventory_id' => $item->inventory_id,
'quantity' => 2,
'status' => 'active',
'expires_at' => $now->copy()->addMinutes(45)->toDateTimeString(),
]);
$this->travelBack();
}
public function test_it_filters_item_images_when_the_tenant_disables_them(): void public function test_it_filters_item_images_when_the_tenant_disables_them(): void
{ {
$tenant = $this->createTenant('acme'); $tenant = $this->createTenant('acme');