feat: implement full CRUD support and database schema for multi-tenant architecture

This commit is contained in:
2026-06-26 16:57:59 -03:00
parent 8708cea122
commit 31b65eb676
10 changed files with 562 additions and 58 deletions

View File

@@ -0,0 +1,357 @@
{
"info": {
"_postman_id": "76df1733-a3d8-4f2b-9ffb-7c70e3032549",
"name": "ShopIt - Tenant CRUD",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "List Tenants",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"url": {
"raw": "{{base_url}}/api/tenants",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants"
]
}
},
"response": []
},
{
"name": "Bootstrap Tenant",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"url": {
"raw": "{{base_url}}/api/tenants/bootstrap/acme.com",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants",
"bootstrap",
"acme.com"
]
}
},
"response": []
},
{
"name": "Create Tenant (JSON)",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
},
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"codigo\": \"acme\",\n \"nombre\": \"Acme Corporation\",\n \"dominio\": \"acme.com\",\n \"primary_color\": \"#111111\",\n \"secondary_color\": \"#222222\",\n \"danger_color\": \"#333333\",\n \"header_footer_bg_color\": \"#444444\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n}"
},
"url": {
"raw": "{{base_url}}/api/tenants",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants"
]
}
},
"response": []
},
{
"name": "Create Tenant (Form Data)",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "codigo",
"value": "acme",
"type": "text"
},
{
"key": "nombre",
"value": "Acme Corporation",
"type": "text"
},
{
"key": "dominio",
"value": "acme.com",
"type": "text"
},
{
"key": "primary_color",
"value": "#111111",
"type": "text"
},
{
"key": "secondary_color",
"value": "#222222",
"type": "text"
},
{
"key": "danger_color",
"value": "#333333",
"type": "text"
},
{
"key": "header_footer_bg_color",
"value": "#444444",
"type": "text"
},
{
"key": "header_logo",
"type": "file",
"src": []
},
{
"key": "footer_logo",
"type": "file",
"src": []
}
]
},
"url": {
"raw": "{{base_url}}/api/tenants",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants"
]
}
},
"response": []
},
{
"name": "Show Tenant",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"url": {
"raw": "{{base_url}}/api/tenants/acme",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants",
"acme"
]
}
},
"response": []
},
{
"name": "Update Tenant (JSON)",
"request": {
"method": "PUT",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
},
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"codigo\": \"acme\",\n \"nombre\": \"Acme Corporation Updated\",\n \"dominio\": \"acme.com\",\n \"primary_color\": \"#555555\",\n \"secondary_color\": \"#666666\",\n \"danger_color\": \"#777777\",\n \"header_footer_bg_color\": \"#888888\"\n}"
},
"url": {
"raw": "{{base_url}}/api/tenants/acme",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants",
"acme"
]
}
},
"response": []
},
{
"name": "Update Tenant (Form Data)",
"request": {
"method": "POST",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "_method",
"value": "PUT",
"type": "text"
},
{
"key": "codigo",
"value": "acme",
"type": "text"
},
{
"key": "nombre",
"value": "Acme Corporation Updated",
"type": "text"
},
{
"key": "dominio",
"value": "acme.com",
"type": "text"
},
{
"key": "primary_color",
"value": "#555555",
"type": "text"
},
{
"key": "secondary_color",
"value": "#666666",
"type": "text"
},
{
"key": "danger_color",
"value": "#777777",
"type": "text"
},
{
"key": "header_footer_bg_color",
"value": "#888888",
"type": "text"
},
{
"key": "header_logo",
"type": "file",
"src": []
},
{
"key": "footer_logo",
"type": "file",
"src": []
}
]
},
"url": {
"raw": "{{base_url}}/api/tenants/acme",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants",
"acme"
]
}
},
"response": []
},
{
"name": "Delete Tenant",
"request": {
"method": "DELETE",
"header": [
{
"key": "Accept",
"value": "application/json",
"type": "text"
}
],
"url": {
"raw": "{{base_url}}/api/tenants/acme",
"host": [
"{{base_url}}"
],
"path": [
"api",
"tenants",
"acme"
]
}
},
"response": []
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"key": "base_url",
"value": "http://localhost:8000",
"type": "string"
}
]
}

View File

