feat(cart): enhance item update functionality to support variant changes and stock management; update related request validation and resource formatting; add tests for variant handling in cart

This commit is contained in:
2026-08-10 12:08:09 -03:00
parent 2693df2cbf
commit c6351d706f
8 changed files with 225 additions and 15 deletions

View File

@@ -54,16 +54,26 @@ class CartController extends Controller
Tenant $tenant,
CartItem $cartItem,
): CartResource {
$updatesVariant = $request->exists('variant_id');
return CartResource::make(
$this->cartService->updateItemQuantity(
$this->cartService->updateItem(
$tenant,
$request,
$cartItem->getKey(),
(int) $request->validated('cantidad'),
$updatesVariant
? ($request->validated('variant_id') !== null
? (int) $request->validated('variant_id')
: null)
: $cartItem->variant_id,
$updatesVariant,
)
)->additional([
'code' => 'cart.quantity_updated',
'message' => __('api.cart.quantity_updated'),
'code' => $updatesVariant ? 'cart.item_updated' : 'cart.quantity_updated',
'message' => $updatesVariant
? __('api.cart.item_updated')
: __('api.cart.quantity_updated'),
]);
}

View File

@@ -124,27 +124,85 @@ class Cart extends Model
});
}
public function updateItem(int $cartItemId, int $quantity): CartItem
{
public function updateItem(
int $cartItemId,
int $quantity,
?int $variantId = null,
bool $updateVariant = false,
): CartItem {
if ($quantity <= 0) {
throw ValidationException::withMessages([
'cantidad' => __('api.cart.positive_quantity'),
]);
}
return DB::transaction(function () use ($cartItemId, $quantity): CartItem {
return DB::transaction(function () use (
$cartItemId,
$quantity,
$variantId,
$updateVariant,
): CartItem {
/** @var CartItem $item */
$item = $this->items()
->where('id', $cartItemId)
->lockForUpdate()
->firstOrFail();
$selectedItem = $this->resolveScopedItem(
$currentSelection = $this->resolveScopedItem(
$item->catalog_item_id,
$item->variant_id,
true,
);
$inventoryService = app(CatalogInventoryService::class);
if ($updateVariant && $variantId !== $item->variant_id) {
$nextSelection = $this->resolveScopedItem(
$item->catalog_item_id,
$variantId,
true,
);
$otherVariantsQuantity = (int) $this->items()
->where('catalog_item_id', $item->catalog_item_id)
->whereKeyNot($item->getKey())
->sum('cantidad');
$this->assertUserPurchaseLimit(
$nextSelection,
$otherVariantsQuantity + $quantity,
);
$inventoryService->release($currentSelection, $item->cantidad);
$availableQuantity = $inventoryService->availableQuantity($nextSelection);
if ($availableQuantity !== null && $availableQuantity < $quantity) {
throw ValidationException::withMessages([
'variant_id' => __('api.cart.insufficient_stock', ['max' => $availableQuantity]),
]);
}
$targetItem = $this->items()
->where('catalog_item_id', $item->catalog_item_id)
->where('variant_id', $variantId)
->whereKeyNot($item->getKey())
->lockForUpdate()
->first();
$inventoryService->reserve($nextSelection, $quantity);
if ($targetItem !== null) {
$targetItem->cantidad += $quantity;
$targetItem->save();
$item->delete();
return $targetItem->fresh();
}
$item->variant_id = $variantId;
$item->cantidad = $quantity;
$item->save();
return $item->fresh();
}
$delta = $quantity - $item->cantidad;
if ($delta > 0) {
@@ -153,12 +211,12 @@ class Cart extends Model
->whereKeyNot($item->getKey())
->sum('cantidad');
$this->assertUserPurchaseLimit(
$selectedItem,
$currentSelection,
$otherVariantsQuantity + $quantity,
);
}
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
$availableQuantity = $inventoryService->availableQuantity($currentSelection);
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
$maxAvailable = $availableQuantity + $item->cantidad;
@@ -171,11 +229,11 @@ class Cart extends Model
$item->save();
if ($delta > 0) {
$inventoryService->reserve($selectedItem, $delta);
$inventoryService->reserve($currentSelection, $delta);
}
if ($delta < 0) {
$inventoryService->release($selectedItem, abs($delta));
$inventoryService->release($currentSelection, abs($delta));
}
return $item->fresh();

View File

@@ -19,7 +19,7 @@ class UpdateCartItemQuantityRequest extends FormRequest
return [
'cantidad' => ['required', 'integer', 'min:1'],
'catalog_item_id' => ['prohibited'],
'variant_id' => ['prohibited'],
'variant_id' => ['sometimes', 'nullable', 'integer'],
];
}
}

View File

@@ -3,8 +3,11 @@
namespace App\Domains\Cart\Resources;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\Variant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/**
* @mixin CartItem
@@ -36,10 +39,42 @@ class CartItemResource extends JsonResource
'product' => $selectedItem === null ? null : [
'nombre' => $selectedItem->getName(),
'imagen' => $imageUrl,
'variants' => $this->catalogItem->variants
->map(fn (Variant $variant): array => [
'id' => $variant->id,
'precio' => $this->formatMoney($variant->getPrice()),
'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited
? null
: $variant->inventory->availableStock(),
'values' => $this->variantValues($variant),
])
->values(),
],
];
}
/** @return Collection<string, string|array<int, string>> */
private function variantValues(Variant $variant): Collection
{
$values = $variant->selectionValues();
$eventDates = $variant->selectedEventDates();
if ($eventDates->isNotEmpty()) {
$labels = $eventDates
->map(fn ($eventDate): string => $eventDate->date->format('d/m/Y').' · '
.substr($eventDate->time_start, 0, 5).' a '
.substr($eventDate->time_end, 0, 5))
->values();
$values->put(
'event_date',
$labels->count() === 1 ? $labels->first() : $labels->all(),
);
}
return $values;
}
protected function formatMoney(float|int|string|null $amount): string
{
return number_format((float) ($amount ?? 0), 2, '.', '');

View File

@@ -51,11 +51,17 @@ class CartService
];
}
public function updateItemQuantity(Tenant $tenant, Request $request, int $cartItemId, int $quantity): Cart
{
public function updateItem(
Tenant $tenant,
Request $request,
int $cartItemId,
int $quantity,
?int $variantId,
bool $updateVariant,
): Cart {
$identity = $this->requireIdentity($request);
$cart = $this->findCartOrFail($tenant, $identity);
$cart->updateItem($cartItemId, $quantity);
$cart->updateItem($cartItemId, $quantity, $variantId, $updateVariant);
return $this->loadCart($cart);
}
@@ -101,6 +107,10 @@ class CartService
return $cart->fresh()->load([
'items.catalogItem.attachments',
'items.catalogItem.inventory',
'items.catalogItem.variants.inventory',
'items.catalogItem.variants.definitions.itemAttribute.attribute',
'items.catalogItem.variants.eventDates',
'items.catalogItem.variants.eventDate',
'items.variant.attachments',
'items.variant.inventory',
'items.variant.definitions.itemAttribute.attribute',