feat(catalog): expose product unavailability messages

This commit is contained in:
2026-08-21 16:47:15 -03:00
parent e8bcaa2026
commit 1dd2366957
9 changed files with 192 additions and 53 deletions

View File

@@ -20,7 +20,6 @@ class CatalogFeaturedItemResource extends JsonResource
$catalogItem = $this->resource;
/** @var FeaturedGroup $featuredGroup */
$featuredGroup = $catalogItem->getRelation('featuredGroup');
$remainingUserQuota = $catalogItem->getAttribute('remaining_user_quota');
if ($featuredGroup->product_layout === ProductLayout::ColumnWithImage) {
return $this->columnWithImageData($catalogItem);
@@ -30,6 +29,9 @@ class CatalogFeaturedItemResource extends JsonResource
return $this->ticketSelectorData($catalogItem);
}
$remainingUserQuota = $catalogItem->getAttribute('remaining_user_quota');
$availableStock = $catalogItem->availableStock();
$data = [
'id' => $catalogItem->id,
'type' => $catalogItem->type->value,
@@ -37,26 +39,38 @@ class CatalogFeaturedItemResource extends JsonResource
'descripcion' => $catalogItem->descripcion,
'precio' => $catalogItem->precio,
'maximum_addable_quantity' => $this->maximumAddable(
$catalogItem->availableStock(),
$availableStock,
$remainingUserQuota,
),
'variants' => $catalogItem->visibleVariants()
->map(fn (Variant $variant): array => [
'id' => $variant->id,
'event_date_id' => $variant->event_date_id,
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(),
'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(
$catalogItem->inventory_policy === InventoryPolicy::Unlimited
? null
: $variant->inventory->availableStock(),
$remainingUserQuota,
),
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
])
'unavailable_message' => $this->unavailableMessage(
$availableStock,
$remainingUserQuota,
),
'variants' => $catalogItem->variants
->map(function (Variant $variant) use ($catalogItem, $remainingUserQuota): array {
$variantStock = $catalogItem->inventory_policy === InventoryPolicy::Unlimited
? null
: $variant->inventory->availableStock();
return [
'id' => $variant->id,
'event_date_id' => $variant->event_date_id,
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(),
'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(
$variantStock,
$remainingUserQuota,
),
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
];
})
->values(),
];
@@ -66,6 +80,9 @@ class CatalogFeaturedItemResource extends JsonResource
/** @return array<string, mixed> */
private function ticketSelectorData(CatalogItem $catalogItem): array
{
$availableStock = $catalogItem->availableStock();
$remainingUserQuota = $catalogItem->getAttribute('remaining_user_quota');
return [
'id' => $catalogItem->id,
'type' => $catalogItem->type->value,
@@ -73,18 +90,25 @@ 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),
];
}
/** @return array<string, mixed> */
private function columnWithImageData(CatalogItem $catalogItem): array
{
$availableStock = $catalogItem->availableStock();
$remainingUserQuota = $catalogItem->getAttribute('remaining_user_quota');
return [
'id' => $catalogItem->id,
'type' => $catalogItem->type->value,
'nombre' => $catalogItem->nombre,
'precio' => $catalogItem->precio,
'image' => $this->firstImageUrl($catalogItem),
'maximum_addable_quantity' => $this->maximumAddable($availableStock, $remainingUserQuota),
'unavailable_message' => $this->unavailableMessage($availableStock, $remainingUserQuota),
];
}
@@ -103,4 +127,10 @@ class CatalogFeaturedItemResource extends JsonResource
return app(CatalogItemAllowanceService::class)
->maximumAddableQuantity($stock, $remainingUserQuota);
}
private function unavailableMessage(?int $stock, ?int $remainingUserQuota): ?string
{
return app(CatalogItemAllowanceService::class)
->unavailableMessage($stock, $remainingUserQuota);
}
}

View File

