From 794acc4814a78451b2d699a705340167aab89d25 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 29 Sep 2026 14:03:22 -0300 Subject: [PATCH] feat: add admin brand endpoint --- .../Controllers/AdminApp/BrandController.php | 18 ++ .../Resources/AdminApp/BrandResource.php | 66 +++++++ app/Domains/Core/Tenant/routes/adminapp.php | 2 + .../Tenant/AdminAppBrandControllerTest.php | 178 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 app/Domains/Core/Tenant/Controllers/AdminApp/BrandController.php create mode 100644 app/Domains/Core/Tenant/Resources/AdminApp/BrandResource.php create mode 100644 tests/Feature/Tenant/AdminAppBrandControllerTest.php diff --git a/app/Domains/Core/Tenant/Controllers/AdminApp/BrandController.php b/app/Domains/Core/Tenant/Controllers/AdminApp/BrandController.php new file mode 100644 index 00000000..c961bead --- /dev/null +++ b/app/Domains/Core/Tenant/Controllers/AdminApp/BrandController.php @@ -0,0 +1,18 @@ +user()->tenant()->firstOrFail(); + + return BrandResource::make($tenantInformationService->load($tenant)); + } +} diff --git a/app/Domains/Core/Tenant/Resources/AdminApp/BrandResource.php b/app/Domains/Core/Tenant/Resources/AdminApp/BrandResource.php new file mode 100644 index 00000000..70881e3d --- /dev/null +++ b/app/Domains/Core/Tenant/Resources/AdminApp/BrandResource.php @@ -0,0 +1,66 @@ + */ + public function toArray(Request $request): array + { + return [ + 'nombre' => $this->nombre, + 'primary_color' => $this->primary_color, + 'footer_bg_color' => $this->footer_bg_color, + 'header_logo' => $this->headerLogo?->getTemporaryUrl(1440), + 'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440), + 'banner_images' => $this->bannerImages(), + ]; + } + + /** @return array> */ + private function bannerImages(): array + { + if ($this->storefront_website_type_code !== 'shopit') { + return []; + } + + $extra = $this->websiteExtras->first( + fn ($extra) => $extra->is_enabled && $extra->websiteTypeExtra->codigo === 'carousel' + ); + + if ($extra === null) { + return []; + } + + $images = $extra->resolvedConfig(); + + if (! is_array($images)) { + return []; + } + + return collect($images) + ->filter(fn ($image) => $image instanceof Attachment) + ->map(function (Attachment $image): array { + $desktopCrop = $image->cropVariants + ->firstWhere('variant', AttachmentCrop::DESKTOP); + + return [ + 'url' => ($desktopCrop?->croppedAttachment ?? $image)->getTemporaryUrl(1440), + 'original_url' => $image->getTemporaryUrl(1440), + 'crop' => $desktopCrop === null ? null : [ + 'crop_horizontal' => $desktopCrop->crop_horizontal, + 'crop_vertical' => $desktopCrop->crop_vertical, + ], + ]; + }) + ->values() + ->all(); + } +} diff --git a/app/Domains/Core/Tenant/routes/adminapp.php b/app/Domains/Core/Tenant/routes/adminapp.php index 7bdcbb9d..caf331d1 100644 --- a/app/Domains/Core/Tenant/routes/adminapp.php +++ b/app/Domains/Core/Tenant/routes/adminapp.php @@ -1,11 +1,13 @@ middleware(['auth:sanctum', 'adminapp.tenant']) ->group(function (): void { + Route::get('brand', BrandController::class); Route::get('website-extras', [WebsiteExtraController::class, 'show']); Route::get('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'showExtra']); Route::put('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'update']); diff --git a/tests/Feature/Tenant/AdminAppBrandControllerTest.php b/tests/Feature/Tenant/AdminAppBrandControllerTest.php new file mode 100644 index 00000000..5d03926b --- /dev/null +++ b/tests/Feature/Tenant/AdminAppBrandControllerTest.php @@ -0,0 +1,178 @@ +seed(AuthorizationSeeder::class); + Storage::fake('s3'); + } + + public function test_only_the_authenticated_adminapp_tenant_brand_is_returned(): void + { + $websiteType = $this->websiteType('shopit'); + $carousel = $websiteType->extras()->create([ + 'codigo' => 'carousel', + 'nombre' => 'Carrusel principal', + 'is_required' => false, + 'config_schema' => [ + 'request_rules' => ['$' => 'required|array'], + 'transforms' => ['$.*' => ['handler' => 'attachment', 'attachment_type' => 'image']], + ], + ]); + $header = $this->image('header.png'); + $footer = $this->image('footer.png'); + $first = $this->image('first.png'); + $second = $this->image('second.png'); + $cropped = $this->image('first-cropped.png'); + $first->cropVariants()->create([ + 'variant' => AttachmentCrop::DESKTOP, + 'crop_horizontal' => ['start_percentage' => 10, 'end_percentage' => 90], + 'crop_vertical' => ['start_percentage' => 20, 'end_percentage' => 80], + 'cropped_attachment_id' => $cropped->id, + ]); + $tenant = $this->tenant('acme', $websiteType, [ + 'nombre' => 'Acme', + 'primary_color' => '#112233', + 'footer_bg_color' => '#445566', + 'header_logo_id' => $header->id, + 'footer_logo_id' => $footer->id, + ]); + $tenant->websiteExtras()->create([ + 'website_type_extra_id' => $carousel->id, + 'config' => [$first->id, $second->id], + ]); + $other = $this->tenant('other', $websiteType, ['nombre' => 'Other']); + $other->websiteExtras()->create([ + 'website_type_extra_id' => $carousel->id, + 'config' => [$this->image('other.png')->id], + ]); + + Sanctum::actingAs($this->admin($tenant)); + + $response = $this->getJson('/api/v1/adminapp/tenant/brand') + ->assertOk() + ->assertJsonPath('data.nombre', 'Acme') + ->assertJsonPath('data.primary_color', '#112233') + ->assertJsonPath('data.footer_bg_color', '#445566') + ->assertJsonCount(2, 'data.banner_images'); + + foreach (['header_logo' => $header, 'footer_logo' => $footer] as $field => $image) { + $this->assertStringContainsString($image->path, $response->json("data.{$field}")); + } + foreach ([$first, $second] as $index => $image) { + $this->assertStringContainsString( + $image->path, + $response->json("data.banner_images.{$index}.original_url") + ); + } + $this->assertStringContainsString($cropped->path, $response->json('data.banner_images.0.url')); + $response->assertJsonPath('data.banner_images.0.crop.crop_horizontal.start_percentage', 10) + ->assertJsonPath('data.banner_images.0.crop.crop_vertical.end_percentage', 80) + ->assertJsonPath('data.banner_images.1.crop', null); + $this->assertStringNotContainsString('other.png', $response->getContent()); + } + + public function test_missing_and_non_shopit_carousels_return_an_empty_list(): void + { + $shopit = $this->websiteType('shopit'); + $tenant = $this->tenant('acme', $shopit); + Sanctum::actingAs($this->admin($tenant)); + + $this->getJson('/api/v1/adminapp/tenant/brand') + ->assertOk() + ->assertJsonPath('data.header_logo', null) + ->assertJsonPath('data.footer_logo', null) + ->assertJsonPath('data.banner_images', []); + + $otherType = $this->websiteType('other'); + $carousel = $otherType->extras()->create([ + 'codigo' => 'carousel', + 'nombre' => 'Other carousel', + 'is_required' => false, + 'config_schema' => [ + 'request_rules' => ['$' => 'required|array'], + 'transforms' => ['$.*' => ['handler' => 'attachment', 'attachment_type' => 'image']], + ], + ]); + $other = $this->tenant('other', $otherType); + $other->websiteExtras()->create([ + 'website_type_extra_id' => $carousel->id, + 'config' => [$this->image('other.png')->id], + ]); + Sanctum::actingAs($this->admin($other)); + + $this->getJson('/api/v1/adminapp/tenant/brand') + ->assertOk() + ->assertJsonPath('data.banner_images', []); + } + + public function test_the_brand_endpoint_requires_an_adminapp_session(): void + { + $this->getJson('/api/v1/adminapp/tenant/brand')->assertUnauthorized(); + + Sanctum::actingAs(User::factory()->create([ + 'rol_codigo' => RoleCode::User->value, + 'tenant_codigo' => null, + ])); + + $this->getJson('/api/v1/adminapp/tenant/brand')->assertForbidden(); + } + + private function websiteType(string $code): StorefrontWebsiteType + { + return StorefrontWebsiteType::query()->create([ + 'codigo' => $code, + 'nombre' => ucfirst($code), + ]); + } + + private function tenant(string $code, StorefrontWebsiteType $websiteType, array $attributes = []): Tenant + { + return Tenant::query()->create([ + 'codigo' => $code, + 'nombre' => ucfirst($code), + 'dominio' => "{$code}.test", + 'storefront_website_type_code' => $websiteType->codigo, + ...$attributes, + ]); + } + + private function admin(Tenant $tenant): User + { + return User::factory()->create([ + 'rol_codigo' => RoleCode::AdminApp->value, + 'tenant_codigo' => $tenant->codigo, + ]); + } + + private function image(string $filename): Attachment + { + return Attachment::query()->create([ + 'path' => "tenants/test/{$filename}", + 'filename' => $filename, + 'type' => AttachmentType::Image, + 'mime_type' => 'image/png', + 'extension' => 'png', + ]); + } +}