feat: refactor cart item handling to use mapped buyable types and improve method signatures

This commit is contained in:
2026-07-14 16:43:00 -03:00
parent 480ee70393
commit c05206cc51
7 changed files with 50 additions and 31 deletions

View File

@@ -29,7 +29,7 @@ class CartController extends Controller
$result = $this->cartService->addItem( $result = $this->cartService->addItem(
$tenant, $tenant,
$request, $request,
$request->validated('buyable_type'), $request->mappedBuyableType(),
(int) $request->validated('buyable_id'), (int) $request->validated('buyable_id'),
(int) $request->validated('cantidad'), (int) $request->validated('cantidad'),
); );
@@ -48,24 +48,22 @@ class CartController extends Controller
public function updateItemQuantity( public function updateItemQuantity(
UpdateCartItemQuantityRequest $request, UpdateCartItemQuantityRequest $request,
Tenant $tenant, Tenant $tenant,
string $buyableType, \App\Domains\Cart\Models\CartItem $cartItem,
int $buyableId,
): CartResource { ): CartResource {
return CartResource::make( return CartResource::make(
$this->cartService->updateItemQuantity( $this->cartService->updateItemQuantity(
$tenant, $tenant,
$request, $request,
$buyableType, $cartItem->getKey(),
$buyableId,
(int) $request->validated('cantidad'), (int) $request->validated('cantidad'),
) )
)->additional(['message' => 'Cantidad de producto actualizada.']); )->additional(['message' => 'Cantidad de producto actualizada.']);
} }
public function removeItem(Request $request, Tenant $tenant, string $buyableType, int $buyableId): CartResource public function removeItem(Request $request, Tenant $tenant, \App\Domains\Cart\Models\CartItem $cartItem): CartResource
{ {
return CartResource::make( return CartResource::make(
$this->cartService->removeItem($tenant, $request, $buyableType, $buyableId) $this->cartService->removeItem($tenant, $request, $cartItem->getKey())
)->additional(['message' => 'Producto eliminado del carrito.']); )->additional(['message' => 'Producto eliminado del carrito.']);
} }
} }

View File

@@ -116,7 +116,7 @@ class Cart extends Model
}); });
} }
public function updateItem(string $buyableType, int $buyableId, int $quantity): CartItem public function updateItem(int $cartItemId, int $quantity): CartItem
{ {
if ($quantity <= 0) { if ($quantity <= 0) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@@ -124,17 +124,14 @@ class Cart extends Model
]); ]);
} }
return DB::transaction(function () use ($buyableType, $buyableId, $quantity): CartItem { return DB::transaction(function () use ($cartItemId, $quantity): CartItem {
$canonicalType = $this->resolveBuyableClass($buyableType);
/** @var CartItem $item */ /** @var CartItem $item */
$item = $this->items() $item = $this->items()
->where('buyable_type', $canonicalType) ->where('id', $cartItemId)
->where('buyable_id', $buyableId)
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
$buyable = $this->resolveScopedBuyable($buyableType, $buyableId, true); $buyable = $this->resolveScopedBuyable($item->buyable_type, $item->buyable_id, true);
$delta = $quantity - $item->cantidad; $delta = $quantity - $item->cantidad;
$availableQuantity = $buyable->availableQuantity(); $availableQuantity = $buyable->availableQuantity();
@@ -160,19 +157,16 @@ class Cart extends Model
}); });
} }
public function removeItem(string $buyableType, int $buyableId): void public function removeItem(int $cartItemId): void
{ {
DB::transaction(function () use ($buyableType, $buyableId): void { DB::transaction(function () use ($cartItemId): void {
$canonicalType = $this->resolveBuyableClass($buyableType);
/** @var CartItem $item */ /** @var CartItem $item */
$item = $this->items() $item = $this->items()
->where('buyable_type', $canonicalType) ->where('id', $cartItemId)
->where('buyable_id', $buyableId)
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
$buyable = $this->resolveScopedBuyable($buyableType, $buyableId, true); $buyable = $this->resolveScopedBuyable($item->buyable_type, $item->buyable_id, true);
$buyable->decrementReservedStock($item->cantidad); $buyable->decrementReservedStock($item->cantidad);
$item->delete(); $item->delete();
}); });

View File

@@ -22,4 +22,13 @@ class AddCartItemRequest extends FormRequest
'cantidad' => ['required', 'integer', 'min:1'], 'cantidad' => ['required', 'integer', 'min:1'],
]; ];
} }
public function mappedBuyableType(): string
{
return match ($this->input('buyable_type')) {
'variant' => \App\Domains\Catalog\Models\ProductVariant::class,
'bundle' => \App\Domains\Bundle\Models\Bundle::class,
default => throw new \InvalidArgumentException('Invalid buyable type'),
};
}
} }

View File

