From 6c8a0e7e9f4a2c423f6e5cfcec03f1ec0e9b41b2 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 12 Jul 2026 20:07:34 +0000 Subject: [PATCH] Brand patient emails with organization logo. Wrap Bird patient mail in a customer-branded layout and show the logo preview in settings. Co-authored-by: Cursor --- .../Care/PatientMessageController.php | 4 +- app/Support/OrganizationBranding.php | 23 ++++ resources/views/care/settings/edit.blade.php | 14 ++- .../views/email/customer-branded.blade.php | 22 ++++ tests/Feature/CareEmailBrandingTest.php | 117 ++++++++++++++++++ 5 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 resources/views/email/customer-branded.blade.php create mode 100644 tests/Feature/CareEmailBrandingTest.php diff --git a/app/Http/Controllers/Care/PatientMessageController.php b/app/Http/Controllers/Care/PatientMessageController.php index 7d1d942..322cc43 100644 --- a/app/Http/Controllers/Care/PatientMessageController.php +++ b/app/Http/Controllers/Care/PatientMessageController.php @@ -10,6 +10,7 @@ use App\Services\Care\OrganizationResolver; use App\Services\Messaging\CustomerEmailClient; use App\Services\Messaging\CustomerSmsClient; use App\Services\Messaging\MessagingCredentialsService; +use App\Support\OrganizationBranding; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -46,7 +47,8 @@ class PatientMessageController extends Controller ]); $owner = $this->ownerRef($request); - $html = nl2br(e($data['body'])); + $organization = $this->organization($request); + $html = OrganizationBranding::wrapEmailHtml(nl2br(e($data['body'])), $organization); $sent = $email->send( $apiKey, (string) $credential->bird_from_email, diff --git a/app/Support/OrganizationBranding.php b/app/Support/OrganizationBranding.php index 126614f..5cf25ce 100644 --- a/app/Support/OrganizationBranding.php +++ b/app/Support/OrganizationBranding.php @@ -47,4 +47,27 @@ class OrganizationBranding Storage::disk('public')->delete($organization->logo_path); } } + + public static function emailLogoUrl(Organization $organization): ?string + { + if (! self::hasCustomLogo($organization)) { + return null; + } + $ext = strtolower(pathinfo($organization->logo_path, PATHINFO_EXTENSION)); + if ($ext === 'svg') { + return null; + } + $version = $organization->updated_at?->getTimestamp() ?? time(); + + return Storage::disk('public')->url($organization->logo_path).'?v='.$version; + } + + public static function wrapEmailHtml(string $bodyHtml, Organization $organization): string + { + return view('email.customer-branded', [ + 'logoUrl' => self::emailLogoUrl($organization), + 'companyName' => $organization->name, + 'bodyHtml' => $bodyHtml, + ])->render(); + } } diff --git a/resources/views/care/settings/edit.blade.php b/resources/views/care/settings/edit.blade.php index 99366f7..efeb6eb 100644 --- a/resources/views/care/settings/edit.blade.php +++ b/resources/views/care/settings/edit.blade.php @@ -30,11 +30,19 @@ @if ($canManage)
- - + +

PNG, JPG, WebP, or SVG up to 2 MB. Shown in patient emails.

@if (\App\Support\OrganizationBranding::hasCustomLogo($organization)) - + {{ $organization->name }} + @endif +
@endif diff --git a/resources/views/email/customer-branded.blade.php b/resources/views/email/customer-branded.blade.php new file mode 100644 index 0000000..81411c5 --- /dev/null +++ b/resources/views/email/customer-branded.blade.php @@ -0,0 +1,22 @@ + + + + + + + +
+
+ @if (! empty($logoUrl)) + {{ $companyName }} + @else +
{{ $companyName }}
+ @endif +
+
+ {!! $bodyHtml !!} +
+

{{ $companyName }}

+
+ + diff --git a/tests/Feature/CareEmailBrandingTest.php b/tests/Feature/CareEmailBrandingTest.php new file mode 100644 index 0000000..f509da9 --- /dev/null +++ b/tests/Feature/CareEmailBrandingTest.php @@ -0,0 +1,117 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $this->user = User::create([ + 'public_id' => 'test-user-brand-001', + 'name' => 'Test User', + 'email' => 'brand@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->user->public_id, + 'name' => 'Test Clinic', + 'slug' => 'test-clinic-brand', + 'timezone' => 'UTC', + 'settings' => ['onboarded' => true, 'facility_type' => 'clinic'], + ]); + + Member::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->user->public_id, + 'role' => 'hospital_admin', + ]); + + Branch::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Main', + 'is_active' => true, + ]); + } + + public function test_patient_email_contains_organization_logo(): void + { + Storage::fake('public'); + + $path = UploadedFile::fake()->image('clinic-logo.png', 400, 120) + ->store('care/organizations/'.$this->organization->id, 'public'); + $this->organization->update(['logo_path' => $path, 'name' => 'Test Clinic']); + + $plain = 'lsk_live_'.str_repeat('d', 32); + MessagingCredential::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'bird_api_key_encrypted' => Crypt::encryptString($plain), + 'bird_api_key_prefix' => substr($plain, 0, 16), + 'bird_from_email' => 'clinic@example.test', + 'bird_from_name' => 'Clinic', + 'bird_status' => MessagingCredential::STATUS_VALID, + 'bird_validated_at' => now(), + ]); + + Http::fake([ + '*/api/smtp/send' => Http::response(['success' => true, 'message_id' => 'x']), + ]); + + $patient = Patient::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => Branch::first()->id, + 'patient_number' => 'LC-BRAND-00001', + 'first_name' => 'Ada', + 'last_name' => 'Lovelace', + 'email' => 'ada@example.com', + ]); + + $this->actingAs($this->user) + ->post(route('care.patients.email', $patient), [ + 'subject' => 'Appointment reminder', + 'body' => 'Please arrive 10 minutes early.', + ]) + ->assertRedirect() + ->assertSessionHas('success'); + + $logoUrl = OrganizationBranding::emailLogoUrl($this->organization->fresh()); + + Http::assertSent(function ($request) use ($logoUrl) { + $html = $request['html'] ?? ''; + + return str_contains($request->url(), '/api/smtp/send') + && str_contains($html, 'Test Clinic') + && str_contains($html, $logoUrl) + && ! str_contains($html, 'ladillcare-logo') + && ! str_contains($html, 'Sent via Ladill'); + }); + } +}