From 2b1ef5f8a9e795770d954eb22e7d20c328016f96 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 23 Jul 2026 16:23:19 -0300 Subject: [PATCH] feat(menu): implement TenantMenuController, StoreTenantMenuRequest, and TenantMenuService for managing tenant-specific menus --- .../Menu/Controllers/TenantMenuController.php | 32 +++++ .../Menu/Requests/StoreTenantMenuRequest.php | 49 +++++++ .../Menu/Services/TenantMenuService.php | 26 ++++ app/Domains/Menu/routes/api.php | 6 + .../Feature/Menu/TenantMenuControllerTest.php | 130 ++++++++++++++++++ 5 files changed, 243 insertions(+) create mode 100644 app/Domains/Menu/Controllers/TenantMenuController.php create mode 100644 app/Domains/Menu/Requests/StoreTenantMenuRequest.php create mode 100644 app/Domains/Menu/Services/TenantMenuService.php create mode 100644 tests/Feature/Menu/TenantMenuControllerTest.php diff --git a/app/Domains/Menu/Controllers/TenantMenuController.php b/app/Domains/Menu/Controllers/TenantMenuController.php new file mode 100644 index 0000000..bd0a2a1 --- /dev/null +++ b/app/Domains/Menu/Controllers/TenantMenuController.php @@ -0,0 +1,32 @@ +where('codigo', $tenantCode)->firstOrFail(); + $menu = Menu::query()->where('code', $menuCode)->firstOrFail(); + + $tenantMenu = $this->tenantMenuService->configure( + $tenant, + $menu, + $request->validated('static_content'), + ); + + return response()->json($tenantMenu); + } +} diff --git a/app/Domains/Menu/Requests/StoreTenantMenuRequest.php b/app/Domains/Menu/Requests/StoreTenantMenuRequest.php new file mode 100644 index 0000000..abc2e1f --- /dev/null +++ b/app/Domains/Menu/Requests/StoreTenantMenuRequest.php @@ -0,0 +1,49 @@ +menuModel = Menu::query() + ->where('code', $this->route('menu_code')) + ->first(); + + if (! $this->menuModel) { + throw ValidationException::withMessages([ + 'menu_code' => 'El menĂº indicado no existe.', + ]); + } + } + + public function rules(): array + { + if ($this->menuModel?->content_type !== Menu::CONTENT_TYPE_STATIC) { + return [ + 'static_content' => ['nullable', 'array'], + ]; + } + + $rules = [ + 'static_content' => ['required', 'array'], + ]; + + foreach ($this->menuModel->static_content_schema as $field => $rule) { + $rules["static_content.{$field}"] = $rule; + } + + return $rules; + } +} diff --git a/app/Domains/Menu/Services/TenantMenuService.php b/app/Domains/Menu/Services/TenantMenuService.php new file mode 100644 index 0000000..102c498 --- /dev/null +++ b/app/Domains/Menu/Services/TenantMenuService.php @@ -0,0 +1,26 @@ + TenantMenu::query()->updateOrCreate( + [ + 'tenant_code' => $tenant->codigo, + 'menu_code' => $menu->code, + ], + [ + 'static_content' => $menu->content_type === Menu::CONTENT_TYPE_STATIC + ? $staticContent + : null, + ], + )); + } +} diff --git a/app/Domains/Menu/routes/api.php b/app/Domains/Menu/routes/api.php index 1c708b9..3d03a24 100644 --- a/app/Domains/Menu/routes/api.php +++ b/app/Domains/Menu/routes/api.php @@ -1,6 +1,12 @@ createTenant(); + $menu = Menu::query()->create([ + 'code' => 'about', + 'content_type' => Menu::CONTENT_TYPE_STATIC, + 'static_content_schema' => [ + 'title' => 'required|string|max:20', + 'sections' => 'required|array|min:1', + 'sections.*.body' => 'required|string', + ], + 'route' => '/about', + ]); + + $this->postJson("/api/{$tenant->codigo}/menues/{$menu->code}", [ + 'static_content' => [ + 'title' => 123, + 'sections' => [], + ], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors([ + 'static_content.title', + 'static_content.sections', + ]); + + $this->assertDatabaseMissing('tenants_menues', [ + 'tenant_code' => $tenant->codigo, + 'menu_code' => $menu->code, + ]); + } + + public function test_it_configures_a_static_menu_when_content_matches_the_schema(): void + { + $tenant = $this->createTenant(); + $menu = Menu::query()->create([ + 'code' => 'about', + 'content_type' => Menu::CONTENT_TYPE_STATIC, + 'static_content_schema' => [ + 'title' => 'required|string|max:20', + 'sections' => 'required|array|min:1', + 'sections.*.body' => 'required|string', + ], + 'route' => '/about', + ]); + + $content = [ + 'title' => 'Nosotros', + 'sections' => [ + ['body' => 'Nuestra historia'], + ], + ]; + + $this->postJson("/api/{$tenant->codigo}/menues/{$menu->code}", [ + 'static_content' => $content, + ]) + ->assertOk() + ->assertJsonPath('static_content.title', 'Nosotros') + ->assertJsonPath('static_content.sections.0.body', 'Nuestra historia'); + + $this->assertSame( + $content, + $tenant->menues()->where('menues.code', $menu->code)->firstOrFail()->pivot->static_content + ); + } + + public function test_dynamic_menus_do_not_require_static_content(): void + { + $tenant = $this->createTenant(); + $menu = Menu::query()->create([ + 'code' => 'catalog', + 'route' => '/catalog', + ]); + + $this->postJson("/api/{$tenant->codigo}/menues/{$menu->code}") + ->assertOk() + ->assertJsonPath('static_content', null); + + $this->assertDatabaseHas('tenants_menues', [ + 'tenant_code' => $tenant->codigo, + 'menu_code' => $menu->code, + 'static_content' => null, + ]); + } + + private function createTenant(): Tenant + { + $headerLogo = $this->createAttachment('header.png'); + $footerLogo = $this->createAttachment('footer.png'); + + return Tenant::query()->create([ + 'codigo' => 'acme', + 'nombre' => 'Acme', + 'dominio' => 'acme.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', + ]); + } +}