feat(tenant): implement website extras functionality and validation in tenant creation
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Create Tenant (JSON)",
|
"name": "Create Tenant OnTicket (JSON)",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"header": [
|
"header": [
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"codigo\": \"acme\",\n \"nombre\": \"Acme\",\n \"dominio\": \"acme.test\",\n \"primary_color\": \"#111111\",\n \"secondary_color\": \"#222222\",\n \"danger_color\": \"#ff0000\",\n \"header_bg_color\": \"#333333\",\n \"footer_bg_color\": \"#333333\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n}"
|
"raw": "{\n \"codigo\": \"festival-demo\",\n \"nombre\": \"Festival Demo\",\n \"dominio\": \"festival-demo.test\",\n \"primary_color\": \"#111111\",\n \"secondary_color\": \"#222222\",\n \"danger_color\": \"#ff0000\",\n \"success_color\": \"#00aa55\",\n \"header_bg_color\": \"#333333\",\n \"footer_bg_color\": \"#333333\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"search_product_layout\": \"column_with_image\",\n \"search_group_layout\": \"paginated\",\n \"search_items_per_page\": 12,\n \"website_type_code\": \"onticket\",\n \"extras\": {\n \"heroConfig\": {\n \"title_html\": \"<h1>Festival Demo</h1>\",\n \"description_html\": \"<p>Una experiencia inolvidable</p>\",\n \"button_text\": \"Comprar entradas\",\n \"button_href\": \"/tickets\",\n \"background_image_id\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n },\n \"eventConfig\": {\n \"title\": \"Festival Demo\",\n \"location\": \"Buenos Aires\",\n \"dates\": [\n \"2026-10-10\",\n \"2026-10-11\"\n ]\n }\n }\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{base_url}}/api/tenants",
|
"raw": "{{base_url}}/api/tenants",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Create Tenant (Form Data)",
|
"name": "Create Tenant ShopIt (Form Data)",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"header": [
|
"header": [
|
||||||
@@ -110,6 +110,11 @@
|
|||||||
"value": "#ff0000",
|
"value": "#ff0000",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "success_color",
|
||||||
|
"value": "#00aa55",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "header_bg_color",
|
"key": "header_bg_color",
|
||||||
"value": "#333333",
|
"value": "#333333",
|
||||||
@@ -129,6 +134,32 @@
|
|||||||
"key": "footer_logo",
|
"key": "footer_logo",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"src": []
|
"src": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "search_product_layout",
|
||||||
|
"value": "column_with_image",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "search_group_layout",
|
||||||
|
"value": "paginated",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "search_items_per_page",
|
||||||
|
"value": "12",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "website_type_code",
|
||||||
|
"value": "shopit",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "extras[carousel][]",
|
||||||
|
"type": "file",
|
||||||
|
"src": [],
|
||||||
|
"description": "Se puede repetir esta key hasta 10 veces. También acepta una imagen base64 o el UUID de un attachment."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ class TenantController extends Controller
|
|||||||
$tenant = $this->tenantService->create($request->validated());
|
$tenant = $this->tenantService->create($request->validated());
|
||||||
|
|
||||||
return TenantResource::make(
|
return TenantResource::make(
|
||||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'socialMedia'])
|
$tenant->loadMissing([
|
||||||
|
'headerLogo',
|
||||||
|
'footerLogo',
|
||||||
|
'socialMedia',
|
||||||
|
'websiteType',
|
||||||
|
'websiteExtras.websiteTypeExtra',
|
||||||
|
])
|
||||||
)->response()->setStatusCode(201);
|
)->response()->setStatusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Domains\Tenant\Requests;
|
|||||||
use App\Domains\Catalog\Enums\GroupLayout;
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
use App\Domains\Catalog\Enums\ProductLayout;
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||||
|
use App\Domains\Tenant\Services\WebsiteExtraService;
|
||||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
@@ -39,7 +40,7 @@ class StoreTenantRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
$logoRule = ['required', new ImageOrBase64Rule];
|
$logoRule = ['required', new ImageOrBase64Rule];
|
||||||
|
|
||||||
return [
|
return array_merge([
|
||||||
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')],
|
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')],
|
||||||
'nombre' => ['required', 'string', 'max:255'],
|
'nombre' => ['required', 'string', 'max:255'],
|
||||||
'dominio' => [
|
'dominio' => [
|
||||||
@@ -74,6 +75,12 @@ class StoreTenantRequest extends FormRequest
|
|||||||
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
||||||
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
||||||
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
||||||
];
|
'website_type_code' => [
|
||||||
|
'required_with:extras',
|
||||||
|
'sometimes',
|
||||||
|
'string',
|
||||||
|
Rule::exists('website_type', 'codigo'),
|
||||||
|
],
|
||||||
|
], app(WebsiteExtraService::class)->requestRules($this->input('website_type_code')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,21 @@ class TenantResource extends JsonResource
|
|||||||
'success_color' => $this->success_color,
|
'success_color' => $this->success_color,
|
||||||
'header_bg_color' => $this->header_bg_color,
|
'header_bg_color' => $this->header_bg_color,
|
||||||
'footer_bg_color' => $this->footer_bg_color,
|
'footer_bg_color' => $this->footer_bg_color,
|
||||||
|
'website_type_code' => $this->website_type_code,
|
||||||
|
'website_type' => $this->whenLoaded(
|
||||||
|
'websiteType',
|
||||||
|
fn () => $this->websiteType ? [
|
||||||
|
'codigo' => $this->websiteType->codigo,
|
||||||
|
'nombre' => $this->websiteType->nombre,
|
||||||
|
] : null
|
||||||
|
),
|
||||||
|
'extras' => $this->whenLoaded(
|
||||||
|
'websiteExtras',
|
||||||
|
fn () => $this->websiteExtras
|
||||||
|
->mapWithKeys(fn ($extra) => [
|
||||||
|
$extra->websiteTypeExtra->nombre => $extra->config,
|
||||||
|
])
|
||||||
|
),
|
||||||
// 1 day
|
// 1 day
|
||||||
'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),
|
'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),
|
||||||
'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440),
|
'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440),
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ use Illuminate\Support\Str;
|
|||||||
|
|
||||||
class TenantService
|
class TenantService
|
||||||
{
|
{
|
||||||
public function __construct(protected AttachmentService $attachmentService) {}
|
public function __construct(
|
||||||
|
protected AttachmentService $attachmentService,
|
||||||
|
protected WebsiteExtraService $websiteExtraService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new tenant and store its logos.
|
* Create a new tenant and store its logos.
|
||||||
@@ -23,11 +26,13 @@ class TenantService
|
|||||||
$headerLogo = $data['header_logo'] ?? null;
|
$headerLogo = $data['header_logo'] ?? null;
|
||||||
$footerLogo = $data['footer_logo'] ?? null;
|
$footerLogo = $data['footer_logo'] ?? null;
|
||||||
$socialMedia = $data['social_media'] ?? [];
|
$socialMedia = $data['social_media'] ?? [];
|
||||||
|
$extras = $data['extras'] ?? [];
|
||||||
|
|
||||||
unset(
|
unset(
|
||||||
$data['header_logo'],
|
$data['header_logo'],
|
||||||
$data['footer_logo'],
|
$data['footer_logo'],
|
||||||
$data['social_media']
|
$data['social_media'],
|
||||||
|
$data['extras'],
|
||||||
);
|
);
|
||||||
|
|
||||||
$headerAttachmentId = null;
|
$headerAttachmentId = null;
|
||||||
@@ -58,6 +63,7 @@ class TenantService
|
|||||||
/** @var Tenant $tenant */
|
/** @var Tenant $tenant */
|
||||||
$tenant = Tenant::query()->create($data);
|
$tenant = Tenant::query()->create($data);
|
||||||
$this->syncSocialMedia($tenant, $socialMedia);
|
$this->syncSocialMedia($tenant, $socialMedia);
|
||||||
|
$this->websiteExtraService->createForTenant($tenant, $extras);
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
});
|
});
|
||||||
|
|||||||
305
app/Domains/Tenant/Services/WebsiteExtraService.php
Normal file
305
app/Domains/Tenant/Services/WebsiteExtraService.php
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Tenant\Services;
|
||||||
|
|
||||||
|
use App\Domains\Attachable\Enums\AttachmentType;
|
||||||
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
|
use App\Domains\Attachable\Services\AttachmentService;
|
||||||
|
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||||
|
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\Validator;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
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);
|
||||||
|
$allowedNames = $definitions->pluck('nombre')->all();
|
||||||
|
$hasRequiredExtras = $definitions->contains(
|
||||||
|
fn (WebsiteTypeExtra $definition): bool => $definition->is_required
|
||||||
|
);
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'extras' => [
|
||||||
|
$hasRequiredExtras ? 'required' : 'sometimes',
|
||||||
|
'array',
|
||||||
|
function (string $attribute, mixed $value, \Closure $fail) use ($allowedNames): void {
|
||||||
|
if (! is_array($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unknownNames = array_diff(array_keys($value), $allowedNames);
|
||||||
|
|
||||||
|
if ($unknownNames !== []) {
|
||||||
|
$fail(
|
||||||
|
'The '.$attribute.' field contains extras not supported by the website type: '
|
||||||
|
.implode(', ', $unknownNames).'.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
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->nombre}"] = $rootRules;
|
||||||
|
|
||||||
|
foreach ($schemaRules as $path => $pathRules) {
|
||||||
|
if ($path === '$') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules[$this->requestAttribute($definition->nombre, $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->website_type_code)
|
||||||
|
->keyBy('nombre');
|
||||||
|
|
||||||
|
foreach ($extras as $name => $config) {
|
||||||
|
/** @var WebsiteTypeExtra|null $definition */
|
||||||
|
$definition = $definitions->get($name);
|
||||||
|
|
||||||
|
if (! $definition) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'extras' => ["The extra {$name} is not supported by the selected website type."],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transformedConfig = $this->applyTransforms($tenant, $definition, $config);
|
||||||
|
$this->validateDatabaseConfig($definition, $transformedConfig);
|
||||||
|
|
||||||
|
$tenant->websiteExtras()->create([
|
||||||
|
'website_type_extra_id' => $definition->id,
|
||||||
|
'config' => $transformedConfig,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->unsetRelation('websiteExtras');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, WebsiteTypeExtra>
|
||||||
|
*/
|
||||||
|
private function definitionsFor(string $websiteTypeCode): Collection
|
||||||
|
{
|
||||||
|
$websiteType = WebsiteType::query()
|
||||||
|
->where('codigo', $websiteTypeCode)
|
||||||
|
->with('extras')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $websiteType?->extras ?? collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|array<int, mixed> $rules
|
||||||
|
* @return array<int, mixed>
|
||||||
|
*/
|
||||||
|
private function compileRules(string|array $rules): array
|
||||||
|
{
|
||||||
|
$compiled = is_string($rules) ? explode('|', $rules) : $rules;
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
fn (mixed $rule): mixed => $rule === 'image_or_base64'
|
||||||
|
? new ImageOrBase64Rule
|
||||||
|
: $rule,
|
||||||
|
$compiled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requestAttribute(string $extraName, string $path): string
|
||||||
|
{
|
||||||
|
if ($path === '$') {
|
||||||
|
return "extras.{$extraName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($path, '$.')) {
|
||||||
|
$path = substr($path, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "extras.{$extraName}.{$path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyTransforms(
|
||||||
|
Tenant $tenant,
|
||||||
|
WebsiteTypeExtra $definition,
|
||||||
|
mixed $config
|
||||||
|
): mixed {
|
||||||
|
foreach ($definition->config_schema['transforms'] ?? [] as $path => $transform) {
|
||||||
|
$segments = $this->pathSegments($path);
|
||||||
|
$config = $this->transformAtPath(
|
||||||
|
$config,
|
||||||
|
$segments,
|
||||||
|
fn (mixed $value): mixed => $this->transformValue(
|
||||||
|
$tenant,
|
||||||
|
$definition,
|
||||||
|
$path,
|
||||||
|
$value,
|
||||||
|
$transform
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function pathSegments(string $path): array
|
||||||
|
{
|
||||||
|
$path = ltrim($path, '$');
|
||||||
|
$path = ltrim($path, '.');
|
||||||
|
|
||||||
|
return $path === '' ? [] : explode('.', $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function transformAtPath(mixed $value, array $segments, callable $transform): mixed
|
||||||
|
{
|
||||||
|
if ($segments === []) {
|
||||||
|
return $transform($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$segment = array_shift($segments);
|
||||||
|
|
||||||
|
if ($segment === '*') {
|
||||||
|
foreach ($value as $key => $item) {
|
||||||
|
$value[$key] = $this->transformAtPath($item, $segments, $transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists($segment, $value)) {
|
||||||
|
$value[$segment] = $this->transformAtPath($value[$segment], $segments, $transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $transform
|
||||||
|
*/
|
||||||
|
private function transformValue(
|
||||||
|
Tenant $tenant,
|
||||||
|
WebsiteTypeExtra $definition,
|
||||||
|
string $path,
|
||||||
|
mixed $value,
|
||||||
|
array $transform
|
||||||
|
): mixed {
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($transform['handler'] ?? null) !== 'attachment') {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Unsupported transform handler for {$definition->nombre}: ".($transform['handler'] ?? 'null')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && Str::isUuid($value)) {
|
||||||
|
$attachment = Attachment::query()->where('key', $value)->first();
|
||||||
|
|
||||||
|
if (! $attachment) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
$this->requestAttribute($definition->nombre, $path) => [
|
||||||
|
'The selected attachment does not exist.',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$attachment = $this->attachmentService->store(
|
||||||
|
$value,
|
||||||
|
"tenants/{$tenant->codigo}/extras/{$definition->nombre}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedType = $transform['attachment_type'] ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
is_string($expectedType)
|
||||||
|
&& $attachment->type instanceof AttachmentType
|
||||||
|
&& $attachment->type->value !== $expectedType
|
||||||
|
) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
$this->requestAttribute($definition->nombre, $path) => [
|
||||||
|
"The attachment must be of type {$expectedType}.",
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachment->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateDatabaseConfig(
|
||||||
|
WebsiteTypeExtra $definition,
|
||||||
|
mixed $config
|
||||||
|
): void {
|
||||||
|
$schemaRules = $definition->config_schema['database_rules'] ?? [];
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
foreach ($schemaRules as $path => $pathRules) {
|
||||||
|
$attribute = $path === '$'
|
||||||
|
? 'config'
|
||||||
|
: 'config.'.ltrim(str_starts_with($path, '$.') ? substr($path, 2) : $path, '.');
|
||||||
|
$rules[$attribute] = $this->compileRules($pathRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = Validator::make(['config' => $config], $rules);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
foreach ($validator->errors()->toArray() as $attribute => $errors) {
|
||||||
|
$suffix = $attribute === 'config' ? '' : substr($attribute, strlen('config'));
|
||||||
|
$messages["extras.{$definition->nombre}{$suffix}"] = $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ValidationException::withMessages($messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
tests/Feature/Tenant/StoreTenantWithExtrasTest.php
Normal file
142
tests/Feature/Tenant/StoreTenantWithExtrasTest.php
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Tenant;
|
||||||
|
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Database\Seeders\WebsiteTypeSeeder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class StoreTenantWithExtrasTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Storage::fake('s3');
|
||||||
|
$this->seed(WebsiteTypeSeeder::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_creates_a_tenant_and_validates_and_stores_its_website_extras(): void
|
||||||
|
{
|
||||||
|
$response = $this->postJson('/api/tenants', array_merge($this->tenantData(), [
|
||||||
|
'website_type_code' => 'onticket',
|
||||||
|
'extras' => [
|
||||||
|
'eventConfig' => [
|
||||||
|
'title' => 'Festival',
|
||||||
|
'location' => 'Buenos Aires',
|
||||||
|
'dates' => ['2026-10-10', '2026-10-11'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertCreated()
|
||||||
|
->assertJsonPath('data.website_type_code', 'onticket')
|
||||||
|
->assertJsonPath('data.website_type.codigo', 'onticket')
|
||||||
|
->assertJsonPath('data.extras.eventConfig.title', 'Festival');
|
||||||
|
|
||||||
|
$tenant = Tenant::query()->where('codigo', 'festival')->sole();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('websites_extras', [
|
||||||
|
'website_code' => $tenant->codigo,
|
||||||
|
]);
|
||||||
|
$this->assertSame(
|
||||||
|
[
|
||||||
|
'title' => 'Festival',
|
||||||
|
'location' => 'Buenos Aires',
|
||||||
|
'dates' => ['2026-10-10', '2026-10-11'],
|
||||||
|
],
|
||||||
|
$tenant->websiteExtras()->sole()->config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_an_extra_not_supported_by_the_selected_website_type(): void
|
||||||
|
{
|
||||||
|
$this->postJson('/api/tenants', array_merge($this->tenantData(), [
|
||||||
|
'website_type_code' => 'shopit',
|
||||||
|
'extras' => [
|
||||||
|
'eventConfig' => [
|
||||||
|
'title' => 'Not supported',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]))
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors(['extras']);
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('tenants', ['codigo' => 'festival']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_applies_the_extra_schema_rules(): void
|
||||||
|
{
|
||||||
|
$this->postJson('/api/tenants', array_merge($this->tenantData(), [
|
||||||
|
'website_type_code' => 'onticket',
|
||||||
|
'extras' => [
|
||||||
|
'eventConfig' => [
|
||||||
|
'dates' => ['not-a-date'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]))
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors(['extras.eventConfig.dates.0']);
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('tenants', ['codigo' => 'festival']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_transforms_extra_images_to_attachment_ids(): void
|
||||||
|
{
|
||||||
|
$image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/tenants', array_merge($this->tenantData(), [
|
||||||
|
'website_type_code' => 'shopit',
|
||||||
|
'extras' => [
|
||||||
|
'carousel' => [$image],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
|
||||||
|
$config = Tenant::query()
|
||||||
|
->where('codigo', 'festival')
|
||||||
|
->sole()
|
||||||
|
->websiteExtras()
|
||||||
|
->sole()
|
||||||
|
->config;
|
||||||
|
|
||||||
|
$this->assertCount(1, $config);
|
||||||
|
$this->assertIsInt($config[0]);
|
||||||
|
$this->assertDatabaseHas('attachments', [
|
||||||
|
'id' => $config[0],
|
||||||
|
'type' => 'image',
|
||||||
|
]);
|
||||||
|
$response->assertJsonPath('data.extras.carousel.0', $config[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function tenantData(): array
|
||||||
|
{
|
||||||
|
$image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'codigo' => 'festival',
|
||||||
|
'nombre' => 'Festival',
|
||||||
|
'dominio' => 'festival.test',
|
||||||
|
'primary_color' => '#111111',
|
||||||
|
'secondary_color' => '#222222',
|
||||||
|
'danger_color' => '#333333',
|
||||||
|
'success_color' => '#444444',
|
||||||
|
'header_bg_color' => '#ffffff',
|
||||||
|
'footer_bg_color' => '#ffffff',
|
||||||
|
'header_logo' => $image,
|
||||||
|
'footer_logo' => $image,
|
||||||
|
'search_product_layout' => 'column_with_image',
|
||||||
|
'search_group_layout' => 'paginated',
|
||||||
|
'search_items_per_page' => 12,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user