@@ -42,11 +42,15 @@ class CatalogItemDetailResource extends JsonResource
$selectedVariant === null,
fn () => $this->maximumAddable($this->availableStock()),
),
'unavailable_message' => $this->when(
$selectedVariant === null,
fn () => $this->unavailableMessage($this->availableStock()),
),
'images' => $this->when(
$selectedVariant === null,
fn () => $this->imageUrls($this->attachments),
),
'variants' => $this->visibleVariants()
'variants' => $this->variants
->map(fn (Variant $variant): array => $this->variantData($variant))
->values(),
'selected_variant' => $this->when(
@@ -159,6 +163,7 @@ class CatalogItemDetailResource extends JsonResource
{
$values = $variant->selectionOptions($this->itemAttributes);
$eventDates = $variant->selectedEventDates();
$variantStock = $this->variantStock($variant);
return [
'id' => $variant->id,
@@ -168,7 +173,8 @@ 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($this->variantStock($variant)),
'maximum_addable_quantity' => $this->maximumAddable($variantStock),
'unavailable_message' => $this->unavailableMessage($variantStock),
'values' => $values,
];
}
@@ -195,4 +201,12 @@ class CatalogItemDetailResource extends JsonResource
$this->getAttribute('remaining_user_quota'),
);
}
private function unavailableMessage(?int $stock): ?string
{
return app(CatalogItemAllowanceService::class)->unavailableMessage(
$stock,
$this->getAttribute('remaining_user_quota'),
);
}
}

View File

@@ -15,6 +15,7 @@ class CatalogSearchItemResource extends JsonResource
/** @return array<string, mixed> */
public function toArray(Request $request): array
{
$availableStock = $this->availableStock();
$attachment = $this->attachments->first()
?? $this->variants
->flatMap(fn (Variant $variant) => $variant->attachments)
@@ -27,23 +28,27 @@ class CatalogSearchItemResource extends JsonResource
'descripcion' => $this->descripcion,
'precio' => $this->precio,
'image' => $attachment?->getTemporaryUrl(1440),
'maximum_addable_quantity' => $this->maximumAddable($this->availableStock()),
'variants' => $this->visibleVariants()
->map(fn (Variant $variant): array => [
'id' => $variant->id,
'event_date_id' => $variant->event_date_id,
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(),
'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(
$this->inventory_policy === InventoryPolicy::Unlimited
? null
: $variant->inventory?->availableStock(),
),
'values' => $variant->selectorOptions($this->itemAttributes),
])
'maximum_addable_quantity' => $this->maximumAddable($availableStock),
'unavailable_message' => $this->unavailableMessage($availableStock),
'variants' => $this->variants
->map(function (Variant $variant): array {
$variantStock = $this->inventory_policy === InventoryPolicy::Unlimited
? null
: $variant->inventory?->availableStock();
return [
'id' => $variant->id,
'event_date_id' => $variant->event_date_id,
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(),
'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),
'values' => $variant->selectorOptions($this->itemAttributes),
];
})
->values(),
];
}
@@ -55,4 +60,12 @@ class CatalogSearchItemResource extends JsonResource
$this->getAttribute('remaining_user_quota'),
);
}
private function unavailableMessage(?int $stock): ?string
{
return app(CatalogItemAllowanceService::class)->unavailableMessage(
$stock,
$this->getAttribute('remaining_user_quota'),
);
}
}

View File

@@ -8,6 +8,10 @@ use Illuminate\Support\Collection;
class CatalogItemAllowanceService
{
private const USER_QUOTA_REACHED_MESSAGE = 'Alcanzaste el cupo máximo permitido para este producto.';
private const OUT_OF_STOCK_MESSAGE = 'Este producto no tiene stock disponible.';
public function __construct(
private readonly UserPurchaseLimitService $purchaseLimits,
) {}
@@ -37,4 +41,17 @@ class CatalogItemAllowanceService
return min($availableStock, $remainingUserQuota);
}
public function unavailableMessage(?int $availableStock, ?int $remainingUserQuota): ?string
{
if ($remainingUserQuota !== null && $remainingUserQuota <= 0) {
return self::USER_QUOTA_REACHED_MESSAGE;
}
if ($availableStock !== null && $availableStock <= 0) {
return self::OUT_OF_STOCK_MESSAGE;
}
return null;
}
}

View File

@@ -231,7 +231,6 @@ class CatalogService
$paginator = CatalogItem::query()
->where('tenant_code', $tenant->codigo)
->whereVariantsAvailable()
->where(function (Builder $query) use ($containsPattern): void {
$query
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
@@ -281,7 +280,6 @@ class CatalogService
return CatalogItem::query()
->where('tenant_code', $tenant->codigo)
->where('category_id', $category->id)
->whereVariantsAvailable()
->with([
'attachments',
'inventory',

View File

@@ -49,7 +49,6 @@ class FeaturedGroupService
{
$query = CatalogItem::query()
->where('catalog_items.tenant_code', $featuredGroup->tenant_code)
->whereVariantsAvailable()
->with([
'inventory',
'attachments',