refactor(backend): reorganize domains into Core, Commerce, Ticketing and Shared

This commit is contained in:
2026-09-18 10:14:02 -03:00
parent 4659c1049d
commit 1241e1f7e8
425 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Domains\Catalog\Models;
use App\Domains\Event\Models\EventDate;
use App\Domains\Shared\Enums\FieldType;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'tenant_codigo',
'codigo',
'nombre',
'is_required',
'metadata_schema',
'type',
])]
class Attribute extends Model
{
use HasFactory;
protected $table = 'attribute';
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_required' => 'boolean',
'metadata_schema' => 'array',
'type' => FieldType::class,
];
}
/**
* @return BelongsTo<Tenant, $this>
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
}
/**
* @return HasMany<AttributeOption, $this>
*/
public function options(): HasMany
{
return $this->hasMany(AttributeOption::class, 'attribute_id')->orderBy('sort_order');
}
/**
* @return HasMany<EventDate, $this>
*/
public function eventDates(): HasMany
{
return $this->hasMany(EventDate::class, 'tenant_code', 'tenant_codigo')
->whereNull('rescheduled_to_event_date_id')
->whereNull('suspended_at')
->orderBy('date')
->orderBy('time_start');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Domains\Catalog\Models;
use App\Domains\Ticket\Models\ValidityTime;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'attribute_id',
'validity_time_id',
'value',
'label',
'sort_order',
'metadata',
])]
class AttributeOption extends Model
{
use HasFactory;
protected $table = 'attribute_options';
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'validity_time_id' => 'integer',
'sort_order' => 'integer',
'metadata' => 'array',
];
}
/**
* @return BelongsTo<Attribute, $this>
*/
public function attribute(): BelongsTo
{
return $this->belongsTo(Attribute::class, 'attribute_id');
}
/** @return BelongsTo<ValidityTime, $this> */
public function validityTime(): BelongsTo
{
return $this->belongsTo(ValidityTime::class);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Domains\Catalog\Models;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'tenant_codigo',
'nombre',
'descripcion',
])]
class Brand extends Model
{
use HasFactory;
protected $table = 'brands';
/**
* @return BelongsTo<Tenant, $this>
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
}
/** @return HasMany<CatalogItem, $this> */
public function catalogItems(): HasMany
{
return $this->hasMany(CatalogItem::class);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Domains\Catalog\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'bundle_catalog_item_id',
'component_catalog_item_id',
'component_variant_id',
'quantity',
])]
class BundleComponent extends Model
{
use HasFactory;
public $timestamps = false;
protected function casts(): array
{
return [
'bundle_catalog_item_id' => 'integer',
'component_catalog_item_id' => 'integer',
'component_variant_id' => 'integer',
'quantity' => 'integer',
];
}
/** @return BelongsTo<CatalogItem, $this> */
public function bundle(): BelongsTo
{
return $this->belongsTo(CatalogItem::class, 'bundle_catalog_item_id');
}
/** @return BelongsTo<CatalogItem, $this> */
public function catalogItem(): BelongsTo
{
return $this->belongsTo(CatalogItem::class, 'component_catalog_item_id');
}
/** @return BelongsTo<Variant, $this> */
public function variant(): BelongsTo
{
return $this->belongsTo(Variant::class, 'component_variant_id');
}
}

View File

