diff --git a/app/Domains/Integration/Services/MailService.php b/app/Domains/Integration/Services/MailService.php index 6be767d..4b7b0f8 100644 --- a/app/Domains/Integration/Services/MailService.php +++ b/app/Domains/Integration/Services/MailService.php @@ -3,6 +3,8 @@ namespace App\Domains\Integration\Services; use App\Domains\Client\Models\Client; +use App\Domains\Tenant\Models\Tenant; +use App\Domains\Tenant\Models\WebsiteType; use Exception; use Illuminate\Contracts\Mail\Factory as MailFactory; use Illuminate\Contracts\Mail\Mailer; @@ -70,24 +72,31 @@ class MailService extends BaseIntegrationService return []; } - public function send(string|array $recipient, string $subject, string $content): void - { + public function send( + string|array $recipient, + string $subject, + string $content, + Tenant|WebsiteType|null $brand = null, + ): void { if (! $this->mailer || ! $this->tenant) { throw new Exception('MailService no está configurado. Llamá a forTenant() o forClient() primero.'); } - $this->tenant->loadMissing(['headerLogo', 'footerLogo']); + $brand ??= $this->tenant; + $branding = $this->brandingFor($brand); $html = Blade::render( <<<'BLADE' - + {!! $content !!} BLADE, [ - 'tenant' => $this->tenant, - 'headerLogoUrl' => $this->tenant->headerLogo?->getTemporaryUrl(1440), - 'footerLogoUrl' => $this->tenant->footerLogo?->getTemporaryUrl(1440), + 'branding' => $branding, + 'headerLogoUrl' => $brand instanceof WebsiteType + ? $brand->siteLogo?->getTemporaryUrl(1440) + : $brand->headerLogo?->getTemporaryUrl(1440), + 'footerLogoUrl' => $brand->footerLogo?->getTemporaryUrl(1440), 'content' => $content, ], ); @@ -106,6 +115,36 @@ class MailService extends BaseIntegrationService : (string) config('mail.default'); } + /** @return array{name: string, primary_color: string, body_color: string, background_color: string, surface_color: string, header_bg_color: string, footer_bg_color: string} */ + private function brandingFor(Tenant|WebsiteType $brand): array + { + if ($brand instanceof WebsiteType) { + $brand->loadMissing(['siteLogo', 'footerLogo']); + + return [ + 'name' => $brand->nombre, + 'primary_color' => $brand->primary_color ?? '#FF7006', + 'body_color' => $brand->body_color ?? '#666666', + 'background_color' => $brand->background_color ?? '#f8f8f8', + 'surface_color' => $brand->surface_color ?? '#ffffff', + 'header_bg_color' => $brand->surface_color ?? '#ffffff', + 'footer_bg_color' => $brand->login_header_footer_color ?? '#838383', + ]; + } + + $brand->loadMissing(['headerLogo', 'footerLogo']); + + return [ + 'name' => $brand->nombre, + 'primary_color' => $brand->primary_color ?? '#6376f3', + 'body_color' => '#334155', + 'background_color' => '#f1f5f9', + 'surface_color' => '#ffffff', + 'header_bg_color' => $brand->header_bg_color ?? '#ffffff', + 'footer_bg_color' => $brand->footer_bg_color ?? '#334155', + ]; + } + public function onSetup(): void { if (! $this->mailer || ! $this->clientContext) { diff --git a/app/Domains/MailTest/Mailables/TestMail.php b/app/Domains/MailTest/Mailables/TestMail.php index fd0e1fb..bff67ab 100644 --- a/app/Domains/MailTest/Mailables/TestMail.php +++ b/app/Domains/MailTest/Mailables/TestMail.php @@ -28,10 +28,21 @@ class TestMail extends Mailable { $this->tenant->loadMissing(['headerLogo', 'footerLogo']); + $branding = [ + 'name' => $this->tenant->nombre, + 'primary_color' => $this->tenant->primary_color ?? '#6376f3', + 'body_color' => '#334155', + 'background_color' => '#f1f5f9', + 'surface_color' => '#ffffff', + 'header_bg_color' => $this->tenant->header_bg_color ?? '#ffffff', + 'footer_bg_color' => $this->tenant->footer_bg_color ?? '#334155', + ]; + return new Content( view: 'mail.test', with: [ 'tenant' => $this->tenant, + 'branding' => $branding, 'headerLogoUrl' => $this->tenant->headerLogo?->getTemporaryUrl(1440), 'footerLogoUrl' => $this->tenant->footerLogo?->getTemporaryUrl(1440), ], diff --git a/app/Domains/Notification/Services/NotificationMailService.php b/app/Domains/Notification/Services/NotificationMailService.php index 6912028..dba369f 100644 --- a/app/Domains/Notification/Services/NotificationMailService.php +++ b/app/Domains/Notification/Services/NotificationMailService.php @@ -21,7 +21,7 @@ class NotificationMailService public function sendWelcome(int $userId, string $tenantCode): void { - $tenant = Tenant::query()->where('codigo', $tenantCode)->firstOrFail(); + $tenant = Tenant::query()->with('websiteType')->where('codigo', $tenantCode)->firstOrFail(); $user = User::query()->findOrFail($userId); $this->mailService @@ -30,6 +30,7 @@ class NotificationMailService $user->email, "Bienvenido a {$tenant->nombre}", view('mail.notifications.welcome', compact('tenant', 'user'))->render(), + $tenant->websiteType ?? $tenant, ); } @@ -81,7 +82,13 @@ class NotificationMailService ->send( $attempt->user->email, "Código para recuperar tu contraseña - {$tenant->nombre}", - view('mail.notifications.password-reset', compact('tenant', 'attempt', 'recoveryUrl'))->render(), + view('mail.notifications.password-reset', [ + 'tenant' => $tenant, + 'attempt' => $attempt, + 'recoveryUrl' => $recoveryUrl, + 'brand' => $tenant->websiteType ?? $tenant, + ])->render(), + $tenant->websiteType ?? $tenant, ); } diff --git a/app/Domains/Notification/documentacion/README.md b/app/Domains/Notification/documentacion/README.md index 6d7dd16..6d87fbf 100644 --- a/app/Domains/Notification/documentacion/README.md +++ b/app/Domains/Notification/documentacion/README.md @@ -23,4 +23,6 @@ No expone rutas HTTP. Consume datos de `Auth`, `Tenant`, `Purchase` y `Ticket`, - Los listeners reciben identificadores y vuelven a cargar los modelos, evitando transportar entidades obsoletas. - La recuperación no se envía si el intento dejó de estar pendiente. +- Los correos de cuenta (bienvenida y recuperación de contraseña) usan la identidad visual del `WebsiteType` asociado al tenant, con fallback al tenant si no tiene uno configurado. +- Los correos transaccionales (pago confirmado y tickets disponibles) usan la identidad visual del tenant/evento de la compra. - Los handlers deben permanecer idempotentes o tolerantes a reintentos de cola. diff --git a/resources/views/components/mail/branded-layout.blade.php b/resources/views/components/mail/branded-layout.blade.php index c612670..ea137a7 100644 --- a/resources/views/components/mail/branded-layout.blade.php +++ b/resources/views/components/mail/branded-layout.blade.php @@ -1,4 +1,4 @@ -@props(['tenant', 'headerLogoUrl' => null, 'footerLogoUrl' => null]) +@props(['branding', 'headerLogoUrl' => null, 'footerLogoUrl' => null]) @@ -6,7 +6,7 @@ - {{ $tenant->nombre }} + {{ $branding['name'] }} - - + +
- + - @@ -34,11 +34,11 @@ - diff --git a/resources/views/mail/notifications/password-reset.blade.php b/resources/views/mail/notifications/password-reset.blade.php index 1a47a29..4fd19a9 100644 --- a/resources/views/mail/notifications/password-reset.blade.php +++ b/resources/views/mail/notifications/password-reset.blade.php @@ -1,4 +1,4 @@ -

+

Recuperá tu contraseña

@@ -19,8 +19,8 @@

Ingresá este código en {{ $tenant->nombre }}:

-
- +
+ {{ $attempt->codigo }}
@@ -28,7 +28,7 @@ @if($recoveryUrl) diff --git a/resources/views/mail/test.blade.php b/resources/views/mail/test.blade.php index ba5d695..3cdc2eb 100644 --- a/resources/views/mail/test.blade.php +++ b/resources/views/mail/test.blade.php @@ -1,4 +1,4 @@ - +

Prueba de correo de Shopit

diff --git a/tests/Feature/Notification/NotificationMailServiceTest.php b/tests/Feature/Notification/NotificationMailServiceTest.php index a343e1e..348aa45 100644 --- a/tests/Feature/Notification/NotificationMailServiceTest.php +++ b/tests/Feature/Notification/NotificationMailServiceTest.php @@ -71,6 +71,8 @@ class NotificationMailServiceTest extends TestCase public function test_it_sends_a_branded_welcome_email(): void { + $this->useWebsiteTypeBranding(); + app(NotificationMailService::class)->sendWelcome($this->user->id, $this->tenant->codigo); Mail::assertSent(Mailable::class, function (Mailable $mail): bool { @@ -78,12 +80,15 @@ class NotificationMailServiceTest extends TestCase $mail->assertHasSubject('Bienvenido a Mail Tenant'); return str_contains($mail->render(), 'Ada Lovelace') - && str_contains($mail->render(), 'Mail Tenant'); + && str_contains($mail->render(), 'Mail Tenant') + && str_contains($mail->render(), 'OnTicket') + && str_contains($mail->render(), 'border-top: 4px solid #ff7006'); }); } public function test_it_sends_a_branded_password_reset_email(): void { + $this->useWebsiteTypeBranding(); $attempt = $this->user->resetPasswordAttempts()->create([ 'codigo' => '0123', ]); @@ -101,7 +106,8 @@ class NotificationMailServiceTest extends TestCase return str_contains($rendered, '0123') && str_contains($rendered, 'Ada Lovelace') && str_contains($rendered, 'Mail Tenant') - && str_contains($rendered, '#112233'); + && str_contains($rendered, '#ff7006') + && ! str_contains($rendered, 'border: 2px solid #112233'); }); } @@ -137,6 +143,7 @@ class NotificationMailServiceTest extends TestCase public function test_it_sends_purchase_and_ticket_emails_to_the_purchase_recipient(): void { + $this->useWebsiteTypeBranding(); $purchase = Purchase::query()->create([ 'tenant_codigo' => $this->tenant->codigo, 'user_id' => $this->user->id, @@ -180,13 +187,34 @@ class NotificationMailServiceTest extends TestCase $mail->assertTo('checkout@example.com'); return $mail->subject === "Pago confirmado - Compra #{$purchase->id}" - && str_contains($mail->render(), 'Total pagado'); + && str_contains($mail->render(), 'Total pagado') + && str_contains($mail->render(), 'border-top: 4px solid #112233') + && ! str_contains($mail->render(), 'border-top: 4px solid #ff7006'); }); Mail::assertSent(Mailable::class, function (Mailable $mail): bool { $mail->assertTo('checkout@example.com'); return $mail->subject === 'Tus tickets ya están disponibles' - && str_contains($mail->render(), 'Entrada general'); + && str_contains($mail->render(), 'Entrada general') + && str_contains($mail->render(), 'border-top: 4px solid #112233') + && ! str_contains($mail->render(), 'border-top: 4px solid #ff7006'); }); } + + private function useWebsiteTypeBranding(): void + { + $websiteType = WebsiteType::query()->create([ + 'codigo' => 'onticket', + 'nombre' => 'OnTicket', + 'dominio' => 'onticket.local', + 'primary_color' => '#ff7006', + 'body_color' => '#666666', + 'background_color' => '#f8f8f8', + 'surface_color' => '#ffffff', + 'login_header_footer_color' => '#838383', + ]); + + $this->tenant->update(['website_type_code' => $websiteType->codigo]); + $this->tenant->unsetRelation('websiteType'); + } }