feat(api): organisation email branding and shared mail layout
- Add email branding columns to organisations table (logo, color, reply-to, sender name, footer)
- Create MailBrandingService for resolving per-org branding with defaults
- Create CrewliMailable abstract base class with branded from/reply-to
- Create shared Blade layout (mail.layouts.crewli) with inline CSS
- Refactor Registration*Mail and InvitationMail to extend CrewliMailable
- Add config/crewli.php for platform-wide defaults (portal_url, app_url, logo)
- Add dev-only /mail-preview/{type} route for browser email previewing
- Update Organisation model, resource, and form requests with branding fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
90
api/tests/Feature/Mail/MailBrandingServiceTest.php
Normal file
90
api/tests/Feature/Mail/MailBrandingServiceTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Mail;
|
||||
|
||||
use App\Models\Organisation;
|
||||
use App\Services\MailBrandingService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MailBrandingServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private MailBrandingService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->service = new MailBrandingService();
|
||||
}
|
||||
|
||||
public function test_returns_org_branding_when_configured(): void
|
||||
{
|
||||
$org = Organisation::factory()->create([
|
||||
'name' => 'Festival BV',
|
||||
'email_logo_url' => 'https://example.com/logo.png',
|
||||
'email_primary_color' => '#ff5500',
|
||||
'email_reply_to' => 'info@festival.nl',
|
||||
'email_sender_name' => 'Festival Team',
|
||||
'email_footer_text' => 'Stichting Festival, Amsterdam',
|
||||
]);
|
||||
|
||||
$branding = $this->service->getBranding($org);
|
||||
|
||||
$this->assertEquals('Festival BV', $branding['organisation_name']);
|
||||
$this->assertEquals('https://example.com/logo.png', $branding['logo_url']);
|
||||
$this->assertEquals('#ff5500', $branding['primary_color']);
|
||||
$this->assertEquals('info@festival.nl', $branding['reply_to']);
|
||||
$this->assertEquals('Festival Team', $branding['sender_name']);
|
||||
$this->assertEquals('Stichting Festival, Amsterdam', $branding['footer_text']);
|
||||
}
|
||||
|
||||
public function test_falls_back_to_defaults_when_not_configured(): void
|
||||
{
|
||||
$org = Organisation::factory()->create([
|
||||
'name' => 'Lege Org',
|
||||
'email_logo_url' => null,
|
||||
'email_primary_color' => null,
|
||||
'email_reply_to' => null,
|
||||
'email_sender_name' => null,
|
||||
'email_footer_text' => null,
|
||||
]);
|
||||
|
||||
$branding = $this->service->getBranding($org);
|
||||
|
||||
$this->assertEquals('Lege Org', $branding['organisation_name']);
|
||||
$this->assertNull($branding['logo_url']);
|
||||
$this->assertEquals('#6366f1', $branding['primary_color']);
|
||||
$this->assertNull($branding['reply_to']);
|
||||
$this->assertEquals('Lege Org', $branding['sender_name']);
|
||||
$this->assertNull($branding['footer_text']);
|
||||
}
|
||||
|
||||
public function test_sender_name_falls_back_to_org_name(): void
|
||||
{
|
||||
$org = Organisation::factory()->create([
|
||||
'name' => 'Mijn Organisatie',
|
||||
'email_sender_name' => null,
|
||||
]);
|
||||
|
||||
$branding = $this->service->getBranding($org);
|
||||
|
||||
$this->assertEquals('Mijn Organisatie', $branding['sender_name']);
|
||||
}
|
||||
|
||||
public function test_logo_url_falls_back_to_config_default(): void
|
||||
{
|
||||
config(['crewli.default_logo_url' => 'https://crewli.app/logo.png']);
|
||||
|
||||
$org = Organisation::factory()->create([
|
||||
'email_logo_url' => null,
|
||||
]);
|
||||
|
||||
$branding = $this->service->getBranding($org);
|
||||
|
||||
$this->assertEquals('https://crewli.app/logo.png', $branding['logo_url']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user