fix(event): backfill active event social media
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('tenants')
|
||||
->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.
|
||||
}
|
||||
};
|
||||
119
tests/Feature/Migrations/RestoreEventsMigrationTest.php
Normal file
119
tests/Feature/Migrations/RestoreEventsMigrationTest.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Core\Tenant\Resources\TenantResource;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RestoreEventsMigrationTest extends TestCase
|
||||
{
|
||||
public function test_it_moves_single_event_data_and_leaves_multi_event_selection_empty(): void
|
||||
{
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user