feat(events): add event categories and filtering

This commit is contained in:
2026-09-21 09:16:28 -03:00
parent b0702b7e15
commit 79d402e210
13 changed files with 134 additions and 8 deletions

View File

@@ -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'),
]
);
}

View File

@@ -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<EventCategory, $this> */
public function eventCategories(): HasMany
{
return $this->hasMany(EventCategory::class, 'tenant_code', 'codigo');
}
/** @return HasMany<ScanAttempt, $this> */
public function scanAttempts(): HasMany
{

View File

@@ -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)),
];
}

View File

@@ -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

View File

@@ -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<EventCategory, $this> */
public function eventCategory(): BelongsTo
{
return $this->belongsTo(EventCategory::class);
}
/** @return BelongsTo<Attachment, $this> */
public function attachment(): BelongsTo
{

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Domains\Ticketing\Event\Models;
use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['tenant_code', 'nombre'])]
class EventCategory extends Model
{
/** @return BelongsTo<Tenant, $this> */
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/** @return HasMany<Event, $this> */
public function events(): HasMany
{
return $this->hasMany(Event::class);
}
}

View File

@@ -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'],
];
}
}

View File

@@ -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,

View File

@@ -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.'%')

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('event_categories', function (Blueprint $table): void {
$table->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');
}
};

View File

@@ -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]);
}

View File

@@ -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']);

View File

@@ -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();