From 61314d516633fd64cae24c3c469e7329eec674c2 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 14 Sep 2026 14:49:30 -0300 Subject: [PATCH] feat(storefront): expose event date notices --- .../Services/TenantBootstrapService.php | 1 + .../Resources/EventDateChangeResource.php | 24 ++++ .../Services/EventDateNoticeFormatter.php | 109 ++++++++++++++++++ .../Event/Services/EventDateTextFormatter.php | 25 +++- app/Domains/Tenant/Models/Tenant.php | 9 ++ .../Tenant/Resources/TenantResource.php | 10 ++ .../Event/AdminAppEventControllerTest.php | 36 ++++++ .../Event/EventDateNoticeFormatterTest.php | 97 ++++++++++++++++ .../Unit/Event/EventDateTextFormatterTest.php | 8 ++ 9 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 app/Domains/Event/Resources/EventDateChangeResource.php create mode 100644 app/Domains/Event/Services/EventDateNoticeFormatter.php create mode 100644 tests/Unit/Event/EventDateNoticeFormatterTest.php diff --git a/app/Domains/Bootstrap/Services/TenantBootstrapService.php b/app/Domains/Bootstrap/Services/TenantBootstrapService.php index 0327c4e..b4c6352 100644 --- a/app/Domains/Bootstrap/Services/TenantBootstrapService.php +++ b/app/Domains/Bootstrap/Services/TenantBootstrapService.php @@ -32,6 +32,7 @@ class TenantBootstrapService return $this->tenantInformationService->load( $tenant, [ + 'eventDateChanges', 'menues' => fn ($query) => $query->whereHas( 'roles', fn ($query) => $query->where('codigo', RoleCode::User->value) diff --git a/app/Domains/Event/Resources/EventDateChangeResource.php b/app/Domains/Event/Resources/EventDateChangeResource.php new file mode 100644 index 0000000..025e053 --- /dev/null +++ b/app/Domains/Event/Resources/EventDateChangeResource.php @@ -0,0 +1,24 @@ + */ + public function toArray(Request $request): array + { + return [ + 'type' => $this->change_type->value, + 'source_event_date_id' => $this->source_event_date_id, + 'destination_event_date_id' => $this->destination_event_date_id, + 'previous_date' => $this->previous_date->format('Y-m-d'), + 'new_date' => $this->new_date?->format('Y-m-d'), + 'occurred_at' => $this->created_at->toISOString(), + ]; + } +} diff --git a/app/Domains/Event/Services/EventDateNoticeFormatter.php b/app/Domains/Event/Services/EventDateNoticeFormatter.php new file mode 100644 index 0000000..ee38744 --- /dev/null +++ b/app/Domains/Event/Services/EventDateNoticeFormatter.php @@ -0,0 +1,109 @@ + $changes + * @return list + * }> + */ + public function format(Collection $changes): array + { + return collect([ + $this->suspensionNotice( + $changes->where('change_type', EventDateChangeType::Suspended) + ), + $this->rescheduleNotice( + $changes->where('change_type', EventDateChangeType::Rescheduled) + ), + ])->filter()->values()->all(); + } + + /** + * @param Collection $changes + * @return array{type: string, title: string, message: list}|null + */ + private function suspensionNotice(Collection $changes): ?array + { + $dates = $this->formatDates($changes, 'previous_date'); + + if ($dates === null) { + return null; + } + + $plural = $changes->count() > 1; + + return [ + 'type' => EventDateChangeType::Suspended->value, + 'title' => $plural ? 'FECHAS CANCELADAS!' : 'FECHA CANCELADA!', + 'message' => [ + ['text' => $plural ? 'Las fechas del ' : 'La fecha del ', 'bold' => false], + ['text' => $dates, 'bold' => true], + ['text' => $plural ? ' han sido canceladas.' : ' ha sido cancelada.', 'bold' => false], + ], + ]; + } + + /** + * @param Collection $changes + * @return array{type: string, title: string, message: list}|null + */ + private function rescheduleNotice(Collection $changes): ?array + { + $changes = $changes->whereNotNull('new_date'); + $sourceDates = $this->formatDates($changes, 'previous_date'); + $destinationDates = $this->formatDates($changes, 'new_date'); + + if ($sourceDates === null || $destinationDates === null) { + return null; + } + + $plural = $changes->count() > 1; + $message = [ + ['text' => $plural ? 'Las fechas del ' : 'La fecha del ', 'bold' => false], + ['text' => $sourceDates, 'bold' => true], + [ + 'text' => $plural ? ' han sido reprogramadas para el ' : ' ha sido reprogramada para el ', + 'bold' => false, + ], + ['text' => $destinationDates, 'bold' => true], + ]; + + if ($plural) { + $message[] = ['text' => ', ', 'bold' => false]; + $message[] = ['text' => 'respectivamente', 'bold' => true]; + } + + $message[] = ['text' => '.', 'bold' => false]; + + return [ + 'type' => EventDateChangeType::Rescheduled->value, + 'title' => $plural ? 'FECHAS REPROGRAMADAS!' : 'FECHA REPROGRAMADA!', + 'message' => $message, + ]; + } + + /** + * @param Collection $changes + */ + private function formatDates(Collection $changes, string $attribute): ?string + { + return $this->dateTextFormatter->formatForSentence( + $changes + ->pluck($attribute) + ->filter() + ->map(fn ($date): string => $date->format('Y-m-d')) + ); + } +} diff --git a/app/Domains/Event/Services/EventDateTextFormatter.php b/app/Domains/Event/Services/EventDateTextFormatter.php index 2731390..99d2f31 100644 --- a/app/Domains/Event/Services/EventDateTextFormatter.php +++ b/app/Domains/Event/Services/EventDateTextFormatter.php @@ -25,6 +25,21 @@ class EventDateTextFormatter /** @param iterable $dates */ public function format(iterable $dates): ?string { + return $this->formatWithOptions($dates, false, false); + } + + /** @param iterable $dates */ + public function formatForSentence(iterable $dates): ?string + { + return $this->formatWithOptions($dates, true, true); + } + + /** @param iterable $dates */ + private function formatWithOptions( + iterable $dates, + bool $padDays, + bool $includeYearPreposition, + ): ?string { $normalizedDates = collect($dates) ->map(fn (string $date): DateTimeImmutable => new DateTimeImmutable($date)) ->unique(fn (DateTimeImmutable $date): string => $date->format('Y-m-d')) @@ -37,12 +52,14 @@ class EventDateTextFormatter $years = $normalizedDates ->groupBy(fn (DateTimeImmutable $date): string => $date->format('Y')) - ->map(function ($yearDates, string $year): string { + ->map(function ($yearDates, string $year) use ($padDays, $includeYearPreposition): string { $months = $yearDates ->groupBy(fn (DateTimeImmutable $date): string => $date->format('n')) - ->map(function ($monthDates, string $month): string { + ->map(function ($monthDates, string $month) use ($padDays): string { $days = $monthDates - ->map(fn (DateTimeImmutable $date): string => (string) ((int) $date->format('j'))) + ->map(fn (DateTimeImmutable $date): string => $padDays + ? $date->format('d') + : (string) ((int) $date->format('j'))) ->values() ->all(); @@ -51,7 +68,7 @@ class EventDateTextFormatter ->values() ->all(); - return $this->join($months).' '.$year; + return $this->join($months).($includeYearPreposition ? ' de ' : ' ').$year; }) ->values() ->all(); diff --git a/app/Domains/Tenant/Models/Tenant.php b/app/Domains/Tenant/Models/Tenant.php index e575def..e119319 100644 --- a/app/Domains/Tenant/Models/Tenant.php +++ b/app/Domains/Tenant/Models/Tenant.php @@ -9,6 +9,7 @@ use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; use App\Domains\Client\Models\Client; use App\Domains\Event\Models\EventDate; +use App\Domains\Event\Models\EventDateChange; use App\Domains\Menu\Models\Menu; use App\Domains\Menu\Models\TenantMenu; use App\Domains\Tenant\Enums\CartEditingPolicy; @@ -229,6 +230,14 @@ class Tenant extends Model ->orderBy('time_start'); } + /** @return HasMany */ + public function eventDateChanges(): HasMany + { + return $this->hasMany(EventDateChange::class, 'tenant_code', 'codigo') + ->orderBy('created_at') + ->orderBy('id'); + } + /** * @return HasMany */ diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index 1b94e48..c15e28e 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -6,6 +6,8 @@ use App\Domains\Attachable\Models\Attachment; use App\Domains\Attachable\Models\AttachmentCrop; use App\Domains\Catalog\Models\Category; use App\Domains\Event\Models\EventDate; +use App\Domains\Event\Resources\EventDateChangeResource; +use App\Domains\Event\Services\EventDateNoticeFormatter; use App\Domains\Menu\Models\Menu; use App\Domains\Tenant\Models\Tenant; use Illuminate\Http\Request; @@ -61,6 +63,14 @@ class TenantResource extends JsonResource 'time_start' => $eventDate->time_start, 'time_end' => $eventDate->time_end, ])->values(), + 'date_changes' => $this->whenLoaded( + 'eventDateChanges', + fn () => EventDateChangeResource::collection($this->eventDateChanges) + ), + 'date_notices' => $this->whenLoaded( + 'eventDateChanges', + fn () => app(EventDateNoticeFormatter::class)->format($this->eventDateChanges) + ), ]), 'extras' => $this->whenLoaded( 'websiteExtras', diff --git a/tests/Feature/Event/AdminAppEventControllerTest.php b/tests/Feature/Event/AdminAppEventControllerTest.php index 74e050d..cd6c7c3 100644 --- a/tests/Feature/Event/AdminAppEventControllerTest.php +++ b/tests/Feature/Event/AdminAppEventControllerTest.php @@ -309,6 +309,21 @@ class AdminAppEventControllerTest extends TestCase ->assertOk() ->assertJsonCount(1, 'data.event.dates') ->assertJsonPath('data.event.dates.0.id', $destination->id) + ->assertJsonCount(1, 'data.event.date_changes') + ->assertJsonPath('data.event.date_changes.0.type', 'rescheduled') + ->assertJsonPath('data.event.date_changes.0.source_event_date_id', $original->id) + ->assertJsonPath('data.event.date_changes.0.destination_event_date_id', $destination->id) + ->assertJsonPath('data.event.date_changes.0.previous_date', '2027-10-09') + ->assertJsonPath('data.event.date_changes.0.new_date', '2027-10-20') + ->assertJsonPath('data.event.date_changes.0.occurred_at', fn ($value) => is_string($value)) + ->assertJsonMissingPath('data.event.date_changes.0.created_by_user_id') + ->assertJsonCount(1, 'data.event.date_notices') + ->assertJsonPath('data.event.date_notices.0.type', 'rescheduled') + ->assertJsonPath('data.event.date_notices.0.title', 'FECHA REPROGRAMADA!') + ->assertJsonPath('data.event.date_notices.0.message.0.text', 'La fecha del ') + ->assertJsonPath('data.event.date_notices.0.message.1.text', '09 de Octubre de 2027') + ->assertJsonPath('data.event.date_notices.0.message.1.bold', true) + ->assertJsonPath('data.event.date_notices.0.message.3.text', '20 de Octubre de 2027') ->assertJsonPath('data.event_date_text', '20 de Octubre 2027'); $this->assertSame( '2027-10-20 11:00:00', @@ -344,6 +359,15 @@ class AdminAppEventControllerTest extends TestCase ->assertOk() ->assertJsonCount(1, 'data.event.dates') ->assertJsonPath('data.event.dates.0.date', '2027-10-25') + ->assertJsonCount(2, 'data.event.date_changes') + ->assertJsonPath('data.event.date_changes.1.type', 'rescheduled') + ->assertJsonPath('data.event.date_changes.1.previous_date', '2027-10-20') + ->assertJsonPath('data.event.date_changes.1.new_date', '2027-10-25') + ->assertJsonPath('data.event.date_notices.0.title', 'FECHAS REPROGRAMADAS!') + ->assertJsonPath('data.event.date_notices.0.message.1.text', '09 y 20 de Octubre de 2027') + ->assertJsonPath('data.event.date_notices.0.message.3.text', '20 y 25 de Octubre de 2027') + ->assertJsonPath('data.event.date_notices.0.message.5.text', 'respectivamente') + ->assertJsonPath('data.event.date_notices.0.message.5.bold', true) ->assertJsonPath('data.event_date_text', '25 de Octubre 2027'); $this->assertSame( '2027-10-25 11:00:00', @@ -443,6 +467,18 @@ class AdminAppEventControllerTest extends TestCase ->assertOk() ->assertJsonCount(1, 'data.event.dates') ->assertJsonPath('data.event.dates.0.id', $otherDate->id) + ->assertJsonCount(1, 'data.event.date_changes') + ->assertJsonPath('data.event.date_changes.0.type', 'suspended') + ->assertJsonPath('data.event.date_changes.0.source_event_date_id', $suspendedDate->id) + ->assertJsonPath('data.event.date_changes.0.destination_event_date_id', null) + ->assertJsonPath('data.event.date_changes.0.previous_date', '2027-10-09') + ->assertJsonPath('data.event.date_changes.0.new_date', null) + ->assertJsonMissingPath('data.event.date_changes.0.created_by_user_id') + ->assertJsonCount(1, 'data.event.date_notices') + ->assertJsonPath('data.event.date_notices.0.type', 'suspended') + ->assertJsonPath('data.event.date_notices.0.title', 'FECHA CANCELADA!') + ->assertJsonPath('data.event.date_notices.0.message.1.text', '09 de Octubre de 2027') + ->assertJsonPath('data.event.date_notices.0.message.1.bold', true) ->assertJsonPath('data.event_date_text', '10 de Octubre 2027'); $this->assertSame( '2027-10-10 09:00:00', diff --git a/tests/Unit/Event/EventDateNoticeFormatterTest.php b/tests/Unit/Event/EventDateNoticeFormatterTest.php new file mode 100644 index 0000000..af53628 --- /dev/null +++ b/tests/Unit/Event/EventDateNoticeFormatterTest.php @@ -0,0 +1,97 @@ +format(collect([ + $this->change(EventDateChangeType::Suspended, '2026-10-09'), + $this->change(EventDateChangeType::Rescheduled, '2026-10-10', '2026-10-13'), + ])); + + $this->assertSame([ + [ + 'type' => 'suspended', + 'title' => 'FECHA CANCELADA!', + 'message' => [ + ['text' => 'La fecha del ', 'bold' => false], + ['text' => '09 de Octubre de 2026', 'bold' => true], + ['text' => ' ha sido cancelada.', 'bold' => false], + ], + ], + [ + 'type' => 'rescheduled', + 'title' => 'FECHA REPROGRAMADA!', + 'message' => [ + ['text' => 'La fecha del ', 'bold' => false], + ['text' => '10 de Octubre de 2026', 'bold' => true], + ['text' => ' ha sido reprogramada para el ', 'bold' => false], + ['text' => '13 de Octubre de 2026', 'bold' => true], + ['text' => '.', 'bold' => false], + ], + ], + ], $notices); + } + + public function test_it_formats_suspension_and_reschedule_notices_for_the_storefront(): void + { + $formatter = new EventDateNoticeFormatter(new EventDateTextFormatter); + + $notices = $formatter->format(collect([ + $this->change(EventDateChangeType::Rescheduled, '2026-10-09', '2026-10-13'), + $this->change(EventDateChangeType::Suspended, '2026-10-09'), + $this->change(EventDateChangeType::Suspended, '2026-10-10'), + $this->change(EventDateChangeType::Rescheduled, '2026-10-10', '2026-10-14'), + ])); + + $this->assertSame([ + [ + 'type' => 'suspended', + 'title' => 'FECHAS CANCELADAS!', + 'message' => [ + ['text' => 'Las fechas del ', 'bold' => false], + ['text' => '09 y 10 de Octubre de 2026', 'bold' => true], + ['text' => ' han sido canceladas.', 'bold' => false], + ], + ], + [ + 'type' => 'rescheduled', + 'title' => 'FECHAS REPROGRAMADAS!', + 'message' => [ + ['text' => 'Las fechas del ', 'bold' => false], + ['text' => '09 y 10 de Octubre de 2026', 'bold' => true], + ['text' => ' han sido reprogramadas para el ', 'bold' => false], + ['text' => '13 y 14 de Octubre de 2026', 'bold' => true], + ['text' => ', ', 'bold' => false], + ['text' => 'respectivamente', 'bold' => true], + ['text' => '.', 'bold' => false], + ], + ], + ], $notices); + } + + private function change( + EventDateChangeType $type, + string $previousDate, + ?string $newDate = null, + ): EventDateChange { + $change = new EventDateChange; + $change->setRawAttributes([ + 'change_type' => $type->value, + 'previous_date' => $previousDate, + 'new_date' => $newDate, + ]); + + return $change; + } +} diff --git a/tests/Unit/Event/EventDateTextFormatterTest.php b/tests/Unit/Event/EventDateTextFormatterTest.php index ca86e70..a5f1f7b 100644 --- a/tests/Unit/Event/EventDateTextFormatterTest.php +++ b/tests/Unit/Event/EventDateTextFormatterTest.php @@ -35,4 +35,12 @@ class EventDateTextFormatterTest extends TestCase ], ]; } + + public function test_it_formats_dates_for_use_inside_sentences(): void + { + $this->assertSame( + '09 y 10 de Octubre de 2026', + (new EventDateTextFormatter)->formatForSentence(['2026-10-10', '2026-10-09']) + ); + } }