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:
138
api/tests/Feature/Organisation/OrganisationEmailBrandingTest.php
Normal file
138
api/tests/Feature/Organisation/OrganisationEmailBrandingTest.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Organisation;
|
||||
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class OrganisationEmailBrandingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $orgAdmin;
|
||||
private Organisation $organisation;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RoleSeeder::class);
|
||||
|
||||
$this->organisation = Organisation::factory()->create();
|
||||
$this->orgAdmin = User::factory()->create();
|
||||
$this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']);
|
||||
}
|
||||
|
||||
public function test_update_organisation_with_email_branding(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}", [
|
||||
'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',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('organisations', [
|
||||
'id' => $this->organisation->id,
|
||||
'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',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_validate_email_primary_color_hex_format(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}", [
|
||||
'email_primary_color' => 'red',
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonValidationErrors('email_primary_color');
|
||||
}
|
||||
|
||||
public function test_validate_email_primary_color_accepts_valid_hex(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}", [
|
||||
'email_primary_color' => '#aaBB11',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_validate_email_reply_to_format(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}", [
|
||||
'email_reply_to' => 'not-an-email',
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonValidationErrors('email_reply_to');
|
||||
}
|
||||
|
||||
public function test_validate_email_logo_url_format(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}", [
|
||||
'email_logo_url' => 'not-a-url',
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonValidationErrors('email_logo_url');
|
||||
}
|
||||
|
||||
public function test_organisation_resource_includes_branding_fields(): void
|
||||
{
|
||||
$this->organisation->update([
|
||||
'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' => 'Voetje tekst',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->getJson("/api/v1/organisations/{$this->organisation->id}");
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.email_logo_url', 'https://example.com/logo.png')
|
||||
->assertJsonPath('data.email_primary_color', '#ff5500')
|
||||
->assertJsonPath('data.email_reply_to', 'info@festival.nl')
|
||||
->assertJsonPath('data.email_sender_name', 'Festival Team')
|
||||
->assertJsonPath('data.email_footer_text', 'Voetje tekst');
|
||||
}
|
||||
|
||||
public function test_branding_fields_nullable(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->putJson("/api/v1/organisations/{$this->organisation->id}", [
|
||||
'email_logo_url' => null,
|
||||
'email_primary_color' => null,
|
||||
'email_reply_to' => null,
|
||||
'email_sender_name' => null,
|
||||
'email_footer_text' => null,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user