refactor(tenants): remove general write endpoints

This commit is contained in:
2026-09-21 15:18:48 -03:00
parent fcb8386735
commit f4de2ff5b2
15 changed files with 40 additions and 1608 deletions

View File

@@ -3,11 +3,8 @@
namespace App\Domains\Core\Tenant\Controllers;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Requests\StoreTenantRequest;
use App\Domains\Core\Tenant\Requests\UpdateTenantRequest;
use App\Domains\Core\Tenant\Resources\TenantResource;
use App\Domains\Core\Tenant\Services\TenantInformationService;
use App\Domains\Core\Tenant\Services\TenantService;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
@@ -15,7 +12,6 @@ use Illuminate\Http\Response;
class TenantController extends Controller
{
public function __construct(
protected TenantService $tenantService,
protected TenantInformationService $tenantInformationService,
) {}
@@ -30,15 +26,6 @@ class TenantController extends Controller
return TenantResource::collection($tenants)->response();
}
public function store(StoreTenantRequest $request): JsonResponse
{
$tenant = $this->tenantService->create($request->validated());
return TenantResource::make(
$this->tenantInformationService->load($tenant)
)->response()->setStatusCode(201);
}
public function show(Tenant $tenant): TenantResource
{
return TenantResource::make(
@@ -46,15 +33,6 @@ class TenantController extends Controller
);
}
public function update(UpdateTenantRequest $request, Tenant $tenant): TenantResource
{
$tenant = $this->tenantService->update($tenant, $request->validated());
return TenantResource::make(
$this->tenantInformationService->load($tenant)
);
}
public function destroy(Tenant $tenant): Response
{
$tenant->delete();

View File

@@ -1,139 +0,0 @@
<?php
namespace App\Domains\Core\Tenant\Requests;
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
use App\Shared\Rules\ImageOrBase64Rule;
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
use App\Domains\Core\Tenant\Services\WebsiteExtraService;
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreTenantRequest extends FormRequest
{
protected bool $hasInvalidDomain = false;
protected bool $hasInvalidBasePath = false;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$rawDomain = $this->input('dominio');
$hasExplicitBasePath = $this->has('base_path');
$rawBasePath = $hasExplicitBasePath
? $this->input('base_path')
: TenantDomainNormalizer::pathFromDomain($rawDomain);
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
$normalizedBasePath = TenantDomainNormalizer::normalizePath($rawBasePath);
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
&& $normalizedDomain === null;
$this->hasInvalidBasePath = ($hasExplicitBasePath && ! is_string($rawBasePath))
|| $normalizedBasePath === null;
$this->merge([
'dominio' => $normalizedDomain,
'base_path' => $normalizedBasePath,
]);
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
$logoRule = ['required', new ImageOrBase64Rule];
return array_merge([
'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')],
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')],
'nombre' => ['required', 'string', 'max:255'],
'dominio' => [
'bail',
function (string $attribute, mixed $value, Closure $fail): void {
if ($this->hasInvalidDomain) {
$fail("The {$attribute} field must contain a valid domain or URL.");
}
},
'required',
'string',
'max:255',
Rule::unique('tenants', 'dominio')
->where('base_path', $this->input('base_path')),
],
'base_path' => [
'bail',
function (string $attribute, mixed $value, Closure $fail): void {
if ($this->hasInvalidBasePath) {
$fail("The {$attribute} field must contain a valid URL path.");
}
},
'required',
'string',
'max:255',
Rule::unique('tenants', 'base_path')
->where('dominio', $this->input('dominio')),
],
'site_title' => ['sometimes', 'nullable', 'string', 'max:255'],
'address' => ['sometimes', 'nullable', 'string', 'max:255'],
'phone' => ['sometimes', 'nullable', 'string', 'max:255'],
'primary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'secondary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'danger_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'success_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_bg_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'footer_bg_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_logo' => $logoRule,
'footer_logo' => $logoRule,
'favicon' => ['sometimes', 'nullable', new ImageOrBase64Rule],
'header_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
'footer_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
'social_media' => ['sometimes', 'array'],
'social_media.*.code' => [
'required',
'string',
'distinct',
Rule::exists('social_media', 'code'),
],
'social_media.*.url' => ['required', 'url', 'max:2048'],
'social_media.*.orden' => ['sometimes', 'integer', 'min:0', 'distinct'],
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
'display_categories' => ['sometimes', 'boolean'],
'display_seach_bar' => ['sometimes', 'boolean'],
'display_cart' => ['sometimes', 'boolean'],
'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)],
'checkout_editing_policy' => [
'sometimes',
Rule::in([CartEditingPolicy::Disabled->value]),
],
'display_cart_item_images' => ['sometimes', 'boolean'],
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
'allow_ticket_refund' => ['sometimes', 'boolean'],
'allow_ticket_total_refund' => ['sometimes', 'boolean'],
'allow_ticket_partial_refund' => ['sometimes', 'boolean'],
'ticket_partial_refund_percentage' => [
'sometimes',
'numeric',
'decimal:0,2',
'min:0',
'max:99.99',
],
'storefront_website_type_code' => [
'required_with:extras',
'sometimes',
'string',
Rule::exists('storefront_website_types', 'codigo'),
],
'admin_website_type_code' => ['sometimes', 'nullable', 'string', Rule::exists('admin_website_types', 'codigo')],
], app(WebsiteExtraService::class)->requestRules($this->input('storefront_website_type_code')));
}
}

