feat(catalog): expose availability policies
This commit is contained in:
@@ -38,11 +38,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
'nombre' => $catalogItem->nombre,
|
||||
'descripcion' => $catalogItem->descripcion,
|
||||
'precio' => $catalogItem->precio,
|
||||
'maximum_addable_quantity' => $this->maximumAddable(
|
||||
$availableStock,
|
||||
$remainingUserQuota,
|
||||
),
|
||||
'unavailable_message' => $this->unavailableMessage(
|
||||
'availability' => $this->availability(
|
||||
$availableStock,
|
||||
$remainingUserQuota,
|
||||
),
|
||||
@@ -60,13 +56,10 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||
'descripcion' => $variant->getDescription(),
|
||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||
'maximum_addable_quantity' => $this->maximumAddable(
|
||||
$variantStock,
|
||||
$remainingUserQuota,
|
||||
),
|
||||
'unavailable_message' => $this->unavailableMessage(
|
||||
'availability' => $this->availability(
|
||||
$variantStock,
|
||||
$remainingUserQuota,
|
||||
'variant',
|
||||
),
|
||||
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
|
||||
];
|
||||
@@ -90,8 +83,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
'descripcion' => $catalogItem->descripcion,
|
||||
'precio' => $catalogItem->precio,
|
||||
'image' => $this->firstImageUrl($catalogItem),
|
||||
'maximum_addable_quantity' => $this->maximumAddable($availableStock, $remainingUserQuota),
|
||||
'unavailable_message' => $this->unavailableMessage($availableStock, $remainingUserQuota),
|
||||
'availability' => $this->availability($availableStock, $remainingUserQuota),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -107,8 +99,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
'nombre' => $catalogItem->nombre,
|
||||
'precio' => $catalogItem->precio,
|
||||
'image' => $this->firstImageUrl($catalogItem),
|
||||
'maximum_addable_quantity' => $this->maximumAddable($availableStock, $remainingUserQuota),
|
||||
'unavailable_message' => $this->unavailableMessage($availableStock, $remainingUserQuota),
|
||||
'availability' => $this->availability($availableStock, $remainingUserQuota),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -122,15 +113,13 @@ class CatalogFeaturedItemResource extends JsonResource
|
||||
return $attachment?->getTemporaryUrl(1440);
|
||||
}
|
||||
|
||||
private function maximumAddable(?int $stock, ?int $remainingUserQuota): ?int
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
private function availability(
|
||||
?int $stock,
|
||||
?int $remainingUserQuota,
|
||||
string $scope = 'product',
|
||||
): array {
|
||||
return app(CatalogItemAllowanceService::class)
|
||||
->maximumAddableQuantity($stock, $remainingUserQuota);
|
||||
}
|
||||
|
||||
private function unavailableMessage(?int $stock, ?int $remainingUserQuota): ?string
|
||||
{
|
||||
return app(CatalogItemAllowanceService::class)
|
||||
->unavailableMessage($stock, $remainingUserQuota);
|
||||
->availability($stock, $remainingUserQuota, $scope);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,7 @@ class CatalogItemDetailResource extends JsonResource
|
||||
'max_units_per_user' => $this->max_units_per_user,
|
||||
'has_tickets' => $this->has_tickets,
|
||||
'attributes' => $this->attributesData(),
|
||||
'maximum_addable_quantity' => $this->when(
|
||||
$selectedVariant === null,
|
||||
fn () => $this->maximumAddable($this->availableStock()),
|
||||
),
|
||||
'unavailable_message' => $this->when(
|
||||
$selectedVariant === null,
|
||||
fn () => $this->unavailableMessage($this->availableStock()),
|
||||
),
|
||||
'availability' => $this->availability($this->availableStock()),
|
||||
'images' => $this->when(
|
||||
$selectedVariant === null,
|
||||
fn () => $this->imageUrls($this->attachments),
|
||||
@@ -173,8 +166,7 @@ class CatalogItemDetailResource extends JsonResource
|
||||
'event_dates' => $eventDates->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||
'descripcion' => $variant->getDescription(),
|
||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||
'maximum_addable_quantity' => $this->maximumAddable($variantStock),
|
||||
'unavailable_message' => $this->unavailableMessage($variantStock),
|
||||
'availability' => $this->availability($variantStock, 'variant'),
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
@@ -194,19 +186,13 @@ class CatalogItemDetailResource extends JsonResource
|
||||
: $variant->inventory->availableStock();
|
||||
}
|
||||
|
||||
private function maximumAddable(?int $stock): ?int
|
||||
/** @return array<string, mixed> */
|
||||
private function availability(?int $stock, string $scope = 'product'): array
|
||||
{
|
||||
return app(CatalogItemAllowanceService::class)->maximumAddableQuantity(
|
||||
$stock,
|
||||
$this->getAttribute('remaining_user_quota'),
|
||||
);
|
||||
}
|
||||
|
||||
private function unavailableMessage(?int $stock): ?string
|
||||
{
|
||||
return app(CatalogItemAllowanceService::class)->unavailableMessage(
|
||||
return app(CatalogItemAllowanceService::class)->availability(
|
||||
$stock,
|
||||
$this->getAttribute('remaining_user_quota'),
|
||||
$scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@ class CatalogSearchItemResource extends JsonResource
|
||||
'descripcion' => $this->descripcion,
|
||||
'precio' => $this->precio,
|
||||
'image' => $attachment?->getTemporaryUrl(1440),
|
||||
'maximum_addable_quantity' => $this->maximumAddable($availableStock),
|
||||
'unavailable_message' => $this->unavailableMessage($availableStock),
|
||||
'availability' => $this->availability($availableStock),
|
||||
'variants' => $this->variants
|
||||
->map(function (Variant $variant): array {
|
||||
$variantStock = $this->inventory_policy === InventoryPolicy::Unlimited
|
||||
@@ -44,8 +43,7 @@ class CatalogSearchItemResource extends JsonResource
|
||||
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||
'descripcion' => $variant->getDescription(),
|
||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||
'maximum_addable_quantity' => $this->maximumAddable($variantStock),
|
||||
'unavailable_message' => $this->unavailableMessage($variantStock),
|
||||
'availability' => $this->availability($variantStock, 'variant'),
|
||||
'values' => $variant->selectorOptions($this->itemAttributes),
|
||||
];
|
||||
})
|
||||
@@ -53,19 +51,13 @@ class CatalogSearchItemResource extends JsonResource
|
||||
];
|
||||
}
|
||||
|
||||
private function maximumAddable(?int $stock): ?int
|
||||
/** @return array<string, mixed> */
|
||||
private function availability(?int $stock, string $scope = 'product'): array
|
||||
{
|
||||
return app(CatalogItemAllowanceService::class)->maximumAddableQuantity(
|
||||
$stock,
|
||||
$this->getAttribute('remaining_user_quota'),
|
||||
);
|
||||
}
|
||||
|
||||
private function unavailableMessage(?int $stock): ?string
|
||||
{
|
||||
return app(CatalogItemAllowanceService::class)->unavailableMessage(
|
||||
return app(CatalogItemAllowanceService::class)->availability(
|
||||
$stock,
|
||||
$this->getAttribute('remaining_user_quota'),
|
||||
$scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,16 +42,75 @@ class CatalogItemAllowanceService
|
||||
return min($availableStock, $remainingUserQuota);
|
||||
}
|
||||
|
||||
public function unavailableMessage(?int $availableStock, ?int $remainingUserQuota): ?string
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* maximum_quantity: int|null,
|
||||
* restrictions: list<array{code: string, message: string, scope: string}>,
|
||||
* capabilities: array{select_variant: bool, change_quantity: bool, add_to_cart: bool, buy_now: bool}
|
||||
* }
|
||||
*/
|
||||
public function availability(
|
||||
?int $availableStock,
|
||||
?int $remainingUserQuota,
|
||||
string $scope = 'product',
|
||||
): array {
|
||||
$restrictions = [];
|
||||
|
||||
if ($remainingUserQuota !== null && $remainingUserQuota <= 0) {
|
||||
return self::USER_QUOTA_REACHED_MESSAGE;
|
||||
$restrictions[] = [
|
||||
'code' => 'user_quota_reached',
|
||||
'message' => self::USER_QUOTA_REACHED_MESSAGE,
|
||||
'scope' => $scope,
|
||||
];
|
||||
}
|
||||
|
||||
if ($availableStock !== null && $availableStock <= 0) {
|
||||
return self::OUT_OF_STOCK_MESSAGE;
|
||||
$restrictions[] = [
|
||||
'code' => 'out_of_stock',
|
||||
'message' => self::OUT_OF_STOCK_MESSAGE,
|
||||
'scope' => $scope,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
$capabilities = [
|
||||
'select_variant' => true,
|
||||
'change_quantity' => true,
|
||||
'add_to_cart' => true,
|
||||
'buy_now' => true,
|
||||
];
|
||||
|
||||
foreach ($restrictions as $restriction) {
|
||||
/** @var array<string, bool> $policy */
|
||||
$policy = config("catalog.availability_policies.{$restriction['code']}", []);
|
||||
|
||||
foreach ($capabilities as $capability => $allowed) {
|
||||
$capabilities[$capability] = $allowed && ($policy[$capability] ?? true);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'maximum_quantity' => $this->maximumAddableQuantity(
|
||||
$availableStock,
|
||||
$remainingUserQuota,
|
||||
),
|
||||
'restrictions' => $restrictions,
|
||||
'capabilities' => $capabilities,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function purchaseLimitExceededAvailability(int $maximumQuantity, string $message): array
|
||||
{
|
||||
$availability = $this->availability(null, $maximumQuantity);
|
||||
|
||||
if ($maximumQuantity > 0) {
|
||||
$availability['restrictions'][] = [
|
||||
'code' => 'requested_quantity_exceeds_user_quota',
|
||||
'message' => $message,
|
||||
'scope' => 'product',
|
||||
];
|
||||
}
|
||||
|
||||
return $availability;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||
use App\Domains\Catalog\Services\CatalogItemAllowanceService;
|
||||
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||
use App\Domains\Purchase\Exceptions\PurchaseExpiredException;
|
||||
use App\Domains\Purchase\Exceptions\PurchaseLimitExceededException;
|
||||
@@ -100,7 +101,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'errors' => $exception->errors(),
|
||||
'catalog_item_id' => $exception->catalogItemId,
|
||||
'catalog_item_name' => $exception->catalogItemName,
|
||||
'maximum_addable_quantity' => $exception->maximumAddableQuantity,
|
||||
'availability' => app(CatalogItemAllowanceService::class)
|
||||
->purchaseLimitExceededAvailability(
|
||||
$exception->maximumAddableQuantity,
|
||||
$exception->getMessage(),
|
||||
),
|
||||
], 422);
|
||||
});
|
||||
$exceptions->render(function (PurchaseExpiredException $exception, Request $request) {
|
||||
|
||||
@@ -2,4 +2,18 @@
|
||||
|
||||
return [
|
||||
'stock_reservation_expiration_minutes' => (int) env('STOCK_RESERVATION_EXPIRATION_MINUTES', 30),
|
||||
'availability_policies' => [
|
||||
'user_quota_reached' => [
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
],
|
||||
'out_of_stock' => [
|
||||
'select_variant' => false,
|
||||
'change_quantity' => false,
|
||||
'add_to_cart' => false,
|
||||
'buy_now' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user