@@ -22,7 +22,7 @@ class CartItemResource extends JsonResource
$precio = $buyable?->getPrice(); $precio = $buyable?->getPrice();
$imageUrl = null; $imageUrl = null;
if ($this->buyable_type === 'variant' && $buyable && $buyable->relationLoaded('attachments')) { if ($this->buyable_type === \App\Domains\Catalog\Models\ProductVariant::class && $buyable && $buyable->relationLoaded('attachments')) {
$firstAttachment = $buyable->attachments->first(); $firstAttachment = $buyable->attachments->first();
if ($firstAttachment) { if ($firstAttachment) {
$imageUrl = $firstAttachment->getTemporaryUrl(1440); $imageUrl = $firstAttachment->getTemporaryUrl(1440);
@@ -33,7 +33,7 @@ class CartItemResource extends JsonResource
'id' => $this->id, 'id' => $this->id,
'cantidad' => $this->cantidad, 'cantidad' => $this->cantidad,
'precio_unitario' => $this->formatMoney($precio), 'precio_unitario' => $this->formatMoney($precio),
'buyable_type' => $this->buyable_type, 'buyable_type' => $this->mapBuyableTypeToAlias($this->buyable_type),
'buyable_id' => $this->buyable_id, 'buyable_id' => $this->buyable_id,
'product' => $buyable === null ? null : [ 'product' => $buyable === null ? null : [
'nombre' => $productName, 'nombre' => $productName,
@@ -42,6 +42,15 @@ class CartItemResource extends JsonResource
]; ];
} }
protected function mapBuyableTypeToAlias(?string $type): string
{
return match ($type) {
\App\Domains\Catalog\Models\ProductVariant::class => 'variant',
\App\Domains\Bundle\Models\Bundle::class => 'bundle',
default => 'unknown',
};
}
protected function formatMoney(float|int|string|null $amount): string protected function formatMoney(float|int|string|null $amount): string
{ {
return number_format((float) ($amount ?? 0), 2, '.', ''); return number_format((float) ($amount ?? 0), 2, '.', '');

View File

@@ -45,20 +45,20 @@ class CartService
]; ];
} }
public function updateItemQuantity(Tenant $tenant, Request $request, string $buyableType, int $buyableId, int $quantity): Cart public function updateItemQuantity(Tenant $tenant, Request $request, int $cartItemId, int $quantity): Cart
{ {
$identity = $this->requireIdentity($request); $identity = $this->requireIdentity($request);
$cart = $this->findCartOrFail($tenant, $identity); $cart = $this->findCartOrFail($tenant, $identity);
$cart->updateItem($buyableType, $buyableId, $quantity); $cart->updateItem($cartItemId, $quantity);
return $this->loadCart($cart); return $this->loadCart($cart);
} }
public function removeItem(Tenant $tenant, Request $request, string $buyableType, int $buyableId): Cart public function removeItem(Tenant $tenant, Request $request, int $cartItemId): Cart
{ {
$identity = $this->requireIdentity($request); $identity = $this->requireIdentity($request);
$cart = $this->findCartOrFail($tenant, $identity); $cart = $this->findCartOrFail($tenant, $identity);
$cart->removeItem($buyableType, $buyableId); $cart->removeItem($cartItemId);
return $this->loadCart($cart); return $this->loadCart($cart);
} }

View File

@@ -8,6 +8,6 @@ Route::prefix('tenants/{tenant:codigo}')
->group(function (): void { ->group(function (): void {
Route::get('cart', [CartController::class, 'show']); Route::get('cart', [CartController::class, 'show']);
Route::post('cart/items', [CartController::class, 'addItem']); Route::post('cart/items', [CartController::class, 'addItem']);
Route::patch('cart/items/{buyableType}/{buyableId}', [CartController::class, 'updateItemQuantity']); Route::patch('cart/items/{cartItem}', [CartController::class, 'updateItemQuantity']);
Route::delete('cart/items/{buyableType}/{buyableId}', [CartController::class, 'removeItem']); Route::delete('cart/items/{cartItem}', [CartController::class, 'removeItem']);
}); });

View File

@@ -25,7 +25,7 @@ class PurchaseItemResource extends JsonResource
$imageUrl = null; $imageUrl = null;
$attributes = []; $attributes = [];
if ($this->buyable_type === 'variant' && $buyable) { if ($this->buyable_type === \App\Domains\Catalog\Models\ProductVariant::class && $buyable) {
$imageUrl = $this->resolveImageUrl($buyable); $imageUrl = $this->resolveImageUrl($buyable);
$attributes = $this->resolveAttributes($buyable); $attributes = $this->resolveAttributes($buyable);
} }
@@ -35,7 +35,7 @@ class PurchaseItemResource extends JsonResource
'quantity' => $quantity, 'quantity' => $quantity,
'unit_price' => $this->formatMoney($unitPrice), 'unit_price' => $this->formatMoney($unitPrice),
'line_total' => $this->formatMoney($lineTotal), 'line_total' => $this->formatMoney($lineTotal),
'buyable_type' => $this->buyable_type, 'buyable_type' => $this->mapBuyableTypeToAlias($this->buyable_type),
'buyable_id' => $this->buyable_id, 'buyable_id' => $this->buyable_id,
'item_details' => $buyable === null ? null : [ 'item_details' => $buyable === null ? null : [
'nombre' => $buyable->getName(), 'nombre' => $buyable->getName(),
@@ -45,6 +45,15 @@ class PurchaseItemResource extends JsonResource
]; ];
} }
protected function mapBuyableTypeToAlias(?string $type): string
{
return match ($type) {
\App\Domains\Catalog\Models\ProductVariant::class => 'variant',
\App\Domains\Bundle\Models\Bundle::class => 'bundle',
default => 'unknown',
};
}
protected function resolveUnitPrice(): float protected function resolveUnitPrice(): float
{ {
if ($this->resource instanceof PurchaseItem) { if ($this->resource instanceof PurchaseItem) {