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\Services\CatalogInventoryService;
|
||||
use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||
use App\Domains\Purchase\Services\UserPurchaseLimitService;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -70,6 +71,7 @@ class StartCheckoutService
|
||||
$lines = collect(array_values($directItems))
|
||||
->map(function (array $item, int $index): array {
|
||||
return [
|
||||
'index' => $index,
|
||||
'catalog_item_id' => (int) $item['catalog_item_id'],
|
||||
'variant_id' => isset($item['variant_id']) ? (int) $item['variant_id'] : null,
|
||||
'quantity' => (int) $item['cantidad'],
|
||||
@@ -124,30 +126,32 @@ class StartCheckoutService
|
||||
);
|
||||
});
|
||||
|
||||
$unavailableItems = $resolvedLines
|
||||
->map(function (array $line): ?array {
|
||||
$availableQuantity = $this->inventory->availableQuantity($line['selection']);
|
||||
|
||||
if ($availableQuantity === null || $availableQuantity >= $line['quantity']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->unavailableItem($line, $availableQuantity);
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($unavailableItems !== []) {
|
||||
throw new InsufficientStockException($unavailableItems);
|
||||
}
|
||||
|
||||
foreach ($resolvedLines as $line) {
|
||||
$availableQuantity = $this->inventory->availableQuantity($line['selection']);
|
||||
|
||||
if ($availableQuantity !== null && $availableQuantity < $line['quantity']) {
|
||||
throw ValidationException::withMessages([
|
||||
"{$line['field']}.cantidad" => $this->stockMessages->build(
|
||||
$line['catalog_item'],
|
||||
$line['selection'],
|
||||
$availableQuantity,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->inventory->reserve($line['selection'], $line['quantity']);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$availableQuantity = $this->inventory->availableQuantity($line['selection']) ?? 0;
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
"{$line['field']}.cantidad" => $this->stockMessages->build(
|
||||
$line['catalog_item'],
|
||||
$line['selection'],
|
||||
$availableQuantity,
|
||||
),
|
||||
throw new InsufficientStockException([
|
||||
$this->unavailableItem($line, $availableQuantity),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -176,6 +180,33 @@ class StartCheckoutService
|
||||
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 */
|
||||
private function startFromCart(
|
||||
Tenant $tenant,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||
use App\Domains\Ticket\Exceptions\TicketNotAvailableException;
|
||||
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||
use App\Http\Middleware\EnsureAdminAppTenant;
|
||||
use App\Http\Middleware\EnsureScannerTenant;
|
||||
use App\Http\Middleware\EnsureTenantHasMenu;
|
||||
@@ -74,6 +75,18 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'message' => __('api.errors.forbidden'),
|
||||
], 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) {
|
||||
if (! $request->is('api/*')) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user