diff --git a/app/Services/Frontdesk/NotificationDispatcher.php b/app/Services/Frontdesk/NotificationDispatcher.php index 6813d5b..47e50fc 100644 --- a/app/Services/Frontdesk/NotificationDispatcher.php +++ b/app/Services/Frontdesk/NotificationDispatcher.php @@ -13,6 +13,7 @@ use App\Notifications\FrontdeskAlertNotification; use App\Services\Messaging\CustomerEmailClient; use App\Services\Messaging\CustomerSmsClient; use App\Services\Messaging\MessagingCredentialsService; +use App\Support\OrganizationBranding; use Illuminate\Support\Facades\Log; class NotificationDispatcher @@ -218,7 +219,7 @@ class NotificationDispatcher ]); } else { try { - $html = nl2br(e($message)); + $html = OrganizationBranding::wrapEmailHtml(nl2br(e($message)), $organization); $sent = $this->customerEmail->send( $apiKey, (string) $credential->bird_from_email, diff --git a/app/Support/OrganizationBranding.php b/app/Support/OrganizationBranding.php index 8686612..7570973 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/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/FrontdeskEmailBrandingTest.php b/tests/Feature/FrontdeskEmailBrandingTest.php new file mode 100644 index 0000000..20ad2a2 --- /dev/null +++ b/tests/Feature/FrontdeskEmailBrandingTest.php @@ -0,0 +1,124 @@ +withoutMiddleware(EnsurePlatformSession::class); + config(['billing.api_url' => 'https://billing.test']); + + $this->user = User::create([ + 'public_id' => 'fd-brand-001', + 'name' => 'Test User', + 'email' => 'frontdesk@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->user->public_id, + 'name' => 'Frontdesk HQ', + 'slug' => 'frontdesk-hq', + 'timezone' => 'UTC', + 'settings' => [ + 'onboarded' => true, + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + 'badge_expiry_hours' => 8, + 'kiosk_reset_seconds' => 120, + 'notification_channels' => ['email'], + ], + ]); + + Member::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->user->public_id, + 'role' => 'org_admin', + ]); + + Branch::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Main Office', + 'is_active' => true, + ]); + } + + public function test_host_alert_email_contains_organization_branding(): void + { + Storage::fake('public'); + + $path = UploadedFile::fake()->image('hq-logo.png', 400, 120) + ->store('frontdesk/organizations/'.$this->organization->id, 'public'); + $this->organization->update(['logo_path' => $path, 'name' => 'Frontdesk HQ']); + + $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' => 'frontdesk@example.test', + 'bird_from_name' => 'Frontdesk', + 'bird_status' => MessagingCredential::STATUS_VALID, + 'bird_validated_at' => now(), + ]); + + Http::fake([ + 'billing.test/*' => Http::response(['affordable' => true, 'ok' => true]), + '*/api/smtp/send' => Http::response(['success' => true, 'message_id' => 'x']), + ]); + + $host = Host::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Jane Host', + 'email' => 'jane@example.com', + 'is_available' => true, + ]); + + $this->actingAs($this->user) + ->post(route('frontdesk.visits.store'), [ + 'full_name' => 'John Visitor', + 'host_id' => $host->id, + 'visitor_type' => 'visitor', + 'policies_accepted' => '1', + ]) + ->assertRedirect(); + + $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, 'Frontdesk HQ') + && str_contains($html, $logoUrl) + && ! str_contains($html, 'ladillfrontdesk-logo') + && ! str_contains($html, 'Sent via Ladill'); + }); + } +}