@@ -37,7 +37,7 @@ class StoreTenantRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
$logoRule = [ $logoRule = [
'nullable', 'required',
static function (string $attribute, mixed $value, Closure $fail): void { static function (string $attribute, mixed $value, Closure $fail): void {
if ($value instanceof UploadedFile) { if ($value instanceof UploadedFile) {
$mime = $value->getMimeType(); $mime = $value->getMimeType();
@@ -92,15 +92,15 @@ class StoreTenantRequest extends FormRequest
$fail("The {$attribute} field must contain a valid domain or URL."); $fail("The {$attribute} field must contain a valid domain or URL.");
} }
}, },
'nullable', 'required',
'string', 'string',
'max:255', 'max:255',
Rule::unique('tenants', 'dominio'), Rule::unique('tenants', 'dominio'),
], ],
'primary_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], 'primary_color' => ['required', '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})$/'], 'secondary_color' => ['required', '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})$/'], 'danger_color' => ['required', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'],
'header_footer_bg_color' => ['nullable', 'string', 'regex:/^#([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/'], 'header_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, 'header_logo' => $logoRule,
'footer_logo' => $logoRule, 'footer_logo' => $logoRule,
]; ];

View File

@@ -21,15 +21,17 @@ class UpdateTenantRequest extends FormRequest
protected function prepareForValidation(): void protected function prepareForValidation(): void
{ {
$rawDomain = $this->input('dominio'); if ($this->has('dominio')) {
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain); $rawDomain = $this->input('dominio');
$normalizedDomain = TenantDomainNormalizer::normalize($rawDomain);
$this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain) $this->hasInvalidDomain = TenantDomainNormalizer::hasValue($rawDomain)
&& $normalizedDomain === null; && $normalizedDomain === null;
$this->merge([ $this->merge([
'dominio' => $normalizedDomain, 'dominio' => $normalizedDomain,
]); ]);
}
} }
/** /**
@@ -88,12 +90,12 @@ class UpdateTenantRequest extends FormRequest
return [ return [
'codigo' => [ 'codigo' => [
'sometimes', 'nullable',
'string', 'string',
'max:255', 'max:255',
Rule::unique('tenants', 'codigo')->ignore($tenant?->id), Rule::unique('tenants', 'codigo')->ignore($tenant?->id),
], ],
'nombre' => ['sometimes', 'string', 'max:255'], 'nombre' => ['nullable', 'string', 'max:255'],
'dominio' => [ 'dominio' => [
'bail', 'bail',
function (string $attribute, mixed $value, Closure $fail): void { function (string $attribute, mixed $value, Closure $fail): void {

View File

@@ -24,8 +24,9 @@ class TenantResource extends JsonResource
'secondary_color' => $this->secondary_color, 'secondary_color' => $this->secondary_color,
'danger_color' => $this->danger_color, 'danger_color' => $this->danger_color,
'header_footer_bg_color' => $this->header_footer_bg_color, 'header_footer_bg_color' => $this->header_footer_bg_color,
'header_logo' => $this->headerLogo?->getTemporaryUrl(), // 1 day
'footer_logo' => $this->footerLogo?->getTemporaryUrl(), 'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),
'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440 ),
]; ];
} }
} }

View File

@@ -27,33 +27,40 @@ class TenantService
unset($data['header_logo'], $data['footer_logo']); unset($data['header_logo'], $data['footer_logo']);
/** @var Tenant $tenant */ $headerAttachmentId = null;
$tenant = Tenant::query()->create($data);
if ($headerLogo) { if ($headerLogo) {
$attachment = Str::isUuid($headerLogo) $attachment = Str::isUuid($headerLogo)
? \App\Domains\Attachable\Models\Attachment::query()->where('key', $headerLogo)->first() ? \App\Domains\Attachable\Models\Attachment::query()->where('key', $headerLogo)->first()
: $this->attachmentService->store($headerLogo, 'tenants'); : $this->attachmentService->store($headerLogo, 'tenants');
if ($attachment) { if ($attachment) {
$tenant->header_logo_id = $attachment->id; $headerAttachmentId = $attachment->id;
$tenant->attachments()->attach($attachment->id);
} }
} }
$footerAttachmentId = null;
if ($footerLogo) { if ($footerLogo) {
$attachment = Str::isUuid($footerLogo) $attachment = Str::isUuid($footerLogo)
? \App\Domains\Attachable\Models\Attachment::query()->where('key', $footerLogo)->first() ? \App\Domains\Attachable\Models\Attachment::query()->where('key', $footerLogo)->first()
: $this->attachmentService->store($footerLogo, 'tenants'); : $this->attachmentService->store($footerLogo, 'tenants');
if ($attachment) { if ($attachment) {
$tenant->footer_logo_id = $attachment->id; $footerAttachmentId = $attachment->id;
$tenant->attachments()->attach($attachment->id);
} }
} }
if ($headerLogo || $footerLogo) { $data['header_logo_id'] = $headerAttachmentId;
$tenant->save(); $data['footer_logo_id'] = $footerAttachmentId;
/** @var Tenant $tenant */
$tenant = Tenant::query()->create($data);
if ($headerAttachmentId) {
$tenant->attachments()->attach($headerAttachmentId);
}
if ($footerAttachmentId) {
$tenant->attachments()->attach($footerAttachmentId);
} }
return $tenant; return $tenant;

