diff --git a/app/Domains/Tenant/Controllers/AdminApp/WebsiteExtraController.php b/app/Domains/Tenant/Controllers/AdminApp/WebsiteExtraController.php new file mode 100644 index 0000000..9ce51dd --- /dev/null +++ b/app/Domains/Tenant/Controllers/AdminApp/WebsiteExtraController.php @@ -0,0 +1,50 @@ +loadTenant($request->user()) + ); + } + + public function update(UpdateWebsiteExtrasRequest $request): WebsiteExtrasResource + { + $tenant = $request->user()->tenant()->firstOrFail(); + + $this->websiteExtraService->replaceForTenant( + $tenant, + $request->validated('extras', []) + ); + + return WebsiteExtrasResource::make( + $this->loadTenant($request->user()) + ); + } + + private function loadTenant(User $user): Tenant + { + $tenant = $user->tenant()->firstOrFail(); + + return $this->tenantInformationService->load($tenant, [ + 'websiteType.extras', + ]); + } +} diff --git a/app/Domains/Tenant/Requests/AdminApp/UpdateWebsiteExtrasRequest.php b/app/Domains/Tenant/Requests/AdminApp/UpdateWebsiteExtrasRequest.php new file mode 100644 index 0000000..c063648 --- /dev/null +++ b/app/Domains/Tenant/Requests/AdminApp/UpdateWebsiteExtrasRequest.php @@ -0,0 +1,24 @@ + + */ + public function rules(): array + { + return app(WebsiteExtraService::class)->requestRules( + $this->user()?->tenant?->website_type_code + ); + } +} diff --git a/app/Domains/Tenant/Resources/AdminApp/WebsiteExtrasResource.php b/app/Domains/Tenant/Resources/AdminApp/WebsiteExtrasResource.php new file mode 100644 index 0000000..e979d0d --- /dev/null +++ b/app/Domains/Tenant/Resources/AdminApp/WebsiteExtrasResource.php @@ -0,0 +1,67 @@ + + */ + public function toArray(Request $request): array + { + $websiteExtras = $this->websiteExtras->keyBy( + fn ($extra) => $extra->websiteTypeExtra->nombre + ); + + return [ + 'website_type' => $this->websiteType ? [ + 'codigo' => $this->websiteType->codigo, + 'nombre' => $this->websiteType->nombre, + ] : null, + 'definitions' => $this->websiteType?->extras + ->mapWithKeys(fn ($definition) => [ + $definition->nombre => [ + 'descripcion' => $definition->descripcion, + 'is_required' => $definition->is_required, + 'request_rules' => $definition->config_schema['request_rules'] ?? [], + ], + ]) ?? [], + 'extras' => $websiteExtras->mapWithKeys(fn ($extra) => [ + $extra->websiteTypeExtra->nombre => $this->formatConfig( + $extra->resolvedConfig(), + fn (Attachment $attachment): string => $attachment->key + ), + ]), + 'resolved_extras' => $websiteExtras->mapWithKeys(fn ($extra) => [ + $extra->websiteTypeExtra->nombre => $this->formatConfig( + $extra->resolvedConfig(), + fn (Attachment $attachment): string => $attachment->getTemporaryUrl(1440) + ), + ]), + ]; + } + + private function formatConfig(mixed $value, callable $formatAttachment): mixed + { + if ($value instanceof Attachment) { + return $formatAttachment($value); + } + + if (! is_array($value)) { + return $value; + } + + return array_map( + fn (mixed $item): mixed => $this->formatConfig($item, $formatAttachment), + $value + ); + } +} diff --git a/app/Domains/Tenant/Services/WebsiteExtraService.php b/app/Domains/Tenant/Services/WebsiteExtraService.php index 443ff25..ecd10e8 100644 --- a/app/Domains/Tenant/Services/WebsiteExtraService.php +++ b/app/Domains/Tenant/Services/WebsiteExtraService.php @@ -10,6 +10,7 @@ use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\WebsiteType; use App\Domains\Tenant\Models\WebsiteTypeExtra; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; @@ -118,6 +119,21 @@ class WebsiteExtraService $tenant->unsetRelation('websiteExtras'); } + /** + * Replace all configured extras for a tenant. + * + * @param array $extras + */ + public function replaceForTenant(Tenant $tenant, array $extras): void + { + DB::transaction(function () use ($tenant, $extras): void { + $tenant->websiteExtras()->delete(); + $this->createForTenant($tenant, $extras); + }); + + $tenant->unsetRelation('websiteExtras'); + } + /** * @return Collection */ diff --git a/app/Domains/Tenant/routes/adminapp.php b/app/Domains/Tenant/routes/adminapp.php new file mode 100644 index 0000000..14ae745 --- /dev/null +++ b/app/Domains/Tenant/routes/adminapp.php @@ -0,0 +1,13 @@ +middleware(['auth:sanctum', 'adminapp.tenant']) + ->group(function (): void { + Route::get('website-extras', [WebsiteExtraController::class, 'show']) + ->name('adminapp.tenant.website-extras.show'); + Route::put('website-extras', [WebsiteExtraController::class, 'update']) + ->name('adminapp.tenant.website-extras.update'); + }); diff --git a/app/Domains/Tenant/routes/api.php b/app/Domains/Tenant/routes/api.php index 69464a8..cc12f08 100644 --- a/app/Domains/Tenant/routes/api.php +++ b/app/Domains/Tenant/routes/api.php @@ -8,3 +8,5 @@ Route::get('tenants/bootstrap/{dominio}', BootstrapTenantController::class) ->where('dominio', '.*'); Route::apiResource('tenants', TenantController::class); + +require __DIR__.'/adminapp.php'; diff --git a/app/Http/Middleware/EnsureAdminAppTenant.php b/app/Http/Middleware/EnsureAdminAppTenant.php new file mode 100644 index 0000000..83c5c68 --- /dev/null +++ b/app/Http/Middleware/EnsureAdminAppTenant.php @@ -0,0 +1,30 @@ +user(); + + if ( + ! $user + || $user->rol_codigo !== RoleCode::AdminApp->value + || ! $user->tenant_codigo + ) { + throw new AuthorizationException; + } + + return $next($request); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index f240d99..01e3e9d 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -2,6 +2,7 @@ use App\Domains\Auth\Exceptions\AccountLockedException; use App\Domains\Ticket\Exceptions\TicketNotAvailableException; +use App\Http\Middleware\EnsureAdminAppTenant; use App\Http\Middleware\SetApiLocale; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; @@ -21,6 +22,9 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->alias([ + 'adminapp.tenant' => EnsureAdminAppTenant::class, + ]); $middleware->encryptCookies(except: [ 'guest_token', ]); diff --git a/tests/Feature/Tenant/AdminAppWebsiteExtraControllerTest.php b/tests/Feature/Tenant/AdminAppWebsiteExtraControllerTest.php new file mode 100644 index 0000000..a783a2d --- /dev/null +++ b/tests/Feature/Tenant/AdminAppWebsiteExtraControllerTest.php @@ -0,0 +1,155 @@ +seed(AuthorizationSeeder::class); + + $this->websiteType = WebsiteType::query()->create([ + 'codigo' => 'test-store', + 'nombre' => 'Test Store', + ]); + + $this->websiteType->extras()->create([ + 'nombre' => 'contactConfig', + 'descripcion' => 'Datos de contacto visibles en la tienda.', + 'is_required' => false, + 'config_schema' => [ + 'request_rules' => [ + '$' => 'required|array', + 'phone' => 'required|string|max:30', + ], + 'transforms' => [], + 'database_rules' => [ + '$' => 'required|array', + 'phone' => 'required|string|max:30', + ], + ], + ]); + } + + public function test_authentication_is_required(): void + { + $this->getJson('/api/v1/adminapp/tenant/website-extras') + ->assertUnauthorized(); + } + + public function test_a_customer_cannot_access_adminapp_website_extras(): void + { + $customer = User::factory()->create([ + 'rol_codigo' => RoleCode::User->value, + 'tenant_codigo' => null, + ]); + + Sanctum::actingAs($customer); + + $this->getJson('/api/v1/adminapp/tenant/website-extras') + ->assertForbidden(); + } + + public function test_adminapp_user_can_read_definitions_and_current_values(): void + { + $tenant = $this->createTenant('acme'); + $definition = $this->websiteType->extras()->firstOrFail(); + $tenant->websiteExtras()->create([ + 'website_type_extra_id' => $definition->id, + 'config' => ['phone' => '+54 341 555 0101'], + ]); + + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->getJson('/api/v1/adminapp/tenant/website-extras') + ->assertOk() + ->assertJsonPath('data.website_type.codigo', 'test-store') + ->assertJsonPath('data.definitions.contactConfig.is_required', false) + ->assertJsonPath('data.extras.contactConfig.phone', '+54 341 555 0101') + ->assertJsonPath('data.resolved_extras.contactConfig.phone', '+54 341 555 0101'); + } + + public function test_adminapp_user_replaces_only_its_tenant_extras(): void + { + $tenant = $this->createTenant('acme'); + $otherTenant = $this->createTenant('other'); + $definition = $this->websiteType->extras()->firstOrFail(); + + $tenant->websiteExtras()->create([ + 'website_type_extra_id' => $definition->id, + 'config' => ['phone' => 'old'], + ]); + $otherTenant->websiteExtras()->create([ + 'website_type_extra_id' => $definition->id, + 'config' => ['phone' => 'untouched'], + ]); + + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->putJson('/api/v1/adminapp/tenant/website-extras', [ + 'extras' => [ + 'contactConfig' => [ + 'phone' => '+54 341 555 9999', + ], + ], + ]) + ->assertOk() + ->assertJsonPath('data.extras.contactConfig.phone', '+54 341 555 9999'); + + $this->assertSame( + ['phone' => '+54 341 555 9999'], + $tenant->websiteExtras()->firstOrFail()->config + ); + $this->assertSame( + ['phone' => 'untouched'], + $otherTenant->websiteExtras()->firstOrFail()->config + ); + } + + public function test_update_rejects_extras_not_supported_by_the_website_type(): void + { + $tenant = $this->createTenant('acme'); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->putJson('/api/v1/adminapp/tenant/website-extras', [ + 'extras' => [ + 'unknown' => ['enabled' => true], + ], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors('extras'); + } + + private function createTenant(string $code): Tenant + { + return Tenant::query()->create([ + 'codigo' => $code, + 'nombre' => ucfirst($code), + 'dominio' => "{$code}.test", + 'website_type_code' => $this->websiteType->codigo, + ]); + } + + private function createAdminAppUser(Tenant $tenant): User + { + return User::factory()->create([ + 'rol_codigo' => RoleCode::AdminApp->value, + 'tenant_codigo' => $tenant->codigo, + ]); + } +}