fix(event): backfill active event social media
This commit is contained in:
@@ -2,36 +2,52 @@
|
|||||||
|
|
||||||
## Propósito
|
## 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.
|
`Tenant` tiene muchos `Event`. Cada evento tiene título, subtítulo, descripción,
|
||||||
- `Services/EventService.php`: obtiene y actualiza la configuración de evento del tenant.
|
ubicación, texto de fechas y `published_at`. Sus fechas se guardan en `event_dates`
|
||||||
- `Services/EventDateNoticeService.php`: reclama y agrupa los cambios pendientes de cada usuario.
|
mediante `event_id`. El catálogo `social_media` define las plataformas;
|
||||||
- `Controllers/AdminApp/EventController.php`: consulta y modificación desde AdminApp.
|
`event_social_media` guarda la URL y el orden de cada cuenta del evento.
|
||||||
- `UpdateEventRequest`: valida datos y reglas cruzadas de fechas.
|
|
||||||
- `EventResource`: serializa la configuración de salida.
|
|
||||||
|
|
||||||
## 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.
|
- `onticket` muestra el evento señalado por `tenants.active_event_id`.
|
||||||
- `PUT`: actualiza la configuración.
|
- `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
|
Cuando hay un evento activo, el campo `social_media` del bootstrap usa sus redes,
|
||||||
reprogramaciones. Cada cambio se muestra como máximo tres veces por usuario.
|
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
|
## Dependencias
|
||||||
|
|
||||||
Depende de `Tenant`. Las fechas se vinculan con variantes de `Catalog`, que a su vez pueden generar tickets.
|
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
|
||||||
## Consideraciones
|
después de excluir los cambios que el usuario ya vio tres veces.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
@@ -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