feat(catalog): add sales_end_at field to catalog items and implement sale logic
feat(event): include catalog items in event resource and update event service test(seeder): add FiestaTradicionArrufoSeeder for event and catalog item setup config: set event timezone for local event dates
This commit is contained in:
@@ -31,6 +31,7 @@ use Illuminate\Support\Collection;
|
||||
'group_order',
|
||||
'descripcion',
|
||||
'precio',
|
||||
'sales_end_at',
|
||||
'inventory_policy',
|
||||
'inventory_subject',
|
||||
'max_units_per_user',
|
||||
@@ -75,6 +76,7 @@ class CatalogItem extends Model
|
||||
'type' => CatalogItemType::class,
|
||||
'group_order' => 'integer',
|
||||
'precio' => 'decimal:2',
|
||||
'sales_end_at' => 'datetime',
|
||||
'inventory_policy' => InventoryPolicy::class,
|
||||
'inventory_subject' => InventorySubject::class,
|
||||
'max_units_per_user' => 'integer',
|
||||
@@ -191,6 +193,10 @@ class CatalogItem extends Model
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
if (! $this->isSaleOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->type === CatalogItemType::Bundle) {
|
||||
$availableStock = $this->availableStock();
|
||||
|
||||
@@ -208,6 +214,9 @@ class CatalogItem extends Model
|
||||
public function scopeWhereAvailable(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query): void {
|
||||
$query->whereNull('catalog_items.sales_end_at')
|
||||
->orWhere('catalog_items.sales_end_at', '>', now());
|
||||
})->where(function (Builder $query): void {
|
||||
$query
|
||||
->where(function (Builder $unlimitedQuery): void {
|
||||
$unlimitedQuery
|
||||
@@ -247,10 +256,16 @@ class CatalogItem extends Model
|
||||
});
|
||||
}
|
||||
|
||||
public function isSaleOpen(): bool
|
||||
{
|
||||
return $this->sales_end_at === null || now()->lt($this->sales_end_at);
|
||||
}
|
||||
|
||||
/** @return Collection<int, Variant> */
|
||||
public function visibleVariants(?int $includedVariantId = null): Collection
|
||||
{
|
||||
return $this->variants
|
||||
->each(fn (Variant $variant) => $variant->setRelation('catalogItem', $this))
|
||||
->filter(fn (Variant $variant): bool => $variant->hasOnlyActiveEventDates()
|
||||
&& (($includedVariantId !== null && $variant->id === $includedVariantId)
|
||||
|| ($variant->isSellable() && (
|
||||
|
||||
@@ -96,6 +96,7 @@ class Variant extends Model
|
||||
{
|
||||
return $this->sales_disabled_at === null
|
||||
&& $this->replaced_by_variant_id === null
|
||||
&& $this->catalogItem->isSaleOpen()
|
||||
&& $this->hasOnlyActiveEventDates();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ class CatalogSelectionResolver
|
||||
throw new NotFoundHttpException('Catalog item not found for tenant.');
|
||||
}
|
||||
|
||||
if (! $catalogItem->isSaleOpen()) {
|
||||
throw ValidationException::withMessages([
|
||||
"{$fieldPrefix}.catalog_item_id" => ['La venta de este producto finalizó.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($catalogItem->isBundle()) {
|
||||
if ($variantId !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Domains\Ticketing\Event\Models;
|
||||
|
||||
use App\Domains\Core\Tenant\Models\SocialMedia;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -40,6 +41,12 @@ class Event extends Model
|
||||
return $this->hasMany(EventDate::class)->orderBy('date')->orderBy('time_start');
|
||||
}
|
||||
|
||||
/** @return HasMany<CatalogItem, $this> */
|
||||
public function catalogItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(CatalogItem::class)->whereAvailable()->orderBy('group_order')->orderBy('id');
|
||||
}
|
||||
|
||||
/** @return BelongsToMany<SocialMedia, $this> */
|
||||
public function socialMedia(): BelongsToMany
|
||||
{
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
namespace App\Domains\Ticketing\Event\Resources;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
@@ -17,8 +21,50 @@ class PublicEventResource extends JsonResource
|
||||
'location' => $this->location,
|
||||
'exact_location' => $this->exact_location,
|
||||
'date_text' => $this->date_text,
|
||||
'start_time' => $this->whenLoaded('dates', fn () => $this->dates->first()?->time_start),
|
||||
'starts_at' => $this->whenLoaded('dates', function (): ?string {
|
||||
$first = $this->dates->first();
|
||||
if ($first === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Carbon::parse(
|
||||
$first->date->format('Y-m-d').' '.$first->time_start,
|
||||
config('app.event_timezone'),
|
||||
)->toISOString();
|
||||
}),
|
||||
'dates' => $this->whenLoaded('dates', fn () => $this->dates->map(fn ($date): array => [
|
||||
'id' => $date->id,
|
||||
'date' => $date->date->format('Y-m-d'),
|
||||
'time_start' => $date->time_start,
|
||||
])->values()),
|
||||
'social_media' => $this->whenLoaded('socialMedia', fn () => $this->socialMedia->map(fn ($social): array => [
|
||||
'code' => $social->code,
|
||||
'url' => $social->pivot->url,
|
||||
])->values()),
|
||||
'attachment_id' => $this->attachment_id,
|
||||
'image' => $this->attachment?->getTemporaryUrl(1440),
|
||||
'catalog_items' => $this->whenLoaded('catalogItems', fn () => $this->catalogItems
|
||||
->map(fn (CatalogItem $item): array => [
|
||||
'id' => $item->id,
|
||||
'name' => $item->nombre,
|
||||
'description' => $item->descripcion,
|
||||
'price' => $item->precio,
|
||||
'image' => $item->attachments->first()?->getTemporaryUrl(1440),
|
||||
'requires_selection' => $item->itemAttributes->contains(fn ($attribute): bool =>
|
||||
$attribute->show_in_selector && $attribute->attribute?->codigo !== 'event_date'),
|
||||
'maximum_quantity' => $item->inventory_policy === InventoryPolicy::Unlimited
|
||||
? null
|
||||
: $item->availableStock(),
|
||||
'variants' => $item->visibleVariants()->map(fn (Variant $variant): array => [
|
||||
'id' => $variant->id,
|
||||
'price' => number_format($variant->getPrice(), 2, '.', ''),
|
||||
'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(),
|
||||
'maximum_quantity' => $item->inventory_policy === InventoryPolicy::Unlimited
|
||||
? null
|
||||
: $variant->inventory?->availableStock(),
|
||||
])->values(),
|
||||
])->values()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@ class PublicEventService
|
||||
&& $event->published_at !== null
|
||||
&& $event->published_at->isPast(), 404);
|
||||
|
||||
return $event->load('attachment');
|
||||
return $event->load([
|
||||
'attachment',
|
||||
'dates' => fn ($query) => $query->whereNull('rescheduled_to_event_date_id')->whereNull('suspended_at'),
|
||||
'socialMedia',
|
||||
'catalogItems.attachments',
|
||||
'catalogItems.inventory',
|
||||
'catalogItems.itemAttributes.attribute',
|
||||
'catalogItems.variants.inventory',
|
||||
'catalogItems.variants.eventDate',
|
||||
'catalogItems.variants.eventDates',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user