refactor(tenants): remove general write endpoints
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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')));
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 === '$') {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user