diff --git a/app/Domains/Purchase/Exceptions/InsufficientStockException.php b/app/Domains/Purchase/Exceptions/InsufficientStockException.php new file mode 100644 index 0000000..62f7787 --- /dev/null +++ b/app/Domains/Purchase/Exceptions/InsufficientStockException.php @@ -0,0 +1,36 @@ + $unavailableItems + */ + public function __construct( + public readonly array $unavailableItems, + ) { + parent::__construct( + collect($unavailableItems)->pluck('message')->unique()->implode(' '), + ); + } + + /** @return array> */ + public function errors(): array + { + return collect($this->unavailableItems) + ->mapWithKeys(fn (array $item): array => [ + "direct_items.{$item['index']}.cantidad" => [$item['message']], + ]) + ->all(); + } +} diff --git a/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php b/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php index 152dadf..6c3c7a3 100644 --- a/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php @@ -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 $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 $purchaseData */ private function startFromCart( Tenant $tenant, diff --git a/bootstrap/app.php b/bootstrap/app.php index 3e6b9b1..8a1b5e6 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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;