From 4aaa66dc387e2cf314c7801d0eb75aa6ed5e372c Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 10 Aug 2026 10:42:12 -0300 Subject: [PATCH] feat(event): add EventDateTextFormatter for formatting event dates; update Tenant model and resource to include event_date_text; create migration for event_date_text column; enhance tests for event date formatting and tenant updates --- .../Event/Services/EventDateTextFormatter.php | 73 +++++++++++++++++++ app/Domains/Event/Services/EventService.php | 5 ++ app/Domains/Tenant/Models/Tenant.php | 1 + .../Tenant/Resources/TenantResource.php | 1 + ..._000000_add_event_date_text_to_tenants.php | 37 ++++++++++ tests/Feature/Catalog/CatalogSchemaTest.php | 1 + .../Event/AdminAppEventControllerTest.php | 23 ++++++ .../Tenant/BootstrapTenantControllerTest.php | 2 + .../Unit/Event/EventDateTextFormatterTest.php | 38 ++++++++++ 9 files changed, 181 insertions(+) create mode 100644 app/Domains/Event/Services/EventDateTextFormatter.php create mode 100644 database/migrations/2026_08_10_000000_add_event_date_text_to_tenants.php create mode 100644 tests/Unit/Event/EventDateTextFormatterTest.php diff --git a/app/Domains/Event/Services/EventDateTextFormatter.php b/app/Domains/Event/Services/EventDateTextFormatter.php new file mode 100644 index 0000000..2731390 --- /dev/null +++ b/app/Domains/Event/Services/EventDateTextFormatter.php @@ -0,0 +1,73 @@ + */ + private const MONTHS = [ + 1 => 'Enero', + 2 => 'Febrero', + 3 => 'Marzo', + 4 => 'Abril', + 5 => 'Mayo', + 6 => 'Junio', + 7 => 'Julio', + 8 => 'Agosto', + 9 => 'Septiembre', + 10 => 'Octubre', + 11 => 'Noviembre', + 12 => 'Diciembre', + ]; + + /** @param iterable $dates */ + public function format(iterable $dates): ?string + { + $normalizedDates = collect($dates) + ->map(fn (string $date): DateTimeImmutable => new DateTimeImmutable($date)) + ->unique(fn (DateTimeImmutable $date): string => $date->format('Y-m-d')) + ->sortBy(fn (DateTimeImmutable $date): string => $date->format('Y-m-d')) + ->values(); + + if ($normalizedDates->isEmpty()) { + return null; + } + + $years = $normalizedDates + ->groupBy(fn (DateTimeImmutable $date): string => $date->format('Y')) + ->map(function ($yearDates, string $year): string { + $months = $yearDates + ->groupBy(fn (DateTimeImmutable $date): string => $date->format('n')) + ->map(function ($monthDates, string $month): string { + $days = $monthDates + ->map(fn (DateTimeImmutable $date): string => (string) ((int) $date->format('j'))) + ->values() + ->all(); + + return $this->join($days).' de '.self::MONTHS[(int) $month]; + }) + ->values() + ->all(); + + return $this->join($months).' '.$year; + }) + ->values() + ->all(); + + return $this->join($years); + } + + /** @param array $parts */ + private function join(array $parts): string + { + if (count($parts) <= 1) { + return $parts[0] ?? ''; + } + + $last = array_pop($parts); + + return implode(', ', $parts).' y '.$last; + } +} diff --git a/app/Domains/Event/Services/EventService.php b/app/Domains/Event/Services/EventService.php index 793d546..4ec12e4 100644 --- a/app/Domains/Event/Services/EventService.php +++ b/app/Domains/Event/Services/EventService.php @@ -13,6 +13,8 @@ class EventService 'facebook_url' => 'facebook', ]; + public function __construct(protected EventDateTextFormatter $eventDateTextFormatter) {} + public function forTenant(Tenant $tenant): Tenant { return $tenant->load(['eventDates', 'socialMedia']); @@ -26,6 +28,9 @@ class EventService $tenant->update([ 'event_title' => $data['title'], 'event_location' => $data['location'], + 'event_date_text' => $this->eventDateTextFormatter->format( + array_column($data['dates'], 'date') + ), ]); $this->syncDates($tenant, $data['dates']); diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Tenant/Models/Tenant.php index e19b470..ab4cd5d 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Tenant/Models/Tenant.php @@ -35,6 +35,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'search_items_per_page', 'event_title', 'event_location', + 'event_date_text', ])] class Tenant extends Model { diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index 903d45b..f022ea1 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -32,6 +32,7 @@ class TenantResource extends JsonResource 'header_bg_color' => $this->header_bg_color, 'footer_bg_color' => $this->footer_bg_color, 'website_type_code' => $this->website_type_code, + 'event_date_text' => $this->event_date_text, 'event' => $this->whenLoaded('eventDates', fn () => $this->event_title === null ? null : [ diff --git a/database/migrations/2026_08_10_000000_add_event_date_text_to_tenants.php b/database/migrations/2026_08_10_000000_add_event_date_text_to_tenants.php new file mode 100644 index 0000000..99a00be --- /dev/null +++ b/database/migrations/2026_08_10_000000_add_event_date_text_to_tenants.php @@ -0,0 +1,37 @@ +text('event_date_text')->nullable()->after('event_location'); + }); + + $formatter = new EventDateTextFormatter; + + DB::table('tenants')->orderBy('id')->each(function (object $tenant) use ($formatter): void { + $dates = DB::table('event_dates') + ->where('tenant_code', $tenant->codigo) + ->orderBy('date') + ->pluck('date'); + + DB::table('tenants')->where('id', $tenant->id)->update([ + 'event_date_text' => $formatter->format($dates), + ]); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table): void { + $table->dropColumn('event_date_text'); + }); + } +}; diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index ed6e8ea..f6f8255 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -191,6 +191,7 @@ class CatalogSchemaTest extends TestCase $this->assertTrue(Schema::hasColumns('tenants', [ 'event_title', 'event_location', + 'event_date_text', ])); $this->assertEqualsCanonicalizing([ 'id', diff --git a/tests/Feature/Event/AdminAppEventControllerTest.php b/tests/Feature/Event/AdminAppEventControllerTest.php index e00c4b0..3d317b1 100644 --- a/tests/Feature/Event/AdminAppEventControllerTest.php +++ b/tests/Feature/Event/AdminAppEventControllerTest.php @@ -56,6 +56,7 @@ class AdminAppEventControllerTest extends TestCase 'id' => $tenant->id, 'event_title' => 'Festival Acme', 'event_location' => 'Predio Ferial, Rosario', + 'event_date_text' => '9 de Octubre 2026', ]); $this->assertDatabaseHas('event_dates', [ 'tenant_code' => $tenant->codigo, @@ -137,6 +138,7 @@ class AdminAppEventControllerTest extends TestCase 'date' => '2026-11-15', ]); $this->assertDatabaseMissing('event_dates', ['id' => $removedDate->id]); + $this->assertSame('15 de Noviembre 2026', $tenant->fresh()->event_date_text); $this->assertDatabaseMissing('tenant_social_media', [ 'tenant_code' => $tenant->codigo, 'social_media_code' => 'facebook', @@ -148,6 +150,27 @@ class AdminAppEventControllerTest extends TestCase ]); } + public function test_updating_event_dates_recalculates_the_tenant_date_text(): void + { + $tenant = $this->createTenant('acme'); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + $payload = $this->eventPayload(); + $payload['dates'] = collect([9, 10, 11, 12]) + ->map(fn (int $day): array => [ + 'date' => sprintf('2026-10-%02d', $day), + 'start_time' => '09:00', + 'end_time' => '18:30', + ]) + ->all(); + + $this->putJson('/api/v1/adminapp/tenant/event', $payload)->assertOk(); + + $this->assertSame( + '9, 10, 11 y 12 de Octubre 2026', + $tenant->fresh()->event_date_text + ); + } + public function test_update_validates_event_dates_and_contact_urls(): void { $tenant = $this->createTenant('acme'); diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index ac87b6c..37c9140 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -52,6 +52,7 @@ class BootstrapTenantControllerTest extends TestCase 'footer_bg_color' => '#ffffff', 'header_logo_id' => $headerAttachment->id, 'footer_logo_id' => $footerAttachment->id, + 'event_date_text' => '9, 10, 11 y 12 de Octubre 2026', ]); $response = $this->getJson('/api/tenants/bootstrap/acme.com'); @@ -64,6 +65,7 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.secondary_color', '#00ff00') ->assertJsonPath('data.danger_color', '#0000ff') ->assertJsonPath('data.success_color', '#00ff00') + ->assertJsonPath('data.event_date_text', '9, 10, 11 y 12 de Octubre 2026') ->assertJsonPath('data.header_bg_color', '#ffffff')->assertJsonPath('data.footer_bg_color', '#ffffff'); $headerUrl = $response->json('data.header_logo'); diff --git a/tests/Unit/Event/EventDateTextFormatterTest.php b/tests/Unit/Event/EventDateTextFormatterTest.php new file mode 100644 index 0000000..ca86e70 --- /dev/null +++ b/tests/Unit/Event/EventDateTextFormatterTest.php @@ -0,0 +1,38 @@ + $dates */ + #[DataProvider('dateCases')] + public function test_it_formats_event_dates_in_spanish(array $dates, ?string $expected): void + { + $this->assertSame($expected, (new EventDateTextFormatter)->format($dates)); + } + + /** @return array, string|null}> */ + public static function dateCases(): array + { + return [ + 'no dates' => [[], null], + 'one date' => [['2026-10-09'], '9 de Octubre 2026'], + 'same month sorted' => [ + ['2026-10-12', '2026-10-09', '2026-10-11', '2026-10-10'], + '9, 10, 11 y 12 de Octubre 2026', + ], + 'different months' => [ + ['2026-11-01', '2026-10-31'], + '31 de Octubre y 1 de Noviembre 2026', + ], + 'different years' => [ + ['2027-01-01', '2026-12-31'], + '31 de Diciembre 2026 y 1 de Enero 2027', + ], + ]; + } +}