View File

@@ -15,13 +15,13 @@ return new class extends Migration
$table->id(); $table->id();
$table->string('codigo')->unique(); $table->string('codigo')->unique();
$table->string('nombre'); $table->string('nombre');
$table->string('dominio')->nullable(); $table->string('dominio');
$table->string('primary_color')->nullable(); $table->string('primary_color');
$table->string('secondary_color')->nullable(); $table->string('secondary_color');
$table->string('danger_color')->nullable(); $table->string('danger_color');
$table->string('header_footer_bg_color')->nullable(); $table->string('header_footer_bg_color');
$table->unsignedBigInteger('header_logo_id')->nullable(); $table->unsignedBigInteger('header_logo_id');
$table->unsignedBigInteger('footer_logo_id')->nullable(); $table->unsignedBigInteger('footer_logo_id');
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@@ -33,12 +33,12 @@ return new class extends Migration
$table->foreign('header_logo_id') $table->foreign('header_logo_id')
->references('id') ->references('id')
->on('attachments') ->on('attachments')
->nullOnDelete(); ->cascadeOnDelete();
$table->foreign('footer_logo_id') $table->foreign('footer_logo_id')
->references('id') ->references('id')
->on('attachments') ->on('attachments')
->nullOnDelete(); ->cascadeOnDelete();
}); });
} }

View File

@@ -17,11 +17,7 @@ class CartControllerTest extends TestCase
public function test_it_returns_an_empty_guest_cart_when_cart_does_not_exist(): void public function test_it_returns_an_empty_guest_cart_when_cart_does_not_exist(): void
{ {
Tenant::create([ $this->createTenant('acme', 'Acme', 'acme.com');
'codigo' => 'acme',
'nombre' => 'Acme',
'dominio' => 'acme.com',
]);
$this->getJson('/api/tenants/acme/cart') $this->getJson('/api/tenants/acme/cart')
->assertOk() ->assertOk()
@@ -272,10 +268,10 @@ class CartControllerTest extends TestCase
string $price, string $price,
string $slugPrefix = 'shirt', string $slugPrefix = 'shirt',
): ProductVariant { ): ProductVariant {
Tenant::query()->firstOrCreate( $tenant = Tenant::query()->where('codigo', $tenantCode)->first();
['codigo' => $tenantCode], if (! $tenant) {
['nombre' => ucfirst($tenantCode), 'dominio' => "{$tenantCode}.com"], $this->createTenant($tenantCode, ucfirst($tenantCode), "{$tenantCode}.com");
); }
$category = \App\Domains\Catalog\Models\Category::query()->create([ $category = \App\Domains\Catalog\Models\Category::query()->create([
'tenant_code' => $tenantCode, 'tenant_code' => $tenantCode,
@@ -300,4 +296,37 @@ class CartControllerTest extends TestCase
'precio' => $price, 'precio' => $price,
])->load('product'); ])->load('product');
} }
protected function createTenant(string $codigo, string $nombre, string $dominio): Tenant
{
$hdrKey = (string) \Illuminate\Support\Str::uuid();
$ftrKey = (string) \Illuminate\Support\Str::uuid();
$headerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $hdrKey,
'path' => 'tenants/' . $hdrKey . '.png',
'filename' => 'logo_header.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $ftrKey,
'path' => 'tenants/' . $ftrKey . '.png',
'filename' => 'logo_footer.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
return Tenant::create([
'codigo' => $codigo,
'nombre' => $nombre,
'dominio' => $dominio,
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,
]);
}
} }

