refactor(event): move event configuration onto tenant

This commit is contained in:
2026-08-07 09:08:14 -03:00
parent 60b8415c59
commit 7b7efeb4cc
27 changed files with 433 additions and 271 deletions

View File

@@ -7,7 +7,6 @@ use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Enums\EventProductType;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Services\CatalogInventoryService;
use App\Domains\Event\Models\Event;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket;
use App\Domains\Ticket\Models\ValidityTime;
@@ -20,7 +19,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'tenant_code',
'event_id',
'event_product_type',
'category_id',
'brand_id',
@@ -55,7 +53,6 @@ class CatalogItem extends Model
'category_id' => 'integer',
'brand_id' => 'integer',
'inventory_id' => 'integer',
'event_id' => 'integer',
'event_product_type' => EventProductType::class,
'type' => CatalogItemType::class,
'precio' => 'decimal:2',
@@ -72,12 +69,6 @@ class CatalogItem extends Model
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/** @return BelongsTo<Event, $this> */
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
/** @return BelongsTo<Category, $this> */
public function category(): BelongsTo
{

View File

@@ -26,18 +26,9 @@ class StoreCatalogItemRequest extends FormRequest
return [
'tenant_code' => ['prohibited'],
'type' => ['sometimes', Rule::enum(CatalogItemType::class)],
'event_id' => [
'sometimes',
'nullable',
'required_with:event_product_type',
Rule::exists('events', 'id')->where(
fn ($query) => $query->where('tenant_code', $tenantCode)
),
],
'event_product_type' => [
'sometimes',
'nullable',
'required_with:event_id',
Rule::enum(EventProductType::class),
],
'category_id' => [
@@ -93,7 +84,7 @@ class StoreCatalogItemRequest extends FormRequest
'nullable',
'integer',
Rule::exists('event_dates', 'id')->where(
fn ($query) => $query->where('event_id', $this->input('event_id'))
fn ($query) => $query->where('tenant_code', $tenantCode)
),
],
'variants.*.inventory_id' => ['prohibited'],

View File

@@ -23,7 +23,6 @@ class CatalogItemDetailResource extends JsonResource
return [
'id' => $this->id,
'type' => $this->type->value,
'event_id' => $this->event_id,
'event_product_type' => $this->event_product_type?->value,
'category_id' => $this->category_id,
'brand_id' => $this->brand_id,
@@ -38,9 +37,7 @@ class CatalogItemDetailResource extends JsonResource
'has_tickets' => $this->has_tickets,
'validity_time_id' => $this->validity_time_id,
'validity_time' => ValidityTimeResource::make($this->validityTime),
'attributes' => $this->itemAttributes
->map(fn (ItemAttribute $itemAttribute): array => $this->attributeData($itemAttribute))
->values(),
'attributes' => $this->attributesData(),
'stock_tecnico' => $this->when(
$selectedVariant === null,
fn () => $this->availableStock(),
@@ -111,19 +108,70 @@ class CatalogItemDetailResource extends JsonResource
];
}
/** @return Collection<int, array<string, mixed>> */
private function attributesData(): Collection
{
$attributes = $this->itemAttributes
->map(fn (ItemAttribute $itemAttribute): array => $this->attributeData($itemAttribute));
if ($this->variants->isEmpty() || $this->variants->contains(
fn (Variant $variant): bool => $variant->event_date_id === null
)) {
return $attributes->values();
}
$eventDateAttribute = [
'id' => -1,
'codigo' => 'event_date',
'nombre' => 'Fecha',
'is_required' => true,
'metadata_schema' => null,
'type' => 'event_date',
'options' => $this->variants
->pluck('eventDate')
->filter()
->unique('id')
->sortBy(fn ($eventDate): string => $eventDate->date->format('Y-m-d').' '.$eventDate->time_start)
->values()
->map(fn ($eventDate, int $index): array => [
'id' => $eventDate->id,
'value' => (string) $eventDate->id,
'label' => $eventDate->date->format('d/m/Y').' · '
.substr($eventDate->time_start, 0, 5).' a '
.substr($eventDate->time_end, 0, 5),
'sort_order' => $index,
'validity_time_id' => null,
'validity_time' => null,
'metadata' => [
'date' => $eventDate->date->format('Y-m-d'),
'time_start' => $eventDate->time_start,
'time_end' => $eventDate->time_end,
],
]),
];
return collect([$eventDateAttribute])->concat($attributes)->values();
}
/** @return array<string, mixed> */
private function variantData(Variant $variant): array
{
$values = $variant->definitions
->mapWithKeys(fn ($definition) => [
$definition->itemAttribute?->attribute?->codigo => $definition->value,
])
->filter(fn ($value, $key): bool => $key !== null);
if ($variant->event_date_id !== null) {
$values->put('event_date', (string) $variant->event_date_id);
}
return [
'id' => $variant->id,
'event_date_id' => $variant->event_date_id,
'event_date' => $variant->eventDate?->date?->format('Y-m-d'),
'stock_tecnico' => $this->variantStock($variant),
'values' => $variant->definitions
->mapWithKeys(fn ($definition) => [
$definition->itemAttribute?->attribute?->codigo => $definition->value,
])
->filter(fn ($value, $key): bool => $key !== null),
'values' => $values,
];
}

View File

@@ -16,7 +16,6 @@ class CatalogItemResource extends JsonResource
return [
'id' => $this->id,
'type' => $this->type->value,
'event_id' => $this->event_id,
'event_product_type' => $this->event_product_type?->value,
'category_id' => $this->category_id,
'brand_id' => $this->brand_id,

View File

@@ -12,7 +12,6 @@ use App\Domains\Catalog\Models\Category;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\ItemAttribute;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Event\Models\Event;
use App\Domains\Event\Models\EventDate;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Builder;
@@ -47,7 +46,14 @@ class CatalogService
);
$hasVariants = $attributeCodes !== [] || $hasEventDateVariants;
$this->validateEventData($data);
if ($hasEventDateVariants && in_array('event_date', $attributeCodes, true)) {
throw ValidationException::withMessages([
'attribute_codes' => ['The event_date code is reserved for event dates.'],
]);
}
$this->validateEventProductType($data);
$this->validateUniqueVariantCombinations($variants, $attributeCodes);
if ($type === CatalogItemType::Bundle) {
$this->validateBundleData($data, $components);
@@ -127,7 +133,6 @@ class CatalogService
'inventory',
'category',
'brand',
'event',
'validityTime',
'itemAttributes.attribute',
'variants.inventory',
@@ -147,7 +152,6 @@ class CatalogService
'inventory',
'category',
'brand',
'event',
'validityTime',
'itemAttributes.attribute.options.validityTime',
'variants' => fn ($query) => $query->orderBy('id'),
@@ -492,16 +496,15 @@ class CatalogService
if (
$eventDateId !== null
&& (
$catalogItem->event_id === null
|| ! EventDate::query()
! EventDate::query()
->whereKey($eventDateId)
->where('event_id', $catalogItem->event_id)
->where('tenant_code', $catalogItem->tenant_code)
->exists()
)
) {
throw ValidationException::withMessages([
"variants.{$index}.event_date_id" => [
'The event date must belong to the catalog item event.',
'The event date must belong to the catalog item tenant.',
],
]);
}
@@ -533,30 +536,14 @@ class CatalogService
}
/** @param array<string, mixed> $data */
private function validateEventData(array $data): void
private function validateEventProductType(array $data): void
{
$eventId = $data['event_id'] ?? null;
$eventProductType = $data['event_product_type'] ?? null;
if (($eventId === null) !== ($eventProductType === null)) {
throw ValidationException::withMessages([
'event_id' => ['Event and event product type must be provided together.'],
]);
}
if ($eventId === null) {
if ($eventProductType === null) {
return;
}
if (! Event::query()
->whereKey($eventId)
->where('tenant_code', $data['tenant_code'] ?? null)
->exists()) {
throw ValidationException::withMessages([
'event_id' => ['The event must belong to the catalog item tenant.'],
]);
}
if (! in_array($eventProductType, EventProductType::values(), true)) {
throw ValidationException::withMessages([
'event_product_type' => ['The event product type is invalid.'],
@@ -564,6 +551,35 @@ class CatalogService
}
}
/**
* @param array<int, array<string, mixed>> $variants
* @param array<int, string> $attributeCodes
*/
private function validateUniqueVariantCombinations(array $variants, array $attributeCodes): void
{
$seen = [];
$attributeCodes = array_values(array_unique($attributeCodes));
sort($attributeCodes);
foreach (array_values($variants) as $index => $variant) {
$combination = [(string) ($variant['event_date_id'] ?? '')];
foreach ($attributeCodes as $attributeCode) {
$value = trim((string) ($variant['values'][$attributeCode] ?? ''));
$combination[] = Str::ascii(mb_strtolower($value));
}
$key = implode('|', $combination);
if (isset($seen[$key])) {
throw ValidationException::withMessages([
"variants.{$index}" => ['The variant combination must be unique.'],
]);
}
$seen[$key] = true;
}
}
/**
* @param array<string, mixed> $data
* @param array<int, array<string, mixed>> $variants

View File

@@ -15,14 +15,14 @@ class EventController extends Controller
public function show(Request $request): EventResource
{
return EventResource::make(
$this->eventService->activeForTenant($request->user()->tenant()->firstOrFail())
$this->eventService->forTenant($request->user()->tenant()->firstOrFail())
);
}
public function update(UpdateEventRequest $request): EventResource
{
return EventResource::make(
$this->eventService->updateActiveForTenant(
$this->eventService->updateForTenant(
$request->user()->tenant()->firstOrFail(),
$request->validated()
)

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Domains\Event\Models;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Purchase\Models\Purchase;
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_code',
'name',
'address',
])]
class Event extends Model
{
use HasFactory;
public $timestamps = false;
/** @return BelongsTo<Tenant, $this> */
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/** @return HasMany<EventDate, $this> */
public function dates(): HasMany
{
return $this->hasMany(EventDate::class)->orderBy('date')->orderBy('time_start');
}
/** @return HasMany<CatalogItem, $this> */
public function catalogItems(): HasMany
{
return $this->hasMany(CatalogItem::class);
}
/** @return HasMany<Purchase, $this> */
public function purchases(): HasMany
{
return $this->hasMany(Purchase::class);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Domains\Event\Models;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Tenant\Models\Tenant;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -12,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
#[Fillable([
'event_id',
'tenant_code',
'date',
'time_start',
'time_end',
@@ -26,15 +27,14 @@ class EventDate extends Model
protected function casts(): array
{
return [
'event_id' => 'integer',
'date' => 'date:Y-m-d',
];
}
/** @return BelongsTo<Event, $this> */
public function event(): BelongsTo
/** @return BelongsTo<Tenant, $this> */
public function tenant(): BelongsTo
{
return $this->belongsTo(Event::class);
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/** @return HasMany<Variant, $this> */

View File

@@ -2,29 +2,29 @@
namespace App\Domains\Event\Resources;
use App\Domains\Event\Models\Event;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Event */
/** @mixin Tenant */
class EventResource extends JsonResource
{
/** @return array<string, mixed> */
public function toArray(Request $request): array
{
$socialMedia = $this->tenant->socialMedia->keyBy('code');
$socialMedia = $this->socialMedia->keyBy('code');
return [
'id' => $this->id,
'title' => $this->name,
'location' => $this->address,
'dates' => $this->dates->map(fn ($eventDate): array => [
'title' => $this->event_title,
'location' => $this->event_location,
'dates' => $this->eventDates->map(fn ($eventDate): array => [
'id' => $eventDate->id,
'date' => $eventDate->date->format('Y-m-d'),
'start_time' => substr($eventDate->time_start, 0, 5),
'end_time' => substr($eventDate->time_end, 0, 5),
])->values(),
'social_media' => $this->tenant->socialMedia->map(fn ($item): array => [
'social_media' => $this->socialMedia->map(fn ($item): array => [
'code' => $item->code,
'url' => $item->pivot->url,
'orden' => $item->pivot->orden,

View File

@@ -2,7 +2,6 @@
namespace App\Domains\Event\Services;
use App\Domains\Event\Models\Event;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Support\Facades\DB;
@@ -14,53 +13,36 @@ class EventService
'facebook_url' => 'facebook',
];
public function activeForTenant(Tenant $tenant): Event
public function forTenant(Tenant $tenant): Tenant
{
return $tenant->events()
->whereKey($tenant->active_event_id)
->with(['dates', 'tenant.socialMedia'])
->firstOrFail();
return $tenant->load(['eventDates', 'socialMedia']);
}
/** @param array<string, mixed> $data */
public function updateActiveForTenant(Tenant $tenant, array $data): Event
public function updateForTenant(Tenant $tenant, array $data): Tenant
{
return DB::transaction(function () use ($tenant, $data): Event {
return DB::transaction(function () use ($tenant, $data): Tenant {
$tenant = Tenant::query()->whereKey($tenant->getKey())->lockForUpdate()->firstOrFail();
$event = $tenant->active_event_id === null
? $tenant->events()->create([
'name' => $data['title'],
'address' => $data['location'],
])
: $tenant->events()
->whereKey($tenant->active_event_id)
->lockForUpdate()
->firstOrFail();
$event->update([
'name' => $data['title'],
'address' => $data['location'],
$tenant->update([
'event_title' => $data['title'],
'event_location' => $data['location'],
]);
if ($tenant->active_event_id === null) {
$tenant->update(['active_event_id' => $event->id]);
}
$this->syncDates($event, $data['dates']);
$this->syncDates($tenant, $data['dates']);
if (array_key_exists('social_media', $data)) {
$this->syncSocialMedia($tenant, $data['social_media']);
} else {
$this->syncLegacyContact($tenant, $data['contact']);
}
return $event->load(['dates', 'tenant.socialMedia']);
return $tenant->load(['eventDates', 'socialMedia']);
});
}
/** @param array<int, array{date: string, start_time: string, end_time: string}> $dates */
private function syncDates(Event $event, array $dates): void
private function syncDates(Tenant $tenant, array $dates): void
{
$existingDates = $event->dates()->get()->values();
$existingDates = $tenant->eventDates()->get()->values();
foreach (array_values($dates) as $index => $date) {
$attributes = [
@@ -74,12 +56,12 @@ class EventService
if ($existingDate) {
$existingDate->update($attributes);
} else {
$event->dates()->create($attributes);
$tenant->eventDates()->create($attributes);
}
}
$existingDates->slice(count($dates))->each->delete();
$event->unsetRelation('dates');
$tenant->unsetRelation('eventDates');
}
/** @param array<string, string|null> $contact */

View File

@@ -4,7 +4,6 @@ namespace App\Domains\Purchase\Models;
use App\Domains\Auth\Models\User;
use App\Domains\Cart\Models\Cart;
use App\Domains\Event\Models\Event;
use App\Domains\Logging\Models\Concerns\LogsValueChanges;
use App\Domains\Purchase\Events\PurchasePaid;
use App\Domains\Tenant\Models\Tenant;
@@ -20,7 +19,6 @@ use Illuminate\Support\Facades\DB;
#[Fillable([
'cart_id',
'tenant_codigo',
'event_id',
'user_id',
'status',
'payment_method',
@@ -72,7 +70,6 @@ class Purchase extends Model
{
return [
'cart_id' => 'integer',
'event_id' => 'integer',
'user_id' => 'integer',
'expires_at' => 'datetime',
'total' => 'decimal:2',
@@ -87,12 +84,6 @@ class Purchase extends Model
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
}
/** @return BelongsTo<Event, $this> */
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
/**
* @return BelongsTo<User, $this>
*/

View File

@@ -42,7 +42,6 @@ class PurchaseResource extends JsonResource
'id' => $this->id,
'cart_id' => $this->cart_id,
'tenant_codigo' => $this->tenant_codigo,
'event_id' => $this->event_id,
'user_id' => $this->user_id,
'created_at' => $this->created_at,
'status' => $this->status,

View File

@@ -229,7 +229,6 @@ class StartCheckoutService
...$purchaseData,
'cart_id' => $cartId,
'tenant_codigo' => $tenant->codigo,
'event_id' => $tenant->active_event_id,
'user_id' => $userId,
'status' => Purchase::STATUS_CREATED,
'payment_method' => null,

View File

@@ -7,7 +7,7 @@ use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Category;
use App\Domains\Event\Models\Event;
use App\Domains\Event\Models\EventDate;
use App\Domains\Menu\Models\Menu;
use App\Domains\Menu\Models\TenantMenu;
use Illuminate\Database\Eloquent\Attributes\Fillable;
@@ -33,7 +33,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
'search_product_layout',
'search_group_layout',
'search_items_per_page',
'active_event_id',
'event_title',
'event_location',
])]
class Tenant extends Model
{
@@ -61,7 +62,6 @@ class Tenant extends Model
'search_product_layout' => ProductLayout::class,
'search_group_layout' => GroupLayout::class,
'search_items_per_page' => 'integer',
'active_event_id' => 'integer',
];
}
@@ -94,16 +94,12 @@ class Tenant extends Model
return $this->hasMany(CatalogItem::class, 'tenant_code', 'codigo');
}
/** @return HasMany<Event, $this> */
public function events(): HasMany
/** @return HasMany<EventDate, $this> */
public function eventDates(): HasMany
{
return $this->hasMany(Event::class, 'tenant_code', 'codigo');
}
/** @return BelongsTo<Event, $this> */
public function activeEvent(): BelongsTo
{
return $this->belongsTo(Event::class, 'active_event_id');
return $this->hasMany(EventDate::class, 'tenant_code', 'codigo')
->orderBy('date')
->orderBy('time_start');
}
/**

View File

@@ -32,14 +32,12 @@ class TenantResource extends JsonResource
'header_bg_color' => $this->header_bg_color,
'footer_bg_color' => $this->footer_bg_color,
'website_type_code' => $this->website_type_code,
'active_event_id' => $this->active_event_id,
'active_event' => $this->whenLoaded('activeEvent', fn () => $this->activeEvent === null
'event' => $this->whenLoaded('eventDates', fn () => $this->event_title === null
? null
: [
'id' => $this->activeEvent->id,
'name' => $this->activeEvent->name,
'address' => $this->activeEvent->address,
'dates' => $this->activeEvent->dates->map(fn ($eventDate): array => [
'title' => $this->event_title,
'location' => $this->event_location,
'dates' => $this->eventDates->map(fn ($eventDate): array => [
'id' => $eventDate->id,
'date' => $eventDate->date->format('Y-m-d'),
'time_start' => $eventDate->time_start,

View File

@@ -15,7 +15,7 @@ class TenantInformationService
'footerLogo',
'socialMedia',
'websiteExtras.websiteTypeExtra',
'activeEvent.dates',
'eventDates',
];
/**