@@ -0,0 +1,257 @@
<?php
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\Models\Ticket;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
#[Fillable([
'tenant_code',
'category_id',
'brand_id',
'inventory_id',
'type',
'slug',
'nombre',
'group_order',
'descripcion',
'precio',
'inventory_policy',
'inventory_subject',
'max_units_per_user',
'has_tickets',
])]
class CatalogItem extends Model
{
use HasFactory, SoftDeletes;
public $timestamps = false;
protected $table = 'catalog_items';
protected $attributes = [
'type' => CatalogItemType::Standard->value,
'inventory_policy' => InventoryPolicy::Tracked->value,
'inventory_subject' => InventorySubject::Product->value,
'has_tickets' => false,
'group_order' => 0,
];
protected function casts(): array
{
return [
'category_id' => 'integer',
'brand_id' => 'integer',
'inventory_id' => 'integer',
'type' => CatalogItemType::class,
'group_order' => 'integer',
'precio' => 'decimal:2',
'inventory_policy' => InventoryPolicy::class,
'inventory_subject' => InventorySubject::class,
'max_units_per_user' => 'integer',
'has_tickets' => 'boolean',
];
}
/** @return BelongsTo<Tenant, $this> */
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/** @return BelongsTo<Category, $this> */
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/** @return BelongsTo<Brand, $this> */
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
/** @return BelongsTo<Inventory, $this> */
public function inventory(): BelongsTo
{
return $this->belongsTo(Inventory::class);
}
/** @return HasMany<BundleComponent, $this> */
public function bundleComponents(): HasMany
{
return $this->hasMany(BundleComponent::class, 'bundle_catalog_item_id');
}
/** @return HasMany<BundleComponent, $this> */
public function bundleComponentUsages(): HasMany
{
return $this->hasMany(BundleComponent::class, 'component_catalog_item_id');
}
/** @return HasMany<Variant, $this> */
public function variants(): HasMany
{
return $this->hasMany(Variant::class);
}
/** @return HasMany<Ticket, $this> */
public function sourceTickets(): HasMany
{
return $this->hasMany(Ticket::class, 'source_catalog_item_id');
}
/** @return BelongsToMany<Attribute, $this> */
public function attributes(): BelongsToMany
{
return $this->belongsToMany(Attribute::class, 'item_attributes')
->withTimestamps();
}
/** @return HasMany<ItemAttribute, $this> */
public function itemAttributes(): HasMany
{
return $this->hasMany(ItemAttribute::class);
}
/** @return HasMany<FeaturedItem, $this> */
public function featuredItems(): HasMany
{
return $this->hasMany(FeaturedItem::class);
}
/** @return BelongsToMany<Attachment, $this> */
public function attachments(): BelongsToMany
{
return $this->allAttachments()->wherePivot('is_enabled', true);
}
/** @return BelongsToMany<Attachment, $this> */
public function allAttachments(): BelongsToMany
{
return $this->belongsToMany(
Attachment::class,
'catalog_items_attachments',
'catalog_item_id',
'attachment_id'
)
->withPivot(['orden', 'is_enabled'])
->wherePivotNull('variant_id')
->orderByPivot('orden');
}
public function availableStock(): ?int
{
return app(CatalogInventoryService::class)->availableQuantity($this);
}
public function isAvailable(): bool
{
if ($this->type === CatalogItemType::Bundle) {
$availableStock = $this->availableStock();
return $availableStock === null || $availableStock > 0;
}
if ($this->inventory_policy === InventoryPolicy::Unlimited) {
return true;
}
return ($this->availableStock() ?? 0) > 0;
}
/** @param Builder<CatalogItem> $query */
public function scopeWhereAvailable(Builder $query): Builder
{
return $query->where(function (Builder $query): void {
$query
->where(function (Builder $unlimitedQuery): void {
$unlimitedQuery
->where('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
->where(function (Builder $selectionQuery): void {
$selectionQuery
->whereDoesntHave('variants')
->orWhereHas('variants', fn (Builder $variantQuery): Builder => $variantQuery
->whereNull('sales_disabled_at')
->whereNull('replaced_by_variant_id'));
});
})
->orWhereHas(
'variants',
fn (Builder $variantQuery): Builder => $variantQuery
->whereNull('sales_disabled_at')
->whereNull('replaced_by_variant_id')
->whereHas(
'inventory',
fn (Builder $inventoryQuery): Builder => $inventoryQuery
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
)
)
->orWhere(function (Builder $directItemQuery): void {
$directItemQuery
->whereDoesntHave('variants')
->where(function (Builder $inventoryQuery): void {
$inventoryQuery
->whereNull('catalog_items.inventory_id')
->orWhereHas(
'inventory',
fn (Builder $availableInventoryQuery): Builder => $availableInventoryQuery
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
);
});
});
});
}
/** @return Collection<int, Variant> */
public function visibleVariants(?int $includedVariantId = null): Collection
{
return $this->variants
->filter(fn (Variant $variant): bool => $variant->hasOnlyActiveEventDates()
&& (($includedVariantId !== null && $variant->id === $includedVariantId)
|| ($variant->isSellable() && (
$this->inventory_policy === InventoryPolicy::Unlimited
|| ($variant->inventory?->availableStock() ?? 0) > 0
))))
->values();
}
public function getPrice(): float
{
return (float) $this->precio;
}
public function getName(): string
{
return $this->nombre;
}
public function getSelectionLabel(): string
{
return $this->getName();
}
public function getDescription(): ?string
{
return $this->descripcion;
}
public function isBundle(): bool
{
return $this->type === CatalogItemType::Bundle;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Domains\Catalog\Models;
use App\Domains\Auth\Models\User;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'tenant_code',
'categoria_id',
'nombre',
'is_enabled',
])]
class Category extends Model
{
use HasFactory;
protected $table = 'categorias';
protected $attributes = [
'is_enabled' => true,
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'categoria_id' => 'integer',
'is_enabled' => 'boolean',
];
}
public function isGlobal(): bool
{
return $this->tenant_code === null;
}
/**
* @return BelongsTo<Tenant, $this>
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/**
* @return BelongsTo<Category, $this>
*/
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'categoria_id');
}
/**
* @return HasMany<Category, $this>
*/
public function subCategories(): HasMany
{
return $this->hasMany(self::class, 'categoria_id');
}
/** @return HasMany<CatalogItem, $this> */
public function catalogItems(): HasMany
{
return $this->hasMany(CatalogItem::class);
}
/** @return BelongsToMany<User, $this> */
public function scanners(): BelongsToMany
{
return $this->belongsToMany(
User::class,
'category_scanners',
'categoria_id',
'user_id',
)->withTimestamps();
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Domains\Catalog\Models;
use App\Domains\Catalog\Enums\FeaturedGroupSource;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
#[Fillable([
'tenant_code',
'code',
'source_type',
'category_id',
'product_layout',
'group_layout',
'group_name',
'group_order',
])]
class FeaturedGroup extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'featured_groups';
protected $attributes = [
'source_type' => FeaturedGroupSource::Manual->value,
];
protected static function booted(): void
{
static::creating(function (FeaturedGroup $featuredGroup): void {
if (filled($featuredGroup->code)) {
return;
}
$baseCode = Str::slug($featuredGroup->group_name) ?: 'group';
$code = $baseCode;
$suffix = 2;
while (static::query()
->where('tenant_code', $featuredGroup->tenant_code)
->where('code', $code)
->exists()) {
$code = "{$baseCode}-{$suffix}";
$suffix++;
}
$featuredGroup->code = $code;
});
}
protected function casts(): array
{
return [
'source_type' => FeaturedGroupSource::class,
'category_id' => 'integer',
'product_layout' => ProductLayout::class,
'group_layout' => GroupLayout::class,
'group_order' => 'integer',
];
}
/** @return BelongsTo<Tenant, $this> */
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/** @return BelongsTo<Category, $this> */
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/** @return HasMany<FeaturedItem, $this> */
public function featuredItems(): HasMany
{
return $this->hasMany(FeaturedItem::class)->orderBy('order');
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Domains\Catalog\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'featured_group_id',
'catalog_item_id',
'order',
])]
class FeaturedItem extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'featured_items';
protected function casts(): array
{
return [
'featured_group_id' => 'integer',
'catalog_item_id' => 'integer',
'order' => 'integer',
];
}
/** @return BelongsTo<FeaturedGroup, $this> */
public function featuredGroup(): BelongsTo
{
return $this->belongsTo(FeaturedGroup::class);
}
/** @return BelongsTo<CatalogItem, $this> */
public function catalogItem(): BelongsTo
{
return $this->belongsTo(CatalogItem::class);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Domains\Catalog\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
#[Fillable([
'sold_units',
'refunded_units',
'reserved_stock',
'real_stock',
])]
class Inventory extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'inventories';
protected $attributes = [
'sold_units' => 0,
'refunded_units' => 0,
'reserved_stock' => 0,
'real_stock' => 0,
];
protected function casts(): array
{
return [
'sold_units' => 'integer',
'refunded_units' => 'integer',
'reserved_stock' => 'integer',
'real_stock' => 'integer',
];
}
/** @return HasOne<CatalogItem, $this> */
public function catalogItem(): HasOne
{
return $this->hasOne(CatalogItem::class);
}
/** @return HasMany<Variant, $this> */
public function variants(): HasMany
{
return $this->hasMany(Variant::class);
}
/** @return HasMany<StockReservationLine, $this> */
public function stockReservationLines(): HasMany
{
return $this->hasMany(StockReservationLine::class);
}
public function availableStock(): int
{
return max(0, $this->real_stock - $this->reserved_stock);
}
public function reserve(int $amount, bool $tracksInventory): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('La cantidad a reservar debe ser positiva.');
}
if ($tracksInventory && $this->availableStock() < $amount) {
throw new \InvalidArgumentException('No hay suficiente stock disponible para reservar.');
}
$this->reserved_stock += $amount;
$this->save();
}
public function release(int $amount): void
{
if ($amount < 0 || $this->reserved_stock < $amount) {
throw new \InvalidArgumentException('La cantidad reservada no es válida.');
}
$this->reserved_stock -= $amount;
$this->save();
}
public function buy(int $amount, bool $tracksInventory): void
{
if ($amount < 0 || $this->reserved_stock < $amount) {
throw new \InvalidArgumentException('La cantidad reservada no alcanza para confirmar la compra.');
}
if ($tracksInventory && $this->real_stock < $amount) {
throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la compra.');
}
if ($tracksInventory) {
$this->real_stock -= $amount;
}
$this->reserved_stock -= $amount;
$this->sold_units += $amount;
$this->save();
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Domains\Catalog\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'catalog_item_id',
'attribute_id',
'allow_multi_select',
'sort_order',
'show_in_selector',
'ticket_label',
])]
class ItemAttribute extends Model
{
use HasFactory;
protected $table = 'item_attributes';
protected $attributes = [
'show_in_selector' => true,
];
protected function casts(): array
{
return [
'catalog_item_id' => 'integer',
'attribute_id' => 'integer',
'allow_multi_select' => 'boolean',
'sort_order' => 'integer',
'show_in_selector' => 'boolean',
];
}
/** @return BelongsTo<CatalogItem, $this> */
public function catalogItem(): BelongsTo
{
return $this->belongsTo(CatalogItem::class);
}
/** @return BelongsTo<Attribute, $this> */
public function attribute(): BelongsTo
{
return $this->belongsTo(Attribute::class);
}
/** @return HasMany<VariantDefinition, $this> */
public function variantDefinitions(): HasMany
{
return $this->hasMany(VariantDefinition::class);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Domains\Catalog\Models;
use App\Domains\Cart\Models\Cart;
use App\Domains\Purchase\Models\Purchase;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
#[Fillable([
'status',
'expires_at',
'committed_at',
'released_at',
'expired_at',
'release_reason',
])]
class StockReservation extends Model
{
public const STATUS_ACTIVE = 'active';
public const STATUS_COMMITTED = 'committed';
public const STATUS_RELEASED = 'released';
public const STATUS_EXPIRED = 'expired';
protected function casts(): array
{
return [
'expires_at' => 'datetime',
'committed_at' => 'datetime',
'released_at' => 'datetime',
'expired_at' => 'datetime',
];
}
/** @return HasMany<StockReservationLine, $this> */
public function lines(): HasMany
{
return $this->hasMany(StockReservationLine::class);
}
/** @return HasOne<Cart, $this> */
public function currentCart(): HasOne
{
return $this->hasOne(Cart::class, 'current_stock_reservation_id');
}
/** @return HasOne<Purchase, $this> */
public function purchase(): HasOne
{
return $this->hasOne(Purchase::class);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Domains\Catalog\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'stock_reservation_id',
'inventory_id',
'quantity',
'tracks_inventory',
])]
class StockReservationLine extends Model
{
protected function casts(): array
{
return [
'stock_reservation_id' => 'integer',
'inventory_id' => 'integer',
'quantity' => 'integer',
'tracks_inventory' => 'boolean',
];
}
/** @return BelongsTo<StockReservation, $this> */
public function reservation(): BelongsTo
{
return $this->belongsTo(StockReservation::class, 'stock_reservation_id');
}
/** @return BelongsTo<Inventory, $this> */
public function inventory(): BelongsTo
{
return $this->belongsTo(Inventory::class);
}
}

