refactor(catalog): Complete catalog refactor to simplify its data model and its querying.
source commits: refactor/catalog
This commit is contained in:
@@ -3,9 +3,10 @@
|
||||
namespace App\Domains\Cart\Models;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Bundle\Models\Bundle;
|
||||
use App\Domains\Catalog\Models\ProductVariant;
|
||||
use App\Domains\Shared\Contracts\Buyable;
|
||||
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;
|
||||
@@ -65,15 +66,16 @@ class Cart extends Model
|
||||
{
|
||||
$items = $this->relationLoaded('items')
|
||||
? $this->getRelation('items')
|
||||
: $this->items()->with('buyable')->get();
|
||||
: $this->items()->with(['catalogItem', 'variant'])->get();
|
||||
|
||||
return (float) $items->reduce(
|
||||
fn (float $carry, $item): float => $carry + ($item->buyable?->getPrice() * $item->cantidad),
|
||||
fn (float $carry, CartItem $item): float => $carry
|
||||
+ (($item->selectedItem()?->getPrice() ?? 0) * $item->cantidad),
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
|
||||
public function addItem(string $buyableType, int $buyableId, int $quantity): CartItem
|
||||
public function addItem(int $catalogItemId, ?int $variantId, int $quantity): CartItem
|
||||
{
|
||||
if ($quantity <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
@@ -81,10 +83,11 @@ class Cart extends Model
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($buyableType, $buyableId, $quantity): CartItem {
|
||||
$buyable = $this->resolveScopedBuyable($buyableType, $buyableId, true);
|
||||
$canonicalType = $buyable::class;
|
||||
$availableQuantity = $buyable->availableQuantity();
|
||||
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([
|
||||
@@ -94,15 +97,15 @@ class Cart extends Model
|
||||
|
||||
/** @var CartItem|null $item */
|
||||
$item = $this->items()
|
||||
->where('buyable_type', $canonicalType)
|
||||
->where('buyable_id', $buyable->getKey())
|
||||
->where('catalog_item_id', $catalogItemId)
|
||||
->where('variant_id', $variantId)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($item === null) {
|
||||
$item = $this->items()->create([
|
||||
'buyable_type' => $canonicalType,
|
||||
'buyable_id' => $buyable->getKey(),
|
||||
'catalog_item_id' => $catalogItemId,
|
||||
'variant_id' => $variantId,
|
||||
'cantidad' => $quantity,
|
||||
]);
|
||||
} else {
|
||||
@@ -110,7 +113,7 @@ class Cart extends Model
|
||||
$item->save();
|
||||
}
|
||||
|
||||
$buyable->reserveStock($quantity);
|
||||
$inventoryService->reserve($selectedItem, $quantity);
|
||||
|
||||
return $item->fresh();
|
||||
});
|
||||
@@ -131,9 +134,14 @@ class Cart extends Model
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$buyable = $this->resolveScopedBuyable($item->buyable_type, $item->buyable_id, true);
|
||||
$selectedItem = $this->resolveScopedItem(
|
||||
$item->catalog_item_id,
|
||||
$item->variant_id,
|
||||
true,
|
||||
);
|
||||
$inventoryService = app(CatalogInventoryService::class);
|
||||
$delta = $quantity - $item->cantidad;
|
||||
$availableQuantity = $buyable->availableQuantity();
|
||||
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
|
||||
|
||||
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
|
||||
$maxAvailable = $availableQuantity + $item->cantidad;
|
||||
@@ -146,11 +154,11 @@ class Cart extends Model
|
||||
$item->save();
|
||||
|
||||
if ($delta > 0) {
|
||||
$buyable->reserveStock($delta);
|
||||
$inventoryService->reserve($selectedItem, $delta);
|
||||
}
|
||||
|
||||
if ($delta < 0) {
|
||||
$buyable->decrementReservedStock(abs($delta));
|
||||
$inventoryService->release($selectedItem, abs($delta));
|
||||
}
|
||||
|
||||
return $item->fresh();
|
||||
@@ -166,45 +174,96 @@ class Cart extends Model
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$buyable = $this->resolveScopedBuyable($item->buyable_type, $item->buyable_id, true);
|
||||
$buyable->decrementReservedStock($item->cantidad);
|
||||
$selectedItem = $this->resolveScopedItem(
|
||||
$item->catalog_item_id,
|
||||
$item->variant_id,
|
||||
true,
|
||||
);
|
||||
app(CatalogInventoryService::class)->release(
|
||||
$selectedItem,
|
||||
$item->cantidad,
|
||||
);
|
||||
$item->delete();
|
||||
});
|
||||
}
|
||||
|
||||
protected function resolveScopedBuyable(string $buyableType, int $buyableId, bool $lockForUpdate = false): Buyable
|
||||
{
|
||||
$buyableClass = $this->resolveBuyableClass($buyableType);
|
||||
protected function resolveScopedItem(
|
||||
int $catalogItemId,
|
||||
?int $variantId,
|
||||
bool $lockForUpdate = false,
|
||||
): CatalogItem|Variant {
|
||||
$catalogItemQuery = CatalogItem::query()
|
||||
->whereKey($catalogItemId)
|
||||
->where('tenant_code', $this->tenant_codigo);
|
||||
|
||||
if ($buyableClass === ProductVariant::class) {
|
||||
$query = ProductVariant::query()
|
||||
->whereKey($buyableId)
|
||||
->whereHas('product', fn ($query) => $query->where('tenant_codigo', $this->tenant_codigo));
|
||||
} else {
|
||||
$query = Bundle::query()
|
||||
->whereKey($buyableId)
|
||||
->where('tenant_codigo', $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();
|
||||
}
|
||||
|
||||
$buyable = $query->first();
|
||||
|
||||
if ($buyable === null) {
|
||||
throw new NotFoundHttpException('Buyable not found for tenant.');
|
||||
}
|
||||
|
||||
return $buyable;
|
||||
}
|
||||
|
||||
protected function resolveBuyableClass(string $buyableType): string
|
||||
{
|
||||
return match ($buyableType) {
|
||||
'variant', ProductVariant::class => ProductVariant::class,
|
||||
'bundle', Bundle::class => Bundle::class,
|
||||
default => throw new \InvalidArgumentException('Invalid buyable type'),
|
||||
};
|
||||
return $query->firstOrFail();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user