From 79d402e210444359325e3874fdbd975ac2747769 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 21 Sep 2026 09:16:28 -0300 Subject: [PATCH] feat(events): add event categories and filtering --- .../Services/TenantBootstrapService.php | 3 +- app/Domains/Core/Tenant/Models/Tenant.php | 7 ++++ .../Core/Tenant/Resources/TenantResource.php | 12 ++++-- .../Controllers/PublicEventController.php | 7 +++- app/Domains/Ticketing/Event/Models/Event.php | 8 +++- .../Ticketing/Event/Models/EventCategory.php | 25 +++++++++++ .../Requests/ListPublicEventsRequest.php | 1 + .../Event/Resources/PublicEventResource.php | 1 + .../Event/Services/PublicEventService.php | 7 +++- ...8_000600_create_event_categories_table.php | 41 +++++++++++++++++++ database/seeders/TenantSeeder.php | 4 ++ database/seeders/TestEventsSeeder.php | 10 +++++ .../Tenant/BootstrapTenantControllerTest.php | 16 ++++++++ 13 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 app/Domains/Ticketing/Event/Models/EventCategory.php create mode 100644 database/migrations/2026_09_18_000600_create_event_categories_table.php diff --git a/app/Domains/Core/Bootstrap/Services/TenantBootstrapService.php b/app/Domains/Core/Bootstrap/Services/TenantBootstrapService.php index 72cdbdc..9ccb8aa 100644 --- a/app/Domains/Core/Bootstrap/Services/TenantBootstrapService.php +++ b/app/Domains/Core/Bootstrap/Services/TenantBootstrapService.php @@ -37,7 +37,8 @@ class TenantBootstrapService 'roles', fn ($query) => $query->where('codigo', RoleCode::User->value) ), - 'categories' => fn ($query) => $query->orderBy('nombre'), + $tenant->storefront_website_type_code === 'onticket_multi_event' + ? 'eventCategories' : 'categories' => fn ($query) => $query->orderBy('nombre'), ] ); } diff --git a/app/Domains/Core/Tenant/Models/Tenant.php b/app/Domains/Core/Tenant/Models/Tenant.php index 3d573cc..d1420a5 100644 --- a/app/Domains/Core/Tenant/Models/Tenant.php +++ b/app/Domains/Core/Tenant/Models/Tenant.php @@ -11,6 +11,7 @@ use App\Domains\Core\Client\Models\Client; use App\Domains\Ticketing\Event\Models\EventDate; use App\Domains\Ticketing\Event\Models\EventDateChange; use App\Domains\Ticketing\Event\Models\Event; +use App\Domains\Ticketing\Event\Models\EventCategory; use App\Domains\Core\Menu\Models\Menu; use App\Domains\Core\Menu\Models\TenantMenu; use App\Domains\Core\Tenant\Enums\CartEditingPolicy; @@ -265,6 +266,12 @@ class Tenant extends Model return $this->hasMany(Category::class, 'tenant_code', 'codigo'); } + /** @return HasMany */ + public function eventCategories(): HasMany + { + return $this->hasMany(EventCategory::class, 'tenant_code', 'codigo'); + } + /** @return HasMany */ public function scanAttempts(): HasMany { diff --git a/app/Domains/Core/Tenant/Resources/TenantResource.php b/app/Domains/Core/Tenant/Resources/TenantResource.php index 2d792db..b4cf5da 100644 --- a/app/Domains/Core/Tenant/Resources/TenantResource.php +++ b/app/Domains/Core/Tenant/Resources/TenantResource.php @@ -99,10 +99,14 @@ class TenantResource extends JsonResource 'menues', fn () => $this->menuTree($this->menues) ), - 'categories' => $this->whenLoaded( - 'categories', - fn () => $this->categoryTree($this->categories) - ), + 'categories' => $this->storefront_website_type_code === 'onticket_multi_event' + ? $this->whenLoaded('eventCategories', fn () => $this->eventCategories + ->map(fn ($category) => [ + 'id' => $category->id, + 'nombre' => $category->nombre, + 'subcategories' => [], + ])->values()) + : $this->whenLoaded('categories', fn () => $this->categoryTree($this->categories)), ]; } diff --git a/app/Domains/Ticketing/Event/Controllers/PublicEventController.php b/app/Domains/Ticketing/Event/Controllers/PublicEventController.php index 8346b89..9785392 100644 --- a/app/Domains/Ticketing/Event/Controllers/PublicEventController.php +++ b/app/Domains/Ticketing/Event/Controllers/PublicEventController.php @@ -17,7 +17,12 @@ class PublicEventController extends Controller abort_unless($tenant->storefront_website_type_code === 'onticket_multi_event', 404); $term = trim((string) $request->validated('q', '')); - return PublicEventResource::collection($events->list($tenant, $term, (int) $request->validated('page', 1))); + return PublicEventResource::collection($events->list( + $tenant, + $term, + (int) $request->validated('page', 1), + $request->validated('category_id', null) === null ? null : (int) $request->validated('category_id'), + )); } public function show(Tenant $tenant, Event $event, PublicEventService $events): PublicEventResource diff --git a/app/Domains/Ticketing/Event/Models/Event.php b/app/Domains/Ticketing/Event/Models/Event.php index 5389085..1379c7c 100644 --- a/app/Domains/Ticketing/Event/Models/Event.php +++ b/app/Domains/Ticketing/Event/Models/Event.php @@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -#[Fillable(['tenant_code', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])] +#[Fillable(['tenant_code', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])] class Event extends Model { use HasFactory; @@ -29,6 +29,12 @@ class Event extends Model return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); } + /** @return BelongsTo */ + public function eventCategory(): BelongsTo + { + return $this->belongsTo(EventCategory::class); + } + /** @return BelongsTo */ public function attachment(): BelongsTo { diff --git a/app/Domains/Ticketing/Event/Models/EventCategory.php b/app/Domains/Ticketing/Event/Models/EventCategory.php new file mode 100644 index 0000000..dbed1e3 --- /dev/null +++ b/app/Domains/Ticketing/Event/Models/EventCategory.php @@ -0,0 +1,25 @@ + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); + } + + /** @return HasMany */ + public function events(): HasMany + { + return $this->hasMany(Event::class); + } +} diff --git a/app/Domains/Ticketing/Event/Requests/ListPublicEventsRequest.php b/app/Domains/Ticketing/Event/Requests/ListPublicEventsRequest.php index 5431fb8..9419c54 100644 --- a/app/Domains/Ticketing/Event/Requests/ListPublicEventsRequest.php +++ b/app/Domains/Ticketing/Event/Requests/ListPublicEventsRequest.php @@ -11,6 +11,7 @@ class ListPublicEventsRequest extends FormRequest return [ 'q' => ['sometimes', 'string', 'max:120'], 'page' => ['sometimes', 'integer', 'min:1'], + 'category_id' => ['sometimes', 'integer', 'min:1'], ]; } } diff --git a/app/Domains/Ticketing/Event/Resources/PublicEventResource.php b/app/Domains/Ticketing/Event/Resources/PublicEventResource.php index ff51827..b6430aa 100644 --- a/app/Domains/Ticketing/Event/Resources/PublicEventResource.php +++ b/app/Domains/Ticketing/Event/Resources/PublicEventResource.php @@ -15,6 +15,7 @@ class PublicEventResource extends JsonResource { return [ 'id' => $this->id, + 'event_category_id' => $this->event_category_id, 'title' => $this->title, 'subtitle' => $this->subtitle, 'description' => $this->description, diff --git a/app/Domains/Ticketing/Event/Services/PublicEventService.php b/app/Domains/Ticketing/Event/Services/PublicEventService.php index b9cca10..39c38eb 100644 --- a/app/Domains/Ticketing/Event/Services/PublicEventService.php +++ b/app/Domains/Ticketing/Event/Services/PublicEventService.php @@ -8,12 +8,17 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator; class PublicEventService { - public function list(Tenant $tenant, string $term, int $page): LengthAwarePaginator + public function list(Tenant $tenant, string $term, int $page, ?int $categoryId = null): LengthAwarePaginator { $query = $tenant->events() ->whereNotNull('published_at') ->where('published_at', '<=', now()); + if ($categoryId !== null) { + abort_unless($tenant->eventCategories()->whereKey($categoryId)->exists(), 404); + $query->where('event_category_id', $categoryId); + } + if ($term !== '') { $query->where(function ($query) use ($term): void { $query->where('title', 'like', '%'.$term.'%') diff --git a/database/migrations/2026_09_18_000600_create_event_categories_table.php b/database/migrations/2026_09_18_000600_create_event_categories_table.php new file mode 100644 index 0000000..d7f3203 --- /dev/null +++ b/database/migrations/2026_09_18_000600_create_event_categories_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('tenant_code'); + $table->string('nombre'); + $table->timestamps(); + $table->foreign('tenant_code')->references('codigo')->on('tenants')->cascadeOnUpdate()->cascadeOnDelete(); + $table->unique(['tenant_code', 'nombre']); + }); + + Schema::table('events', function (Blueprint $table): void { + $table->foreignId('event_category_id')->nullable()->constrained('event_categories')->nullOnDelete(); + }); + + if (DB::table('tenants')->where('codigo', 'onticket')->exists()) { + $now = now(); + foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) { + DB::table('event_categories')->insert([ + 'tenant_code' => 'onticket', 'nombre' => $nombre, + 'created_at' => $now, 'updated_at' => $now, + ]); + } + } + } + + public function down(): void + { + Schema::table('events', fn (Blueprint $table) => $table->dropConstrainedForeignId('event_category_id')); + Schema::dropIfExists('event_categories'); + } +}; diff --git a/database/seeders/TenantSeeder.php b/database/seeders/TenantSeeder.php index 0ec0395..9f46119 100644 --- a/database/seeders/TenantSeeder.php +++ b/database/seeders/TenantSeeder.php @@ -113,6 +113,10 @@ class TenantSeeder extends Seeder $onTicketTenant = Tenant::query()->where('codigo', 'onticket')->firstOrFail(); + foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) { + $onTicketTenant->eventCategories()->firstOrCreate(['nombre' => $nombre]); + } + if ($onTicketTenant->storefront_website_type_code === 'onticket_multi_event' && ! $onTicketTenant->display_seach_bar) { $onTicketTenant->update(['display_seach_bar' => true]); } diff --git a/database/seeders/TestEventsSeeder.php b/database/seeders/TestEventsSeeder.php index b28c719..b2d2173 100644 --- a/database/seeders/TestEventsSeeder.php +++ b/database/seeders/TestEventsSeeder.php @@ -20,6 +20,7 @@ class TestEventsSeeder extends Seeder $events = [ [ 'title' => '[Prueba] Noche de Música en Vivo', + 'category' => 'Música', 'subtitle' => 'Una noche para cantar y bailar', 'description' => 'Evento de prueba para explorar la cartelera de OnTicket.', 'location' => 'Teatro Broadway, Rosario', @@ -30,6 +31,7 @@ class TestEventsSeeder extends Seeder ], [ 'title' => '[Prueba] Festival de Sabores', + 'category' => 'Otros', 'subtitle' => 'Gastronomía y música para toda la familia', 'description' => 'Evento de prueba con dos jornadas disponibles.', 'location' => 'Parque de España, Rosario', @@ -41,6 +43,7 @@ class TestEventsSeeder extends Seeder ], [ 'title' => '[Prueba] Stand Up en el Centro', + 'category' => 'Teatro', 'subtitle' => 'Humor para compartir', 'description' => 'Evento de prueba para visualizar distintas propuestas.', 'location' => 'Centro Cultural La Comedia, Rosario', @@ -60,9 +63,16 @@ class TestEventsSeeder extends Seeder 'description' => $definition['description'], 'location' => $definition['location'], 'published_at' => now(), + 'event_category_id' => $tenant->eventCategories() + ->where('nombre', $definition['category'])->value('id'), ], ); + if ($event->event_category_id === null) { + $event->update(['event_category_id' => $tenant->eventCategories() + ->where('nombre', $definition['category'])->value('id')]); + } + if ($event->attachment_id === null) { $path = public_path('images/tennants/onticket/test-events/'.$definition['image']); diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index f56b2de..73e364c 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -23,6 +23,22 @@ class BootstrapTenantControllerTest extends TestCase { use RefreshDatabase; + public function test_multi_event_bootstrap_returns_event_categories_instead_of_catalog_categories(): void + { + $this->seed(WebsiteTypeSeeder::class); + $tenant = $this->createTenant(); + $tenant->update(['storefront_website_type_code' => 'onticket_multi_event']); + $tenant->categories()->create(['nombre' => 'Producto']); + $eventCategory = $tenant->eventCategories()->create(['nombre' => 'Música']); + + $this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F') + ->assertOk() + ->assertJsonCount(1, 'data.categories') + ->assertJsonPath('data.categories.0.id', $eventCategory->id) + ->assertJsonPath('data.categories.0.nombre', 'Música') + ->assertJsonPath('data.categories.0.subcategories', []); + } + public function test_it_bootstraps_a_tenant_by_domain(): void { $hdrKey = (string) Str::uuid();