feat(messaging): suite-channel patient email/SMS and entitlement sync
Deploy Ladill Care / deploy (push) Successful in 40s

Prefer platform suite messaging for patient email/SMS, fall back to product
keys; write suite entitlements on Pro wallet, prepaid, and renewal.
This commit is contained in:
isaacclad
2026-07-16 11:24:21 +00:00
parent 7a0a7fcb88
commit c9f5df6ed7
9 changed files with 409 additions and 128 deletions
+5 -3
View File
@@ -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
@@ -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);
+31 -4
View File
@@ -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],
],
);
}
}
+44
View File
@@ -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;
}
}
}
+50 -10
View File
@@ -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;
}
+23 -72
View File
@@ -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<array<string, mixed>> */
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;
}
+36 -3
View File
@@ -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]);
+6 -2
View File
@@ -1,9 +1,13 @@
<?php
return [
'platform_api_url' => 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',
),
];
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Branch;
use App\Models\Department;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class CareSuiteMessagingTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Organization $organization;
protected Branch $branch;
protected function setUp(): void
{
parent::setUp();
$this->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);
});
}
}