feat(catalog): expose availability policies
This commit is contained in:
@@ -38,11 +38,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'nombre' => $catalogItem->nombre,
|
'nombre' => $catalogItem->nombre,
|
||||||
'descripcion' => $catalogItem->descripcion,
|
'descripcion' => $catalogItem->descripcion,
|
||||||
'precio' => $catalogItem->precio,
|
'precio' => $catalogItem->precio,
|
||||||
'maximum_addable_quantity' => $this->maximumAddable(
|
'availability' => $this->availability(
|
||||||
$availableStock,
|
|
||||||
$remainingUserQuota,
|
|
||||||
),
|
|
||||||
'unavailable_message' => $this->unavailableMessage(
|
|
||||||
$availableStock,
|
$availableStock,
|
||||||
$remainingUserQuota,
|
$remainingUserQuota,
|
||||||
),
|
),
|
||||||
@@ -60,13 +56,10 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||||
'descripcion' => $variant->getDescription(),
|
'descripcion' => $variant->getDescription(),
|
||||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||||
'maximum_addable_quantity' => $this->maximumAddable(
|
'availability' => $this->availability(
|
||||||
$variantStock,
|
|
||||||
$remainingUserQuota,
|
|
||||||
),
|
|
||||||
'unavailable_message' => $this->unavailableMessage(
|
|
||||||
$variantStock,
|
$variantStock,
|
||||||
$remainingUserQuota,
|
$remainingUserQuota,
|
||||||
|
'variant',
|
||||||
),
|
),
|
||||||
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
|
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
|
||||||
];
|
];
|
||||||
@@ -90,8 +83,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'descripcion' => $catalogItem->descripcion,
|
'descripcion' => $catalogItem->descripcion,
|
||||||
'precio' => $catalogItem->precio,
|
'precio' => $catalogItem->precio,
|
||||||
'image' => $this->firstImageUrl($catalogItem),
|
'image' => $this->firstImageUrl($catalogItem),
|
||||||
'maximum_addable_quantity' => $this->maximumAddable($availableStock, $remainingUserQuota),
|
'availability' => $this->availability($availableStock, $remainingUserQuota),
|
||||||
'unavailable_message' => $this->unavailableMessage($availableStock, $remainingUserQuota),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +99,7 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
'nombre' => $catalogItem->nombre,
|
'nombre' => $catalogItem->nombre,
|
||||||
'precio' => $catalogItem->precio,
|
'precio' => $catalogItem->precio,
|
||||||
'image' => $this->firstImageUrl($catalogItem),
|
'image' => $this->firstImageUrl($catalogItem),
|
||||||
'maximum_addable_quantity' => $this->maximumAddable($availableStock, $remainingUserQuota),
|
'availability' => $this->availability($availableStock, $remainingUserQuota),
|
||||||
'unavailable_message' => $this->unavailableMessage($availableStock, $remainingUserQuota),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,15 +113,13 @@ class CatalogFeaturedItemResource extends JsonResource
|
|||||||
return $attachment?->getTemporaryUrl(1440);
|
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)
|
return app(CatalogItemAllowanceService::class)
|
||||||
->maximumAddableQuantity($stock, $remainingUserQuota);
|
->availability($stock, $remainingUserQuota, $scope);
|
||||||
}
|
|
||||||
|
|
||||||
private function unavailableMessage(?int $stock, ?int $remainingUserQuota): ?string
|
|
||||||
{
|
|
||||||
return app(CatalogItemAllowanceService::class)
|
|
||||||
->unavailableMessage($stock, $remainingUserQuota);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
'max_units_per_user' => $this->max_units_per_user,
|
'max_units_per_user' => $this->max_units_per_user,
|
||||||
'has_tickets' => $this->has_tickets,
|
'has_tickets' => $this->has_tickets,
|
||||||
'attributes' => $this->attributesData(),
|
'attributes' => $this->attributesData(),
|
||||||
'maximum_addable_quantity' => $this->when(
|
'availability' => $this->availability($this->availableStock()),
|
||||||
$selectedVariant === null,
|
|
||||||
fn () => $this->maximumAddable($this->availableStock()),
|
|
||||||
),
|
|
||||||
'unavailable_message' => $this->when(
|
|
||||||
$selectedVariant === null,
|
|
||||||
fn () => $this->unavailableMessage($this->availableStock()),
|
|
||||||
),
|
|
||||||
'images' => $this->when(
|
'images' => $this->when(
|
||||||
$selectedVariant === null,
|
$selectedVariant === null,
|
||||||
fn () => $this->imageUrls($this->attachments),
|
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(),
|
'event_dates' => $eventDates->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||||
'descripcion' => $variant->getDescription(),
|
'descripcion' => $variant->getDescription(),
|
||||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||||
'maximum_addable_quantity' => $this->maximumAddable($variantStock),
|
'availability' => $this->availability($variantStock, 'variant'),
|
||||||
'unavailable_message' => $this->unavailableMessage($variantStock),
|
|
||||||
'values' => $values,
|
'values' => $values,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -194,19 +186,13 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
: $variant->inventory->availableStock();
|
: $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(
|
return app(CatalogItemAllowanceService::class)->availability(
|
||||||
$stock,
|
|
||||||
$this->getAttribute('remaining_user_quota'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function unavailableMessage(?int $stock): ?string
|
|
||||||
{
|
|
||||||
return app(CatalogItemAllowanceService::class)->unavailableMessage(
|
|
||||||
$stock,
|
$stock,
|
||||||
$this->getAttribute('remaining_user_quota'),
|
$this->getAttribute('remaining_user_quota'),
|
||||||
|
$scope,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ class CatalogSearchItemResource extends JsonResource
|
|||||||
'descripcion' => $this->descripcion,
|
'descripcion' => $this->descripcion,
|
||||||
'precio' => $this->precio,
|
'precio' => $this->precio,
|
||||||
'image' => $attachment?->getTemporaryUrl(1440),
|
'image' => $attachment?->getTemporaryUrl(1440),
|
||||||
'maximum_addable_quantity' => $this->maximumAddable($availableStock),
|
'availability' => $this->availability($availableStock),
|
||||||
'unavailable_message' => $this->unavailableMessage($availableStock),
|
|
||||||
'variants' => $this->variants
|
'variants' => $this->variants
|
||||||
->map(function (Variant $variant): array {
|
->map(function (Variant $variant): array {
|
||||||
$variantStock = $this->inventory_policy === InventoryPolicy::Unlimited
|
$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(),
|
'event_dates' => $variant->selectedEventDates()->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))->values(),
|
||||||
'descripcion' => $variant->getDescription(),
|
'descripcion' => $variant->getDescription(),
|
||||||
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
'precio' => number_format($variant->getPrice(), 2, '.', ''),
|
||||||
'maximum_addable_quantity' => $this->maximumAddable($variantStock),
|
'availability' => $this->availability($variantStock, 'variant'),
|
||||||
'unavailable_message' => $this->unavailableMessage($variantStock),
|
|
||||||
'values' => $variant->selectorOptions($this->itemAttributes),
|
'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(
|
return app(CatalogItemAllowanceService::class)->availability(
|
||||||
$stock,
|
|
||||||
$this->getAttribute('remaining_user_quota'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function unavailableMessage(?int $stock): ?string
|
|
||||||
{
|
|
||||||
return app(CatalogItemAllowanceService::class)->unavailableMessage(
|
|
||||||
$stock,
|
$stock,
|
||||||
$this->getAttribute('remaining_user_quota'),
|
$this->getAttribute('remaining_user_quota'),
|
||||||
|
$scope,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,16 +42,75 @@ class CatalogItemAllowanceService
|
|||||||
return min($availableStock, $remainingUserQuota);
|
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) {
|
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) {
|
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
|
<?php
|
||||||
|
|
||||||
use App\Domains\Auth\Exceptions\AccountLockedException;
|
use App\Domains\Auth\Exceptions\AccountLockedException;
|
||||||
|
use App\Domains\Catalog\Services\CatalogItemAllowanceService;
|
||||||
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
use App\Domains\Purchase\Exceptions\InsufficientStockException;
|
||||||
use App\Domains\Purchase\Exceptions\PurchaseExpiredException;
|
use App\Domains\Purchase\Exceptions\PurchaseExpiredException;
|
||||||
use App\Domains\Purchase\Exceptions\PurchaseLimitExceededException;
|
use App\Domains\Purchase\Exceptions\PurchaseLimitExceededException;
|
||||||
@@ -100,7 +101,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
'errors' => $exception->errors(),
|
'errors' => $exception->errors(),
|
||||||
'catalog_item_id' => $exception->catalogItemId,
|
'catalog_item_id' => $exception->catalogItemId,
|
||||||
'catalog_item_name' => $exception->catalogItemName,
|
'catalog_item_name' => $exception->catalogItemName,
|
||||||
'maximum_addable_quantity' => $exception->maximumAddableQuantity,
|
'availability' => app(CatalogItemAllowanceService::class)
|
||||||
|
->purchaseLimitExceededAvailability(
|
||||||
|
$exception->maximumAddableQuantity,
|
||||||
|
$exception->getMessage(),
|
||||||
|
),
|
||||||
], 422);
|
], 422);
|
||||||
});
|
});
|
||||||
$exceptions->render(function (PurchaseExpiredException $exception, Request $request) {
|
$exceptions->render(function (PurchaseExpiredException $exception, Request $request) {
|
||||||
|
|||||||
@@ -2,4 +2,18 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'stock_reservation_expiration_minutes' => (int) env('STOCK_RESERVATION_EXPIRATION_MINUTES', 30),
|
'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