diff --git a/database/migrations/2026_09_24_000000_provision_mutual_smep_tenant.php b/database/migrations/2026_09_24_000000_provision_mutual_smep_tenant.php new file mode 100644 index 0000000..f118f6e --- /dev/null +++ b/database/migrations/2026_09_24_000000_provision_mutual_smep_tenant.php @@ -0,0 +1,326 @@ + */ + 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 */ + 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 */ + 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(), + ]); + } +}; diff --git a/database/seeders/MenuSeeder.php b/database/seeders/MenuSeeder.php index 183ec63..4953ef9 100644 --- a/database/seeders/MenuSeeder.php +++ b/database/seeders/MenuSeeder.php @@ -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, ]); } } diff --git a/public/images/tennants/mutual_smep/mutual_smep_favicon.png b/public/images/tennants/mutual_smep/mutual_smep_favicon.png new file mode 100644 index 0000000..1ec3be4 Binary files /dev/null and b/public/images/tennants/mutual_smep/mutual_smep_favicon.png differ diff --git a/public/images/tennants/mutual_smep/mutual_smep_footer.png b/public/images/tennants/mutual_smep/mutual_smep_footer.png new file mode 100644 index 0000000..1ec3be4 Binary files /dev/null and b/public/images/tennants/mutual_smep/mutual_smep_footer.png differ diff --git a/public/images/tennants/mutual_smep/mutual_smep_header.png b/public/images/tennants/mutual_smep/mutual_smep_header.png new file mode 100644 index 0000000..c15429b Binary files /dev/null and b/public/images/tennants/mutual_smep/mutual_smep_header.png differ diff --git a/tests/Feature/Migrations/ProvisionMutualSmepTenantTest.php b/tests/Feature/Migrations/ProvisionMutualSmepTenantTest.php new file mode 100644 index 0000000..f9a63b6 --- /dev/null +++ b/tests/Feature/Migrations/ProvisionMutualSmepTenantTest.php @@ -0,0 +1,197 @@ +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']); + }); + } +}