feat(inventory): add InventorySubject enum and integrate into catalog item management
This commit is contained in:
16
app/Domains/Catalog/Enums/InventorySubject.php
Normal file
16
app/Domains/Catalog/Enums/InventorySubject.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Catalog\Enums;
|
||||
|
||||
enum InventorySubject: string
|
||||
{
|
||||
case Product = 'product';
|
||||
case Seat = 'seat';
|
||||
case Ticket = 'ticket';
|
||||
|
||||
/** @return array<int, string> */
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Domains\Catalog\Models;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Catalog\Enums\InventorySubject;
|
||||
use App\Domains\Catalog\Services\CatalogInventoryService;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||
@@ -30,6 +31,7 @@ use Illuminate\Support\Collection;
|
||||
'descripcion',
|
||||
'precio',
|
||||
'inventory_policy',
|
||||
'inventory_subject',
|
||||
'max_units_per_user',
|
||||
'has_tickets',
|
||||
'ticket_generation_policy',
|
||||
@@ -46,6 +48,7 @@ class CatalogItem extends Model
|
||||
protected $attributes = [
|
||||
'type' => CatalogItemType::Standard->value,
|
||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||
'inventory_subject' => InventorySubject::Product->value,
|
||||
'has_tickets' => false,
|
||||
'ticket_generation_policy' => TicketGenerationPolicy::PerEventDate->value,
|
||||
];
|
||||
@@ -59,6 +62,7 @@ class CatalogItem extends Model
|
||||
'type' => CatalogItemType::class,
|
||||
'precio' => 'decimal:2',
|
||||
'inventory_policy' => InventoryPolicy::class,
|
||||
'inventory_subject' => InventorySubject::class,
|
||||
'max_units_per_user' => 'integer',
|
||||
'has_tickets' => 'boolean',
|
||||
'ticket_generation_policy' => TicketGenerationPolicy::class,
|
||||
@@ -208,6 +212,11 @@ class CatalogItem extends Model
|
||||
return $this->nombre;
|
||||
}
|
||||
|
||||
public function getSelectionLabel(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->descripcion;
|
||||
|
||||
@@ -12,6 +12,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Fillable([
|
||||
'catalog_item_id',
|
||||
@@ -113,6 +115,42 @@ class Variant extends Model
|
||||
return $this->catalogItem->nombre;
|
||||
}
|
||||
|
||||
public function getSelectionLabel(): string
|
||||
{
|
||||
$this->loadMissing([
|
||||
'catalogItem.itemAttributes.attribute.options',
|
||||
'definitions.itemAttribute.attribute.options',
|
||||
'eventDates',
|
||||
'eventDate',
|
||||
]);
|
||||
|
||||
$itemAttributes = $this->catalogItem->itemAttributes;
|
||||
|
||||
$label = $this->selectionOptions($itemAttributes)
|
||||
->map(function (array $option, string $attributeCode) use ($itemAttributes): ?string {
|
||||
$itemAttribute = $itemAttributes->first(
|
||||
fn (ItemAttribute $candidate): bool => $candidate->attribute?->codigo === $attributeCode,
|
||||
);
|
||||
$translationKey = "api.catalog.attribute_labels.{$attributeCode}";
|
||||
$attributeName = Lang::has($translationKey)
|
||||
? __($translationKey)
|
||||
: ($itemAttribute?->attribute?->nombre ?? Str::headline($attributeCode));
|
||||
$selectedOptions = array_is_list($option) ? $option : [$option];
|
||||
$selectedLabels = collect($selectedOptions)
|
||||
->pluck('label')
|
||||
->filter()
|
||||
->implode(', ');
|
||||
|
||||
return $selectedLabels === ''
|
||||
? null
|
||||
: "{$attributeName} {$selectedLabels}";
|
||||
})
|
||||
->filter()
|
||||
->implode(' · ');
|
||||
|
||||
return $label !== '' ? $label : $this->getName();
|
||||
}
|
||||
|
||||
/** @return Collection<string, string|array<int, string>> */
|
||||
public function selectionValues(): Collection
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Domains\Catalog\Requests;
|
||||
|
||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Catalog\Enums\InventorySubject;
|
||||
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||
use App\Domains\Ticket\Enums\TicketGenerationPolicy;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
@@ -54,6 +55,7 @@ class StoreCatalogItemRequest extends FormRequest
|
||||
'descripcion' => ['sometimes', 'nullable', 'string'],
|
||||
'precio' => ['required', 'numeric', 'min:0'],
|
||||
'inventory_policy' => [Rule::prohibitedIf($isBundle), 'sometimes', Rule::enum(InventoryPolicy::class)],
|
||||
'inventory_subject' => ['sometimes', Rule::enum(InventorySubject::class)],
|
||||
'max_units_per_user' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||
'has_tickets' => [Rule::prohibitedIf($isBundle), 'sometimes', 'boolean'],
|
||||
'ticket_generation_policy' => [Rule::prohibitedIf($isBundle), 'sometimes', Rule::enum(TicketGenerationPolicy::class)],
|
||||
|
||||
@@ -33,6 +33,7 @@ class CatalogItemDetailResource extends JsonResource
|
||||
'category' => $this->category?->nombre,
|
||||
'brand' => $this->brand?->nombre,
|
||||
'inventory_policy' => $this->inventory_policy?->value,
|
||||
'inventory_subject' => $this->inventory_subject->value,
|
||||
'max_units_per_user' => $this->max_units_per_user,
|
||||
'has_tickets' => $this->has_tickets,
|
||||
'ticket_generation_policy' => $this->ticket_generation_policy->value,
|
||||
|
||||
@@ -23,6 +23,7 @@ class CatalogItemResource extends JsonResource
|
||||
'descripcion' => $this->descripcion,
|
||||
'precio' => $this->precio,
|
||||
'inventory_policy' => $this->inventory_policy?->value,
|
||||
'inventory_subject' => $this->inventory_subject->value,
|
||||
'max_units_per_user' => $this->max_units_per_user,
|
||||
'has_tickets' => $this->has_tickets,
|
||||
'ticket_generation_policy' => $this->ticket_generation_policy->value,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Purchase\Services\Checkout;
|
||||
|
||||
use App\Domains\Catalog\Enums\InventorySubject;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Catalog\Models\Variant;
|
||||
|
||||
class InsufficientStockMessageBuilder
|
||||
{
|
||||
public function build(
|
||||
CatalogItem $catalogItem,
|
||||
CatalogItem|Variant $selection,
|
||||
int $availableQuantity,
|
||||
): string {
|
||||
return match ($catalogItem->inventory_subject) {
|
||||
InventorySubject::Seat => __('api.purchase.stock.seat_unavailable', [
|
||||
'selection' => $selection->getSelectionLabel(),
|
||||
]),
|
||||
InventorySubject::Ticket => __('api.purchase.stock.ticket_unavailable', [
|
||||
'selection' => $selection->getSelectionLabel(),
|
||||
]),
|
||||
InventorySubject::Product => __('api.purchase.stock.product_unavailable', [
|
||||
'selection' => $this->productSelectionLabel($catalogItem, $selection),
|
||||
'max' => $availableQuantity,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
private function productSelectionLabel(
|
||||
CatalogItem $catalogItem,
|
||||
CatalogItem|Variant $selection,
|
||||
): string {
|
||||
if ($selection instanceof CatalogItem) {
|
||||
return $selection->getSelectionLabel();
|
||||
}
|
||||
|
||||
return __('api.purchase.stock.product_selection', [
|
||||
'product' => $catalogItem->getName(),
|
||||
'selection' => $selection->getSelectionLabel(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class StartCheckoutService
|
||||
private readonly UserPurchaseLimitService $purchaseLimits,
|
||||
private readonly CatalogSelectionResolver $selections,
|
||||
private readonly PurchaseItemSnapshotFactory $snapshots,
|
||||
private readonly InsufficientStockMessageBuilder $stockMessages,
|
||||
) {}
|
||||
|
||||
/** @param array<string, mixed> $purchaseData */
|
||||
@@ -128,17 +129,25 @@ class StartCheckoutService
|
||||
|
||||
if ($availableQuantity !== null && $availableQuantity < $line['quantity']) {
|
||||
throw ValidationException::withMessages([
|
||||
"{$line['field']}.cantidad" => __('api.purchase.direct_items_max_stock', [
|
||||
'max' => $availableQuantity,
|
||||
]),
|
||||
"{$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" => __('api.purchase.insufficient_stock'),
|
||||
"{$line['field']}.cantidad" => $this->stockMessages->build(
|
||||
$line['catalog_item'],
|
||||
$line['selection'],
|
||||
$availableQuantity,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user