View File

@@ -0,0 +1,345 @@
<?php
namespace App\Domains\Catalog\Models;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Event\Models\EventDate;
use App\Domains\Ticket\Models\Ticket;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Str;
#[Fillable([
'catalog_item_id',
'event_date_id',
'inventory_id',
'replaced_by_variant_id',
'sales_disabled_at',
'descripcion',
'precio',
])]
class Variant extends Model
{
use HasFactory, SoftDeletes;
public $timestamps = false;
protected $table = 'variantes';
protected function casts(): array
{
return [
'catalog_item_id' => 'integer',
'event_date_id' => 'integer',
'inventory_id' => 'integer',
'replaced_by_variant_id' => 'integer',
'sales_disabled_at' => 'datetime',
'precio' => 'decimal:2',
];
}
/** @return BelongsTo<CatalogItem, $this> */
public function catalogItem(): BelongsTo
{
return $this->belongsTo(CatalogItem::class)->withTrashed();
}
/** @return BelongsTo<EventDate, $this> */
public function eventDate(): BelongsTo
{
return $this->belongsTo(EventDate::class);
}
/** @return BelongsToMany<EventDate, $this> */
public function eventDates(): BelongsToMany
{
return $this->belongsToMany(
EventDate::class,
'variant_event_dates',
'variant_id',
'event_date_id',
)->orderBy('date')->orderBy('time_start');
}
/** @return HasMany<Ticket, $this> */
public function sourceTickets(): HasMany
{
return $this->hasMany(Ticket::class, 'source_variant_id');
}
/** @return BelongsTo<Inventory, $this> */
public function inventory(): BelongsTo
{
return $this->belongsTo(Inventory::class);
}
/** @return BelongsTo<Variant, $this> */
public function replacement(): BelongsTo
{
return $this->belongsTo(self::class, 'replaced_by_variant_id');
}
/** @return HasMany<Variant, $this> */
public function replacedVariants(): HasMany
{
return $this->hasMany(self::class, 'replaced_by_variant_id');
}
public function isSellable(): bool
{
return $this->sales_disabled_at === null
&& $this->replaced_by_variant_id === null
&& $this->hasOnlyActiveEventDates();
}
public function hasOnlyActiveEventDates(): bool
{
if (! $this->exists
&& $this->event_date_id === null
&& ! $this->relationLoaded('eventDates')) {
return true;
}
return $this->selectedEventDates()->every(
fn (EventDate $eventDate): bool => $eventDate->rescheduled_to_event_date_id === null
&& $eventDate->suspended_at === null,
);
}
/** @return HasMany<VariantDefinition, $this> */
public function definitions(): HasMany
{
return $this->hasMany(VariantDefinition::class);
}
/** @return BelongsToMany<Attachment, $this> */
public function attachments(): BelongsToMany
{
return $this->allAttachments()->wherePivot('is_enabled', true);
}
/** @return BelongsToMany<Attachment, $this> */
public function allAttachments(): BelongsToMany
{
$relation = $this->belongsToMany(
Attachment::class,
'catalog_items_attachments',
'variant_id',
'attachment_id'
)
->withPivot(['orden', 'is_enabled'])
->orderByPivot('orden');
if ($this->catalog_item_id !== null) {
$relation->withPivotValue('catalog_item_id', $this->catalog_item_id);
}
return $relation;
}
public function getPrice(): float
{
return (float) ($this->precio ?? $this->catalogItem->precio);
}
public function getDescription(): ?string
{
return $this->descripcion ?? $this->catalogItem->descripcion;
}
public function getName(): string
{
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
{
$values = $this->definitions
->groupBy('item_attribute_id')
->mapWithKeys(function (Collection $definitions): array {
$itemAttribute = $definitions->first()?->itemAttribute;
$attributeCode = $itemAttribute?->attribute?->codigo;
if ($attributeCode === null) {
return [];
}
$definitionValues = $definitions->pluck('value')->values();
return [
$attributeCode => $itemAttribute->allow_multi_select
? $definitionValues->all()
: $definitionValues->first(),
];
});
$eventDateIds = $this->selectedEventDates()
->pluck('id')
->map(fn ($id): string => (string) $id)
->values();
if ($eventDateIds->count() === 1) {
$values->put('event_date', $eventDateIds->first());
} elseif ($eventDateIds->isNotEmpty()) {
$values->put('event_date', $eventDateIds->all());
}
return $values;
}
/**
* @return Collection<string, array{value: string, label: string}|array<int, array{value: string, label: string}>>
*/
public function selectionOptions(?Collection $itemAttributes = null): Collection
{
$options = $this->definitions
->groupBy('item_attribute_id')
->mapWithKeys(function (Collection $definitions): array {
$itemAttribute = $definitions->first()?->itemAttribute;
$attribute = $itemAttribute?->attribute;
$attributeCode = $attribute?->codigo;
if ($attributeCode === null) {
return [];
}
$values = $definitions
->pluck('value')
->values()
->map(function (string $value) use ($attribute): array {
$attributeOption = $attribute->options->firstWhere('value', $value);
return [
'value' => $value,
'label' => $attributeOption?->label ?? $value,
];
});
return [
$attributeCode => $itemAttribute->allow_multi_select
? $values->all()
: $values->first(),
];
});
$eventDateOptions = $this->selectedEventDates()
->map(fn (EventDate $eventDate): array => [
'value' => (string) $eventDate->id,
'label' => $eventDate->date->format('d/m/Y'),
])
->values();
if ($eventDateOptions->count() === 1) {
$options->put('event_date', $eventDateOptions->first());
} elseif ($eventDateOptions->isNotEmpty()) {
$options->put('event_date', $eventDateOptions->all());
}
if ($itemAttributes === null) {
$itemAttributes = $this->definitions
->map(fn (VariantDefinition $definition) => $definition->itemAttribute)
->filter()
->unique('id')
->values();
if ($this->catalogItem !== null) {
$itemAttributes = $itemAttributes
->merge($this->catalogItem->itemAttributes)
->unique('id')
->values();
}
}
$ordering = $itemAttributes->mapWithKeys(function (ItemAttribute $itemAttribute): array {
$attribute = $itemAttribute->attribute;
return $attribute === null
? []
: [$attribute->codigo => [$itemAttribute->sort_order, mb_strtolower($attribute->nombre)]];
});
return $options->sortKeysUsing(function (string $left, string $right) use ($ordering): int {
[$leftOrder, $leftLabel] = $ordering->get($left, [0, mb_strtolower($left)]);
[$rightOrder, $rightLabel] = $ordering->get($right, [0, mb_strtolower($right)]);
return $leftOrder <=> $rightOrder
?: $leftLabel <=> $rightLabel
?: $left <=> $right;
});
}
/**
* @param Collection<int, ItemAttribute> $itemAttributes
* @return Collection<string, array{value: string, label: string}|array<int, array{value: string, label: string}>>
*/
public function selectorOptions(Collection $itemAttributes): Collection
{
$visibleAttributeCodes = $itemAttributes
->filter(fn (ItemAttribute $itemAttribute): bool => $itemAttribute->show_in_selector)
->map(fn (ItemAttribute $itemAttribute): ?string => $itemAttribute->attribute?->codigo)
->filter()
->values();
return $this->selectionOptions($itemAttributes)
->filter(
fn (array $option, string $attributeCode): bool => $visibleAttributeCodes
->contains($attributeCode)
);
}
/** @return Collection<int, EventDate> */
public function selectedEventDates(): Collection
{
$eventDates = $this->eventDates;
if ($eventDates->isEmpty() && $this->event_date_id !== null && $this->eventDate !== null) {
return collect([$this->eventDate]);
}
return $eventDates;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Domains\Catalog\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'variant_id',
'item_attribute_id',
'value',
])]
class VariantDefinition extends Model
{
use HasFactory;
protected $table = 'variant_values';
/** @return BelongsTo<Variant, $this> */
public function variant(): BelongsTo
{
return $this->belongsTo(Variant::class);
}
/** @return BelongsTo<ItemAttribute, $this> */
public function itemAttribute(): BelongsTo
{
return $this->belongsTo(ItemAttribute::class);
}
}