diff --git a/ShopIt_API_Postman_Collection.json b/ShopIt_API_Postman_Collection.json index 162634f..1a5b968 100644 --- a/ShopIt_API_Postman_Collection.json +++ b/ShopIt_API_Postman_Collection.json @@ -2470,45 +2470,6 @@ }, "response": [] }, - { - "name": "Create Tenant", - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `POST /api/tenants`\n\nControlador: `App\\Domains\\Tenant\\Controllers\\TenantController@store`", - "url": { - "raw": "{{base_url}}/api/tenants", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"client_id\": {{client_id}},\n \"codigo\": \"{{tenant_code}}\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"{{tenant_domain}}\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"secondary_color\": \"#2563EB\",\n \"danger_color\": \"#DC2626\",\n \"success_color\": \"#16A34A\",\n \"header_bg_color\": \"#FFFFFF\",\n \"footer_bg_color\": \"#111827\",\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 \"display_categories\": true,\n \"display_seach_bar\": true,\n \"display_cart\": true,\n \"cart_editing_policy\": \"full\",\n \"admin_website_type_code\": \"shopit\",\n \"storefront_website_type_code\": \"shopit\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, { "name": "Get Tenant", "request": { @@ -2535,86 +2496,6 @@ }, "response": [] }, - { - "name": "Update Tenant (PUT)", - "request": { - "method": "PUT", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `PUT /api/tenants/{tenant}`\n\nControlador: `App\\Domains\\Tenant\\Controllers\\TenantController@update`", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_id}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_id}}" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, - { - "name": "Update Tenant (PATCH)", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `PATCH /api/tenants/{tenant}`\n\nControlador: `App\\Domains\\Tenant\\Controllers\\TenantController@update`", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_id}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_id}}" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}", - "options": { - "raw": { - "language": "json" - } - } - } - }, - "response": [] - }, { "name": "Delete Tenant", "request": { diff --git a/app/Domains/Core/Tenant/Controllers/TenantController.php b/app/Domains/Core/Tenant/Controllers/TenantController.php index c4d8510..d4dc588 100644 --- a/app/Domains/Core/Tenant/Controllers/TenantController.php +++ b/app/Domains/Core/Tenant/Controllers/TenantController.php @@ -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(); diff --git a/app/Domains/Core/Tenant/Requests/StoreTenantRequest.php b/app/Domains/Core/Tenant/Requests/StoreTenantRequest.php deleted file mode 100644 index 74ff23c..0000000 --- a/app/Domains/Core/Tenant/Requests/StoreTenantRequest.php +++ /dev/null @@ -1,139 +0,0 @@ -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 - */ - 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'))); - } -} diff --git a/app/Domains/Core/Tenant/Requests/UpdateTenantRequest.php b/app/Domains/Core/Tenant/Requests/UpdateTenantRequest.php deleted file mode 100644 index b6a748e..0000000 --- a/app/Domains/Core/Tenant/Requests/UpdateTenantRequest.php +++ /dev/null @@ -1,165 +0,0 @@ -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 - */ - 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.'); - } - }, - ], - ]; - } -} diff --git a/app/Domains/Core/Tenant/Services/TenantService.php b/app/Domains/Core/Tenant/Services/TenantService.php deleted file mode 100644 index a4a9803..0000000 --- a/app/Domains/Core/Tenant/Services/TenantService.php +++ /dev/null @@ -1,198 +0,0 @@ - $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 $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 $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; - } -} diff --git a/app/Domains/Core/Tenant/Services/WebsiteExtraService.php b/app/Domains/Core/Tenant/Services/WebsiteExtraService.php index 3ec2a1a..196df87 100644 --- a/app/Domains/Core/Tenant/Services/WebsiteExtraService.php +++ b/app/Domains/Core/Tenant/Services/WebsiteExtraService.php @@ -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 - */ - 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 $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 - */ - private function definitionsFor(string $websiteTypeCode): Collection - { - $websiteType = StorefrontWebsiteType::query() - ->where('codigo', $websiteTypeCode) - ->with('extras') - ->first(); - - return $websiteType?->extras ?? collect(); - } - /** * @param string|array $rules * @return array @@ -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 === '$') { diff --git a/app/Domains/Core/Tenant/documentacion/README.md b/app/Domains/Core/Tenant/documentacion/README.md index 31170c7..c005006 100644 --- a/app/Domains/Core/Tenant/documentacion/README.md +++ b/app/Domains/Core/Tenant/documentacion/README.md @@ -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 diff --git a/app/Domains/Core/Tenant/routes/api.php b/app/Domains/Core/Tenant/routes/api.php index 8373d53..393e415 100644 --- a/app/Domains/Core/Tenant/routes/api.php +++ b/app/Domains/Core/Tenant/routes/api.php @@ -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'; diff --git a/postman/generate-shopit-collection.php b/postman/generate-shopit-collection.php index 85a6a31..6afedd2 100644 --- a/postman/generate-shopit-collection.php +++ b/postman/generate-shopit-collection.php @@ -120,36 +120,6 @@ function bodyFor(string $method, string $uri): ?array return jsonBody($exact[$key]); } - if ($key === 'POST api/tenants') { - return jsonBody([ - 'client_id' => '{{client_id}}', - 'codigo' => '{{tenant_code}}', - 'nombre' => 'Tenant Demo', - 'dominio' => '{{tenant_domain}}', - 'site_title' => 'ShopIt Demo', - 'primary_color' => '#111827', - 'secondary_color' => '#2563EB', - 'danger_color' => '#DC2626', - 'success_color' => '#16A34A', - 'header_bg_color' => '#FFFFFF', - 'footer_bg_color' => '#111827', - 'header_logo' => TINY_PNG, - 'footer_logo' => TINY_PNG, - 'search_product_layout' => 'column_with_image', - 'search_group_layout' => 'paginated', - 'search_items_per_page' => 12, - 'display_categories' => true, - 'display_seach_bar' => true, - 'display_cart' => true, - 'cart_editing_policy' => 'full', - 'website_type_code' => 'shopit', - ]); - } - - if (in_array($key, ['PUT api/tenants/{tenant}', 'PATCH api/tenants/{tenant}'], true)) { - return jsonBody(['nombre' => 'Tenant Demo Actualizado', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'cart_editing_policy' => 'full']); - } - if ($key === 'POST api/storage-test/s3/upload') { return formDataBody(['path' => 'postman/test-file.png', 'expires_in_minutes' => '60'], ['file']); } diff --git a/tests/Feature/Catalog/CatalogSearchTest.php b/tests/Feature/Catalog/CatalogSearchTest.php index bc9744c..446cc5a 100644 --- a/tests/Feature/Catalog/CatalogSearchTest.php +++ b/tests/Feature/Catalog/CatalogSearchTest.php @@ -2,13 +2,13 @@ namespace Tests\Feature\Catalog; -use App\Shared\Attachable\Enums\AttachmentType; -use App\Shared\Attachable\Models\Attachment; use App\Domains\Commerce\Catalog\Enums\GroupLayout; use App\Domains\Commerce\Catalog\Enums\ProductLayout; use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -32,15 +32,11 @@ class CatalogSearchTest extends TestCase { $tenant = $this->createTenant('search-config'); - $this->putJson("/api/tenants/{$tenant->codigo}", [ + $tenant->update([ 'search_product_layout' => ProductLayout::Row->value, 'search_group_layout' => GroupLayout::SimpleVertical->value, 'search_items_per_page' => 24, - ]) - ->assertOk() - ->assertJsonPath('data.search_product_layout', ProductLayout::Row->value) - ->assertJsonPath('data.search_group_layout', GroupLayout::SimpleVertical->value) - ->assertJsonPath('data.search_items_per_page', 24); + ]); $this->getJson('/api/tenants/bootstrap?'.http_build_query([ 'dominio' => $tenant->dominio, diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index b4c4fdc..2cb2d97 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -14,7 +14,6 @@ use App\Shared\Attachable\Models\Attachment; use App\Shared\Attachable\Models\AttachmentCrop; use Database\Seeders\WebsiteTypeSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Tests\TestCase; @@ -626,367 +625,6 @@ class BootstrapTenantControllerTest extends TestCase ->assertJsonPath('data.menues.0.submenues.1.code', 'tree.fox'); } - public function test_it_allows_different_domain_paths_and_rejects_duplicate_tenant_keys(): void - { - $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $firstResponse = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'https://ACME.com/puratendencia/', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $base64Image, - 'footer_logo' => $base64Image, - ]); - - $firstResponse - ->assertCreated() - ->assertJsonPath('data.dominio', 'acme.com') - ->assertJsonPath('data.base_path', '/puratendencia') - ->assertJsonPath('data.primary_color', '#111111') - ->assertJsonPath('data.secondary_color', '#222222') - ->assertJsonPath('data.danger_color', '#333333') - ->assertJsonPath('data.success_color', '#555555') - ->assertJsonPath('data.header_bg_color', '#444444')->assertJsonPath('data.footer_bg_color', '#444444'); - - $tenant = Tenant::query()->with(['headerLogo', 'footerLogo'])->where('codigo', 'acme')->firstOrFail(); - - $this->assertNotNull($tenant->header_logo_id); - $this->assertNotNull($tenant->footer_logo_id); - - $this->assertDatabaseHas('tenants', [ - 'codigo' => 'acme', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo_id' => $tenant->header_logo_id, - 'footer_logo_id' => $tenant->footer_logo_id, - ]); - - $headerUrl = $firstResponse->json('data.header_logo'); - $footerUrl = $firstResponse->json('data.footer_logo'); - - $this->assertStringContainsString($tenant->headerLogo->key, $headerUrl); - $this->assertStringContainsString($tenant->footerLogo->key, $footerUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - $this->assertTrue( - str_contains($footerUrl, 'Expires=') || str_contains($footerUrl, 'expiration=') || str_contains($footerUrl, 'X-Amz-Expires=') - ); - - $differentPathResponse = $this->postJson('/api/tenants', [ - 'codigo' => 'pura-tendencia', - 'nombre' => 'Pura Tendencia', - 'dominio' => 'acme.com', - 'base_path' => '/sonder/', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $base64Image, - 'footer_logo' => $base64Image, - ]); - - $differentPathResponse - ->assertCreated() - ->assertJsonPath('data.dominio', 'acme.com') - ->assertJsonPath('data.base_path', '/sonder'); - - $secondResponse = $this->postJson('/api/tenants', [ - 'codigo' => 'globex', - 'nombre' => 'Globex', - 'dominio' => 'acme.com', - 'base_path' => '/puratendencia', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $secondResponse - ->assertUnprocessable() - ->assertJsonValidationErrors(['dominio']); - } - - public function test_it_allows_keeping_the_same_domain_on_update_but_rejects_collisions(): void - { - $tenant = $this->createTenant([ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - ]); - - $otherTenant = $this->createTenant([ - 'codigo' => 'globex', - 'nombre' => 'Globex', - 'dominio' => 'globex.com', - ]); - - $hdrUuid = (string) Str::uuid(); - $ftrUuid = (string) Str::uuid(); - - $hdrAttachment = Attachment::create([ - 'key' => $hdrUuid, - 'path' => 'tenants/'.$hdrUuid.'.png', - 'filename' => 'hdr.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - $ftrAttachment = Attachment::create([ - 'key' => $ftrUuid, - 'path' => 'tenants/'.$ftrUuid.'.png', - 'filename' => 'ftr.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - - $successfulResponse = $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'codigo' => 'acme', - 'nombre' => 'Acme Updated', - 'dominio' => 'https://ACME.com:443/', - 'primary_color' => '#555555', - 'secondary_color' => '#666666', - 'danger_color' => '#777777', - 'success_color' => '#999999', - 'header_bg_color' => '#888888', - 'footer_bg_color' => '#888888', - 'header_logo' => $hdrUuid, - 'footer_logo' => $ftrUuid, - ]); - - $successfulResponse - ->assertOk() - ->assertJsonPath('data.nombre', 'Acme Updated') - ->assertJsonPath('data.dominio', 'acme.com') - ->assertJsonPath('data.primary_color', '#555555') - ->assertJsonPath('data.secondary_color', '#666666') - ->assertJsonPath('data.danger_color', '#777777') - ->assertJsonPath('data.success_color', '#999999') - ->assertJsonPath('data.header_bg_color', '#888888')->assertJsonPath('data.footer_bg_color', '#888888'); - - $headerUrl = $successfulResponse->json('data.header_logo'); - $footerUrl = $successfulResponse->json('data.footer_logo'); - - $this->assertStringContainsString($hdrUuid, $headerUrl); - $this->assertStringContainsString($ftrUuid, $footerUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - $this->assertTrue( - str_contains($footerUrl, 'Expires=') || str_contains($footerUrl, 'expiration=') || str_contains($footerUrl, 'X-Amz-Expires=') - ); - - $this->assertDatabaseHas('tenants', [ - 'id' => $tenant->id, - 'primary_color' => '#555555', - 'secondary_color' => '#666666', - 'danger_color' => '#777777', - 'success_color' => '#999999', - 'header_bg_color' => '#888888', - 'footer_bg_color' => '#888888', - 'header_logo_id' => $hdrAttachment->id, - 'footer_logo_id' => $ftrAttachment->id, - ]); - - $failingResponse = $this->putJson("/api/tenants/{$otherTenant->codigo}", [ - 'codigo' => 'globex', - 'nombre' => 'Globex', - 'dominio' => 'https://ACME.com/', - ]); - - $failingResponse - ->assertUnprocessable() - ->assertJsonValidationErrors(['dominio']); - } - - public function test_it_allows_partial_update_without_required_fields(): void - { - $tenant = $this->createTenant([ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#ffffff', - ]); - - $response = $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'primary_color' => '#000000', - 'cart_editing_policy' => 'quantity_and_remove', - 'checkout_editing_policy' => 'disabled', - 'display_cart_item_images' => false, - ]); - - $response - ->assertOk() - ->assertJsonPath('data.codigo', 'acme') - ->assertJsonPath('data.nombre', 'Acme') - ->assertJsonPath('data.primary_color', '#000000') - ->assertJsonPath('data.cart_editing_policy.code', 'quantity_and_remove') - ->assertJsonPath('data.cart_editing_policy.allow_modify', true) - ->assertJsonPath('data.cart_editing_policy.allow_delete', true) - ->assertJsonPath('data.cart_editing_policy.allow_update_quantity', true) - ->assertJsonPath('data.cart_editing_policy.allow_update_variant', false) - ->assertJsonPath('data.checkout_editing_policy.code', 'disabled') - ->assertJsonPath('data.checkout_editing_policy.allow_modify', false) - ->assertJsonPath('data.display_cart_item_images', false); - - $this->assertDatabaseHas('tenants', [ - 'id' => $tenant->id, - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'primary_color' => '#000000', - 'cart_editing_policy' => 'quantity_and_remove', - 'checkout_editing_policy' => 'disabled', - 'display_cart_item_images' => false, - ]); - } - - public function test_it_validates_aesthetic_colors(): void - { - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => 'invalid-color', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response->assertJsonValidationErrors(['primary_color']); - - $response2 = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#12345', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response2->assertJsonValidationErrors(['primary_color']); - - $response3 = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => 'invalid-color', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => (string) Str::uuid(), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response3->assertJsonValidationErrors(['success_color']); - } - - public function test_it_validates_logo_must_be_image_or_svg(): void - { - Storage::fake('s3'); - - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => UploadedFile::fake()->create('document.pdf', 10, 'application/pdf'), - 'footer_logo' => (string) Str::uuid(), - ]); - - $response->assertJsonValidationErrors(['header_logo']); - - $response2 = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => 'data:application/pdf;base64,JVBERi0xLjQKJdcfqksKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAvUGFnZXMgMiAwIFI...', - 'footer_logo' => (string) Str::uuid(), - ]); - - $response2->assertJsonValidationErrors(['header_logo']); - } - - public function test_it_stores_uploaded_file_logos_in_tenants_directory(): void - { - Storage::fake('s3'); - - $header = UploadedFile::fake()->image('header.png'); - $footer = UploadedFile::fake()->image('footer.svg', 100, 100); - - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $header, - 'footer_logo' => $footer, - ]); - - $response->assertCreated(); - - $tenant = Tenant::query()->with(['headerLogo', 'footerLogo'])->where('codigo', 'acme')->firstOrFail(); - - $this->assertNotNull($tenant->header_logo_id); - $this->assertNotNull($tenant->footer_logo_id); - - $headerUrl = $response->json('data.header_logo'); - $footerUrl = $response->json('data.footer_logo'); - - $this->assertStringContainsString($tenant->headerLogo->key, $headerUrl); - $this->assertStringContainsString($tenant->footerLogo->key, $footerUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - $this->assertTrue( - str_contains($footerUrl, 'Expires=') || str_contains($footerUrl, 'expiration=') || str_contains($footerUrl, 'X-Amz-Expires=') - ); - - $this->assertDatabaseHas('attachments', ['key' => $tenant->headerLogo->key]); - $this->assertDatabaseHas('attachments', ['key' => $tenant->footerLogo->key]); - } - private function createTenant(array $attributes = []): Tenant { $hdrKey = (string) Str::uuid(); @@ -1021,56 +659,4 @@ class BootstrapTenantControllerTest extends TestCase 'footer_logo_id' => $footerAttachment->id, ], $attributes)); } - - public function test_it_stores_base64_logos_in_tenants_directory(): void - { - Storage::fake('s3'); - - $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $ftrUuid = (string) Str::uuid(); - $footerAttachment = Attachment::create([ - 'key' => $ftrUuid, - 'path' => 'tenants/'.$ftrUuid.'.png', - 'filename' => 'logo_footer.png', - 'type' => AttachmentType::Image, - 'mime_type' => 'image/png', - ]); - - $response = $this->postJson('/api/tenants', [ - 'codigo' => 'acme', - 'nombre' => 'Acme', - 'dominio' => 'acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#555555', - 'header_bg_color' => '#444444', - 'footer_bg_color' => '#444444', - 'header_logo' => $base64Image, - 'footer_logo' => $ftrUuid, - 'site_title' => 'Acme Store', - 'favicon' => $base64Image, - ]); - - $response->assertCreated(); - - $tenant = Tenant::query()->with(['headerLogo', 'favicon'])->where('codigo', 'acme')->firstOrFail(); - - $this->assertNotNull($tenant->header_logo_id); - $this->assertNotNull($tenant->favicon_id); - - $headerUrl = $response->json('data.header_logo'); - $faviconUrl = $response->json('data.favicon'); - - $response->assertJsonPath('data.site_title', 'Acme Store'); - $this->assertStringContainsString($tenant->headerLogo->key, $headerUrl); - $this->assertStringContainsString($tenant->favicon->key, $faviconUrl); - $this->assertTrue( - str_contains($headerUrl, 'Expires=') || str_contains($headerUrl, 'expiration=') || str_contains($headerUrl, 'X-Amz-Expires=') - ); - - $this->assertDatabaseHas('attachments', ['key' => $tenant->headerLogo->key]); - $this->assertDatabaseHas('attachments', ['key' => $tenant->favicon->key]); - } } diff --git a/tests/Feature/Tenant/StoreTenantWithExtrasTest.php b/tests/Feature/Tenant/StoreTenantWithExtrasTest.php deleted file mode 100644 index 8c89964..0000000 --- a/tests/Feature/Tenant/StoreTenantWithExtrasTest.php +++ /dev/null @@ -1,220 +0,0 @@ -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(), [ - 'admin_website_type_code' => 'shopit', - 'storefront_website_type_code' => 'onticket', - 'extras' => [ - 'eventConfig' => [ - 'title' => 'Festival', - 'location' => 'Buenos Aires', - 'dates_text' => '10 y 11 de octubre de 2026', - 'dates' => [ - [ - 'date' => '2026-10-10', - 'start_time' => '09:00', - 'end_time' => '18:00', - ], - [ - 'date' => '2026-10-11', - 'start_time' => '10:00', - 'end_time' => '17:00', - ], - ], - ], - ], - ])); - - $response - ->assertCreated() - ->assertJsonPath('data.admin_website_type_code', 'shopit') - ->assertJsonPath('data.storefront_website_type_code', 'onticket') - ->assertJsonMissingPath('data.website_type') - ->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_text' => '10 y 11 de octubre de 2026', - 'dates' => [ - [ - 'date' => '2026-10-10', - 'start_time' => '09:00', - 'end_time' => '18:00', - ], - [ - 'date' => '2026-10-11', - 'start_time' => '10:00', - 'end_time' => '17:00', - ], - ], - ], - $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(), [ - 'storefront_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(), [ - 'storefront_website_type_code' => 'onticket', - 'extras' => [ - 'eventConfig' => [ - 'dates' => [[ - 'date' => 'not-a-date', - 'start_time' => '09:00', - 'end_time' => '18:00', - ]], - ], - ], - ])) - ->assertUnprocessable() - ->assertJsonValidationErrors(['extras.eventConfig.dates.0.date']); - - $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(), [ - 'storefront_website_type_code' => 'shopit', - 'extras' => [ - 'carousel' => [$image], - ], - ])); - - $response->assertCreated(); - - $tenant = Tenant::query() - ->where('codigo', 'festival') - ->sole(); - $config = $tenant->websiteExtras() - ->sole() - ->config; - - $this->assertCount(1, $config); - $this->assertIsInt($config[0]); - $this->assertDatabaseHas('attachments', [ - 'id' => $config[0], - 'type' => 'image', - ]); - - $attachment = Attachment::query()->findOrFail($config[0]); - $carouselUrl = $response->json('data.extras.carousel.0'); - - $this->assertIsString($carouselUrl); - $this->assertStringContainsString($attachment->key, $carouselUrl); - - app(TenantInformationService::class)->load($tenant); - - $this->assertInstanceOf( - Attachment::class, - $tenant->websiteExtras->sole()->resolvedConfig()[0] - ); - } - - public function test_it_returns_scalar_attachment_fields_as_temporary_urls(): void - { - $image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - - $response = $this->postJson('/api/tenants', array_merge($this->tenantData(), [ - 'storefront_website_type_code' => 'onticket', - 'extras' => [ - 'heroConfig' => [ - 'title_html' => '

Festival

', - 'background_image_id' => $image, - ], - ], - ])); - - $response->assertCreated(); - - $heroConfig = Tenant::query() - ->where('codigo', 'festival') - ->sole() - ->websiteExtras() - ->whereHas( - 'websiteTypeExtra', - fn ($query) => $query->where('codigo', 'heroConfig') - ) - ->sole() - ->config; - - $attachment = Attachment::query()->findOrFail($heroConfig['background_image_id']); - $backgroundUrl = $response->json('data.extras.heroConfig.background_image_id'); - - $this->assertIsString($backgroundUrl); - $this->assertStringContainsString($attachment->key, $backgroundUrl); - } - - /** - * @return array - */ - 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, - ]; - } -} diff --git a/tests/Feature/Tenant/TenantBootstrapRouteTest.php b/tests/Feature/Tenant/TenantBootstrapRouteTest.php index 900593d..424acad 100644 --- a/tests/Feature/Tenant/TenantBootstrapRouteTest.php +++ b/tests/Feature/Tenant/TenantBootstrapRouteTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Tenant; use App\Domains\Core\Bootstrap\Controllers\TenantBootstrapController; use Illuminate\Http\Request; +use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Tests\TestCase; class TenantBootstrapRouteTest extends TestCase @@ -16,4 +17,24 @@ class TenantBootstrapRouteTest extends TestCase $this->assertSame(TenantBootstrapController::class, $route->getActionName()); } + + public function test_tenant_creation_and_update_routes_are_not_available(): void + { + $routes = app('router')->getRoutes(); + $this->assertNull($routes->getByName('tenants.store')); + $this->assertNull($routes->getByName('tenants.update')); + + foreach ([['POST', '/api/tenants'], ['PUT', '/api/tenants/demo'], ['PATCH', '/api/tenants/demo']] as [$method, $uri]) { + try { + $routes->match(Request::create($uri, $method)); + $this->fail("Unexpected route: {$method} {$uri}"); + } catch (MethodNotAllowedHttpException $exception) { + $this->assertSame(405, $exception->getStatusCode()); + } + } + + foreach (['tenants.index', 'tenants.show', 'tenants.destroy'] as $name) { + $this->assertNotNull($routes->getByName($name)); + } + } } diff --git a/tests/Feature/Tenant/TenantRefundConfigurationTest.php b/tests/Feature/Tenant/TenantRefundConfigurationTest.php index 38c17e9..1bf3a41 100644 --- a/tests/Feature/Tenant/TenantRefundConfigurationTest.php +++ b/tests/Feature/Tenant/TenantRefundConfigurationTest.php @@ -2,9 +2,9 @@ namespace Tests\Feature\Tenant; +use App\Domains\Core\Tenant\Models\Tenant; use App\Shared\Attachable\Enums\AttachmentType; use App\Shared\Attachable\Models\Attachment; -use App\Domains\Core\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -31,41 +31,6 @@ class TenantRefundConfigurationTest extends TestCase ->assertJsonPath('data.ticket_partial_refund_percentage', '0.00'); } - public function test_refund_configuration_can_be_updated_and_validates_its_precision(): void - { - $tenant = $this->createTenant('refund-update'); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'allow_ticket_refund' => true, - 'allow_ticket_total_refund' => true, - 'allow_ticket_partial_refund' => true, - 'ticket_partial_refund_percentage' => 25.50, - ]) - ->assertOk() - ->assertJsonPath('data.allow_ticket_refund', true) - ->assertJsonPath('data.allow_ticket_total_refund', true) - ->assertJsonPath('data.allow_ticket_partial_refund', true) - ->assertJsonPath('data.ticket_partial_refund_percentage', '25.50'); - - $this->assertDatabaseHas('tenants', [ - 'id' => $tenant->id, - 'allow_ticket_refund' => true, - 'allow_ticket_total_refund' => true, - 'allow_ticket_partial_refund' => true, - 'ticket_partial_refund_percentage' => 25.50, - ]); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'ticket_partial_refund_percentage' => 100, - ])->assertUnprocessable() - ->assertJsonValidationErrors('ticket_partial_refund_percentage'); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'ticket_partial_refund_percentage' => 12.345, - ])->assertUnprocessable() - ->assertJsonValidationErrors('ticket_partial_refund_percentage'); - } - public function test_tenant_allow_refund_logic(): void { $tenant = new Tenant([ diff --git a/tests/Feature/Tenant/TenantSocialMediaTest.php b/tests/Feature/Tenant/TenantSocialMediaTest.php index 464729f..1ba1405 100644 --- a/tests/Feature/Tenant/TenantSocialMediaTest.php +++ b/tests/Feature/Tenant/TenantSocialMediaTest.php @@ -2,10 +2,10 @@ namespace Tests\Feature\Tenant; -use App\Shared\Attachable\Enums\AttachmentType; -use App\Shared\Attachable\Models\Attachment; use App\Domains\Core\Tenant\Models\SocialMedia; use App\Domains\Core\Tenant\Models\Tenant; +use App\Shared\Attachable\Enums\AttachmentType; +use App\Shared\Attachable\Models\Attachment; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Schema; use Tests\TestCase; @@ -79,64 +79,18 @@ class TenantSocialMediaTest extends TestCase ]); } - public function test_tenant_update_synchronizes_social_media_and_returns_pivot_urls(): void - { - $tenant = $this->createTenant(); - $instagram = $this->createSocialMedia('instagram', 'Instagram'); - $facebook = $this->createSocialMedia('facebook', 'Facebook'); - - $tenant->socialMedia()->attach($facebook->code, [ - 'url' => 'https://facebook.com/old-acme', - ]); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [ - [ - 'code' => $instagram->code, - 'url' => 'https://instagram.com/acme', - 'orden' => 4, - ], - ], - ]) - ->assertOk() - ->assertJsonPath('data.social_media.0.code', 'instagram') - ->assertJsonPath('data.social_media.0.icon', 'instagram') - ->assertJsonPath('data.social_media.0.name', 'Instagram') - ->assertJsonPath('data.social_media.0.url', 'https://instagram.com/acme') - ->assertJsonCount(1, 'data.social_media'); - - $this->assertDatabaseHas('tenant_social_media', [ - 'tenant_code' => $tenant->codigo, - 'social_media_code' => $instagram->code, - 'url' => 'https://instagram.com/acme', - 'orden' => 4, - ]); - $this->assertDatabaseMissing('tenant_social_media', [ - 'tenant_code' => $tenant->codigo, - 'social_media_code' => $facebook->code, - ]); - } - public function test_tenant_social_media_are_returned_in_configured_order(): void { $tenant = $this->createTenant(); $instagram = $this->createSocialMedia('instagram', 'Instagram'); $facebook = $this->createSocialMedia('facebook', 'Facebook'); - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [ - [ - 'code' => $instagram->code, - 'url' => 'https://instagram.com/acme', - 'orden' => 20, - ], - [ - 'code' => $facebook->code, - 'url' => 'https://facebook.com/acme', - 'orden' => 10, - ], - ], - ]) + $tenant->socialMedia()->sync([ + $instagram->code => ['url' => 'https://instagram.com/acme', 'orden' => 20], + $facebook->code => ['url' => 'https://facebook.com/acme', 'orden' => 10], + ]); + + $this->getJson("/api/tenants/{$tenant->codigo}") ->assertOk() ->assertJsonPath('data.social_media.0.code', 'facebook') ->assertJsonPath('data.social_media.1.code', 'instagram') @@ -144,80 +98,6 @@ class TenantSocialMediaTest extends TestCase ->assertJsonMissingPath('data.social_media.1.orden'); } - public function test_tenant_store_synchronizes_social_media(): void - { - $instagram = $this->createSocialMedia('instagram', 'Instagram'); - $headerLogo = $this->createAttachment('new-header.png'); - $footerLogo = $this->createAttachment('new-footer.png'); - - $this->postJson('/api/tenants', [ - 'codigo' => 'new-acme', - 'nombre' => 'New Acme', - 'dominio' => 'new-acme.com', - 'primary_color' => '#111111', - 'secondary_color' => '#222222', - 'danger_color' => '#333333', - 'success_color' => '#444444', - 'header_bg_color' => '#ffffff', - 'footer_bg_color' => '#ffffff', - 'header_logo' => $headerLogo->key, - 'footer_logo' => $footerLogo->key, - 'social_media' => [ - [ - 'code' => $instagram->code, - 'url' => 'https://instagram.com/new-acme', - ], - ], - ]) - ->assertCreated() - ->assertJsonPath('data.social_media.0.code', 'instagram') - ->assertJsonPath('data.social_media.0.url', 'https://instagram.com/new-acme'); - - $this->assertDatabaseHas('tenant_social_media', [ - 'tenant_code' => 'new-acme', - 'social_media_code' => $instagram->code, - 'url' => 'https://instagram.com/new-acme', - ]); - } - - public function test_tenant_update_can_clear_social_media(): void - { - $tenant = $this->createTenant(); - $instagram = $this->createSocialMedia('instagram', 'Instagram'); - $tenant->socialMedia()->attach($instagram->code, [ - 'url' => 'https://instagram.com/acme', - ]); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [], - ]) - ->assertOk() - ->assertJsonPath('data.social_media', []); - - $this->assertDatabaseMissing('tenant_social_media', [ - 'tenant_code' => $tenant->codigo, - ]); - } - - public function test_tenant_update_validates_social_media_codes_and_urls(): void - { - $tenant = $this->createTenant(); - - $this->putJson("/api/tenants/{$tenant->codigo}", [ - 'social_media' => [ - [ - 'code' => 'unknown', - 'url' => 'not-a-url', - ], - ], - ]) - ->assertUnprocessable() - ->assertJsonValidationErrors([ - 'social_media.0.code', - 'social_media.0.url', - ]); - } - private function createTenant(): Tenant { $headerLogo = $this->createAttachment('header.png');