commit1dc4e29c69Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 13:58:03 2026 -0300 refactor(reservations): unify expiration command commit093e894cc3Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 13:48:09 2026 -0300 feat(cart): expire abandoned stock reservations commitfdf0f3328fAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:53:12 2026 -0300 refactor(stock): implement expiration for stock reservations and add configuration commit8d6bcdcc43Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:38:21 2026 -0300 refactor(cart): invalidate payment on actual changes commit3206e293ebAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:24:48 2026 -0300 refactor(cart): own checkout item editing commitaed99bd05eAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:14:57 2026 -0300 refactor(checkout): remove legacy purchase item reservations commitf1649e0e4bAuthor: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:06:29 2026 -0300 refactor(checkout): materialize purchase items on confirmation commite6c4b40a37Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 19 12:06:19 2026 -0300 feat(inventory): add traceable cart stock reservations
81 lines
2.9 KiB
PHP
81 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Purchase\Services;
|
|
|
|
use App\Domains\Cart\Models\CartItem;
|
|
use App\Domains\Catalog\Models\CatalogItem;
|
|
use App\Domains\Purchase\Models\Purchase;
|
|
use App\Domains\Purchase\Models\PurchaseItem;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class UserPurchaseLimitService
|
|
{
|
|
public function assertCanPurchase(
|
|
CatalogItem $catalogItem,
|
|
int $userId,
|
|
int $requestedQuantity,
|
|
?int $excludedPurchaseId = null,
|
|
string $field = 'quantity',
|
|
): void {
|
|
DB::transaction(function () use (
|
|
$catalogItem,
|
|
$userId,
|
|
$requestedQuantity,
|
|
$excludedPurchaseId,
|
|
$field,
|
|
): void {
|
|
/** @var CatalogItem $catalogItem */
|
|
$catalogItem = CatalogItem::query()
|
|
->whereKey($catalogItem->getKey())
|
|
->lockForUpdate()
|
|
->firstOrFail();
|
|
$limit = $catalogItem->max_units_per_user;
|
|
|
|
if ($limit === null) {
|
|
return;
|
|
}
|
|
|
|
$purchasedQuantity = (int) PurchaseItem::query()
|
|
->where('source_catalog_item_id', $catalogItem->getKey())
|
|
->whereHas('purchase', function ($query) use ($userId, $excludedPurchaseId): void {
|
|
$query
|
|
->where('user_id', $userId)
|
|
->whereIn('status', [
|
|
Purchase::STATUS_CREATED,
|
|
Purchase::STATUS_PENDING_PAYMENT,
|
|
Purchase::STATUS_PAID,
|
|
])
|
|
->when(
|
|
$excludedPurchaseId !== null,
|
|
fn ($query) => $query->whereKeyNot($excludedPurchaseId),
|
|
);
|
|
})
|
|
->sum('cantidad');
|
|
|
|
$checkoutQuantity = (int) CartItem::query()
|
|
->where('catalog_item_id', $catalogItem->getKey())
|
|
->whereHas('cart.purchases', function ($query) use ($userId, $excludedPurchaseId): void {
|
|
$query
|
|
->where('user_id', $userId)
|
|
->whereIn('status', [
|
|
Purchase::STATUS_CREATED,
|
|
Purchase::STATUS_PENDING_PAYMENT,
|
|
])
|
|
->whereDoesntHave('items')
|
|
->when(
|
|
$excludedPurchaseId !== null,
|
|
fn ($query) => $query->whereKeyNot($excludedPurchaseId),
|
|
);
|
|
})
|
|
->sum('cantidad');
|
|
|
|
if ($purchasedQuantity + $checkoutQuantity + $requestedQuantity > $limit) {
|
|
throw ValidationException::withMessages([
|
|
$field => __('api.purchase_limit.exceeded', ['max' => $limit]),
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
}
|