feat(mail-test): enhance email functionality with tenant branding and routing
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Domains\MailTest\Controllers;
|
||||
|
||||
use App\Domains\MailTest\Requests\SendTestMailRequest;
|
||||
use App\Domains\MailTest\Services\MailTestService;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
@@ -13,10 +14,15 @@ class MailTestController extends Controller
|
||||
protected MailTestService $mailTestService,
|
||||
) {}
|
||||
|
||||
public function __invoke(SendTestMailRequest $request): JsonResponse
|
||||
public function __invoke(SendTestMailRequest $request, string $tenantCode): JsonResponse
|
||||
{
|
||||
$tenant = Tenant::query()
|
||||
->where('codigo', $tenantCode)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json(
|
||||
$this->mailTestService->send(
|
||||
$tenant,
|
||||
$request->validated('to'),
|
||||
$request->validated('subject'),
|
||||
$request->validated('message'),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Domains\MailTest\Mailables;
|
||||
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
@@ -15,6 +16,7 @@ class TestMail extends Mailable
|
||||
public function __construct(
|
||||
public readonly string $mailSubject,
|
||||
public readonly string $mailMessage,
|
||||
public readonly Tenant $tenant,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
@@ -24,14 +26,15 @@ class TestMail extends Mailable
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
$message = nl2br(e($this->mailMessage));
|
||||
$this->tenant->loadMissing(['headerLogo', 'footerLogo']);
|
||||
|
||||
return new Content(
|
||||
htmlString: <<<HTML
|
||||
<h1>Prueba de correo de Shopit</h1>
|
||||
<p>{$message}</p>
|
||||
<p><small>Si recibiste este mensaje, la configuracion de correo funciona correctamente.</small></p>
|
||||
HTML,
|
||||
view: 'mail.test',
|
||||
with: [
|
||||
'tenant' => $this->tenant,
|
||||
'headerLogoUrl' => $this->tenant->headerLogo?->getTemporaryUrl(1440),
|
||||
'footerLogoUrl' => $this->tenant->footerLogo?->getTemporaryUrl(1440),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Domains\MailTest\Services;
|
||||
|
||||
use App\Domains\MailTest\Mailables\TestMail;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class MailTestService
|
||||
@@ -10,16 +11,17 @@ class MailTestService
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function send(string $recipient, ?string $subject = null, ?string $message = null): array
|
||||
public function send(Tenant $tenant, string $recipient, ?string $subject = null, ?string $message = null): array
|
||||
{
|
||||
$subject ??= 'Prueba de correo de Shopit';
|
||||
$message ??= 'Este es un correo de prueba enviado desde Shopit.';
|
||||
|
||||
Mail::to($recipient)->send(new TestMail($subject, $message));
|
||||
Mail::to($recipient)->send(new TestMail($subject, $message, $tenant));
|
||||
|
||||
return [
|
||||
'message' => 'Correo de prueba enviado correctamente.',
|
||||
'recipient' => $recipient,
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'mailer' => (string) config('mail.default'),
|
||||
'sent_at' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
use App\Domains\MailTest\Controllers\MailTestController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('mail-test/send', MailTestController::class);
|
||||
Route::post('{tenant_code}/mail-test/send', MailTestController::class);
|
||||
|
||||
49
resources/views/components/mail/branded-layout.blade.php
Normal file
49
resources/views/components/mail/branded-layout.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@props(['tenant', 'headerLogoUrl' => null, 'footerLogoUrl' => null])
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>{{ $tenant->nombre }}</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 620px) {
|
||||
.mail-container { width: 100% !important; }
|
||||
.mail-content { padding: 32px 24px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f1f5f9; color: #334155; font-family: Arial, Helvetica, sans-serif;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: #f1f5f9;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 12px;">
|
||||
<table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" class="mail-container" style="width: 600px; max-width: 600px; background-color: #ffffff; border-top: 4px solid {{ $tenant->primary_color }}; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);">
|
||||
<tr>
|
||||
<td align="center" bgcolor="{{ $tenant->header_bg_color }}" style="padding: 24px 32px; background-color: {{ $tenant->header_bg_color }};">
|
||||
@if ($headerLogoUrl)
|
||||
<img src="{{ $headerLogoUrl }}" alt="{{ $tenant->nombre }}" width="180" style="display: block; width: auto; max-width: 180px; max-height: 64px; border: 0;">
|
||||
@else
|
||||
<span style="color: {{ $tenant->primary_color }}; font-size: 24px; font-weight: 700; line-height: 1.2;">{{ $tenant->nombre }}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="mail-content" style="padding: 40px 48px; font-size: 16px; line-height: 1.6;">
|
||||
{{ $slot }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" bgcolor="{{ $tenant->footer_bg_color }}" style="padding: 24px 32px; background-color: {{ $tenant->footer_bg_color }}; color: #ffffff; font-size: 12px; line-height: 1.5;">
|
||||
@if ($footerLogoUrl)
|
||||
<img src="{{ $footerLogoUrl }}" alt="{{ $tenant->nombre }}" width="140" style="display: block; width: auto; max-width: 140px; max-height: 48px; margin: 0 auto 16px; border: 0;">
|
||||
@endif
|
||||
{{ $footer ?? 'Este correo fue enviado por '.$tenant->nombre.'.' }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
11
resources/views/mail/test.blade.php
Normal file
11
resources/views/mail/test.blade.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<x-mail.branded-layout :tenant="$tenant" :header-logo-url="$headerLogoUrl" :footer-logo-url="$footerLogoUrl">
|
||||
<h1 style="margin: 0 0 20px; color: {{ $tenant->primary_color }}; font-size: 26px; line-height: 1.3;">
|
||||
Prueba de correo de Shopit
|
||||
</h1>
|
||||
|
||||
<p style="margin: 0 0 16px;">{!! nl2br(e($mailMessage)) !!}</p>
|
||||
|
||||
<p style="margin: 24px 0 0; color: #64748b; font-size: 13px;">
|
||||
Si recibiste este mensaje, la configuración de correo funciona correctamente.
|
||||
</p>
|
||||
</x-mail.branded-layout>
|
||||
@@ -2,17 +2,24 @@
|
||||
|
||||
namespace Tests\Feature\MailTest;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\MailTest\Mailables\TestMail;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MailTestControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_sends_a_test_email(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$tenant = $this->createTenant();
|
||||
|
||||
$response = $this->postJson('/api/mail-test/send', [
|
||||
$response = $this->postJson('/api/acme/mail-test/send', [
|
||||
'to' => 'recipient@example.com',
|
||||
'subject' => 'SMTP test',
|
||||
'message' => 'Test message',
|
||||
@@ -21,21 +28,24 @@ class MailTestControllerTest extends TestCase
|
||||
$response->assertOk()
|
||||
->assertJsonPath('message', 'Correo de prueba enviado correctamente.')
|
||||
->assertJsonPath('recipient', 'recipient@example.com')
|
||||
->assertJsonPath('tenant_code', 'acme')
|
||||
->assertJsonPath('mailer', 'array')
|
||||
->assertJsonStructure(['sent_at']);
|
||||
|
||||
Mail::assertSent(TestMail::class, function (TestMail $mail): bool {
|
||||
Mail::assertSent(TestMail::class, function (TestMail $mail) use ($tenant): bool {
|
||||
return $mail->hasTo('recipient@example.com')
|
||||
&& $mail->mailSubject === 'SMTP test'
|
||||
&& $mail->mailMessage === 'Test message';
|
||||
&& $mail->mailMessage === 'Test message'
|
||||
&& $mail->tenant->is($tenant);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_it_uses_default_content_when_optional_fields_are_omitted(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$this->createTenant();
|
||||
|
||||
$this->postJson('/api/mail-test/send', [
|
||||
$this->postJson('/api/acme/mail-test/send', [
|
||||
'to' => 'recipient@example.com',
|
||||
])->assertOk();
|
||||
|
||||
@@ -48,12 +58,110 @@ class MailTestControllerTest extends TestCase
|
||||
public function test_it_validates_the_recipient(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$this->createTenant();
|
||||
|
||||
$this->postJson('/api/mail-test/send', [
|
||||
$this->postJson('/api/acme/mail-test/send', [
|
||||
'to' => 'invalid-email',
|
||||
])->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['to']);
|
||||
|
||||
Mail::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_the_mail_template_uses_the_tenant_branding(): void
|
||||
{
|
||||
$tenant = new Tenant([
|
||||
'codigo' => 'tenant-store',
|
||||
'nombre' => 'Tenant Store',
|
||||
'primary_color' => '#778899',
|
||||
'header_bg_color' => '#112233',
|
||||
'footer_bg_color' => '#445566',
|
||||
]);
|
||||
|
||||
$tenant->setRelation('headerLogo', new class extends Attachment
|
||||
{
|
||||
public function getTemporaryUrl(int $expiresInMinutes = 10): string
|
||||
{
|
||||
return 'https://example.com/header-logo.png';
|
||||
}
|
||||
});
|
||||
$tenant->setRelation('footerLogo', new class extends Attachment
|
||||
{
|
||||
public function getTemporaryUrl(int $expiresInMinutes = 10): string
|
||||
{
|
||||
return 'https://example.com/footer-logo.png';
|
||||
}
|
||||
});
|
||||
|
||||
$mail = new TestMail(
|
||||
'Branded email',
|
||||
'Tenant message',
|
||||
$tenant,
|
||||
);
|
||||
|
||||
$html = $mail->render();
|
||||
|
||||
$this->assertStringContainsString('https://example.com/header-logo.png', $html);
|
||||
$this->assertStringContainsString('https://example.com/footer-logo.png', $html);
|
||||
$this->assertStringContainsString('background-color: #112233', $html);
|
||||
$this->assertStringContainsString('background-color: #445566', $html);
|
||||
$this->assertStringContainsString('color: #778899', $html);
|
||||
$this->assertStringContainsString('Tenant Store', $html);
|
||||
$this->assertStringContainsString('Tenant message', $html);
|
||||
}
|
||||
|
||||
public function test_it_returns_not_found_for_an_unknown_tenant(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
$this->postJson('/api/unknown/mail-test/send', [
|
||||
'to' => 'recipient@example.com',
|
||||
])->assertNotFound();
|
||||
|
||||
Mail::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_it_does_not_resolve_the_tenant_by_id(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$tenant = $this->createTenant();
|
||||
|
||||
$this->postJson("/api/{$tenant->id}/mail-test/send", [
|
||||
'to' => 'recipient@example.com',
|
||||
])->assertNotFound();
|
||||
|
||||
Mail::assertNothingSent();
|
||||
}
|
||||
|
||||
private function createTenant(): Tenant
|
||||
{
|
||||
$headerLogo = Attachment::create([
|
||||
'path' => 'tenants/header.png',
|
||||
'filename' => 'header.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
'extension' => 'png',
|
||||
]);
|
||||
$footerLogo = Attachment::create([
|
||||
'path' => 'tenants/footer.png',
|
||||
'filename' => 'footer.png',
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
'extension' => 'png',
|
||||
]);
|
||||
|
||||
return Tenant::create([
|
||||
'codigo' => 'acme',
|
||||
'nombre' => 'Acme Store',
|
||||
'dominio' => 'acme.example.com',
|
||||
'primary_color' => '#778899',
|
||||
'secondary_color' => '#64748b',
|
||||
'danger_color' => '#dc2626',
|
||||
'success_color' => '#16a34a',
|
||||
'header_bg_color' => '#112233',
|
||||
'footer_bg_color' => '#445566',
|
||||
'header_logo_id' => $headerLogo->id,
|
||||
'footer_logo_id' => $footerLogo->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user