View File

@@ -1,165 +0,0 @@
<?php
namespace App\Domains\Core\Tenant\Requests;
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
use App\Shared\Rules\ImageOrBase64Rule;
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateTenantRequest extends FormRequest
{
protected bool $hasInvalidDomain = false;
protected bool $hasInvalidBasePath = false;
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
if ($this->has('dominio')) {
$rawDomain = $this->input('dominio');
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
$embeddedBasePath = TenantDomainNormalizer::pathFromDomain($rawDomain);
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
&& ($normalizedDomain === null || $embeddedBasePath === null);
$this->merge([
'dominio' => $normalizedDomain,
]);
if (! $this->has('base_path') && $embeddedBasePath !== null && $embeddedBasePath !== '/') {
$this->merge(['base_path' => $embeddedBasePath]);
}
}
if ($this->has('base_path')) {
$rawBasePath = $this->input('base_path');
$normalizedBasePath = TenantDomainNormalizer::normalizePath($rawBasePath);
$this->hasInvalidBasePath = ! is_string($rawBasePath)
|| $normalizedBasePath === null;
$this->merge(['base_path' => $normalizedBasePath]);
}
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var Tenant|null $tenant */
$tenant = $this->route('tenant');
$domain = $this->input('dominio', $tenant?->dominio);
$basePath = $this->input('base_path', $tenant?->base_path ?? '/');
$logoRule = ['nullable', new ImageOrBase64Rule];
return [
'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')],
'codigo' => [
'nullable',
'string',
'max:255',
Rule::unique('tenants', 'codigo')->ignore($tenant?->id),
],
'nombre' => ['nullable', 'string', 'max:255'],
'dominio' => [
'bail',
function (string $attribute, mixed $value, Closure $fail): void {
if ($this->hasInvalidDomain) {
$fail("The {$attribute} field must contain a valid domain or URL.");
}
},
'nullable',
'string',
'max:255',
Rule::unique('tenants', 'dominio')
->where('base_path', $basePath)
->ignore($tenant?->id),
],
'base_path' => [
'bail',
function (string $attribute, mixed $value, Closure $fail): void {
if ($this->hasInvalidBasePath) {
$fail("The {$attribute} field must contain a valid URL path.");
}
},
'nullable',
'string',
'max:255',
Rule::unique('tenants', 'base_path')
->where('dominio', $domain)
->ignore($tenant?->id),
],
'site_title' => ['sometimes', 'nullable', 'string', 'max:255'],
'address' => ['sometimes', 'nullable', 'string', 'max:255'],
'phone' => ['sometimes', 'nullable', 'string', 'max:255'],
'primary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'secondary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'danger_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'success_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'footer_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_logo' => $logoRule,
'footer_logo' => $logoRule,
'favicon' => ['sometimes', 'nullable', new ImageOrBase64Rule],
'header_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
'footer_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
'social_media' => ['sometimes', 'array'],
'social_media.*.code' => [
'required',
'string',
'distinct',
Rule::exists('social_media', 'code'),
],
'social_media.*.url' => ['required', 'url', 'max:2048'],
'social_media.*.orden' => ['sometimes', 'integer', 'min:0', 'distinct'],
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
'display_categories' => ['sometimes', 'boolean'],
'display_seach_bar' => ['sometimes', 'boolean'],
'display_cart' => ['sometimes', 'boolean'],
'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)],
'checkout_editing_policy' => [
'sometimes',
Rule::in([CartEditingPolicy::Disabled->value]),
],
'display_cart_item_images' => ['sometimes', 'boolean'],
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
'allow_ticket_refund' => ['sometimes', 'boolean'],
'allow_ticket_total_refund' => ['sometimes', 'boolean'],
'allow_ticket_partial_refund' => ['sometimes', 'boolean'],
'ticket_partial_refund_percentage' => [
'sometimes',
'numeric',
'decimal:0,2',
'min:0',
'max:99.99',
],
'admin_website_type_code' => ['sometimes', 'nullable', 'string', Rule::exists('admin_website_types', 'codigo')],
'storefront_website_type_code' => [
'sometimes',
'nullable',
'string',
Rule::exists('storefront_website_types', 'codigo'),
function (string $attribute, mixed $value, Closure $fail) use ($tenant): void {
if ($tenant && $value !== $tenant->storefront_website_type_code && $tenant->websiteExtras()->exists()) {
$fail('The storefront website type cannot be changed while the tenant has extras.');
}
},
],
];
}
}

