diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index 195742a..241b658 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -2,9 +2,11 @@ namespace App\Domains\Tenant\Resources; +use App\Domains\Menu\Models\Menu; use App\Domains\Tenant\Models\Tenant; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Collection; /** * @mixin Tenant @@ -55,7 +57,49 @@ class TenantResource extends JsonResource ]) ->values() ), - 'menues' => $this->whenLoaded('menues'), + 'menues' => $this->whenLoaded( + 'menues', + fn () => $this->menuTree($this->menues) + ), ]; } + + /** + * @param Collection $menus + * @return Collection> + */ + private function menuTree(Collection $menus): Collection + { + $menuCodes = $menus->pluck('code')->flip(); + $childrenByParent = $menus + ->filter(fn (Menu $menu) => $menu->parent_menu_code !== null) + ->groupBy('parent_menu_code'); + + $formatMenu = function (Menu $menu) use (&$formatMenu, $childrenByParent): array { + $formatted = [ + 'id' => $menu->id, + 'code' => $menu->code, + 'parent_menu_code' => $menu->parent_menu_code, + 'content_type' => $menu->content_type, + 'route' => $menu->route, + ]; + + if ($menu->pivot?->static_content !== null) { + $formatted['static_content'] = $menu->pivot->static_content; + } + + $formatted['submenues'] = $childrenByParent + ->get($menu->code, collect()) + ->map($formatMenu) + ->values(); + + return $formatted; + }; + + return $menus + ->filter(fn (Menu $menu) => $menu->parent_menu_code === null + || ! $menuCodes->has($menu->parent_menu_code)) + ->map($formatMenu) + ->values(); + } } diff --git a/database/seeders/MenuSeeder.php b/database/seeders/MenuSeeder.php index a2b4ad7..18d0565 100644 --- a/database/seeders/MenuSeeder.php +++ b/database/seeders/MenuSeeder.php @@ -2,9 +2,9 @@ namespace Database\Seeders; -use Illuminate\Database\Seeder; use App\Domains\Menu\Models\Menu; use App\Domains\Tenant\Models\Tenant; +use Illuminate\Database\Seeder; class MenuSeeder extends Seeder { @@ -20,15 +20,123 @@ class MenuSeeder extends Seeder ['code' => 'profile', 'route' => '/profile'], ['code' => 'purchases', 'route' => '/purchases'], ['code' => 'tickets', 'route' => '/tickets'], + [ + 'code' => 'help', + 'content_type' => Menu::CONTENT_TYPE_DYNAMIC, + 'route' => '/ayuda', + ], + [ + 'code' => 'help.faq', + 'parent_menu_code' => 'help', + 'content_type' => Menu::CONTENT_TYPE_STATIC, + 'static_content_schema' => [ + '*.pregunta' => 'required|string', + '*.respuesta' => 'required|string', + '*.is_active' => 'sometimes|boolean', + ], + 'route' => '/ayuda/preguntas-frecuentes', + ], + [ + 'code' => 'help.contact', + 'parent_menu_code' => 'help', + 'content_type' => Menu::CONTENT_TYPE_STATIC, + 'static_content_schema' => [ + 'whatsapp' => 'required|array', + 'whatsapp.whatsapp_url' => 'required|url', + 'whatsapp.whatsapp_label' => 'required|string', + 'phone' => 'required|string', + 'locations' => 'required|array|min:1', + 'locations.*.label' => 'required|string', + 'locations.*.addresses' => 'required|array|min:1', + 'locations.*.addresses.*' => 'required|string', + 'map_locations' => 'required|array', + 'map_locations.*' => 'required|array|size:2', + 'map_locations.*.*' => 'required|numeric', + ], + 'route' => '/ayuda/contacto', + ], + [ + 'code' => 'help.payment-methods', + 'parent_menu_code' => 'help', + 'content_type' => Menu::CONTENT_TYPE_DYNAMIC, + 'route' => '/ayuda/medios-de-pago', + ], + [ + 'code' => 'help.shipping', + 'parent_menu_code' => 'help', + 'content_type' => Menu::CONTENT_TYPE_DYNAMIC, + 'route' => '/ayuda/envios', + ], + [ + 'code' => 'help.terms-and-conditions', + 'parent_menu_code' => 'help', + 'content_type' => Menu::CONTENT_TYPE_DYNAMIC, + 'route' => '/ayuda/terminos-y-condiciones', + ], ]; foreach ($menus as $menuData) { - Menu::firstOrCreate(['code' => $menuData['code']], $menuData); + $menuData = array_merge([ + 'parent_menu_code' => null, + 'content_type' => Menu::CONTENT_TYPE_DYNAMIC, + 'static_content_schema' => null, + ], $menuData); + + Menu::updateOrCreate(['code' => $menuData['code']], $menuData); } $tenants = Tenant::all(); $allMenus = Menu::pluck('code')->toArray(); + $helpMenuCodes = [ + 'help', + 'help.faq', + 'help.contact', + 'help.payment-methods', + 'help.shipping', + 'help.terms-and-conditions', + ]; + $helpTenantCodes = [ + 'sonder', + 'fiesta_futbol_infantil', + ]; + $frequentlyAskedQuestions = [ + [ + 'pregunta' => '¿Hay algún límite de compra?', + 'respuesta' => 'No hay un límite general de compra. La cantidad disponible depende del stock de cada producto.', + 'is_active' => true, + ], + [ + 'pregunta' => '¿Cuáles son los medios de pago disponibles?', + 'respuesta' => 'Podés consultar y seleccionar los medios de pago habilitados al momento de finalizar tu compra.', + 'is_active' => false, + ], + [ + 'pregunta' => 'Necesito asesoramiento antes de comprar el producto, ¿cómo puedo hacer?', + 'respuesta' => 'Nuestro equipo comercial está preparado para asesorarte, no dudes en escribirnos por WhatsApp al (0341) 6658247 de lunes a viernes de 8.30 a 20 horas. Los sábados, domingos y feriados te contestaremos lo más rápido posible.', + 'is_active' => false, + ], + ]; + $contactContent = [ + 'whatsapp' => [ + 'whatsapp_url' => 'https://wa.me/543412602222', + 'whatsapp_label' => 'Chatea con nosotros', + ], + 'phone' => '+54 9 (0341) 6658247', + 'locations' => [ + 'rosario' => [ + 'label' => 'Rosario', + 'addresses' => [ + 'Av. San Lorenzo 1542', + ], + ], + ], + 'map_locations' => [ + 'gigante_de_arroyito' => [-32.913997, -60.674567], + 'punto_centro' => [-32.946820, -60.639320], + 'punto_sur' => [-32.971510, -60.650640], + ], + ]; foreach ($tenants as $tenant) { $menuCodes = $allMenus; @@ -41,8 +149,21 @@ class MenuSeeder extends Seeder $menuCodes = array_diff($menuCodes, ['product.detail']); } + if (! in_array($tenant->codigo, $helpTenantCodes, true)) { + $menuCodes = array_diff($menuCodes, $helpMenuCodes); + } + // Usar sync para asociar los menues al tenant $tenant->menues()->sync($menuCodes); + + if (in_array($tenant->codigo, $helpTenantCodes, true)) { + $tenant->menues()->updateExistingPivot('help.faq', [ + 'static_content' => $frequentlyAskedQuestions, + ]); + $tenant->menues()->updateExistingPivot('help.contact', [ + 'static_content' => $contactContent, + ]); + } } } } diff --git a/tests/Feature/Seeders/MenuSeederTest.php b/tests/Feature/Seeders/MenuSeederTest.php new file mode 100644 index 0000000..14761b9 --- /dev/null +++ b/tests/Feature/Seeders/MenuSeederTest.php @@ -0,0 +1,155 @@ +seed(MenuSeeder::class); + + $help = Menu::query() + ->with('children') + ->where('code', 'help') + ->firstOrFail(); + + $this->assertSame(Menu::CONTENT_TYPE_DYNAMIC, $help->content_type); + $this->assertSame([ + 'help.contact', + 'help.faq', + 'help.payment-methods', + 'help.shipping', + 'help.terms-and-conditions', + ], $help->children->pluck('code')->sort()->values()->all()); + + $faq = Menu::query()->where('code', 'help.faq')->firstOrFail(); + $this->assertSame(Menu::CONTENT_TYPE_STATIC, $faq->content_type); + $this->assertEquals([ + '*.pregunta' => 'required|string', + '*.respuesta' => 'required|string', + '*.is_active' => 'sometimes|boolean', + ], $faq->static_content_schema); + + $contact = Menu::query()->where('code', 'help.contact')->firstOrFail(); + $this->assertSame(Menu::CONTENT_TYPE_STATIC, $contact->content_type); + $this->assertSame('required|url', $contact->static_content_schema['whatsapp.whatsapp_url']); + $this->assertSame('required|string', $contact->static_content_schema['phone']); + $this->assertSame( + 'required|array|min:1', + $contact->static_content_schema['locations.*.addresses'] + ); + $this->assertSame( + 'required|array|size:2', + $contact->static_content_schema['map_locations.*'] + ); + } + + public function test_it_assigns_help_to_the_supported_tenants_with_faq_content(): void + { + $sonder = $this->createTenant('sonder'); + $fiesta = $this->createTenant('fiesta_futbol_infantil'); + $otherTenant = $this->createTenant('other'); + + $this->seed(MenuSeeder::class); + + $expectedQuestions = [ + [ + 'pregunta' => '¿Hay algún límite de compra?', + 'respuesta' => 'No hay un límite general de compra. La cantidad disponible depende del stock de cada producto.', + 'is_active' => true, + ], + [ + 'pregunta' => '¿Cuáles son los medios de pago disponibles?', + 'respuesta' => 'Podés consultar y seleccionar los medios de pago habilitados al momento de finalizar tu compra.', + 'is_active' => false, + ], + [ + 'pregunta' => 'Necesito asesoramiento antes de comprar el producto, ¿cómo puedo hacer?', + 'respuesta' => 'Nuestro equipo comercial está preparado para asesorarte, no dudes en escribirnos por WhatsApp al (0341) 6658247 de lunes a viernes de 8.30 a 20 horas. Los sábados, domingos y feriados te contestaremos lo más rápido posible.', + 'is_active' => false, + ], + ]; + $expectedContact = [ + 'whatsapp' => [ + 'whatsapp_url' => 'https://wa.me/543412602222', + 'whatsapp_label' => 'Chatea con nosotros', + ], + 'phone' => '+54 9 (0341) 6658247', + 'locations' => [ + 'rosario' => [ + 'label' => 'Rosario', + 'addresses' => [ + 'Av. San Lorenzo 1542', + ], + ], + ], + 'map_locations' => [ + 'gigante_de_arroyito' => [-32.913997, -60.674567], + 'punto_centro' => [-32.946820, -60.639320], + 'punto_sur' => [-32.971510, -60.650640], + ], + ]; + + foreach ([$sonder, $fiesta] as $tenant) { + $helpMenus = $tenant->menues() + ->where(fn ($query) => $query + ->where('menues.code', 'help') + ->orWhere('menues.parent_menu_code', 'help')) + ->get(); + + $this->assertCount(6, $helpMenus); + $this->assertEquals( + $expectedQuestions, + $helpMenus->firstWhere('code', 'help.faq')->pivot->static_content + ); + $this->assertEquals( + $expectedContact, + $helpMenus->firstWhere('code', 'help.contact')->pivot->static_content + ); + } + + $this->assertFalse( + $otherTenant->menues()->where('menues.code', 'help')->exists() + ); + } + + private function createTenant(string $code): Tenant + { + $headerLogo = $this->createAttachment("{$code}-header.png"); + $footerLogo = $this->createAttachment("{$code}-footer.png"); + + return Tenant::query()->create([ + 'codigo' => $code, + 'nombre' => $code, + 'dominio' => "{$code}.example.com", + 'primary_color' => '#111111', + 'secondary_color' => '#222222', + 'danger_color' => '#333333', + 'success_color' => '#444444', + 'header_bg_color' => '#ffffff', + 'footer_bg_color' => '#ffffff', + 'header_logo_id' => $headerLogo->id, + 'footer_logo_id' => $footerLogo->id, + ]); + } + + private function createAttachment(string $filename): Attachment + { + return Attachment::query()->create([ + 'path' => "test/{$filename}", + 'filename' => $filename, + 'type' => AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + } +} diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index 3334910..c37e8fa 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Tenant; +use App\Domains\Menu\Models\Menu; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; @@ -99,6 +100,73 @@ class BootstrapTenantControllerTest extends TestCase $response->assertNotFound(); } + public function test_it_returns_tenant_menus_as_a_clean_hierarchy(): void + { + $tenant = $this->createTenant(); + $parent = Menu::query()->create([ + 'code' => 'help', + 'content_type' => Menu::CONTENT_TYPE_DYNAMIC, + 'route' => '/ayuda', + ]); + $staticChild = Menu::query()->create([ + 'code' => 'help.faq', + 'parent_menu_code' => $parent->code, + 'content_type' => Menu::CONTENT_TYPE_STATIC, + 'static_content_schema' => [ + '*.question' => 'required|string', + '*.answer' => 'required|string', + ], + 'route' => '/ayuda/preguntas-frecuentes', + ]); + $dynamicChild = Menu::query()->create([ + 'code' => 'help.shipping', + 'parent_menu_code' => $parent->code, + 'content_type' => Menu::CONTENT_TYPE_DYNAMIC, + 'route' => '/ayuda/envios', + ]); + $staticContent = [ + [ + 'question' => '¿Cómo compro?', + 'answer' => 'Desde el catálogo.', + ], + ]; + + $tenant->menues()->sync([ + $parent->code, + $staticChild->code => ['static_content' => $staticContent], + $dynamicChild->code, + ]); + + $response = $this->getJson('/api/tenants/bootstrap/acme.com'); + + $response + ->assertOk() + ->assertJsonCount(1, 'data.menues') + ->assertJsonCount(2, 'data.menues.0.submenues') + ->assertJsonPath('data.menues.0.code', 'help') + ->assertJsonPath('data.menues.0.parent_menu_code', null) + ->assertJsonPath('data.menues.0.submenues.0.code', 'help.faq') + ->assertJsonPath('data.menues.0.submenues.0.parent_menu_code', 'help') + ->assertJsonPath('data.menues.0.submenues.1.code', 'help.shipping'); + + $assertCleanMenu = function (array $menu) use (&$assertCleanMenu): void { + $this->assertArrayNotHasKey('created_at', $menu); + $this->assertArrayNotHasKey('updated_at', $menu); + $this->assertArrayNotHasKey('pivot', $menu); + $this->assertArrayNotHasKey('static_content_schema', $menu); + + foreach ($menu['submenues'] as $submenu) { + $assertCleanMenu($submenu); + } + }; + + $menus = $response->json('data.menues'); + $this->assertEquals($staticContent, $menus[0]['submenues'][0]['static_content']); + $assertCleanMenu($menus[0]); + $this->assertArrayNotHasKey('static_content', $menus[0]); + $this->assertArrayNotHasKey('static_content', $menus[0]['submenues'][1]); + } + public function test_it_rejects_duplicate_domains_after_normalization_when_storing(): void { $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';