diff --git a/app/Domains/Ticketing/Event/documentacion/README.md b/app/Domains/Ticketing/Event/documentacion/README.md index dd46873..5822002 100644 --- a/app/Domains/Ticketing/Event/documentacion/README.md +++ b/app/Domains/Ticketing/Event/documentacion/README.md @@ -2,36 +2,52 @@ ## Propósito -Administra la configuración temporal de un tenant orientado a eventos y sus fechas disponibles. +Administra los eventos de un tenant, sus fechas y sus redes sociales. -## Componentes +## Modelo -- `Models/EventDate.php`: fecha del evento con inicio, fin, tenant y variantes asociadas. -- `Services/EventService.php`: obtiene y actualiza la configuración de evento del tenant. -- `Services/EventDateNoticeService.php`: reclama y agrupa los cambios pendientes de cada usuario. -- `Controllers/AdminApp/EventController.php`: consulta y modificación desde AdminApp. -- `UpdateEventRequest`: valida datos y reglas cruzadas de fechas. -- `EventResource`: serializa la configuración de salida. +`Tenant` tiene muchos `Event`. Cada evento tiene título, subtítulo, descripción, +ubicación, texto de fechas y `published_at`. Sus fechas se guardan en `event_dates` +mediante `event_id`. El catálogo `social_media` define las plataformas; +`event_social_media` guarda la URL y el orden de cada cuenta del evento. -## Endpoints +La migración `2026_09_18_000500_restore_events` crea un evento por cada tenant que +tenía datos de evento o fechas, y migra las fechas y redes correspondientes. +`2026_09_18_000600_associate_active_event_social_media` copia al evento activo las +redes del tenant que falten, sin sobrescribir las URL propias del evento. El seeder +de Fiesta Fútbol Infantil hace lo mismo cuando crea el evento en una base nueva. -Bajo `/v1/adminapp/tenant/event`, protegidos por `auth:sanctum` y `adminapp.tenant`: +## Storefront -- `GET`: obtiene la configuración. -- `PUT`: actualiza la configuración. +- `onticket` muestra el evento señalado por `tenants.active_event_id`. +- `onticket_multi_event` permite varios eventos publicados y mantiene + `tenants.active_event_id` en `NULL`. La presentación y gestión de esos eventos + en el frontend quedan pendientes de diseño. -Para el storefront autenticado: +`active_event_id` selecciona el evento del storefront de evento único; no indica +si está publicado. La asignación de este campo y los cambios de tipo de storefront +se administran directamente en la base de datos. La aplicación no valida ni +automatiza esas operaciones. -- `POST /tenants/{tenant}/event-date-notices/claim`: devuelve hasta un aviso de suspensiones y otro de - reprogramaciones. Cada cambio se muestra como máximo tres veces por usuario. +Cuando hay un evento activo, el campo `social_media` del bootstrap usa sus redes, +igual que `event.social_media`. Las actualizaciones de redes de ese tenant también +se guardan en el evento. Sin evento activo se conservan las redes propias del tenant. + +## API + +Los endpoints de AdminApp usan `auth:sanctum` y `adminapp.tenant`: + +- `GET/PUT /v1/adminapp/tenant/event`: acceso al evento seleccionado para el + storefront de evento único. +- `POST /v1/adminapp/tenant/event-dates`: crea una fecha para ese evento. +- `POST /v1/adminapp/tenant/event-dates/{eventDate}/reschedule` y + `POST /v1/adminapp/tenant/event-dates/{eventDate}/suspend`: cambios de fecha. + +El bootstrap del tenant entrega `event` para el evento seleccionado. No entrega +una lista de eventos ni existe todavía una pantalla para gestionarlos. ## Dependencias -Depende de `Tenant`. Las fechas se vinculan con variantes de `Catalog`, que a su vez pueden generar tickets. - -## Consideraciones - -Los avisos se construyen dinámicamente después de excluir los cambios que el usuario ya vio -tres veces. Al reclamar los avisos se incrementa una vez cada cambio incluido, aunque varios -cambios aparezcan agrupados en el mismo mensaje. El reclamo bloquea al usuario durante la -transacción para impedir que pestañas concurrentes superen el máximo. +Las fechas se vinculan con variantes de `Catalog`, que a su vez pueden generar +tickets. Los avisos por suspensión y reprogramación se construyen dinámicamente +después de excluir los cambios que el usuario ya vio tres veces. diff --git a/database/migrations/2026_09_18_000600_associate_active_event_social_media.php b/database/migrations/2026_09_18_000600_associate_active_event_social_media.php new file mode 100644 index 0000000..ce24b80 --- /dev/null +++ b/database/migrations/2026_09_18_000600_associate_active_event_social_media.php @@ -0,0 +1,48 @@ +whereNotNull('active_event_id') + ->orderBy('id') + ->chunk(100, function ($tenants): void { + foreach ($tenants as $tenant) { + $eventId = DB::table('events') + ->where('id', $tenant->active_event_id) + ->where('tenant_code', $tenant->codigo) + ->value('id'); + + if ($eventId === null) { + continue; + } + + $rows = DB::table('tenant_social_media') + ->where('tenant_code', $tenant->codigo) + ->get() + ->map(fn ($network): array => [ + 'event_id' => $eventId, + 'social_media_code' => $network->social_media_code, + 'url' => $network->url, + 'orden' => $network->orden, + 'created_at' => now(), + 'updated_at' => now(), + ]) + ->all(); + + if ($rows !== []) { + DB::table('event_social_media')->insertOrIgnore($rows); + } + } + }); + } + + public function down(): void + { + // The backfilled rows may have been edited after migration; deleting them would lose data. + } +}; diff --git a/tests/Feature/Migrations/RestoreEventsMigrationTest.php b/tests/Feature/Migrations/RestoreEventsMigrationTest.php new file mode 100644 index 0000000..8a4cff8 --- /dev/null +++ b/tests/Feature/Migrations/RestoreEventsMigrationTest.php @@ -0,0 +1,119 @@ +id(); + $table->string('codigo')->unique(); + $table->string('nombre'); + $table->string('storefront_website_type_code'); + $table->string('search_product_layout')->default('column_with_image'); + $table->string('search_group_layout')->default('paginated'); + $table->string('event_title')->nullable(); + $table->string('event_location')->nullable(); + $table->text('event_date_text')->nullable(); + }); + Schema::create('event_dates', function (Blueprint $table): void { + $table->id(); + $table->string('tenant_code'); + $table->date('date'); + $table->time('time_start'); + $table->time('time_end'); + }); + Schema::create('social_media', function (Blueprint $table): void { + $table->id(); + $table->string('code')->unique(); + $table->string('icon'); + $table->string('name'); + }); + Schema::create('tenant_social_media', function (Blueprint $table): void { + $table->id(); + $table->string('tenant_code'); + $table->string('social_media_code'); + $table->string('url'); + $table->unsignedInteger('orden'); + $table->timestamps(); + }); + + DB::table('tenants')->insert([ + ['codigo' => 'one', 'nombre' => 'Uno', 'storefront_website_type_code' => 'onticket', + 'event_title' => 'Evento uno', 'event_location' => 'Rosario', 'event_date_text' => '1 de octubre'], + ['codigo' => 'many', 'nombre' => 'Varios', 'storefront_website_type_code' => 'onticket_multi_event', + 'event_title' => 'Evento varios', 'event_location' => 'Santa Fe', 'event_date_text' => '2 de octubre'], + ]); + DB::table('event_dates')->insert([ + 'tenant_code' => 'one', 'date' => '2026-10-01', 'time_start' => '10:00:00', 'time_end' => '12:00:00', + ]); + DB::table('social_media')->insert(['code' => 'instagram', 'icon' => 'instagram', 'name' => 'Instagram']); + DB::table('tenant_social_media')->insert([ + 'tenant_code' => 'one', 'social_media_code' => 'instagram', + 'url' => 'https://instagram.com/example', 'orden' => 0, + ]); + + $migration = require database_path('migrations/2026_09_18_000500_restore_events.php'); + $migration->up(); + + $one = DB::table('events')->where('tenant_code', 'one')->first(); + $many = DB::table('events')->where('tenant_code', 'many')->first(); + + $this->assertSame('Evento uno', $one->title); + $this->assertSame('Rosario', $one->location); + $this->assertSame('1 de octubre', $one->date_text); + $this->assertSame($one->id, DB::table('tenants')->where('codigo', 'one')->value('active_event_id')); + $this->assertNull(DB::table('tenants')->where('codigo', 'many')->value('active_event_id')); + $this->assertSame($one->id, DB::table('event_dates')->value('event_id')); + $this->assertSame('https://instagram.com/example', DB::table('event_social_media')->value('url')); + $this->assertFalse(Schema::hasColumn('tenants', 'event_title')); + $this->assertNotNull($many); + + $tenant = Tenant::query()->where('codigo', 'one')->firstOrFail(); + $this->assertSame($one->id, $tenant->activeEvent->id); + $this->assertCount(1, $tenant->events); + $this->assertCount(1, $tenant->activeEvent->dates); + $this->assertSame('instagram', $tenant->activeEvent->socialMedia->first()->code); + + DB::table('event_social_media')->where('event_id', $one->id)->update([ + 'url' => 'https://instagram.com/event-only', + ]); + DB::table('social_media')->insert(['code' => 'whatsapp', 'icon' => 'whatsapp', 'name' => 'WhatsApp']); + DB::table('tenant_social_media')->insert([ + ['tenant_code' => 'one', 'social_media_code' => 'whatsapp', + 'url' => 'https://wa.me/123', 'orden' => 1], + ['tenant_code' => 'many', 'social_media_code' => 'whatsapp', + 'url' => 'https://wa.me/456', 'orden' => 1], + ]); + + $socialMigration = require database_path('migrations/2026_09_18_000600_associate_active_event_social_media.php'); + $socialMigration->up(); + $socialMigration->up(); + $this->assertSame(2, DB::table('event_social_media')->where('event_id', $one->id)->count()); + $this->assertSame('https://instagram.com/event-only', DB::table('event_social_media') + ->where('event_id', $one->id)->where('social_media_code', 'instagram')->value('url')); + $this->assertSame('https://wa.me/123', DB::table('event_social_media') + ->where('event_id', $one->id)->where('social_media_code', 'whatsapp')->value('url')); + $this->assertSame(0, DB::table('event_social_media')->where('event_id', $many->id)->count()); + + $tenant->load(['socialMedia', 'activeEvent.socialMedia', 'activeEvent.dates']); + foreach (['headerLogo', 'footerLogo', 'favicon', 'headerBackgroundImage', + 'footerBackgroundImage', 'storefrontWebsiteType'] as $relation) { + $tenant->setRelation($relation, null); + } + $bootstrap = TenantResource::make($tenant)->toArray(request()); + $this->assertSame('https://instagram.com/event-only', $bootstrap['social_media'][0]['url']); + $this->assertSame('https://instagram.com/event-only', $bootstrap['event']['social_media'][0]['url']); + + $migration->down(); + $this->assertSame('Evento uno', DB::table('tenants')->where('codigo', 'one')->value('event_title')); + } +}