diff --git a/ShopIt_API_Postman_Collection.json b/ShopIt_API_Postman_Collection.json index c69f8a3..e785808 100644 --- a/ShopIt_API_Postman_Collection.json +++ b/ShopIt_API_Postman_Collection.json @@ -34,7 +34,7 @@ "response": [] }, { - "name": "Create Tenant (JSON)", + "name": "Create Tenant OnTicket (JSON)", "request": { "method": "POST", "header": [ @@ -51,7 +51,7 @@ ], "body": { "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\": \"
Una experiencia inolvidable
\",\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": { "raw": "{{base_url}}/api/tenants", @@ -67,7 +67,7 @@ "response": [] }, { - "name": "Create Tenant (Form Data)", + "name": "Create Tenant ShopIt (Form Data)", "request": { "method": "POST", "header": [ @@ -110,6 +110,11 @@ "value": "#ff0000", "type": "text" }, + { + "key": "success_color", + "value": "#00aa55", + "type": "text" + }, { "key": "header_bg_color", "value": "#333333", @@ -129,6 +134,32 @@ "key": "footer_logo", "type": "file", "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." } ] }, diff --git a/app/Domains/Tenant/Controllers/TenantController.php b/app/Domains/Tenant/Controllers/TenantController.php index 6fea5af..24c7f67 100644 --- a/app/Domains/Tenant/Controllers/TenantController.php +++ b/app/Domains/Tenant/Controllers/TenantController.php @@ -30,7 +30,13 @@ class TenantController extends Controller $tenant = $this->tenantService->create($request->validated()); return TenantResource::make( - $tenant->loadMissing(['headerLogo', 'footerLogo', 'socialMedia']) + $tenant->loadMissing([ + 'headerLogo', + 'footerLogo', + 'socialMedia', + 'websiteType', + 'websiteExtras.websiteTypeExtra', + ]) )->response()->setStatusCode(201); } diff --git a/app/Domains/Tenant/Requests/StoreTenantRequest.php b/app/Domains/Tenant/Requests/StoreTenantRequest.php index f808d88..7485980 100644 --- a/app/Domains/Tenant/Requests/StoreTenantRequest.php +++ b/app/Domains/Tenant/Requests/StoreTenantRequest.php @@ -5,6 +5,7 @@ namespace App\Domains\Tenant\Requests; use App\Domains\Catalog\Enums\GroupLayout; use App\Domains\Catalog\Enums\ProductLayout; use App\Domains\Shared\Rules\ImageOrBase64Rule; +use App\Domains\Tenant\Services\WebsiteExtraService; use App\Domains\Tenant\Support\TenantDomainNormalizer; use Closure; use Illuminate\Foundation\Http\FormRequest; @@ -39,7 +40,7 @@ class StoreTenantRequest extends FormRequest { $logoRule = ['required', new ImageOrBase64Rule]; - return [ + return array_merge([ 'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')], 'nombre' => ['required', 'string', 'max:255'], 'dominio' => [ @@ -74,6 +75,12 @@ class StoreTenantRequest extends FormRequest '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'], - ]; + 'website_type_code' => [ + 'required_with:extras', + 'sometimes', + 'string', + Rule::exists('website_type', 'codigo'), + ], + ], app(WebsiteExtraService::class)->requestRules($this->input('website_type_code'))); } } diff --git a/app/Domains/Tenant/Resources/TenantResource.php b/app/Domains/Tenant/Resources/TenantResource.php index 1f6e142..28e2c7d 100644 --- a/app/Domains/Tenant/Resources/TenantResource.php +++ b/app/Domains/Tenant/Resources/TenantResource.php @@ -30,6 +30,21 @@ class TenantResource extends JsonResource 'success_color' => $this->success_color, 'header_bg_color' => $this->header_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 'header_logo' => $this->headerLogo?->getTemporaryUrl(1440), 'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440), diff --git a/app/Domains/Tenant/Services/TenantService.php b/app/Domains/Tenant/Services/TenantService.php index 36e74ce..c2692e1 100644 --- a/app/Domains/Tenant/Services/TenantService.php +++ b/app/Domains/Tenant/Services/TenantService.php @@ -10,7 +10,10 @@ use Illuminate\Support\Str; 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. @@ -23,11 +26,13 @@ class TenantService $headerLogo = $data['header_logo'] ?? null; $footerLogo = $data['footer_logo'] ?? null; $socialMedia = $data['social_media'] ?? []; + $extras = $data['extras'] ?? []; unset( $data['header_logo'], $data['footer_logo'], - $data['social_media'] + $data['social_media'], + $data['extras'], ); $headerAttachmentId = null; @@ -58,6 +63,7 @@ class TenantService /** @var Tenant $tenant */ $tenant = Tenant::query()->create($data); $this->syncSocialMedia($tenant, $socialMedia); + $this->websiteExtraService->createForTenant($tenant, $extras); return $tenant; }); diff --git a/app/Domains/Tenant/Services/WebsiteExtraService.php b/app/Domains/Tenant/Services/WebsiteExtraService.php new file mode 100644 index 0000000..443ff25 --- /dev/null +++ b/app/Domains/Tenant/Services/WebsiteExtraService.php @@ -0,0 +1,305 @@ + + */ + 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