feat(purchase): update direct item handling to support multiple items in checkout

This commit is contained in:
2026-08-12 14:29:10 -03:00
parent 45d74e166f
commit 5b089e71b2
6 changed files with 250 additions and 87 deletions

View File

@@ -19,29 +19,21 @@ class StartCheckoutRequest extends FormRequest
{
return [
'cart_id' => [
'required_without:direct_item',
Rule::prohibitedIf(fn (): bool => $this->has('direct_item')),
'required_without:direct_items',
Rule::prohibitedIf(fn (): bool => $this->has('direct_items')),
'integer',
'exists:carritos,id',
],
'direct_item' => [
'direct_items' => [
'required_without:cart_id',
Rule::prohibitedIf(fn (): bool => $this->has('cart_id')),
'array',
],
'direct_item.catalog_item_id' => [
'required_with:direct_item',
'integer',
],
'direct_item.variant_id' => [
'nullable',
'integer',
],
'direct_item.cantidad' => [
'required_with:direct_item',
'integer',
'min:1',
],
'direct_items.*' => ['required', 'array'],
'direct_items.*.catalog_item_id' => ['required', 'integer'],
'direct_items.*.variant_id' => ['nullable', 'integer'],
'direct_items.*.cantidad' => ['required', 'integer', 'min:1'],
'dni' => ['prohibited'],
'telefono' => ['prohibited'],
'nombre_apellido' => ['prohibited'],

View File

@@ -16,6 +16,7 @@ class CatalogSelectionResolver
Tenant $tenant,
int $catalogItemId,
?int $variantId,
string $fieldPrefix = 'direct_items',
): CatalogItem|Variant {
/** @var CatalogItem|null $catalogItem */
$catalogItem = CatalogItem::query()
@@ -31,13 +32,13 @@ class CatalogSelectionResolver
if ($catalogItem->isBundle()) {
if ($variantId !== null) {
throw ValidationException::withMessages([
'direct_item.variant_id' => __('api.cart.bundle_variant_forbidden'),
"{$fieldPrefix}.variant_id" => __('api.cart.bundle_variant_forbidden'),
]);
}
if (! $catalogItem->bundleComponents()->exists()) {
throw ValidationException::withMessages([
'direct_item.catalog_item_id' => __('api.cart.empty_bundle'),
"{$fieldPrefix}.catalog_item_id" => __('api.cart.empty_bundle'),
]);
}
@@ -47,7 +48,7 @@ class CatalogSelectionResolver
if ($variantId === null) {
if ($catalogItem->inventory_id === null) {
throw ValidationException::withMessages([
'direct_item.variant_id' => __('api.cart.variant_required'),
"{$fieldPrefix}.variant_id" => __('api.cart.variant_required'),
]);
}

View File

@@ -33,12 +33,17 @@ class StartCheckoutService
->lockForUpdate()
->findOrFail($tenant->getKey());
$directItem = $purchaseData['direct_item'] ?? null;
$directItems = $purchaseData['direct_items'] ?? null;
$cartId = isset($purchaseData['cart_id']) ? (int) $purchaseData['cart_id'] : null;
unset($purchaseData['direct_item'], $purchaseData['cart_id']);
unset($purchaseData['direct_items'], $purchaseData['cart_id']);
if (is_array($directItem)) {
return $this->startDirect($tenant, $userId, $purchaseData, $directItem);
if (is_array($directItems)) {
return $this->startDirectItems(
$tenant,
$userId,
$purchaseData,
$directItems,
);
}
if ($cartId === null) {
@@ -53,58 +58,110 @@ class StartCheckoutService
/**
* @param array<string, mixed> $purchaseData
* @param array<string, mixed> $directItem
* @param array<int, array<string, mixed>> $directItems
*/
private function startDirect(
private function startDirectItems(
Tenant $tenant,
int $userId,
array $purchaseData,
array $directItem,
array $directItems,
): Purchase {
$catalogItemId = (int) $directItem['catalog_item_id'];
$variantId = isset($directItem['variant_id']) ? (int) $directItem['variant_id'] : null;
$quantity = (int) $directItem['cantidad'];
$selection = $this->selections->resolve($tenant, $catalogItemId, $variantId);
$catalogItem = $selection instanceof Variant ? $selection->catalogItem : $selection;
$lines = collect(array_values($directItems))
->map(function (array $item, int $index): array {
return [
'catalog_item_id' => (int) $item['catalog_item_id'],
'variant_id' => isset($item['variant_id']) ? (int) $item['variant_id'] : null,
'quantity' => (int) $item['cantidad'],
'field' => "direct_items.{$index}",
];
})
->groupBy(fn (array $line): string => sprintf(
'%d:%s',
$line['catalog_item_id'],
$line['variant_id'] === null ? 'none' : (string) $line['variant_id'],
))
->map(function (Collection $duplicateLines): array {
$line = $duplicateLines->first();
$line['quantity'] = (int) $duplicateLines->sum('quantity');
$this->purchaseLimits->assertCanPurchase(
$catalogItem,
$userId,
$quantity,
field: 'direct_item.cantidad',
);
return $line;
})
->sortBy(fn (array $line): string => sprintf(
'%020d:%020d',
$line['catalog_item_id'],
$line['variant_id'] ?? 0,
))
->values();
$availableQuantity = $this->inventory->availableQuantity($selection);
$resolvedLines = $lines->map(function (array $line) use ($tenant): array {
$selection = $this->selections->resolve(
$tenant,
$line['catalog_item_id'],
$line['variant_id'],
$line['field'],
);
if ($availableQuantity !== null && $availableQuantity < $quantity) {
throw ValidationException::withMessages([
'direct_item.cantidad' => __('api.purchase.direct_item_max_stock', ['max' => $availableQuantity]),
]);
}
return [
...$line,
'selection' => $selection,
'catalog_item' => $selection instanceof Variant
? $selection->catalogItem
: $selection,
];
});
try {
$this->inventory->reserve($selection, $quantity);
} catch (\InvalidArgumentException) {
throw ValidationException::withMessages([
'direct_item.cantidad' => __('api.purchase.insufficient_stock'),
]);
$resolvedLines
->groupBy(fn (array $line): int => $line['catalog_item']->getKey())
->each(function (Collection $catalogLines) use ($userId): void {
/** @var CatalogItem $catalogItem */
$catalogItem = $catalogLines->first()['catalog_item'];
$this->purchaseLimits->assertCanPurchase(
$catalogItem,
$userId,
(int) $catalogLines->sum('quantity'),
field: 'direct_items',
);
});
foreach ($resolvedLines as $line) {
$availableQuantity = $this->inventory->availableQuantity($line['selection']);
if ($availableQuantity !== null && $availableQuantity < $line['quantity']) {
throw ValidationException::withMessages([
"{$line['field']}.cantidad" => __('api.purchase.direct_items_max_stock', [
'max' => $availableQuantity,
]),
]);
}
try {
$this->inventory->reserve($line['selection'], $line['quantity']);
} catch (\InvalidArgumentException) {
throw ValidationException::withMessages([
"{$line['field']}.cantidad" => __('api.purchase.insufficient_stock'),
]);
}
}
$purchase = $this->createPurchase(
$tenant,
$userId,
$purchaseData,
$selection->getPrice() * $quantity,
(float) $resolvedLines->sum(
fn (array $line): float => $line['selection']->getPrice() * $line['quantity'],
),
null,
);
$directCartItem = $this->makeDirectCartItem(
$selection,
$catalogItemId,
$variantId,
$quantity,
);
$directCartItems = $resolvedLines->map(fn (array $line): CartItem => $this->makeDirectCartItem(
$line['selection'],
$line['catalog_item_id'],
$line['variant_id'],
$line['quantity'],
));
$purchase->items()->createMany(
$this->snapshots->fromCartItems(collect([$directCartItem])),
$this->snapshots->fromCartItems($directCartItems),
);
return $this->loadPurchase($purchase);