refactor(tenants): remove general write endpoints
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
namespace App\Domains\Core\Tenant\Controllers;
|
||||
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Core\Tenant\Requests\StoreTenantRequest;
|
||||
use App\Domains\Core\Tenant\Requests\UpdateTenantRequest;
|
||||
use App\Domains\Core\Tenant\Resources\TenantResource;
|
||||
use App\Domains\Core\Tenant\Services\TenantInformationService;
|
||||
use App\Domains\Core\Tenant\Services\TenantService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -15,7 +12,6 @@ use Illuminate\Http\Response;
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected TenantService $tenantService,
|
||||
protected TenantInformationService $tenantInformationService,
|
||||
) {}
|
||||
|
||||
@@ -30,15 +26,6 @@ class TenantController extends Controller
|
||||
return TenantResource::collection($tenants)->response();
|
||||
}
|
||||
|
||||
public function store(StoreTenantRequest $request): JsonResponse
|
||||
{
|
||||
$tenant = $this->tenantService->create($request->validated());
|
||||
|
||||
return TenantResource::make(
|
||||
$this->tenantInformationService->load($tenant)
|
||||
)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Tenant $tenant): TenantResource
|
||||
{
|
||||
return TenantResource::make(
|
||||
@@ -46,15 +33,6 @@ class TenantController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function update(UpdateTenantRequest $request, Tenant $tenant): TenantResource
|
||||
{
|
||||
$tenant = $this->tenantService->update($tenant, $request->validated());
|
||||
|
||||
return TenantResource::make(
|
||||
$this->tenantInformationService->load($tenant)
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(Tenant $tenant): Response
|
||||
{
|
||||
$tenant->delete();
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Core\Tenant\Requests;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
|
||||
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
||||
use App\Shared\Rules\ImageOrBase64Rule;
|
||||
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
|
||||
use App\Domains\Core\Tenant\Services\WebsiteExtraService;
|
||||
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreTenantRequest extends FormRequest
|
||||
{
|
||||
protected bool $hasInvalidDomain = false;
|
||||
|
||||
protected bool $hasInvalidBasePath = false;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$rawDomain = $this->input('dominio');
|
||||
$hasExplicitBasePath = $this->has('base_path');
|
||||
$rawBasePath = $hasExplicitBasePath
|
||||
? $this->input('base_path')
|
||||
: TenantDomainNormalizer::pathFromDomain($rawDomain);
|
||||
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
|
||||
$normalizedBasePath = TenantDomainNormalizer::normalizePath($rawBasePath);
|
||||
|
||||
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
|
||||
&& $normalizedDomain === null;
|
||||
$this->hasInvalidBasePath = ($hasExplicitBasePath && ! is_string($rawBasePath))
|
||||
|| $normalizedBasePath === null;
|
||||
|
||||
$this->merge([
|
||||
'dominio' => $normalizedDomain,
|
||||
'base_path' => $normalizedBasePath,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$logoRule = ['required', new ImageOrBase64Rule];
|
||||
|
||||
return array_merge([
|
||||
'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')],
|
||||
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenants', 'codigo')],
|
||||
'nombre' => ['required', 'string', 'max:255'],
|
||||
'dominio' => [
|
||||
'bail',
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if ($this->hasInvalidDomain) {
|
||||
$fail("The {$attribute} field must contain a valid domain or URL.");
|
||||
}
|
||||
},
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('tenants', 'dominio')
|
||||
->where('base_path', $this->input('base_path')),
|
||||
],
|
||||
'base_path' => [
|
||||
'bail',
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if ($this->hasInvalidBasePath) {
|
||||
$fail("The {$attribute} field must contain a valid URL path.");
|
||||
}
|
||||
},
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('tenants', 'base_path')
|
||||
->where('dominio', $this->input('dominio')),
|
||||
],
|
||||
'site_title' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'address' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'phone' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'primary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'secondary_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'danger_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'success_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_bg_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'footer_bg_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_logo' => $logoRule,
|
||||
'footer_logo' => $logoRule,
|
||||
'favicon' => ['sometimes', 'nullable', new ImageOrBase64Rule],
|
||||
'header_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
|
||||
'footer_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
|
||||
'social_media' => ['sometimes', 'array'],
|
||||
'social_media.*.code' => [
|
||||
'required',
|
||||
'string',
|
||||
'distinct',
|
||||
Rule::exists('social_media', 'code'),
|
||||
],
|
||||
'social_media.*.url' => ['required', 'url', 'max:2048'],
|
||||
'social_media.*.orden' => ['sometimes', 'integer', 'min:0', 'distinct'],
|
||||
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
||||
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
||||
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
||||
'display_categories' => ['sometimes', 'boolean'],
|
||||
'display_seach_bar' => ['sometimes', 'boolean'],
|
||||
'display_cart' => ['sometimes', 'boolean'],
|
||||
'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)],
|
||||
'checkout_editing_policy' => [
|
||||
'sometimes',
|
||||
Rule::in([CartEditingPolicy::Disabled->value]),
|
||||
],
|
||||
'display_cart_item_images' => ['sometimes', 'boolean'],
|
||||
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
|
||||
'allow_ticket_refund' => ['sometimes', 'boolean'],
|
||||
'allow_ticket_total_refund' => ['sometimes', 'boolean'],
|
||||
'allow_ticket_partial_refund' => ['sometimes', 'boolean'],
|
||||
'ticket_partial_refund_percentage' => [
|
||||
'sometimes',
|
||||
'numeric',
|
||||
'decimal:0,2',
|
||||
'min:0',
|
||||
'max:99.99',
|
||||
],
|
||||
'storefront_website_type_code' => [
|
||||
'required_with:extras',
|
||||
'sometimes',
|
||||
'string',
|
||||
Rule::exists('storefront_website_types', 'codigo'),
|
||||
],
|
||||
'admin_website_type_code' => ['sometimes', 'nullable', 'string', Rule::exists('admin_website_types', 'codigo')],
|
||||
], app(WebsiteExtraService::class)->requestRules($this->input('storefront_website_type_code')));
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Core\Tenant\Requests;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Enums\GroupLayout;
|
||||
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
||||
use App\Shared\Rules\ImageOrBase64Rule;
|
||||
use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Core\Tenant\Support\TenantDomainNormalizer;
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateTenantRequest extends FormRequest
|
||||
{
|
||||
protected bool $hasInvalidDomain = false;
|
||||
|
||||
protected bool $hasInvalidBasePath = false;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('dominio')) {
|
||||
$rawDomain = $this->input('dominio');
|
||||
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
|
||||
$embeddedBasePath = TenantDomainNormalizer::pathFromDomain($rawDomain);
|
||||
|
||||
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
|
||||
&& ($normalizedDomain === null || $embeddedBasePath === null);
|
||||
|
||||
$this->merge([
|
||||
'dominio' => $normalizedDomain,
|
||||
]);
|
||||
|
||||
if (! $this->has('base_path') && $embeddedBasePath !== null && $embeddedBasePath !== '/') {
|
||||
$this->merge(['base_path' => $embeddedBasePath]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->has('base_path')) {
|
||||
$rawBasePath = $this->input('base_path');
|
||||
$normalizedBasePath = TenantDomainNormalizer::normalizePath($rawBasePath);
|
||||
|
||||
$this->hasInvalidBasePath = ! is_string($rawBasePath)
|
||||
|| $normalizedBasePath === null;
|
||||
|
||||
$this->merge(['base_path' => $normalizedBasePath]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var Tenant|null $tenant */
|
||||
$tenant = $this->route('tenant');
|
||||
$domain = $this->input('dominio', $tenant?->dominio);
|
||||
$basePath = $this->input('base_path', $tenant?->base_path ?? '/');
|
||||
|
||||
$logoRule = ['nullable', new ImageOrBase64Rule];
|
||||
|
||||
return [
|
||||
'client_id' => ['sometimes', 'integer', Rule::exists('clients', 'id')],
|
||||
'codigo' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('tenants', 'codigo')->ignore($tenant?->id),
|
||||
],
|
||||
'nombre' => ['nullable', 'string', 'max:255'],
|
||||
'dominio' => [
|
||||
'bail',
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if ($this->hasInvalidDomain) {
|
||||
$fail("The {$attribute} field must contain a valid domain or URL.");
|
||||
}
|
||||
},
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('tenants', 'dominio')
|
||||
->where('base_path', $basePath)
|
||||
->ignore($tenant?->id),
|
||||
],
|
||||
'base_path' => [
|
||||
'bail',
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if ($this->hasInvalidBasePath) {
|
||||
$fail("The {$attribute} field must contain a valid URL path.");
|
||||
}
|
||||
},
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('tenants', 'base_path')
|
||||
->where('dominio', $domain)
|
||||
->ignore($tenant?->id),
|
||||
],
|
||||
'site_title' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'address' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'phone' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'primary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'secondary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'danger_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'success_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'footer_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
|
||||
'header_logo' => $logoRule,
|
||||
'footer_logo' => $logoRule,
|
||||
'favicon' => ['sometimes', 'nullable', new ImageOrBase64Rule],
|
||||
'header_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
|
||||
'footer_bg_image' => ['sometimes', 'nullable', new ImageOrBase64Rule],
|
||||
'social_media' => ['sometimes', 'array'],
|
||||
'social_media.*.code' => [
|
||||
'required',
|
||||
'string',
|
||||
'distinct',
|
||||
Rule::exists('social_media', 'code'),
|
||||
],
|
||||
'social_media.*.url' => ['required', 'url', 'max:2048'],
|
||||
'social_media.*.orden' => ['sometimes', 'integer', 'min:0', 'distinct'],
|
||||
'search_product_layout' => ['sometimes', Rule::enum(ProductLayout::class)],
|
||||
'search_group_layout' => ['sometimes', Rule::enum(GroupLayout::class)],
|
||||
'search_items_per_page' => ['sometimes', 'integer', 'min:4', 'max:48'],
|
||||
'display_categories' => ['sometimes', 'boolean'],
|
||||
'display_seach_bar' => ['sometimes', 'boolean'],
|
||||
'display_cart' => ['sometimes', 'boolean'],
|
||||
'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)],
|
||||
'checkout_editing_policy' => [
|
||||
'sometimes',
|
||||
Rule::in([CartEditingPolicy::Disabled->value]),
|
||||
],
|
||||
'display_cart_item_images' => ['sometimes', 'boolean'],
|
||||
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
|
||||
'allow_ticket_refund' => ['sometimes', 'boolean'],
|
||||
'allow_ticket_total_refund' => ['sometimes', 'boolean'],
|
||||
'allow_ticket_partial_refund' => ['sometimes', 'boolean'],
|
||||
'ticket_partial_refund_percentage' => [
|
||||
'sometimes',
|
||||
'numeric',
|
||||
'decimal:0,2',
|
||||
'min:0',
|
||||
'max:99.99',
|
||||
],
|
||||
'admin_website_type_code' => ['sometimes', 'nullable', 'string', Rule::exists('admin_website_types', 'codigo')],
|
||||
'storefront_website_type_code' => [
|
||||
'sometimes',
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::exists('storefront_website_types', 'codigo'),
|
||||
function (string $attribute, mixed $value, Closure $fail) use ($tenant): void {
|
||||
if ($tenant && $value !== $tenant->storefront_website_type_code && $tenant->websiteExtras()->exists()) {
|
||||
$fail('The storefront website type cannot be changed while the tenant has extras.');
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Core\Tenant\Services;
|
||||
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Shared\Attachable\Services\AttachmentService;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TenantService
|
||||
{
|
||||
public function __construct(
|
||||
protected AttachmentService $attachmentService,
|
||||
protected WebsiteExtraService $websiteExtraService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new tenant and store its logos.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function create(array $data): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($data): Tenant {
|
||||
$headerLogo = $data['header_logo'] ?? null;
|
||||
$footerLogo = $data['footer_logo'] ?? null;
|
||||
$favicon = $data['favicon'] ?? null;
|
||||
$headerBackgroundImage = $data['header_bg_image'] ?? null;
|
||||
$footerBackgroundImage = $data['footer_bg_image'] ?? null;
|
||||
$socialMedia = $data['social_media'] ?? [];
|
||||
$extras = $data['extras'] ?? [];
|
||||
|
||||
unset(
|
||||
$data['header_logo'],
|
||||
$data['footer_logo'],
|
||||
$data['favicon'],
|
||||
$data['header_bg_image'],
|
||||
$data['footer_bg_image'],
|
||||
$data['social_media'],
|
||||
$data['extras'],
|
||||
);
|
||||
|
||||
$headerAttachmentId = null;
|
||||
if ($headerLogo) {
|
||||
$attachment = Str::isUuid($headerLogo)
|
||||
? Attachment::query()->where('key', $headerLogo)->first()
|
||||
: $this->attachmentService->store($headerLogo, 'tenants');
|
||||
|
||||
if ($attachment) {
|
||||
$headerAttachmentId = $attachment->id;
|
||||
}
|
||||
}
|
||||
|
||||
$footerAttachmentId = null;
|
||||
if ($footerLogo) {
|
||||
$attachment = Str::isUuid($footerLogo)
|
||||
? Attachment::query()->where('key', $footerLogo)->first()
|
||||
: $this->attachmentService->store($footerLogo, 'tenants');
|
||||
|
||||
if ($attachment) {
|
||||
$footerAttachmentId = $attachment->id;
|
||||
}
|
||||
}
|
||||
|
||||
$data['header_logo_id'] = $headerAttachmentId;
|
||||
$data['footer_logo_id'] = $footerAttachmentId;
|
||||
$data['favicon_id'] = $this->storeTenantImage($favicon);
|
||||
$data['header_bg_image_id'] = $this->storeTenantImage($headerBackgroundImage);
|
||||
$data['footer_bg_image_id'] = $this->storeTenantImage($footerBackgroundImage);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::query()->create($data);
|
||||
$this->syncSocialMedia($tenant, $socialMedia);
|
||||
$this->websiteExtraService->createForTenant($tenant, $extras);
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing tenant and store new logos if uploaded.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function update(Tenant $tenant, array $data): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($tenant, $data): Tenant {
|
||||
$hasHeaderLogoKey = array_key_exists('header_logo', $data);
|
||||
$hasFooterLogoKey = array_key_exists('footer_logo', $data);
|
||||
$hasFaviconKey = array_key_exists('favicon', $data);
|
||||
$hasHeaderBackgroundImageKey = array_key_exists('header_bg_image', $data);
|
||||
$hasFooterBackgroundImageKey = array_key_exists('footer_bg_image', $data);
|
||||
$hasSocialMediaKey = array_key_exists('social_media', $data);
|
||||
$headerLogo = $data['header_logo'] ?? null;
|
||||
$footerLogo = $data['footer_logo'] ?? null;
|
||||
$favicon = $data['favicon'] ?? null;
|
||||
$headerBackgroundImage = $data['header_bg_image'] ?? null;
|
||||
$footerBackgroundImage = $data['footer_bg_image'] ?? null;
|
||||
$socialMedia = $data['social_media'] ?? [];
|
||||
|
||||
unset(
|
||||
$data['header_logo'],
|
||||
$data['footer_logo'],
|
||||
$data['favicon'],
|
||||
$data['header_bg_image'],
|
||||
$data['footer_bg_image'],
|
||||
$data['social_media']
|
||||
);
|
||||
|
||||
$tenant->fill($data);
|
||||
|
||||
if ($hasHeaderLogoKey) {
|
||||
if ($headerLogo) {
|
||||
$attachment = Str::isUuid($headerLogo)
|
||||
? Attachment::query()->where('key', $headerLogo)->first()
|
||||
: $this->attachmentService->store($headerLogo, 'tenants');
|
||||
|
||||
if ($attachment) {
|
||||
$tenant->header_logo_id = $attachment->id;
|
||||
} else {
|
||||
$tenant->header_logo_id = null;
|
||||
}
|
||||
} else {
|
||||
$tenant->header_logo_id = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasFooterLogoKey) {
|
||||
if ($footerLogo) {
|
||||
$attachment = Str::isUuid($footerLogo)
|
||||
? Attachment::query()->where('key', $footerLogo)->first()
|
||||
: $this->attachmentService->store($footerLogo, 'tenants');
|
||||
|
||||
if ($attachment) {
|
||||
$tenant->footer_logo_id = $attachment->id;
|
||||
} else {
|
||||
$tenant->footer_logo_id = null;
|
||||
}
|
||||
} else {
|
||||
$tenant->footer_logo_id = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasFaviconKey) {
|
||||
$tenant->favicon_id = $this->storeTenantImage($favicon);
|
||||
}
|
||||
|
||||
if ($hasHeaderBackgroundImageKey) {
|
||||
$tenant->header_bg_image_id = $this->storeTenantImage($headerBackgroundImage);
|
||||
}
|
||||
|
||||
if ($hasFooterBackgroundImageKey) {
|
||||
$tenant->footer_bg_image_id = $this->storeTenantImage($footerBackgroundImage);
|
||||
}
|
||||
|
||||
$tenant->save();
|
||||
|
||||
if ($hasSocialMediaKey) {
|
||||
$this->syncSocialMedia($tenant, $socialMedia);
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{code: string, url: string, orden?: int}> $socialMedia
|
||||
*/
|
||||
private function syncSocialMedia(Tenant $tenant, array $socialMedia): void
|
||||
{
|
||||
$owner = $tenant->activeEvent ?? $tenant;
|
||||
$associations = [];
|
||||
|
||||
foreach (array_values($socialMedia) as $index => $item) {
|
||||
$associations[$item['code']] = [
|
||||
'url' => $item['url'],
|
||||
'orden' => $item['orden'] ?? $index,
|
||||
];
|
||||
}
|
||||
|
||||
$owner->socialMedia()->sync($associations);
|
||||
$owner->unsetRelation('socialMedia');
|
||||
}
|
||||
|
||||
private function storeTenantImage(mixed $image): ?int
|
||||
{
|
||||
if (! $image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attachment = is_string($image) && Str::isUuid($image)
|
||||
? Attachment::query()->where('key', $image)->first()
|
||||
: $this->attachmentService->store($image, 'tenants');
|
||||
|
||||
return $attachment?->id;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,14 @@
|
||||
|
||||
namespace App\Domains\Core\Tenant\Services;
|
||||
|
||||
use App\Domains\Core\Tenant\Models\StorefrontWebsiteTypeExtra;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Core\Tenant\Models\WebsiteExtra;
|
||||
use App\Shared\Attachable\Enums\AttachmentType;
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Shared\Attachable\Services\AttachmentService;
|
||||
use App\Shared\Rules\CroppedImageOrBase64Rule;
|
||||
use App\Shared\Rules\ImageOrBase64Rule;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Core\Tenant\Models\WebsiteExtra;
|
||||
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
|
||||
use App\Domains\Core\Tenant\Models\StorefrontWebsiteTypeExtra;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -21,109 +19,6 @@ class WebsiteExtraService
|
||||
{
|
||||
public function __construct(protected AttachmentService $attachmentService) {}
|
||||
|
||||
/**
|
||||
* Build the request rules declared by the selected website type.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function requestRules(?string $websiteTypeCode): array
|
||||
{
|
||||
if (! is_string($websiteTypeCode) || $websiteTypeCode === '') {
|
||||
return [
|
||||
'extras' => ['prohibited'],
|
||||
];
|
||||
}
|
||||
|
||||
$definitions = $this->definitionsFor($websiteTypeCode);
|
||||
$allowedCodes = $definitions->pluck('codigo')->all();
|
||||
$hasRequiredExtras = $definitions->contains(
|
||||
fn (StorefrontWebsiteTypeExtra $definition): bool => $definition->is_required
|
||||
);
|
||||
|
||||
$rules = [
|
||||
'extras' => [
|
||||
$hasRequiredExtras ? 'required' : 'sometimes',
|
||||
'array',
|
||||
function (string $attribute, mixed $value, \Closure $fail) use ($allowedCodes): void {
|
||||
if (! is_array($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unknownCodes = array_diff(array_keys($value), $allowedCodes);
|
||||
|
||||
if ($unknownCodes !== []) {
|
||||
$fail(
|
||||
'The '.$attribute.' field contains extras not supported by the website type: '
|
||||
.implode(', ', $unknownCodes).'.'
|
||||
);
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($definitions as $definition) {
|
||||
$schemaRules = $definition->config_schema['request_rules'] ?? [];
|
||||
$rootRules = $this->compileRules($schemaRules['$'] ?? []);
|
||||
$rootRules = array_values(array_filter(
|
||||
$rootRules,
|
||||
fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true)
|
||||
));
|
||||
array_unshift($rootRules, $definition->is_required ? 'required' : 'sometimes');
|
||||
|
||||
$rules["extras.{$definition->codigo}"] = $rootRules;
|
||||
|
||||
foreach ($schemaRules as $path => $pathRules) {
|
||||
if ($path === '$') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rules[$this->requestAttribute($definition->codigo, $path)] = $this->compileRules($pathRules);
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform and persist each extra selected for a tenant.
|
||||
*
|
||||
* @param array<string, mixed> $extras
|
||||
*/
|
||||
public function createForTenant(Tenant $tenant, array $extras): void
|
||||
{
|
||||
if ($extras === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definitions = $this->definitionsFor((string) $tenant->storefront_website_type_code)
|
||||
->keyBy('codigo');
|
||||
|
||||
foreach ($extras as $name => $config) {
|
||||
/** @var StorefrontWebsiteTypeExtra|null $definition */
|
||||
$definition = $definitions->get($name);
|
||||
|
||||
if (! $definition) {
|
||||
throw ValidationException::withMessages([
|
||||
'extras' => ["The extra {$name} is not supported by the selected website type."],
|
||||
]);
|
||||
}
|
||||
|
||||
$requestRoot = "extras.{$definition->codigo}";
|
||||
$transformedConfig = $this->applyTransforms(
|
||||
$tenant,
|
||||
$definition,
|
||||
$config,
|
||||
$requestRoot
|
||||
);
|
||||
$tenant->websiteExtras()->create([
|
||||
'website_type_extra_id' => $definition->id,
|
||||
'config' => $transformedConfig,
|
||||
]);
|
||||
}
|
||||
|
||||
$tenant->unsetRelation('websiteExtras');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build request rules for one extra addressed by its stable code.
|
||||
*
|
||||
@@ -201,19 +96,6 @@ class WebsiteExtraService
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, StorefrontWebsiteTypeExtra>
|
||||
*/
|
||||
private function definitionsFor(string $websiteTypeCode): Collection
|
||||
{
|
||||
$websiteType = StorefrontWebsiteType::query()
|
||||
->where('codigo', $websiteTypeCode)
|
||||
->with('extras')
|
||||
->first();
|
||||
|
||||
return $websiteType?->extras ?? collect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array<int, mixed> $rules
|
||||
* @return array<int, mixed>
|
||||
@@ -232,11 +114,6 @@ class WebsiteExtraService
|
||||
);
|
||||
}
|
||||
|
||||
private function requestAttribute(string $extraCode, string $path): string
|
||||
{
|
||||
return $this->configAttribute("extras.{$extraCode}", $path);
|
||||
}
|
||||
|
||||
private function configAttribute(string $root, string $path): string
|
||||
{
|
||||
if ($path === '$') {
|
||||
|
||||
@@ -15,7 +15,6 @@ Es la raíz del modelo multi-tenant. Gestiona organizaciones/sitios, tipos de we
|
||||
|
||||
## Servicios
|
||||
|
||||
- `TenantService`: crea y actualiza tenants, incluyendo sus recursos asociados.
|
||||
- `TenantInformationService`: carga un tenant y las relaciones requeridas por cada contexto.
|
||||
- `AdminWebsiteTypeService`: crea o actualiza los tipos de admin y su marca.
|
||||
- `WebsiteExtraService`: construye reglas dinámicas, crea, actualiza y habilita/deshabilita extras.
|
||||
@@ -25,7 +24,8 @@ El par `(dominio, base_path)` es único. Un mismo dominio puede alojar el tenant
|
||||
|
||||
## Endpoints
|
||||
|
||||
- Recurso REST público/administrativo `/tenants`.
|
||||
- `GET /tenants`, `GET /tenants/{codigo}` y `DELETE /tenants/{codigo}`.
|
||||
- La creación y actualización general de tenants se administran mediante seeders o base de datos; no se exponen por API.
|
||||
- Bajo `/v1/adminapp/tenant/website-extras`, con autenticación y contexto de tenant: consulta general, detalle, actualización y activación/desactivación.
|
||||
|
||||
## Dependencias y reglas
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
use App\Domains\Core\Tenant\Controllers\TenantController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::apiResource('tenants', TenantController::class);
|
||||
Route::apiResource('tenants', TenantController::class)->except(['store', 'update']);
|
||||
|
||||
require __DIR__.'/adminapp.php';
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Core\Tenant\Services\TenantInformationService;
|
||||
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(), [
|
||||
'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' => '<h1>Festival</h1>',
|
||||
'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<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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user