diff --git a/app/Http/Controllers/Care/PatientMessageController.php b/app/Http/Controllers/Care/PatientMessageController.php index 227ebeb..0aa370d 100644 --- a/app/Http/Controllers/Care/PatientMessageController.php +++ b/app/Http/Controllers/Care/PatientMessageController.php @@ -42,13 +42,22 @@ class PatientMessageController extends Controller $owner = $this->ownerRef($request); $organization = $this->organization($request); $html = OrganizationBranding::wrapEmailHtml(nl2br(e($data['body'])), $organization); + // Patient-facing From display name is the clinic/company, never the Ladill product. + $fromName = trim((string) $organization->name) ?: null; $sent = false; $error = null; $via = null; if ($platformEmail->isConfigured()) { - $sent = $platformEmail->send($owner, (string) $patient->email, $data['subject'], $html, $data['body']); + $sent = $platformEmail->send( + $owner, + (string) $patient->email, + $data['subject'], + $html, + $data['body'], + $fromName, + ); $via = 'suite'; if (! $sent) { $error = $platformEmail->lastError(); @@ -61,7 +70,7 @@ class PatientMessageController extends Controller $sent = $email->send( $apiKey, (string) $credential->bird_from_email, - $credential->bird_from_name, + $credential->bird_from_name ?: $fromName, (string) $patient->email, $data['subject'], $html, diff --git a/app/Services/Billing/PlatformEmailClient.php b/app/Services/Billing/PlatformEmailClient.php index 6c8e40c..0f51d60 100644 --- a/app/Services/Billing/PlatformEmailClient.php +++ b/app/Services/Billing/PlatformEmailClient.php @@ -73,10 +73,13 @@ class PlatformEmailClient ->withHeaders(['Idempotency-Key' => $key]) ->acceptJson() ->timeout(30) + // from_name: caller should pass the clinic/org name for patient-facing + // mail. Do not default to CARE_SMTP_FROM_NAME ("Ladill Care") — that is + // product branding; suite From display must look like the customer. ->post($this->base().'/messages/send', array_filter([ 'user' => $ownerPublicId, 'channel' => 'suite', - 'from_name' => $fromName ?? config('smtp.from_name'), + 'from_name' => $fromName, 'to' => $to, 'subject' => $subject, 'html' => $html, diff --git a/app/Services/Care/AppointmentPatientNotifier.php b/app/Services/Care/AppointmentPatientNotifier.php index be12f83..5ed5183 100644 --- a/app/Services/Care/AppointmentPatientNotifier.php +++ b/app/Services/Care/AppointmentPatientNotifier.php @@ -5,6 +5,8 @@ namespace App\Services\Care; use App\Models\Appointment; use App\Models\Organization; use App\Models\Patient; +use App\Services\Billing\PlatformEmailClient; +use App\Services\Billing\PlatformSmsClient; use App\Services\Messaging\CustomerEmailClient; use App\Services\Messaging\CustomerSmsClient; use App\Services\Messaging\MessagingCredentialsService; @@ -25,6 +27,8 @@ class AppointmentPatientNotifier private MessagingCredentialsService $credentials, private CustomerSmsClient $sms, private CustomerEmailClient $email, + private PlatformEmailClient $platformEmail, + private PlatformSmsClient $platformSms, ) {} public function organizationWants(Organization $organization, string $setting): bool @@ -129,7 +133,7 @@ class AppointmentPatientNotifier /** * When true, Meet should also email the patient join invite. - * Prefer Care-branded email when Bird is connected; otherwise fall back to Meet invites. + * Prefer Care suite/product email when available; otherwise fall back to Meet invites. */ public function shouldInvitePatientEmailViaMeet(Organization $organization): bool { @@ -137,9 +141,13 @@ class AppointmentPatientNotifier return false; } + if ($this->platformEmail->isConfigured()) { + return false; + } + $credential = $this->credentials->forOrganization($organization); - // Avoid double emails when Care Bird can send the branded notice. + // Avoid double emails when Care can send the branded notice. return ! $credential->hasValidBird(); } @@ -152,16 +160,29 @@ class AppointmentPatientNotifier try { $credential = $this->credentials->forOrganization($organization); - if (! $credential->hasValidSms()) { + $owner = (string) $organization->owner_ref; + $sent = false; + $error = null; + + if ($this->platformSms->isConfigured()) { + $sender = $credential->sms_sender_id ?: null; + $sent = $this->platformSms->send($owner, $phone, $message, $sender ? (string) $sender : null); + if (! $sent) { + $error = $this->platformSms->lastError(); + } + } + + if (! $sent && $credential->hasValidSms() && ($apiKey = $credential->smsApiKey())) { + $sent = $this->sms->send($apiKey, $phone, $message, (string) $credential->sms_sender_id); + if (! $sent) { + $error = $this->sms->lastError() ?: $error; + } + } + + if (! $sent && ! $this->platformSms->isConfigured() && ! $credential->hasValidSms()) { return; } - $apiKey = $credential->smsApiKey(); - if (! $apiKey) { - return; - } - - $sent = $this->sms->send($apiKey, $phone, $message, (string) $credential->sms_sender_id); AuditLogger::record( $organization->owner_ref, $sent ? $auditAction.'_sent' : $auditAction.'_failed', @@ -171,7 +192,7 @@ class AppointmentPatientNotifier $patient->id, [ 'to' => $phone, - 'error' => $sent ? null : $this->sms->lastError(), + 'error' => $sent ? null : $error, ], ); } catch (\Throwable $e) { @@ -197,25 +218,37 @@ class AppointmentPatientNotifier try { $credential = $this->credentials->forOrganization($organization); - if (! $credential->hasValidBird()) { - return; - } - - $apiKey = $credential->birdApiKey(); - if (! $apiKey) { - return; - } - + $owner = (string) $organization->owner_ref; + $fromName = trim((string) $organization->name) ?: null; $html = OrganizationBranding::wrapEmailHtml(nl2br(e($body)), $organization); - $sent = $this->email->send( - $apiKey, - (string) $credential->bird_from_email, - $credential->bird_from_name, - $to, - $subject, - $html, - $body, - ); + $sent = false; + $error = null; + + if ($this->platformEmail->isConfigured()) { + $sent = $this->platformEmail->send($owner, $to, $subject, $html, $body, $fromName); + if (! $sent) { + $error = $this->platformEmail->lastError(); + } + } + + if (! $sent && $credential->hasValidBird() && ($apiKey = $credential->birdApiKey())) { + $sent = $this->email->send( + $apiKey, + (string) $credential->bird_from_email, + $credential->bird_from_name ?: $fromName, + $to, + $subject, + $html, + $body, + ); + if (! $sent) { + $error = $this->email->lastError() ?: $error; + } + } + + if (! $sent && ! $this->platformEmail->isConfigured() && ! $credential->hasValidBird()) { + return; + } AuditLogger::record( $organization->owner_ref, @@ -227,7 +260,7 @@ class AppointmentPatientNotifier [ 'to' => $to, 'subject' => $subject, - 'error' => $sent ? null : $this->email->lastError(), + 'error' => $sent ? null : $error, ], ); } catch (\Throwable $e) { diff --git a/tests/Feature/CareSuiteMessagingTest.php b/tests/Feature/CareSuiteMessagingTest.php index 89333c4..2b4544c 100644 --- a/tests/Feature/CareSuiteMessagingTest.php +++ b/tests/Feature/CareSuiteMessagingTest.php @@ -102,7 +102,8 @@ class CareSuiteMessagingTest extends TestCase Http::assertSent(function ($request) { return str_contains($request->url(), '/messages/send') && ($request['channel'] ?? null) === 'suite' - && ($request['user'] ?? null) === $this->user->public_id; + && ($request['user'] ?? null) === $this->user->public_id + && ($request['from_name'] ?? null) === 'Suite Clinic'; }); }