View File

@@ -12,11 +12,7 @@ class ProductAttributeControllerTest extends TestCase
public function test_it_creates_a_select_attribute_with_options(): void public function test_it_creates_a_select_attribute_with_options(): void
{ {
Tenant::create([ $this->createTenant('acme', 'Acme', 'acme.com');
'codigo' => 'acme',
'nombre' => 'Acme',
'dominio' => 'acme.com',
]);
$response = $this->postJson('/api/tenants/acme/product-attributes', [ $response = $this->postJson('/api/tenants/acme/product-attributes', [
'codigo' => 'color', 'codigo' => 'color',
@@ -62,11 +58,7 @@ class ProductAttributeControllerTest extends TestCase
public function test_it_rejects_options_for_string_attributes(): void public function test_it_rejects_options_for_string_attributes(): void
{ {
Tenant::create([ $this->createTenant('acme', 'Acme', 'acme.com');
'codigo' => 'acme',
'nombre' => 'Acme',
'dominio' => 'acme.com',
]);
$response = $this->postJson('/api/tenants/acme/product-attributes', [ $response = $this->postJson('/api/tenants/acme/product-attributes', [
'codigo' => 'material', 'codigo' => 'material',
@@ -81,4 +73,37 @@ class ProductAttributeControllerTest extends TestCase
->assertUnprocessable() ->assertUnprocessable()
->assertJsonValidationErrors(['options']); ->assertJsonValidationErrors(['options']);
} }
protected function createTenant(string $codigo, string $nombre, string $dominio): Tenant
{
$hdrKey = (string) \Illuminate\Support\Str::uuid();
$ftrKey = (string) \Illuminate\Support\Str::uuid();
$headerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $hdrKey,
'path' => 'tenants/' . $hdrKey . '.png',
'filename' => 'logo_header.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $ftrKey,
'path' => 'tenants/' . $ftrKey . '.png',
'filename' => 'logo_footer.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
return Tenant::create([
'codigo' => $codigo,
'nombre' => $nombre,
'dominio' => $dominio,
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,
]);
}
} }

View File

@@ -73,7 +73,7 @@ class BootstrapTenantControllerTest extends TestCase
public function test_it_bootstraps_a_tenant_from_a_full_url(): void public function test_it_bootstraps_a_tenant_from_a_full_url(): void
{ {
Tenant::create([ $this->createTenant([
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com', 'dominio' => 'acme.com',
@@ -151,6 +151,12 @@ class BootstrapTenantControllerTest extends TestCase
'codigo' => 'globex', 'codigo' => 'globex',
'nombre' => 'Globex', 'nombre' => 'Globex',
'dominio' => 'acme.com', 'dominio' => 'acme.com',
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo' => (string) Str::uuid(),
'footer_logo' => (string) Str::uuid(),
]); ]);
$secondResponse $secondResponse
@@ -160,13 +166,13 @@ class BootstrapTenantControllerTest extends TestCase
public function test_it_allows_keeping_the_same_domain_on_update_but_rejects_collisions(): void public function test_it_allows_keeping_the_same_domain_on_update_but_rejects_collisions(): void
{ {
$tenant = Tenant::create([ $tenant = $this->createTenant([
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com', 'dominio' => 'acme.com',
]); ]);
$otherTenant = Tenant::create([ $otherTenant = $this->createTenant([
'codigo' => 'globex', 'codigo' => 'globex',
'nombre' => 'Globex', 'nombre' => 'Globex',
'dominio' => 'globex.com', 'dominio' => 'globex.com',
@@ -246,7 +252,7 @@ class BootstrapTenantControllerTest extends TestCase
public function test_it_allows_partial_update_without_required_fields(): void public function test_it_allows_partial_update_without_required_fields(): void
{ {
$tenant = Tenant::create([ $tenant = $this->createTenant([
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com', 'dominio' => 'acme.com',
@@ -276,7 +282,13 @@ class BootstrapTenantControllerTest extends TestCase
$response = $this->postJson('/api/tenants', [ $response = $this->postJson('/api/tenants', [
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => 'invalid-color', 'primary_color' => 'invalid-color',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo' => (string) Str::uuid(),
'footer_logo' => (string) Str::uuid(),
]); ]);
$response->assertJsonValidationErrors(['primary_color']); $response->assertJsonValidationErrors(['primary_color']);
@@ -284,7 +296,13 @@ class BootstrapTenantControllerTest extends TestCase
$response2 = $this->postJson('/api/tenants', [ $response2 = $this->postJson('/api/tenants', [
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => '#12345', 'primary_color' => '#12345',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo' => (string) Str::uuid(),
'footer_logo' => (string) Str::uuid(),
]); ]);
$response2->assertJsonValidationErrors(['primary_color']); $response2->assertJsonValidationErrors(['primary_color']);
@@ -297,7 +315,13 @@ class BootstrapTenantControllerTest extends TestCase
$response = $this->postJson('/api/tenants', [ $response = $this->postJson('/api/tenants', [
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo' => UploadedFile::fake()->create('document.pdf', 10, 'application/pdf'), 'header_logo' => UploadedFile::fake()->create('document.pdf', 10, 'application/pdf'),
'footer_logo' => (string) Str::uuid(),
]); ]);
$response->assertJsonValidationErrors(['header_logo']); $response->assertJsonValidationErrors(['header_logo']);
@@ -305,7 +329,13 @@ class BootstrapTenantControllerTest extends TestCase
$response2 = $this->postJson('/api/tenants', [ $response2 = $this->postJson('/api/tenants', [
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo' => 'data:application/pdf;base64,JVBERi0xLjQKJdcfqksKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAvUGFnZXMgMiAwIFI...', 'header_logo' => 'data:application/pdf;base64,JVBERi0xLjQKJdcfqksKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAvUGFnZXMgMiAwIFI...',
'footer_logo' => (string) Str::uuid(),
]); ]);
$response2->assertJsonValidationErrors(['header_logo']); $response2->assertJsonValidationErrors(['header_logo']);
@@ -321,6 +351,11 @@ class BootstrapTenantControllerTest extends TestCase
$response = $this->postJson('/api/tenants', [ $response = $this->postJson('/api/tenants', [
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo' => $header, 'header_logo' => $header,
'footer_logo' => $footer, 'footer_logo' => $footer,
]); ]);
@@ -348,16 +383,64 @@ class BootstrapTenantControllerTest extends TestCase
$this->assertDatabaseHas('attachments', ['key' => $tenant->footerLogo->key]); $this->assertDatabaseHas('attachments', ['key' => $tenant->footerLogo->key]);
} }
private function createTenant(array $attributes = []): Tenant
{
$hdrKey = (string) Str::uuid();
$ftrKey = (string) Str::uuid();
$headerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $hdrKey,
'path' => 'tenants/' . $hdrKey . '.png',
'filename' => 'logo_header.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $ftrKey,
'path' => 'tenants/' . $ftrKey . '.png',
'filename' => 'logo_footer.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
return Tenant::create(array_merge([
'codigo' => 'acme',
'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => '#ff0000',
'secondary_color' => '#00ff00',
'danger_color' => '#0000ff',
'header_footer_bg_color' => '#ffffff',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,
], $attributes));
}
public function test_it_stores_base64_logos_in_tenants_directory(): void public function test_it_stores_base64_logos_in_tenants_directory(): void
{ {
Storage::fake('s3'); Storage::fake('s3');
$base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
$ftrUuid = (string) Str::uuid();
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $ftrUuid,
'path' => 'tenants/' . $ftrUuid . '.png',
'filename' => 'logo_footer.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
$response = $this->postJson('/api/tenants', [ $response = $this->postJson('/api/tenants', [
'codigo' => 'acme', 'codigo' => 'acme',
'nombre' => 'Acme', 'nombre' => 'Acme',
'dominio' => 'acme.com',
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'header_footer_bg_color' => '#444444',
'header_logo' => $base64Image, 'header_logo' => $base64Image,
'footer_logo' => $ftrUuid,
]); ]);
$response->assertCreated(); $response->assertCreated();