Squashed commit of the following:

commit 1dc4e29c69
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 13:58:03 2026 -0300

    refactor(reservations): unify expiration command

commit 093e894cc3
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 13:48:09 2026 -0300

    feat(cart): expire abandoned stock reservations

commit fdf0f3328f
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 12:53:12 2026 -0300

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

commit 8d6bcdcc43
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 12:38:21 2026 -0300

    refactor(cart): invalidate payment on actual changes

commit 3206e293eb
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 12:24:48 2026 -0300

    refactor(cart): own checkout item editing

commit aed99bd05e
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 12:14:57 2026 -0300

    refactor(checkout): remove legacy purchase item reservations

commit f1649e0e4b
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 12:06:29 2026 -0300

    refactor(checkout): materialize purchase items on confirmation

commit e6c4b40a37
Author: ncoronel <ncoronel@quo.ar>
Date:   Wed Aug 19 12:06:19 2026 -0300

    feat(inventory): add traceable cart stock reservations
This commit is contained in:
2026-08-19 13:59:25 -03:00
parent 15878cd9ba
commit 58cbeae9d3
45 changed files with 1391 additions and 544 deletions

View File

@@ -7,6 +7,8 @@ use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Catalog\Services\CatalogInventoryService;
use App\Domains\Catalog\Services\StockReservationService;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Services\UserPurchaseLimitService;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
@@ -24,6 +26,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
'user_id',
'guest_token',
'status',
'origin',
])]
class Cart extends Model
{
@@ -32,6 +35,10 @@ class Cart extends Model
protected $table = 'carritos';
public const ORIGIN_USER = 'user';
public const ORIGIN_DIRECT_CHECKOUT = 'direct_checkout';
protected function casts(): array
{
return [
@@ -63,6 +70,12 @@ class Cart extends Model
return $this->hasMany(CartItem::class, 'cart_id');
}
/** @return HasMany<Purchase, $this> */
public function purchases(): HasMany
{
return $this->hasMany(Purchase::class, 'cart_id');
}
public function getTotalAmount(): float
{
$items = $this->relationLoaded('items')
@@ -114,11 +127,12 @@ class Cart extends Model
'cantidad' => $quantity,
]);
} else {
app(StockReservationService::class)->ensure($item, $selectedItem);
$item->cantidad += $quantity;
$item->save();
}
$inventoryService->reserve($selectedItem, $quantity);
app(StockReservationService::class)->reserve($item, $selectedItem, $quantity);
return $item->fresh();
});
@@ -129,6 +143,7 @@ class Cart extends Model
int $quantity,
?int $variantId = null,
bool $updateVariant = false,
?int $excludedPurchaseId = null,
): CartItem {
if ($quantity <= 0) {
throw ValidationException::withMessages([
@@ -141,6 +156,7 @@ class Cart extends Model
$quantity,
$variantId,
$updateVariant,
$excludedPurchaseId,
): CartItem {
/** @var CartItem $item */
$item = $this->items()
@@ -168,9 +184,10 @@ class Cart extends Model
$this->assertUserPurchaseLimit(
$nextSelection,
$otherVariantsQuantity + $quantity,
$excludedPurchaseId,
);
$inventoryService->release($currentSelection, $item->cantidad);
app(StockReservationService::class)->release($item, $currentSelection, $item->cantidad);
$availableQuantity = $inventoryService->availableQuantity($nextSelection);
if ($availableQuantity !== null && $availableQuantity < $quantity) {
@@ -186,11 +203,11 @@ class Cart extends Model
->lockForUpdate()
->first();
$inventoryService->reserve($nextSelection, $quantity);
if ($targetItem !== null) {
app(StockReservationService::class)->ensure($targetItem, $nextSelection);
$targetItem->cantidad += $quantity;
$targetItem->save();
app(StockReservationService::class)->reserve($targetItem, $nextSelection, $quantity);
$item->delete();
return $targetItem->fresh();
@@ -199,6 +216,7 @@ class Cart extends Model
$item->variant_id = $variantId;
$item->cantidad = $quantity;
$item->save();
app(StockReservationService::class)->reserve($item, $nextSelection, $quantity);
return $item->fresh();
}
@@ -213,6 +231,7 @@ class Cart extends Model
$this->assertUserPurchaseLimit(
$currentSelection,
$otherVariantsQuantity + $quantity,
$excludedPurchaseId,
);
}
@@ -225,17 +244,17 @@ class Cart extends Model
]);
}
$item->cantidad = $quantity;
$item->save();
if ($delta > 0) {
$inventoryService->reserve($currentSelection, $delta);
app(StockReservationService::class)->reserve($item, $currentSelection, $delta);
}
if ($delta < 0) {
$inventoryService->release($currentSelection, abs($delta));
app(StockReservationService::class)->release($item, $currentSelection, abs($delta));
}
$item->cantidad = $quantity;
$item->save();
return $item->fresh();
});
}
@@ -254,7 +273,8 @@ class Cart extends Model
$item->variant_id,
true,
);
app(CatalogInventoryService::class)->release(
app(StockReservationService::class)->release(
$item,
$selectedItem,
$item->cantidad,
);
@@ -334,6 +354,7 @@ class Cart extends Model
private function assertUserPurchaseLimit(
CatalogItem|Variant $selectedItem,
int $cartQuantity,
?int $excludedPurchaseId = null,
): void {
if ($this->user_id === null) {
return;
@@ -347,6 +368,7 @@ class Cart extends Model
$catalogItem,
$this->user_id,
$cartQuantity,
$excludedPurchaseId,
field: 'cantidad',
);
}