feat(purchase): implement InsufficientStockException for handling stock errors in checkout process
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Exceptions;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class InsufficientStockException extends RuntimeException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, array{
|
||||||
|
* index: int,
|
||||||
|
* catalog_item_id: int,
|
||||||
|
* variant_id: int|null,
|
||||||
|
* requested_quantity: int,
|
||||||
|
* available_quantity: int,
|
||||||
|
* message: string
|
||||||
|
* }> $unavailableItems
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $unavailableItems,
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
collect($unavailableItems)->pluck('message')->unique()->implode(' '),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<int, string>> */
|
||||||
|
public function errors(): array
|
||||||
|
{
|
||||||
|
return collect($this->unavailableItems)
|
||||||
|
->mapWithKeys(fn (array $item): array => [
|
||||||
|
"direct_items.{$item['index']}.cantidad" => [$item['message']],
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\Domains\Catalog\Models\CatalogItem;
|
|||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use App\Domains\Catalog\Services\CatalogInventoryService;
|
use App\Domains\Catalog\Services\CatalogInventoryService;
|
||||||
use App\Domains\Purchase\Models\Purchase;
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||||
use App\Domains\Purchase\Services\UserPurchaseLimitService;
|
use App\Domains\Purchase\Services\UserPurchaseLimitService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -70,6 +71,7 @@ class StartCheckoutService
|
|||||||
$lines = collect(array_values($directItems))
|
$lines = collect(array_values($directItems))
|
||||||
->map(function (array $item, int $index): array {
|
->map(function (array $item, int $index): array {
|
||||||
return [
|
return [
|
||||||
|
'index' => $index,
|
||||||
'catalog_item_id' => (int) $item['catalog_item_id'],
|
'catalog_item_id' => (int) $item['catalog_item_id'],
|
||||||
'variant_id' => isset($item['variant_id']) ? (int) $item['variant_id'] : null,
|
'variant_id' => isset($item['variant_id']) ? (int) $item['variant_id'] : null,
|
||||||
'quantity' => (int) $item['cantidad'],
|
'quantity' => (int) $item['cantidad'],
|
||||||
@@ -124,30 +126,32 @@ class StartCheckoutService
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach ($resolvedLines as $line) {
|
$unavailableItems = $resolvedLines
|
||||||
|
->map(function (array $line): ?array {
|
||||||
$availableQuantity = $this->inventory->availableQuantity($line['selection']);
|
$availableQuantity = $this->inventory->availableQuantity($line['selection']);
|
||||||
|
|
||||||
if ($availableQuantity !== null && $availableQuantity < $line['quantity']) {
|
if ($availableQuantity === null || $availableQuantity >= $line['quantity']) {
|
||||||
throw ValidationException::withMessages([
|
return null;
|
||||||
"{$line['field']}.cantidad" => $this->stockMessages->build(
|
|
||||||
$line['catalog_item'],
|
|
||||||
$line['selection'],
|
|
||||||
$availableQuantity,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->unavailableItem($line, $availableQuantity);
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($unavailableItems !== []) {
|
||||||
|
throw new InsufficientStockException($unavailableItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($resolvedLines as $line) {
|
||||||
try {
|
try {
|
||||||
$this->inventory->reserve($line['selection'], $line['quantity']);
|
$this->inventory->reserve($line['selection'], $line['quantity']);
|
||||||
} catch (\InvalidArgumentException) {
|
} catch (\InvalidArgumentException) {
|
||||||
$availableQuantity = $this->inventory->availableQuantity($line['selection']) ?? 0;
|
$availableQuantity = $this->inventory->availableQuantity($line['selection']) ?? 0;
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
throw new InsufficientStockException([
|
||||||
"{$line['field']}.cantidad" => $this->stockMessages->build(
|
$this->unavailableItem($line, $availableQuantity),
|
||||||
$line['catalog_item'],
|
|
||||||
$line['selection'],
|
|
||||||
$availableQuantity,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +180,33 @@ class StartCheckoutService
|
|||||||
return $this->loadPurchase($purchase);
|
return $this->loadPurchase($purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $line
|
||||||
|
* @return array{
|
||||||
|
* index: int,
|
||||||
|
* catalog_item_id: int,
|
||||||
|
* variant_id: int|null,
|
||||||
|
* requested_quantity: int,
|
||||||
|
* available_quantity: int,
|
||||||
|
* message: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function unavailableItem(array $line, int $availableQuantity): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => $line['index'],
|
||||||
|
'catalog_item_id' => $line['catalog_item_id'],
|
||||||
|
'variant_id' => $line['variant_id'],
|
||||||
|
'requested_quantity' => $line['quantity'],
|
||||||
|
'available_quantity' => $availableQuantity,
|
||||||
|
'message' => $this->stockMessages->build(
|
||||||
|
$line['catalog_item'],
|
||||||
|
$line['selection'],
|
||||||
|
$availableQuantity,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $purchaseData */
|
/** @param array<string, mixed> $purchaseData */
|
||||||
private function startFromCart(
|
private function startFromCart(
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||||
use App\Domains\Ticket\Exceptions\TicketNotAvailableException;
|
use App\Domains\Ticket\Exceptions\TicketNotAvailableException;
|
||||||
|
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||||
use App\Http\Middleware\EnsureAdminAppTenant;
|
use App\Http\Middleware\EnsureAdminAppTenant;
|
||||||
use App\Http\Middleware\EnsureScannerTenant;
|
use App\Http\Middleware\EnsureScannerTenant;
|
||||||
use App\Http\Middleware\EnsureTenantHasMenu;
|
use App\Http\Middleware\EnsureTenantHasMenu;
|
||||||
@@ -74,6 +75,18 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
'message' => __('api.errors.forbidden'),
|
'message' => __('api.errors.forbidden'),
|
||||||
], 403);
|
], 403);
|
||||||
});
|
});
|
||||||
|
$exceptions->render(function (InsufficientStockException $exception, Request $request) {
|
||||||
|
if (! $request->is('api/*')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'code' => 'purchase.insufficient_stock',
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
'errors' => $exception->errors(),
|
||||||
|
'unavailable_items' => $exception->unavailableItems,
|
||||||
|
], 422);
|
||||||
|
});
|
||||||
$exceptions->render(function (ModelNotFoundException $exception, Request $request) {
|
$exceptions->render(function (ModelNotFoundException $exception, Request $request) {
|
||||||
if (! $request->is('api/*')) {
|
if (! $request->is('api/*')) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user