View File

@@ -1,198 +0,0 @@
<?php
namespace App\Domains\Core\Tenant\Services;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Services\AttachmentService;
use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class TenantService
{
public function __construct(
protected AttachmentService $attachmentService,
protected WebsiteExtraService $websiteExtraService,
) {}
/**
* Create a new tenant and store its logos.
*
* @param array<string, mixed> $data
*/
public function create(array $data): Tenant
{
return DB::transaction(function () use ($data): Tenant {
$headerLogo = $data['header_logo'] ?? null;
$footerLogo = $data['footer_logo'] ?? null;
$favicon = $data['favicon'] ?? null;
$headerBackgroundImage = $data['header_bg_image'] ?? null;
$footerBackgroundImage = $data['footer_bg_image'] ?? null;
$socialMedia = $data['social_media'] ?? [];
$extras = $data['extras'] ?? [];
unset(
$data['header_logo'],
$data['footer_logo'],
$data['favicon'],
$data['header_bg_image'],
$data['footer_bg_image'],
$data['social_media'],
$data['extras'],
);
$headerAttachmentId = null;
if ($headerLogo) {
$attachment = Str::isUuid($headerLogo)
? Attachment::query()->where('key', $headerLogo)->first()
: $this->attachmentService->store($headerLogo, 'tenants');
if ($attachment) {
$headerAttachmentId = $attachment->id;
}
}
$footerAttachmentId = null;
if ($footerLogo) {
$attachment = Str::isUuid($footerLogo)
? Attachment::query()->where('key', $footerLogo)->first()
: $this->attachmentService->store($footerLogo, 'tenants');
if ($attachment) {
$footerAttachmentId = $attachment->id;
}
}
$data['header_logo_id'] = $headerAttachmentId;
$data['footer_logo_id'] = $footerAttachmentId;
$data['favicon_id'] = $this->storeTenantImage($favicon);
$data['header_bg_image_id'] = $this->storeTenantImage($headerBackgroundImage);
$data['footer_bg_image_id'] = $this->storeTenantImage($footerBackgroundImage);
/** @var Tenant $tenant */
$tenant = Tenant::query()->create($data);
$this->syncSocialMedia($tenant, $socialMedia);
$this->websiteExtraService->createForTenant($tenant, $extras);
return $tenant;
});
}
/**
* Update an existing tenant and store new logos if uploaded.
*
* @param array<string, mixed> $data
*/
public function update(Tenant $tenant, array $data): Tenant
{
return DB::transaction(function () use ($tenant, $data): Tenant {
$hasHeaderLogoKey = array_key_exists('header_logo', $data);
$hasFooterLogoKey = array_key_exists('footer_logo', $data);
$hasFaviconKey = array_key_exists('favicon', $data);
$hasHeaderBackgroundImageKey = array_key_exists('header_bg_image', $data);
$hasFooterBackgroundImageKey = array_key_exists('footer_bg_image', $data);
$hasSocialMediaKey = array_key_exists('social_media', $data);
$headerLogo = $data['header_logo'] ?? null;
$footerLogo = $data['footer_logo'] ?? null;
$favicon = $data['favicon'] ?? null;
$headerBackgroundImage = $data['header_bg_image'] ?? null;
$footerBackgroundImage = $data['footer_bg_image'] ?? null;
$socialMedia = $data['social_media'] ?? [];
unset(
$data['header_logo'],
$data['footer_logo'],
$data['favicon'],
$data['header_bg_image'],
$data['footer_bg_image'],
$data['social_media']
);
$tenant->fill($data);
if ($hasHeaderLogoKey) {
if ($headerLogo) {
$attachment = Str::isUuid($headerLogo)
? Attachment::query()->where('key', $headerLogo)->first()
: $this->attachmentService->store($headerLogo, 'tenants');
if ($attachment) {
$tenant->header_logo_id = $attachment->id;
} else {
$tenant->header_logo_id = null;
}
} else {
$tenant->header_logo_id = null;
}
}
if ($hasFooterLogoKey) {
if ($footerLogo) {
$attachment = Str::isUuid($footerLogo)
? Attachment::query()->where('key', $footerLogo)->first()
: $this->attachmentService->store($footerLogo, 'tenants');
if ($attachment) {
$tenant->footer_logo_id = $attachment->id;
} else {
$tenant->footer_logo_id = null;
}
} else {
$tenant->footer_logo_id = null;
}
}
if ($hasFaviconKey) {
$tenant->favicon_id = $this->storeTenantImage($favicon);
}
if ($hasHeaderBackgroundImageKey) {
$tenant->header_bg_image_id = $this->storeTenantImage($headerBackgroundImage);
}
if ($hasFooterBackgroundImageKey) {
$tenant->footer_bg_image_id = $this->storeTenantImage($footerBackgroundImage);
}
$tenant->save();
if ($hasSocialMediaKey) {
$this->syncSocialMedia($tenant, $socialMedia);
}
return $tenant;
});
}
/**
* @param array<int, array{code: string, url: string, orden?: int}> $socialMedia
*/
private function syncSocialMedia(Tenant $tenant, array $socialMedia): void
{
$owner = $tenant->activeEvent ?? $tenant;
$associations = [];
foreach (array_values($socialMedia) as $index => $item) {
$associations[$item['code']] = [
'url' => $item['url'],
'orden' => $item['orden'] ?? $index,
];
}
$owner->socialMedia()->sync($associations);
$owner->unsetRelation('socialMedia');
}
private function storeTenantImage(mixed $image): ?int
{
if (! $image) {
return null;
}
$attachment = is_string($image) && Str::isUuid($image)
? Attachment::query()->where('key', $image)->first()
: $this->attachmentService->store($image, 'tenants');
return $attachment?->id;
}
}

