270 lines
8.3 KiB
PHP
270 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Cart\Models;
|
|
|
|
use App\Domains\Auth\Models\User;
|
|
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\Tenant\Models\Tenant;
|
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
|
|
#[Fillable([
|
|
'tenant_codigo',
|
|
'user_id',
|
|
'guest_token',
|
|
'status',
|
|
])]
|
|
class Cart extends Model
|
|
{
|
|
use HasFactory;
|
|
use SoftDeletes;
|
|
|
|
protected $table = 'carritos';
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'user_id' => 'integer',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return BelongsTo<Tenant, $this>
|
|
*/
|
|
public function tenant(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
|
}
|
|
|
|
/**
|
|
* @return BelongsTo<User, $this>
|
|
*/
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
/**
|
|
* @return HasMany<CartItem, $this>
|
|
*/
|
|
public function items(): HasMany
|
|
{
|
|
return $this->hasMany(CartItem::class, 'cart_id');
|
|
}
|
|
|
|
public function getTotalAmount(): float
|
|
{
|
|
$items = $this->relationLoaded('items')
|
|
? $this->getRelation('items')
|
|
: $this->items()->with(['catalogItem', 'variant'])->get();
|
|
|
|
return (float) $items->reduce(
|
|
fn (float $carry, CartItem $item): float => $carry
|
|
+ (($item->selectedItem()?->getPrice() ?? 0) * $item->cantidad),
|
|
0.0,
|
|
);
|
|
}
|
|
|
|
public function addItem(int $catalogItemId, ?int $variantId, int $quantity): CartItem
|
|
{
|
|
if ($quantity <= 0) {
|
|
throw ValidationException::withMessages([
|
|
'cantidad' => 'La cantidad debe ser mayor a cero.',
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($catalogItemId, $variantId, $quantity): CartItem {
|
|
self::query()->whereKey($this->getKey())->lockForUpdate()->firstOrFail();
|
|
$selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true);
|
|
$inventoryService = app(CatalogInventoryService::class);
|
|
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
|
|
|
|
if ($availableQuantity !== null && $availableQuantity < $quantity) {
|
|
throw ValidationException::withMessages([
|
|
'cantidad' => "Stock insuficiente para el producto solicitado. Maximo disponible: {$availableQuantity}.",
|
|
]);
|
|
}
|
|
|
|
/** @var CartItem|null $item */
|
|
$item = $this->items()
|
|
->where('catalog_item_id', $catalogItemId)
|
|
->where('variant_id', $variantId)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($item === null) {
|
|
$item = $this->items()->create([
|
|
'catalog_item_id' => $catalogItemId,
|
|
'variant_id' => $variantId,
|
|
'cantidad' => $quantity,
|
|
]);
|
|
} else {
|
|
$item->cantidad += $quantity;
|
|
$item->save();
|
|
}
|
|
|
|
$inventoryService->reserve($selectedItem, $quantity);
|
|
|
|
return $item->fresh();
|
|
});
|
|
}
|
|
|
|
public function updateItem(int $cartItemId, int $quantity): CartItem
|
|
{
|
|
if ($quantity <= 0) {
|
|
throw ValidationException::withMessages([
|
|
'cantidad' => 'La cantidad debe ser mayor a cero.',
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($cartItemId, $quantity): CartItem {
|
|
/** @var CartItem $item */
|
|
$item = $this->items()
|
|
->where('id', $cartItemId)
|
|
->lockForUpdate()
|
|
->firstOrFail();
|
|
|
|
$selectedItem = $this->resolveScopedItem(
|
|
$item->catalog_item_id,
|
|
$item->variant_id,
|
|
true,
|
|
);
|
|
$inventoryService = app(CatalogInventoryService::class);
|
|
$delta = $quantity - $item->cantidad;
|
|
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
|
|
|
|
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
|
|
$maxAvailable = $availableQuantity + $item->cantidad;
|
|
throw ValidationException::withMessages([
|
|
'cantidad' => "El máximo que se puede agregar es {$maxAvailable}.",
|
|
]);
|
|
}
|
|
|
|
$item->cantidad = $quantity;
|
|
$item->save();
|
|
|
|
if ($delta > 0) {
|
|
$inventoryService->reserve($selectedItem, $delta);
|
|
}
|
|
|
|
if ($delta < 0) {
|
|
$inventoryService->release($selectedItem, abs($delta));
|
|
}
|
|
|
|
return $item->fresh();
|
|
});
|
|
}
|
|
|
|
public function removeItem(int $cartItemId): void
|
|
{
|
|
DB::transaction(function () use ($cartItemId): void {
|
|
/** @var CartItem $item */
|
|
$item = $this->items()
|
|
->where('id', $cartItemId)
|
|
->lockForUpdate()
|
|
->firstOrFail();
|
|
|
|
$selectedItem = $this->resolveScopedItem(
|
|
$item->catalog_item_id,
|
|
$item->variant_id,
|
|
true,
|
|
);
|
|
app(CatalogInventoryService::class)->release(
|
|
$selectedItem,
|
|
$item->cantidad,
|
|
);
|
|
$item->delete();
|
|
});
|
|
}
|
|
|
|
protected function resolveScopedItem(
|
|
int $catalogItemId,
|
|
?int $variantId,
|
|
bool $lockForUpdate = false,
|
|
): CatalogItem|Variant {
|
|
$catalogItemQuery = CatalogItem::query()
|
|
->whereKey($catalogItemId)
|
|
->where('tenant_code', $this->tenant_codigo);
|
|
|
|
if ($lockForUpdate) {
|
|
$catalogItemQuery->lockForUpdate();
|
|
}
|
|
|
|
$catalogItem = $catalogItemQuery->first();
|
|
|
|
if ($catalogItem === null) {
|
|
throw new NotFoundHttpException('Catalog item not found for tenant.');
|
|
}
|
|
|
|
if ($catalogItem->isBundle()) {
|
|
if ($variantId !== null) {
|
|
throw ValidationException::withMessages([
|
|
'variant_id' => 'Un bundle no admite una variante.',
|
|
]);
|
|
}
|
|
|
|
if (! $catalogItem->bundleComponents()->exists()) {
|
|
throw ValidationException::withMessages([
|
|
'catalog_item_id' => 'El bundle no tiene componentes.',
|
|
]);
|
|
}
|
|
|
|
return $catalogItem;
|
|
}
|
|
|
|
if ($variantId === null) {
|
|
if ($catalogItem->inventory_id === null) {
|
|
throw ValidationException::withMessages([
|
|
'variant_id' => 'Debe seleccionar una variante para este ítem.',
|
|
]);
|
|
}
|
|
|
|
$inventory = $this->resolveInventory($catalogItem->inventory_id, $lockForUpdate);
|
|
$catalogItem->setRelation('inventory', $inventory);
|
|
|
|
return $catalogItem;
|
|
}
|
|
|
|
$variantQuery = Variant::query()
|
|
->whereKey($variantId)
|
|
->where('catalog_item_id', $catalogItem->id);
|
|
|
|
if ($lockForUpdate) {
|
|
$variantQuery->lockForUpdate();
|
|
}
|
|
|
|
$variant = $variantQuery->first();
|
|
|
|
if ($variant === null) {
|
|
throw new NotFoundHttpException('Variant not found for catalog item.');
|
|
}
|
|
|
|
$inventory = $this->resolveInventory($variant->inventory_id, $lockForUpdate);
|
|
$variant->setRelation('catalogItem', $catalogItem);
|
|
$variant->setRelation('inventory', $inventory);
|
|
|
|
return $variant;
|
|
}
|
|
|
|
protected function resolveInventory(int $inventoryId, bool $lockForUpdate): Inventory
|
|
{
|
|
$query = Inventory::query()->whereKey($inventoryId);
|
|
|
|
if ($lockForUpdate) {
|
|
$query->lockForUpdate();
|
|
}
|
|
|
|
return $query->firstOrFail();
|
|
}
|
|
}
|