diff --git a/.env.example b/.env.example index 9ba406b..111e34d 100644 --- a/.env.example +++ b/.env.example @@ -50,12 +50,14 @@ SMS_API_KEY_CARE= SMS_CUSTOMER_RELAY_URL=https://ladill.com/api/sms SMS_DEFAULT_SENDER_ID=Ladill -# Ladill Bird / SMTP (patient email uses customer keys via Integrations) -SMTP_PLATFORM_API_URL=https://bird.ladill.com/api/smtp +# Platform suite messaging (zero-config — channel=suite; no Bird/SMS product keys required) +# Monolith: MESSAGING_SUITE_PLATFORM_CHANNEL=true, SMTP_API_KEY_CARE + SMS_API_KEY_CARE +SMTP_PLATFORM_API_URL=https://ladill.com/api/smtp SMTP_API_KEY_CARE= -SMTP_CUSTOMER_RELAY_URL=https://bird.ladill.com/api/smtp +SMTP_CUSTOMER_RELAY_URL=https://ladill.com/api/smtp CARE_SMTP_FROM=care@ladill.com CARE_SMTP_FROM_NAME="Ladill Care" +MESSAGING_SETTINGS_URL=https://account.ladill.com/account/settings/messaging?reason=messaging_mailbox_required&from=suite # Afia in-app assistant (uses platform relay when AFIA_API_KEY is empty) AFIA_ENABLED=true diff --git a/app/Http/Controllers/Care/PatientMessageController.php b/app/Http/Controllers/Care/PatientMessageController.php index 322cc43..227ebeb 100644 --- a/app/Http/Controllers/Care/PatientMessageController.php +++ b/app/Http/Controllers/Care/PatientMessageController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers\Care; use App\Http\Controllers\Controller; use App\Http\Controllers\Care\Concerns\ScopesToAccount; use App\Models\Patient; +use App\Services\Billing\PlatformEmailClient; +use App\Services\Billing\PlatformSmsClient; use App\Services\Care\AuditLogger; use App\Services\Care\OrganizationResolver; use App\Services\Messaging\CustomerEmailClient; @@ -23,6 +25,7 @@ class PatientMessageController extends Controller Patient $patient, MessagingCredentialsService $credentials, CustomerEmailClient $email, + PlatformEmailClient $platformEmail, ): RedirectResponse { $this->authorizeAbility($request, 'patients.manage'); $this->authorizePatient($request, $patient); @@ -31,16 +34,6 @@ class PatientMessageController extends Controller return back()->with('error', 'This patient has no valid email on file.'); } - $credential = $credentials->forOrganization($this->organization($request)); - if (! $credential->hasValidBird()) { - return back()->with('error', 'Connect Ladill Bird in Integrations before sending patient email.'); - } - - $apiKey = $credential->birdApiKey(); - if (! $apiKey) { - return back()->with('error', 'Bird credentials could not be decrypted. Reconnect Bird in Integrations.'); - } - $data = $request->validate([ 'subject' => ['required', 'string', 'max:200'], 'body' => ['required', 'string', 'max:5000'], @@ -49,15 +42,39 @@ class PatientMessageController extends Controller $owner = $this->ownerRef($request); $organization = $this->organization($request); $html = OrganizationBranding::wrapEmailHtml(nl2br(e($data['body'])), $organization); - $sent = $email->send( - $apiKey, - (string) $credential->bird_from_email, - $credential->bird_from_name, - (string) $patient->email, - $data['subject'], - $html, - $data['body'], - ); + + $sent = false; + $error = null; + $via = null; + + if ($platformEmail->isConfigured()) { + $sent = $platformEmail->send($owner, (string) $patient->email, $data['subject'], $html, $data['body']); + $via = 'suite'; + if (! $sent) { + $error = $platformEmail->lastError(); + } + } + + if (! $sent) { + $credential = $credentials->forOrganization($organization); + if ($credential->hasValidBird() && ($apiKey = $credential->birdApiKey())) { + $sent = $email->send( + $apiKey, + (string) $credential->bird_from_email, + $credential->bird_from_name, + (string) $patient->email, + $data['subject'], + $html, + $data['body'], + ); + $via = 'bird'; + if (! $sent) { + $error = $email->lastError() ?: $error; + } + } elseif (! $platformEmail->isConfigured()) { + $error = 'Set a Ladill mailbox in Account → Messaging, or connect Ladill Bird in Integrations.'; + } + } AuditLogger::record( $owner, @@ -69,12 +86,13 @@ class PatientMessageController extends Controller [ 'to' => $patient->email, 'subject' => $data['subject'], - 'error' => $sent ? null : $email->lastError(), + 'via' => $via, + 'error' => $sent ? null : $error, ], ); if (! $sent) { - return back()->with('error', $email->lastError() ?: 'Email could not be sent.'); + return back()->with('error', $error ?: 'Email could not be sent.'); } return back()->with('success', 'Email sent to '.$patient->email); @@ -85,6 +103,7 @@ class PatientMessageController extends Controller Patient $patient, MessagingCredentialsService $credentials, CustomerSmsClient $sms, + PlatformSmsClient $platformSms, ): RedirectResponse { $this->authorizeAbility($request, 'patients.manage'); $this->authorizePatient($request, $patient); @@ -93,22 +112,34 @@ class PatientMessageController extends Controller return back()->with('error', 'This patient has no phone number on file.'); } - $credential = $credentials->forOrganization($this->organization($request)); - if (! $credential->hasValidSms()) { - return back()->with('error', 'Connect Ladill SMS in Integrations before sending patient SMS.'); - } - - $apiKey = $credential->smsApiKey(); - if (! $apiKey) { - return back()->with('error', 'SMS credentials could not be decrypted. Reconnect SMS in Integrations.'); - } - $data = $request->validate([ 'message' => ['required', 'string', 'max:480'], ]); $owner = $this->ownerRef($request); - $sent = $sms->send($apiKey, (string) $patient->phone, $data['message'], (string) $credential->sms_sender_id); + $organization = $this->organization($request); + $credential = $credentials->forOrganization($organization); + + $sent = false; + $error = null; + + if ($platformSms->isConfigured()) { + $sender = $credential->sms_sender_id ?: null; + $sent = $platformSms->send($owner, (string) $patient->phone, $data['message'], $sender ? (string) $sender : null); + if (! $sent) { + $error = $platformSms->lastError(); + } + } + + if (! $sent && $credential->hasValidSms() && ($apiKey = $credential->smsApiKey())) { + $sent = $sms->send($apiKey, (string) $patient->phone, $data['message'], (string) $credential->sms_sender_id); + if (! $sent) { + $error = $sms->lastError() ?? $error; + } + } elseif (! $sent && ! $platformSms->isConfigured()) { + $error = 'Connect Ladill SMS in Integrations, or ensure platform SMS is configured (SMS_API_KEY_CARE).'; + } + AuditLogger::record( $owner, @@ -119,12 +150,12 @@ class PatientMessageController extends Controller $patient->id, [ 'to' => $patient->phone, - 'error' => $sent ? null : $sms->lastError(), + 'error' => $sent ? null : $error, ], ); if (! $sent) { - return back()->with('error', $sms->lastError() ?: 'SMS could not be sent.'); + return back()->with('error', $error ?: 'SMS could not be sent.'); } return back()->with('success', 'SMS sent to '.$patient->phone); diff --git a/app/Http/Controllers/Care/ProController.php b/app/Http/Controllers/Care/ProController.php index 70f9157..4dcd395 100644 --- a/app/Http/Controllers/Care/ProController.php +++ b/app/Http/Controllers/Care/ProController.php @@ -258,12 +258,24 @@ class ProController extends Controller $settings['auto_renew'] = true; $settings['billing_method'] = 'wallet_monthly'; $settings['billed_branches'] = $billedBranches; - $settings['plan_expires_at'] = now() - ->addDays((int) config('care.pro.period_days', 30)) - ->toIso8601String(); + $expires = now()->addDays((int) config('care.pro.period_days', 30)); + $settings['plan_expires_at'] = $expires->toIso8601String(); unset($settings['plan_renewal_error'], $settings['enterprise_billed_branches']); $organization->update(['settings' => $settings]); + $billing->syncSuiteEntitlement( + $organization->owner_ref, + $plan === 'enterprise' ? 'enterprise' : 'pro', + 'active', + $reference, + [ + 'period_starts_at' => now()->toIso8601String(), + 'period_ends_at' => $expires->toIso8601String(), + 'source' => 'wallet_debit', + 'metadata' => ['billed_branches' => $billedBranches], + ], + ); + return redirect()->route('care.pro.index')->with('success', $successMessage); } @@ -278,8 +290,23 @@ class ProController extends Controller $settings['auto_renew'] = false; $settings['billing_method'] = 'paystack_prepaid'; $settings['billed_branches'] = $billedBranches; - $settings['plan_expires_at'] = now()->addMonths($months)->toIso8601String(); + $expires = now()->addMonths($months); + $settings['plan_expires_at'] = $expires->toIso8601String(); unset($settings['plan_renewal_error'], $settings['enterprise_billed_branches']); $organization->update(['settings' => $settings]); + + // Prepaid already writes entitlements on platform checkout; sync as repair. + app(BillingClient::class)->syncSuiteEntitlement( + $organization->owner_ref, + $plan === 'enterprise' ? 'enterprise' : 'pro', + 'active', + 'care-prepaid-sync-'.$organization->id.'-'.now()->format('YmdHis'), + [ + 'period_starts_at' => now()->toIso8601String(), + 'period_ends_at' => $expires->toIso8601String(), + 'source' => 'checkout', + 'metadata' => ['billed_branches' => $billedBranches], + ], + ); } } diff --git a/app/Services/Billing/BillingClient.php b/app/Services/Billing/BillingClient.php index 6a287ec..39fa5e5 100644 --- a/app/Services/Billing/BillingClient.php +++ b/app/Services/Billing/BillingClient.php @@ -105,4 +105,48 @@ class BillingClient return $res->json(); } + + /** + * Write suite messaging plan entitlement (SoT for included email/SMS allowances). + * + * @param array{period_starts_at?: string, period_ends_at?: string|null, source?: string, metadata?: array} $options + */ + public function syncSuiteEntitlement( + string $publicId, + string $plan, + string $status, + string $reference, + array $options = [], + ): bool { + if ($this->base() === '' || $this->token() === '') { + return false; + } + + $app = (string) config('billing.service', 'care'); + $starts = $options['period_starts_at'] ?? now()->toIso8601String(); + $ends = array_key_exists('period_ends_at', $options) + ? $options['period_ends_at'] + : now()->addDays(30)->toIso8601String(); + + try { + $res = Http::withToken($this->token()) + ->acceptJson() + ->timeout(15) + ->post($this->base().'/suite-entitlements', array_filter([ + 'user' => $publicId, + 'app' => $app, + 'plan' => $plan, + 'status' => $status, + 'period_starts_at' => $starts, + 'period_ends_at' => $ends, + 'source' => $options['source'] ?? 'wallet_debit', + 'reference' => $reference, + 'metadata' => $options['metadata'] ?? null, + ], static fn ($v) => $v !== null)); + + return ! $res->failed(); + } catch (\Throwable) { + return false; + } + } } diff --git a/app/Services/Billing/PlatformEmailClient.php b/app/Services/Billing/PlatformEmailClient.php index dc0dcd5..6c8e40c 100644 --- a/app/Services/Billing/PlatformEmailClient.php +++ b/app/Services/Billing/PlatformEmailClient.php @@ -4,16 +4,27 @@ namespace App\Services\Billing; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; +/** + * Platform suite email (zero-config): channel=suite, no Bird customer API key. + */ class PlatformEmailClient { private ?string $lastError = null; + private ?string $lastErrorCode = null; + public function lastError(): ?string { return $this->lastError; } + public function lastErrorCode(): ?string + { + return $this->lastErrorCode; + } + private function base(): string { return rtrim((string) config('smtp.platform_api_url', 'https://ladill.com/api/smtp'), '/'); @@ -29,9 +40,25 @@ class PlatformEmailClient return $this->token() !== ''; } - public function send(string $ownerPublicId, string $to, string $subject, string $html, ?string $text = null): bool + public function messagingSettingsUrl(): string { + return (string) config( + 'smtp.messaging_settings_url', + 'https://account.ladill.com/account/settings/messaging?reason=messaging_mailbox_required&from=suite', + ); + } + + public function send( + string $ownerPublicId, + string $to, + string $subject, + string $html, + ?string $text = null, + ?string $fromName = null, + ?string $idempotencyKey = null, + ): bool { $this->lastError = null; + $this->lastErrorCode = null; if (! $this->isConfigured()) { $this->lastError = 'Outbound email is not configured on this Care instance. Set SMTP_API_KEY_CARE.'; @@ -39,28 +66,41 @@ class PlatformEmailClient return false; } + $key = $idempotencyKey ?: ('care-email-'.Str::uuid()); + try { - $res = Http::withToken($this->token())->acceptJson()->timeout(30) + $res = Http::withToken($this->token()) + ->withHeaders(['Idempotency-Key' => $key]) + ->acceptJson() + ->timeout(30) ->post($this->base().'/messages/send', array_filter([ 'user' => $ownerPublicId, - 'from' => config('smtp.from'), - 'from_name' => config('smtp.from_name'), + 'channel' => 'suite', + 'from_name' => $fromName ?? config('smtp.from_name'), 'to' => $to, 'subject' => $subject, 'html' => $html, 'text' => $text, - ], fn ($value) => $value !== null && $value !== '')); + 'reference' => $key, + ], static fn ($v) => $v !== null && $v !== '')); if ($res->status() === 402) { - $this->lastError = (string) ($res->json('error') ?: 'Insufficient Bird email credits. Top up Bird and try again.'); - Log::warning('Care platform email send: insufficient balance', ['user' => $ownerPublicId]); + $this->lastErrorCode = (string) ($res->json('error') ?: 'allowance_exceeded'); + $this->lastError = (string) ($res->json('error') ?: 'Email allowance exceeded. Upgrade Care Pro or top up your wallet.'); + Log::warning('Care suite email: allowance/balance', ['user' => $ownerPublicId]); return false; } if ($res->failed()) { - $this->lastError = (string) ($res->json('error') ?: $res->json('message') ?: 'The email could not be sent.'); - Log::warning('Care platform email send failed', ['status' => $res->status(), 'body' => $res->body()]); + $code = (string) ($res->json('error') ?: 'send_failed'); + $this->lastErrorCode = $code; + if ($code === 'messaging_mailbox_required') { + $this->lastError = 'Set a Ladill mailbox in Account → Messaging: '.$this->messagingSettingsUrl(); + } else { + $this->lastError = (string) ($res->json('error') ?: $res->json('message') ?: 'The email could not be sent.'); + } + Log::warning('Care suite email failed', ['status' => $res->status(), 'body' => $res->body()]); return false; } @@ -68,7 +108,7 @@ class PlatformEmailClient return (bool) ($res->json('success') ?? true); } catch (\Throwable $e) { $this->lastError = 'Could not reach the email service. Please try again.'; - Log::warning('Care platform email send error', ['error' => $e->getMessage()]); + Log::warning('Care suite email error', ['error' => $e->getMessage()]); return false; } diff --git a/app/Services/Billing/PlatformSmsClient.php b/app/Services/Billing/PlatformSmsClient.php index 52a6867..9f36a09 100644 --- a/app/Services/Billing/PlatformSmsClient.php +++ b/app/Services/Billing/PlatformSmsClient.php @@ -4,7 +4,11 @@ namespace App\Services\Billing; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; +/** + * Platform suite SMS (zero-config): channel=suite, no customer SMS API key. + */ class PlatformSmsClient { private ?string $lastError = null; @@ -29,65 +33,13 @@ class PlatformSmsClient return $this->token() !== ''; } - /** @return list> */ - public function services(string $ownerPublicId): array - { - if (! $this->isConfigured()) { - return []; - } - - try { - $res = Http::withToken($this->token())->acceptJson()->timeout(15) - ->get($this->base().'/sms/services', ['user' => $ownerPublicId]); - - if ($res->failed()) { - return []; - } - - return (array) ($res->json('data') ?? []); - } catch (\Throwable $e) { - Log::warning('Care platform SMS services lookup failed', ['error' => $e->getMessage()]); - - return []; - } - } - - public function ensureServiceId(string $ownerPublicId): ?int - { - $services = $this->services($ownerPublicId); - if ($services !== []) { - return (int) ($services[0]['id'] ?? 0) ?: null; - } - - if (! $this->isConfigured()) { - return null; - } - - try { - $res = Http::withToken($this->token())->acceptJson()->timeout(15) - ->post($this->base().'/sms/services', [ - 'user' => $ownerPublicId, - 'name' => 'Ladill Care', - 'brand_name' => 'Care', - ]); - - if ($res->failed()) { - $this->lastError = (string) ($res->json('error') ?: 'Could not create an SMS service for this account.'); - - return null; - } - - return (int) ($res->json('data.id') ?? 0) ?: null; - } catch (\Throwable $e) { - Log::warning('Care platform SMS service create failed', ['error' => $e->getMessage()]); - $this->lastError = 'Could not reach the SMS service.'; - - return null; - } - } - - public function send(string $ownerPublicId, string $to, string $message, ?string $senderId = null): bool - { + public function send( + string $ownerPublicId, + string $to, + string $message, + ?string $senderId = null, + ?string $idempotencyKey = null, + ): bool { $this->lastError = null; if (! $this->isConfigured()) { @@ -96,33 +48,32 @@ class PlatformSmsClient return false; } - $serviceId = $this->ensureServiceId($ownerPublicId); - if (! $serviceId) { - $this->lastError ??= 'No SMS service is available for this account. Open sms.ladill.com once, then retry.'; - - return false; - } + $key = $idempotencyKey ?: ('care-sms-'.Str::uuid()); try { - $res = Http::withToken($this->token())->acceptJson()->timeout(30) + $res = Http::withToken($this->token()) + ->withHeaders(['Idempotency-Key' => $key]) + ->acceptJson() + ->timeout(30) ->post($this->base().'/sms/messages/send', array_filter([ 'user' => $ownerPublicId, - 'sms_service_id' => $serviceId, + 'channel' => 'suite', 'to' => $to, 'text' => $message, 'sender_id' => $senderId ?? config('sms.default_sender_id'), - ])); + 'reference' => $key, + ], static fn ($v) => $v !== null && $v !== '')); if ($res->status() === 402) { - $this->lastError = (string) ($res->json('error') ?: 'Insufficient SMS credit. Top up your Ladill SMS wallet and try again.'); - Log::warning('Care platform SMS send: insufficient balance', ['user' => $ownerPublicId]); + $this->lastError = (string) ($res->json('error') ?: 'SMS allowance exceeded. Upgrade Care Pro or top up your wallet.'); + Log::warning('Care suite SMS: allowance/balance', ['user' => $ownerPublicId]); return false; } if ($res->failed()) { $this->lastError = (string) ($res->json('error') ?: $res->json('message') ?: 'The SMS could not be sent.'); - Log::warning('Care platform SMS send failed', ['status' => $res->status(), 'body' => $res->body()]); + Log::warning('Care suite SMS failed', ['status' => $res->status(), 'body' => $res->body()]); return false; } @@ -130,7 +81,7 @@ class PlatformSmsClient return (bool) ($res->json('success') ?? true); } catch (\Throwable $e) { $this->lastError = 'Could not reach the SMS service. Please try again.'; - Log::warning('Care platform SMS send error', ['error' => $e->getMessage()]); + Log::warning('Care suite SMS error', ['error' => $e->getMessage()]); return false; } diff --git a/app/Services/Care/ProRenewalService.php b/app/Services/Care/ProRenewalService.php index beb8848..d156f58 100644 --- a/app/Services/Care/ProRenewalService.php +++ b/app/Services/Care/ProRenewalService.php @@ -73,12 +73,23 @@ class ProRenewalService $settings['plan'] = $plan; $settings['auto_renew'] = true; $settings['billed_branches'] = $branches; - $settings['plan_expires_at'] = now() - ->addDays((int) config('care.pro.period_days', 30)) - ->toIso8601String(); + $expiresAt = now()->addDays((int) config('care.pro.period_days', 30)); + $settings['plan_expires_at'] = $expiresAt->toIso8601String(); unset($settings['plan_renewal_error'], $settings['enterprise_billed_branches']); $organization->update(['settings' => $settings]); + $this->billing->syncSuiteEntitlement( + $organization->owner_ref, + $isEnterprise ? 'enterprise' : 'pro', + 'active', + $reference, + [ + 'period_starts_at' => now()->toIso8601String(), + 'period_ends_at' => $expiresAt->toIso8601String(), + 'source' => 'wallet_debit', + ], + ); + return; } @@ -88,8 +99,30 @@ class ProRenewalService $settings['plan'] = 'free'; unset($settings['plan_expires_at'], $settings['billed_branches'], $settings['enterprise_billed_branches']); $settings['plan_renewal_error'] = 'Suspended after failed renewal.'; + $this->billing->syncSuiteEntitlement( + $organization->owner_ref, + 'free', + 'cancelled', + $reference.'-free', + [ + 'period_starts_at' => now()->toIso8601String(), + 'period_ends_at' => now()->endOfMonth()->toIso8601String(), + 'source' => 'wallet_debit', + ], + ); } else { $settings['plan_renewal_error'] = 'Renewal failed — top up your wallet.'; + $this->billing->syncSuiteEntitlement( + $organization->owner_ref, + $isEnterprise ? 'enterprise' : 'pro', + 'past_due', + $reference.'-pastdue', + [ + 'period_starts_at' => now()->toIso8601String(), + 'period_ends_at' => $graceEnd->toIso8601String(), + 'source' => 'wallet_debit', + ], + ); } $organization->update(['settings' => $settings]); diff --git a/config/smtp.php b/config/smtp.php index 1e1a83d..a011f78 100644 --- a/config/smtp.php +++ b/config/smtp.php @@ -1,9 +1,13 @@ env('SMTP_PLATFORM_API_URL', 'https://bird.ladill.com/api/smtp'), + 'platform_api_url' => env('SMTP_PLATFORM_API_URL', 'https://ladill.com/api/smtp'), 'platform_api_key' => env('SMTP_API_KEY_CARE'), - 'customer_relay_url' => env('SMTP_CUSTOMER_RELAY_URL', 'https://bird.ladill.com/api/smtp'), + 'customer_relay_url' => env('SMTP_CUSTOMER_RELAY_URL', 'https://ladill.com/api/smtp'), 'from' => env('CARE_SMTP_FROM', 'care@ladill.com'), 'from_name' => env('CARE_SMTP_FROM_NAME', 'Ladill Care'), + 'messaging_settings_url' => env( + 'MESSAGING_SETTINGS_URL', + 'https://account.ladill.com/account/settings/messaging?reason=messaging_mailbox_required&from=suite', + ), ]; diff --git a/tests/Feature/CareSuiteMessagingTest.php b/tests/Feature/CareSuiteMessagingTest.php new file mode 100644 index 0000000..89333c4 --- /dev/null +++ b/tests/Feature/CareSuiteMessagingTest.php @@ -0,0 +1,149 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $this->user = User::create([ + 'public_id' => 'test-suite-user-001', + 'name' => 'Suite User', + 'email' => 'suite@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->user->public_id, + 'name' => 'Suite Clinic', + 'slug' => 'suite-clinic', + 'timezone' => 'UTC', + 'settings' => ['onboarded' => true], + ]); + + Member::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->user->public_id, + 'role' => 'receptionist', + ]); + + $this->branch = Branch::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Main Branch', + 'is_active' => true, + ]); + + Department::create([ + 'owner_ref' => $this->user->public_id, + 'branch_id' => $this->branch->id, + 'name' => 'General Outpatient', + 'type' => 'outpatient', + 'is_active' => true, + ]); + } + + public function test_patient_email_uses_suite_channel_when_platform_configured(): void + { + config([ + 'smtp.platform_api_key' => 'care-smtp-key', + 'smtp.platform_api_url' => 'https://platform.test/api/smtp', + ]); + + Http::fake([ + 'platform.test/api/smtp/messages/send' => Http::response([ + 'success' => true, + 'path' => 'suite', + 'message_id' => 'suite-1', + ]), + ]); + + $patient = Patient::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'LC-SUITE-001', + 'first_name' => 'Ama', + 'last_name' => 'Suite', + 'email' => 'ama.suite@example.com', + ]); + + $this->actingAs($this->user) + ->post(route('care.patients.email', $patient), [ + 'subject' => 'Hello', + 'body' => 'Suite email body', + ]) + ->assertRedirect() + ->assertSessionHas('success'); + + Http::assertSent(function ($request) { + return str_contains($request->url(), '/messages/send') + && ($request['channel'] ?? null) === 'suite' + && ($request['user'] ?? null) === $this->user->public_id; + }); + } + + public function test_patient_sms_uses_suite_channel_when_platform_configured(): void + { + config([ + 'sms.platform_api_key' => 'care-sms-key', + 'sms.platform_api_url' => 'https://platform.test/api', + ]); + + Http::fake([ + 'platform.test/api/sms/messages/send' => Http::response([ + 'success' => true, + 'path' => 'suite', + ]), + ]); + + $patient = Patient::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'LC-SUITE-002', + 'first_name' => 'Kofi', + 'last_name' => 'Suite', + 'phone' => '0244000111', + ]); + + $this->actingAs($this->user) + ->post(route('care.patients.sms', $patient), [ + 'message' => 'Suite SMS body', + ]) + ->assertRedirect() + ->assertSessionHas('success'); + + Http::assertSent(function ($request) { + $data = $request->data(); + + return str_contains($request->url(), '/sms/messages/send') + && ($data['channel'] ?? null) === 'suite' + && ! array_key_exists('sms_service_id', $data); + }); + } +}