feat(tenant): provision Mutual SMEP tenant with branding and menu setup

This commit is contained in:
2026-09-24 11:29:40 -03:00
parent e259bba1d1
commit 2db83271fa
6 changed files with 566 additions and 2 deletions

View File

@@ -0,0 +1,326 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
return new class extends Migration
{
private const CLIENT_CODE = 'mutual_smep';
private const TENANT_CODE = 'mutual_smep';
private const EXCLUDED_MENU_CODES = [
'account.tickets',
'adminapp.tickets',
'adminapp.fiesta-futbol-infantil.entradas',
'adminapp.fiesta-futbol-infantil.alojamientos',
'adminapp.fiesta-futbol-infantil.merchandising',
'adminapp.fiesta-futbol-infantil.comida',
'adminapp.desfile.entradas',
'event.index',
'event.category',
'event.detail',
];
/** @var list<string> */
private array $storedPaths = [];
public function up(): void
{
if (DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
return;
}
if (
! DB::table('admin_website_types')->where('codigo', 'shopit')->exists()
|| ! DB::table('storefront_website_types')->where('codigo', 'shopit')->exists()
) {
// Fresh installations provision website-type reference data separately.
return;
}
try {
DB::transaction(function (): void {
$clientId = $this->clientId();
$headerLogoId = $this->storeImage(
'images/tennants/mutual_smep/mutual_smep_header.png',
'mutual_smep_header.png',
);
$footerLogoId = $this->storeImage(
'images/tennants/mutual_smep/mutual_smep_footer.png',
'mutual_smep_footer.png',
);
$faviconId = $this->storeImage(
'images/tennants/mutual_smep/mutual_smep_favicon.png',
'mutual_smep_favicon.png',
);
$now = now();
DB::table('tenants')->insert([
'client_id' => $clientId,
'codigo' => self::TENANT_CODE,
'nombre' => 'Mutual SMEP',
'timezone' => 'America/Argentina/Buenos_Aires',
'dominio' => 'mutual-smep.localhost',
'base_path' => '/',
'site_title' => 'Tienda Mutual SMEP',
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
'phone' => '+54 9 341 247-4530',
'primary_color' => '#4A7FF5',
'secondary_color' => '#0051A4',
'danger_color' => '#FF8888',
'success_color' => '#198754',
'header_bg_color' => '#FFFFFF',
'footer_bg_color' => '#0051A4',
'header_logo_id' => $headerLogoId,
'footer_logo_id' => $footerLogoId,
'favicon_id' => $faviconId,
'admin_website_type_code' => 'shopit',
'storefront_website_type_code' => 'shopit',
'display_categories' => true,
'display_seach_bar' => true,
'display_cart' => true,
'cart_editing_policy' => 'quantity_and_remove',
'checkout_editing_policy' => 'disabled',
'created_at' => $now,
'updated_at' => $now,
]);
$this->assignSocialMedia($now);
$this->assignMenus($now);
});
} catch (Throwable $throwable) {
Storage::disk('s3')->delete($this->storedPaths);
throw $throwable;
}
}
public function down(): void
{
$tenant = DB::table('tenants')
->where('codigo', self::TENANT_CODE)
->first(['id', 'client_id', 'header_logo_id', 'footer_logo_id', 'favicon_id']);
if ($tenant === null) {
return;
}
$attachmentIds = collect([
$tenant->header_logo_id,
$tenant->footer_logo_id,
$tenant->favicon_id,
])->filter()->unique()->values();
$storedPaths = DB::table('attachments')
->whereIn('id', $attachmentIds)
->pluck('path')
->all();
DB::transaction(function () use ($tenant, $attachmentIds): void {
DB::table('tenant_social_media')
->where('tenant_code', self::TENANT_CODE)
->delete();
DB::table('tenants_menues')
->where('tenant_code', self::TENANT_CODE)
->delete();
DB::table('tenants')
->where('id', $tenant->id)
->delete();
DB::table('attachments')
->whereIn('id', $attachmentIds)
->delete();
$clientHasTenants = DB::table('tenants')
->where('client_id', $tenant->client_id)
->exists();
if (! $clientHasTenants) {
DB::table('clients')
->where('id', $tenant->client_id)
->where('code', self::CLIENT_CODE)
->delete();
}
});
Storage::disk('s3')->delete($storedPaths);
}
private function clientId(): int
{
$clientId = DB::table('clients')->where('code', self::CLIENT_CODE)->value('id');
if ($clientId !== null) {
DB::table('clients')->where('id', $clientId)->update([
'name' => 'Mutual SMEP',
'updated_at' => now(),
]);
return (int) $clientId;
}
return (int) DB::table('clients')->insertGetId([
'code' => self::CLIENT_CODE,
'name' => 'Mutual SMEP',
'created_at' => now(),
'updated_at' => now(),
]);
}
private function assignSocialMedia(DateTimeInterface $now): void
{
$socialMedia = [
[
'code' => 'instagram',
'icon' => 'fa-brands fa-instagram',
'name' => 'Instagram',
'url' => 'https://www.instagram.com/mutualsmep/',
'orden' => 0,
],
[
'code' => 'facebook',
'icon' => 'fa-brands fa-facebook',
'name' => 'Facebook',
'url' => 'https://www.facebook.com/smeprosario',
'orden' => 1,
],
[
'code' => 'whatsapp',
'icon' => 'fa-brands fa-whatsapp',
'name' => 'WhatsApp',
'url' => 'https://wa.me/5493412474530',
'orden' => 2,
],
];
foreach ($socialMedia as $network) {
DB::table('social_media')->insertOrIgnore([
'code' => $network['code'],
'icon' => $network['icon'],
'name' => $network['name'],
'created_at' => $now,
'updated_at' => $now,
]);
DB::table('tenant_social_media')->insertOrIgnore([
'tenant_code' => self::TENANT_CODE,
'social_media_code' => $network['code'],
'url' => $network['url'],
'orden' => $network['orden'],
'created_at' => $now,
'updated_at' => $now,
]);
}
}
private function assignMenus(DateTimeInterface $now): void
{
DB::table('menues')
->whereNotIn('code', self::EXCLUDED_MENU_CODES)
->pluck('code')
->each(function (string $menuCode) use ($now): void {
$staticContent = match ($menuCode) {
'help.faq' => $this->frequentlyAskedQuestions(),
'help.contact' => $this->contactContent(),
default => null,
};
DB::table('tenants_menues')->updateOrInsert(
[
'tenant_code' => self::TENANT_CODE,
'menu_code' => $menuCode,
],
[
'static_content' => $staticContent === null
? null
: json_encode($staticContent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'created_at' => $now,
'updated_at' => $now,
],
);
});
}
/** @return list<array{pregunta: string, respuesta: string, is_active: bool}> */
private function frequentlyAskedQuestions(): array
{
return [
[
'pregunta' => '¿Hay algún límite de compra?',
'respuesta' => 'La cantidad disponible depende del stock de cada producto.',
'is_active' => true,
],
[
'pregunta' => '¿Cuáles son los medios de pago disponibles?',
'respuesta' => 'Podés consultar y seleccionar los medios de pago habilitados al finalizar tu compra.',
'is_active' => false,
],
[
'pregunta' => '¿Cómo puedo recibir asesoramiento antes de comprar?',
'respuesta' => 'Podés comunicarte con Mutual SMEP por WhatsApp al +54 9 341 247-4530.',
'is_active' => false,
],
];
}
/** @return array<string, mixed> */
private function contactContent(): array
{
return [
'whatsapp' => [
'whatsapp_url' => 'https://wa.me/5493412474530',
'whatsapp_label' => 'Chateá con Mutual SMEP',
],
'phone' => '+54 9 341 247-4530',
'locations' => [
'rosario' => [
'label' => 'Rosario',
'addresses' => [
[
'label' => 'Mutual SMEP',
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
'coordinates' => [-32.9431184, -60.6437991],
],
],
],
],
];
}
private function storeImage(string $relativePath, string $filename): int
{
$sourcePath = public_path($relativePath);
if (! is_file($sourcePath)) {
throw new RuntimeException("Image not found at path: {$sourcePath}");
}
$contents = file_get_contents($sourcePath);
if ($contents === false) {
throw new RuntimeException("Could not read image at path: {$sourcePath}");
}
$key = (string) Str::uuid();
$storedPath = 'tenants/'.self::TENANT_CODE."/{$key}.png";
if (! Storage::disk('s3')->put($storedPath, $contents)) {
throw new RuntimeException("Could not store image at path: {$storedPath}");
}
$this->storedPaths[] = $storedPath;
return DB::table('attachments')->insertGetId([
'key' => $key,
'path' => $storedPath,
'filename' => $filename,
'type' => 'image',
'mime_type' => 'image/png',
'extension' => 'png',
'size' => strlen($contents),
'created_at' => now(),
'updated_at' => now(),
]);
}
};

View File

@@ -296,6 +296,7 @@ class MenuSeeder extends Seeder
$helpTenantCodes = [
'sonder',
'fiesta_futbol_infantil',
'mutual_smep',
];
$fiestaCategoryMenuCodes = [
'adminapp.tickets',
@@ -363,6 +364,42 @@ class MenuSeeder extends Seeder
],
],
];
$mutualSmepFrequentlyAskedQuestions = [
[
'pregunta' => '¿Hay algún límite de compra?',
'respuesta' => 'La cantidad disponible depende del stock de cada producto.',
'is_active' => true,
],
[
'pregunta' => '¿Cuáles son los medios de pago disponibles?',
'respuesta' => 'Podés consultar y seleccionar los medios de pago habilitados al finalizar tu compra.',
'is_active' => false,
],
[
'pregunta' => '¿Cómo puedo recibir asesoramiento antes de comprar?',
'respuesta' => 'Podés comunicarte con Mutual SMEP por WhatsApp al +54 9 341 247-4530.',
'is_active' => false,
],
];
$mutualSmepContactContent = [
'whatsapp' => [
'whatsapp_url' => 'https://wa.me/5493412474530',
'whatsapp_label' => 'Chateá con Mutual SMEP',
],
'phone' => '+54 9 341 247-4530',
'locations' => [
'rosario' => [
'label' => 'Rosario',
'addresses' => [
[
'label' => 'Mutual SMEP',
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
'coordinates' => [-32.9431184, -60.6437991],
],
],
],
],
];
foreach ($tenants as $tenant) {
$menuCodes = $allMenus;
@@ -400,10 +437,14 @@ class MenuSeeder extends Seeder
if (in_array($tenant->codigo, $helpTenantCodes, true)) {
$tenant->menues()->updateExistingPivot('help.faq', [
'static_content' => $frequentlyAskedQuestions,
'static_content' => $tenant->codigo === 'mutual_smep'
? $mutualSmepFrequentlyAskedQuestions
: $frequentlyAskedQuestions,
]);
$tenant->menues()->updateExistingPivot('help.contact', [
'static_content' => $contactContent,
'static_content' => $tenant->codigo === 'mutual_smep'
? $mutualSmepContactContent
: $contactContent,
]);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,197 @@
<?php
namespace Tests\Feature\Migrations;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ProvisionMutualSmepTenantTest extends TestCase
{
public function test_it_provisions_the_mutual_smep_tenant_once_with_its_branding(): void
{
Storage::fake('s3');
$this->createSchema();
DB::table('admin_website_types')->insert(['codigo' => 'shopit']);
DB::table('storefront_website_types')->insert(['codigo' => 'shopit']);
DB::table('menues')->insert([
['code' => 'product.detail'],
['code' => 'help.faq'],
['code' => 'help.contact'],
['code' => 'account.tickets'],
['code' => 'event.index'],
]);
$migration = require database_path('migrations/2026_09_24_000000_provision_mutual_smep_tenant.php');
$migration->up();
$migration->up();
$this->assertDatabaseCount('tenants', 1);
$this->assertDatabaseHas('clients', [
'code' => 'mutual_smep',
'name' => 'Mutual SMEP',
]);
$this->assertDatabaseHas('tenants', [
'codigo' => 'mutual_smep',
'nombre' => 'Mutual SMEP',
'dominio' => 'mutual-smep.localhost',
'site_title' => 'Tienda Mutual SMEP',
'primary_color' => '#4A7FF5',
'secondary_color' => '#0051A4',
'header_bg_color' => '#FFFFFF',
'footer_bg_color' => '#0051A4',
'admin_website_type_code' => 'shopit',
'storefront_website_type_code' => 'shopit',
'display_categories' => true,
'display_seach_bar' => true,
'display_cart' => true,
'cart_editing_policy' => 'quantity_and_remove',
'checkout_editing_policy' => 'disabled',
]);
$this->assertSame([
'mutual_smep_favicon.png',
'mutual_smep_footer.png',
'mutual_smep_header.png',
], DB::table('attachments')->orderBy('filename')->pluck('filename')->all());
DB::table('attachments')->orderBy('id')->each(
fn (object $attachment) => Storage::disk('s3')->assertExists($attachment->path)
);
$this->assertSame(
['instagram', 'facebook', 'whatsapp'],
DB::table('tenant_social_media')
->where('tenant_code', 'mutual_smep')
->orderBy('orden')
->pluck('social_media_code')
->all(),
);
$this->assertDatabaseHas('tenants_menues', [
'tenant_code' => 'mutual_smep',
'menu_code' => 'product.detail',
]);
$this->assertDatabaseMissing('tenants_menues', [
'tenant_code' => 'mutual_smep',
'menu_code' => 'account.tickets',
]);
$this->assertDatabaseMissing('tenants_menues', [
'tenant_code' => 'mutual_smep',
'menu_code' => 'event.index',
]);
$contact = json_decode(
DB::table('tenants_menues')
->where('tenant_code', 'mutual_smep')
->where('menu_code', 'help.contact')
->value('static_content'),
true,
flags: JSON_THROW_ON_ERROR,
);
$this->assertSame('https://wa.me/5493412474530', $contact['whatsapp']['whatsapp_url']);
$this->assertSame('San Lorenzo 1543, Rosario, Santa Fe', $contact['locations']['rosario']['addresses'][0]['address']);
$storedPaths = DB::table('attachments')->pluck('path')->all();
$migration->down();
$this->assertDatabaseMissing('tenants', ['codigo' => 'mutual_smep']);
$this->assertDatabaseMissing('clients', ['code' => 'mutual_smep']);
$this->assertDatabaseCount('attachments', 0);
$this->assertDatabaseCount('tenant_social_media', 0);
$this->assertDatabaseCount('tenants_menues', 0);
foreach ($storedPaths as $storedPath) {
Storage::disk('s3')->assertMissing($storedPath);
}
}
private function createSchema(): void
{
Schema::create('clients', function (Blueprint $table): void {
$table->id();
$table->string('code')->unique();
$table->string('name');
$table->timestamps();
});
Schema::create('attachments', function (Blueprint $table): void {
$table->id();
$table->uuid('key');
$table->string('path');
$table->string('filename');
$table->string('type');
$table->string('mime_type');
$table->string('extension');
$table->unsignedBigInteger('size');
$table->timestamps();
});
Schema::create('admin_website_types', function (Blueprint $table): void {
$table->id();
$table->string('codigo')->unique();
});
Schema::create('storefront_website_types', function (Blueprint $table): void {
$table->id();
$table->string('codigo')->unique();
});
Schema::create('tenants', function (Blueprint $table): void {
$table->id();
$table->foreignId('client_id');
$table->string('codigo')->unique();
$table->string('nombre');
$table->string('timezone');
$table->string('dominio');
$table->string('base_path');
$table->string('site_title');
$table->string('address');
$table->string('phone');
$table->string('primary_color');
$table->string('secondary_color');
$table->string('danger_color');
$table->string('success_color');
$table->string('header_bg_color');
$table->string('footer_bg_color');
$table->foreignId('header_logo_id');
$table->foreignId('footer_logo_id');
$table->foreignId('favicon_id');
$table->string('admin_website_type_code');
$table->string('storefront_website_type_code');
$table->boolean('display_categories');
$table->boolean('display_seach_bar');
$table->boolean('display_cart');
$table->string('cart_editing_policy');
$table->string('checkout_editing_policy');
$table->timestamps();
});
Schema::create('social_media', function (Blueprint $table): void {
$table->id();
$table->string('code')->unique();
$table->string('icon');
$table->string('name');
$table->timestamps();
});
Schema::create('tenant_social_media', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code');
$table->string('social_media_code');
$table->string('url');
$table->unsignedInteger('orden');
$table->timestamps();
$table->unique(['tenant_code', 'social_media_code']);
});
Schema::create('menues', function (Blueprint $table): void {
$table->id();
$table->string('code')->unique();
});
Schema::create('tenants_menues', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code');
$table->string('menu_code');
$table->json('static_content')->nullable();
$table->timestamps();
$table->unique(['tenant_code', 'menu_code']);
});
}
}