View File

@@ -2,16 +2,14 @@
namespace App\Domains\Core\Tenant\Services;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteTypeExtra;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\WebsiteExtra;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Services\AttachmentService;
use App\Shared\Rules\CroppedImageOrBase64Rule;
use App\Shared\Rules\ImageOrBase64Rule;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\WebsiteExtra;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteTypeExtra;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
@@ -21,109 +19,6 @@ class WebsiteExtraService
{
public function __construct(protected AttachmentService $attachmentService) {}
/**
* Build the request rules declared by the selected website type.
*
* @return array<string, mixed>
*/
public function requestRules(?string $websiteTypeCode): array
{
if (! is_string($websiteTypeCode) || $websiteTypeCode === '') {
return [
'extras' => ['prohibited'],
];
}
$definitions = $this->definitionsFor($websiteTypeCode);
$allowedCodes = $definitions->pluck('codigo')->all();
$hasRequiredExtras = $definitions->contains(
fn (StorefrontWebsiteTypeExtra $definition): bool => $definition->is_required
);
$rules = [
'extras' => [
$hasRequiredExtras ? 'required' : 'sometimes',
'array',
function (string $attribute, mixed $value, \Closure $fail) use ($allowedCodes): void {
if (! is_array($value)) {
return;
}
$unknownCodes = array_diff(array_keys($value), $allowedCodes);
if ($unknownCodes !== []) {
$fail(
'The '.$attribute.' field contains extras not supported by the website type: '
.implode(', ', $unknownCodes).'.'
);
}
},
],
];
foreach ($definitions as $definition) {
$schemaRules = $definition->config_schema['request_rules'] ?? [];
$rootRules = $this->compileRules($schemaRules['$'] ?? []);
$rootRules = array_values(array_filter(
$rootRules,
fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true)
));
array_unshift($rootRules, $definition->is_required ? 'required' : 'sometimes');
$rules["extras.{$definition->codigo}"] = $rootRules;
foreach ($schemaRules as $path => $pathRules) {
if ($path === '$') {
continue;
}
$rules[$this->requestAttribute($definition->codigo, $path)] = $this->compileRules($pathRules);
}
}
return $rules;
}
/**
* Transform and persist each extra selected for a tenant.
*
* @param array<string, mixed> $extras
*/
public function createForTenant(Tenant $tenant, array $extras): void
{
if ($extras === []) {
return;
}
$definitions = $this->definitionsFor((string) $tenant->storefront_website_type_code)
->keyBy('codigo');
foreach ($extras as $name => $config) {
/** @var StorefrontWebsiteTypeExtra|null $definition */
$definition = $definitions->get($name);
if (! $definition) {
throw ValidationException::withMessages([
'extras' => ["The extra {$name} is not supported by the selected website type."],
]);
}
$requestRoot = "extras.{$definition->codigo}";
$transformedConfig = $this->applyTransforms(
$tenant,
$definition,
$config,
$requestRoot
);
$tenant->websiteExtras()->create([
'website_type_extra_id' => $definition->id,
'config' => $transformedConfig,
]);
}
$tenant->unsetRelation('websiteExtras');
}
/**
* Build request rules for one extra addressed by its stable code.
*
@@ -201,19 +96,6 @@ class WebsiteExtraService
->firstOrFail();
}
/**
* @return Collection<int, StorefrontWebsiteTypeExtra>
*/
private function definitionsFor(string $websiteTypeCode): Collection
{
$websiteType = StorefrontWebsiteType::query()
->where('codigo', $websiteTypeCode)
->with('extras')
->first();
return $websiteType?->extras ?? collect();
}
/**
* @param string|array<int, mixed> $rules
* @return array<int, mixed>
@@ -232,11 +114,6 @@ class WebsiteExtraService
);
}
private function requestAttribute(string $extraCode, string $path): string
{
return $this->configAttribute("extras.{$extraCode}", $path);
}
private function configAttribute(string $root, string $path): string
{
if ($path === '$') {

View File

@@ -15,7 +15,6 @@ Es la raíz del modelo multi-tenant. Gestiona organizaciones/sitios, tipos de we
## Servicios
- `TenantService`: crea y actualiza tenants, incluyendo sus recursos asociados.
- `TenantInformationService`: carga un tenant y las relaciones requeridas por cada contexto.
- `AdminWebsiteTypeService`: crea o actualiza los tipos de admin y su marca.
- `WebsiteExtraService`: construye reglas dinámicas, crea, actualiza y habilita/deshabilita extras.
@@ -25,7 +24,8 @@ El par `(dominio, base_path)` es único. Un mismo dominio puede alojar el tenant
## Endpoints
- Recurso REST público/administrativo `/tenants`.
- `GET /tenants`, `GET /tenants/{codigo}` y `DELETE /tenants/{codigo}`.
- La creación y actualización general de tenants se administran mediante seeders o base de datos; no se exponen por API.
- Bajo `/v1/adminapp/tenant/website-extras`, con autenticación y contexto de tenant: consulta general, detalle, actualización y activación/desactivación.
## Dependencias y reglas

View File

@@ -3,6 +3,6 @@
use App\Domains\Core\Tenant\Controllers\TenantController;
use Illuminate\Support\Facades\Route;
Route::apiResource('tenants', TenantController::class);
Route::apiResource('tenants', TenantController::class)->except(['store', 'update']);
require __DIR__.'/adminapp.php';