diff --git a/app/Http/Controllers/Events/AttendeeController.php b/app/Http/Controllers/Events/AttendeeController.php index fe1d8c0..5dbed01 100644 --- a/app/Http/Controllers/Events/AttendeeController.php +++ b/app/Http/Controllers/Events/AttendeeController.php @@ -154,6 +154,9 @@ class AttendeeController extends Controller 'programme' => $programme, 'commsModes' => $commsModes, 'defaultCommsMode' => $commsModes[0] ?? EventCommsService::MODE_PROGRAMME, + 'messagingSettingsUrl' => function_exists('ladill_account_url') + ? ladill_account_url('/account/settings/messaging') + : (string) config('smtp.messaging_settings_url', 'https://account.ladill.com/account/settings/messaging'), ]); } diff --git a/app/Http/Controllers/Qr/AccountController.php b/app/Http/Controllers/Qr/AccountController.php index 88c4d32..65d949d 100644 --- a/app/Http/Controllers/Qr/AccountController.php +++ b/app/Http/Controllers/Qr/AccountController.php @@ -6,6 +6,8 @@ use App\Http\Controllers\Controller; use App\Models\PaymentGatewaySetting; use App\Models\QrSetting; use App\Services\Billing\BillingClient; +use App\Services\Billing\PlatformEmailClient; +use App\Services\Billing\PlatformSmsClient; use App\Services\Events\SubscriptionService; use App\Services\Payments\MerchantGatewayService; use App\Support\AccountBranding; @@ -86,6 +88,11 @@ class AccountController extends Controller 'frameStyles' => QrFrameStyleCatalog::visible(), 'gateway' => $this->gateway->settingFor($account), 'canUsePaymentGateway' => $this->subscriptions->canUsePaymentGateway($account), + 'messagingSettingsUrl' => function_exists('ladill_account_url') + ? ladill_account_url('/account/settings/messaging') + : (string) config('smtp.messaging_settings_url', 'https://account.ladill.com/account/settings/messaging'), + 'suiteMessagingReady' => app(PlatformEmailClient::class)->isConfigured() + || app(PlatformSmsClient::class)->isConfigured(), ]); } diff --git a/app/Http/Controllers/Qr/IntegrationsController.php b/app/Http/Controllers/Qr/IntegrationsController.php index 3159422..9cf8cbd 100644 --- a/app/Http/Controllers/Qr/IntegrationsController.php +++ b/app/Http/Controllers/Qr/IntegrationsController.php @@ -19,6 +19,9 @@ class IntegrationsController extends Controller return view('qr.account.integrations', [ 'account' => $account, 'credential' => $credentials->forOwner((string) $account->public_id), + 'messagingSettingsUrl' => function_exists('ladill_account_url') + ? ladill_account_url('/account/settings/messaging') + : (string) config('smtp.messaging_settings_url', 'https://account.ladill.com/account/settings/messaging'), ]); } diff --git a/app/Services/Billing/SmsService.php b/app/Services/Billing/SmsService.php index d7852fe..cde30e7 100644 --- a/app/Services/Billing/SmsService.php +++ b/app/Services/Billing/SmsService.php @@ -5,6 +5,10 @@ namespace App\Services\Billing; use App\Services\Messaging\CustomerSmsClient; use App\Services\Messaging\MessagingCredentialsService; +/** + * Attendee SMS: prefer platform suite messaging (zero-config), fall back to + * product SMS API keys when suite is unavailable. + */ class SmsService { private ?string $lastError = null; @@ -12,6 +16,7 @@ class SmsService public function __construct( private readonly CustomerSmsClient $customer, private readonly MessagingCredentialsService $credentials, + private readonly PlatformSmsClient $platformSms, ) {} public function lastError(): ?string @@ -19,6 +24,15 @@ class SmsService return $this->lastError; } + public function isConfiguredForOwner(string $ownerPublicId): bool + { + if ($this->platformSms->isConfigured()) { + return true; + } + + return $this->credentials->forOwner($ownerPublicId)->hasValidSms(); + } + public function send(string $ownerPublicId, string $to, string $message): bool { $this->lastError = null; @@ -37,15 +51,43 @@ class SmsService } $credential = $this->credentials->forOwner($ownerPublicId); + + if ($this->platformSms->isConfigured()) { + $sender = $credential->sms_sender_id ?: null; + $sent = $this->platformSms->send( + $ownerPublicId, + $phone, + $message, + $sender ? (string) $sender : null, + ); + if ($sent) { + return true; + } + + $suiteError = $this->platformSms->lastError(); + if ($this->tryCustomerSms($credential, $phone, $message)) { + return true; + } + + $this->lastError = $suiteError ?: $this->lastError; + + return false; + } + + return $this->tryCustomerSms($credential, $phone, $message); + } + + private function tryCustomerSms(object $credential, string $phone, string $message): bool + { if (! $credential->hasValidSms()) { - $this->lastError = 'Connect Ladill SMS in Integrations before sending attendee SMS.'; + $this->lastError = 'SMS is not available (platform SMS key missing and no product SMS credentials).'; return false; } $apiKey = $credential->smsApiKey(); if (! $apiKey) { - $this->lastError = 'SMS credentials could not be decrypted. Reconnect SMS in Integrations.'; + $this->lastError = 'SMS credentials could not be decrypted. Reconnect SMS under Advanced keys.'; return false; } diff --git a/app/Services/Events/EventCommsService.php b/app/Services/Events/EventCommsService.php index fbf8104..1e056a9 100644 --- a/app/Services/Events/EventCommsService.php +++ b/app/Services/Events/EventCommsService.php @@ -4,6 +4,8 @@ namespace App\Services\Events; use App\Models\QrCode; use App\Models\QrEventRegistration; +use App\Services\Billing\PlatformEmailClient; +use App\Services\Billing\PlatformSmsClient; use App\Services\Billing\SmsService; use App\Services\Messaging\MessagingCredentialsService; use Illuminate\Support\Collection; @@ -23,6 +25,8 @@ class EventCommsService private readonly SmsService $sms, private readonly MessagingCredentialsService $credentials, private readonly EventProgrammeService $programmes, + private readonly PlatformEmailClient $platformEmail, + private readonly PlatformSmsClient $platformSms, ) {} /** @return array{recipients: int, emails: int, sms: int, estimated_ghs: float, affordable: bool, integrations_error: ?string} */ @@ -188,12 +192,15 @@ class EventCommsService $joinUrl, ) !== ''; - if ($needsEmail && ! $credential->hasValidBird()) { - return 'Connect Ladill Bird in Integrations before sending attendee email.'; + $emailReady = $this->platformEmail->isConfigured() || $credential->hasValidBird(); + $smsReady = $this->platformSms->isConfigured() || $credential->hasValidSms(); + + if ($needsEmail && ! $emailReady) { + return 'Email is not available yet. Bind a mailbox under Account → Messaging once platform keys are configured, or connect Bird under Advanced keys.'; } - if ($needsSms && ! $credential->hasValidSms()) { - return 'Connect Ladill SMS in Integrations before sending attendee SMS.'; + if ($needsSms && ! $smsReady) { + return 'SMS is not available yet. Ensure platform SMS is configured on this Events instance, or connect a product SMS key under Advanced keys.'; } return null; diff --git a/app/Services/Events/EventSpeakerInviteService.php b/app/Services/Events/EventSpeakerInviteService.php index 717b8c3..1889c62 100644 --- a/app/Services/Events/EventSpeakerInviteService.php +++ b/app/Services/Events/EventSpeakerInviteService.php @@ -227,7 +227,7 @@ class EventSpeakerInviteService } if (! app(EventMailer::class)->isConfiguredForOwner($ownerRef)) { - return ['ok' => false, 'error' => 'Connect Ladill Bird in Integrations before sending speaker invitations.']; + return ['ok' => false, 'error' => 'Email is not available yet. Bind a mailbox under Account → Messaging once platform keys are configured, or connect Bird under Advanced keys.']; } $token = (string) ($speaker['invite_token'] ?? ''); diff --git a/resources/views/events/attendees.blade.php b/resources/views/events/attendees.blade.php index 0df6529..9dd256d 100644 --- a/resources/views/events/attendees.blade.php +++ b/resources/views/events/attendees.blade.php @@ -54,7 +54,12 @@
Message attendees
-Send programme links, virtual join links, or both. Requires SMS and email credentials. Manage integrations
++ Send programme links, virtual join links, or both. Uses suite messaging by default (no API keys). + Account messaging settings + · + Advanced keys +
recipients ·
diff --git a/resources/views/qr/account/integrations.blade.php b/resources/views/qr/account/integrations.blade.php
index 2ba3472..f9f435e 100644
--- a/resources/views/qr/account/integrations.blade.php
+++ b/resources/views/qr/account/integrations.blade.php
@@ -2,13 +2,17 @@
- Configure + Attendee email and SMS work by default via suite messaging — + set a Ladill mailbox and optional sender ID under + Account → Messaging. + Product SMS and - email - credentials for outbound messaging. + Bird + API keys below are optional (power users / custom keys).
@if (session('success')) diff --git a/resources/views/qr/account/settings.blade.php b/resources/views/qr/account/settings.blade.php index 5c1c016..a5d3c03 100644 --- a/resources/views/qr/account/settings.blade.php +++ b/resources/views/qr/account/settings.blade.php @@ -323,9 +323,20 @@Configure SMS and email credentials for outbound messages.
- Open messaging integrations → ++ Attendee and speaker email/SMS use suite messaging by default — no Bird or SMS API keys required. + Set a Ladill mailbox (From address) and optional SMS sender ID under Account → Messaging. + Plan allowances cover included sends; Pro overage bills your wallet. +
+diff --git a/tests/Feature/EventsMessagingIntegrationsTest.php b/tests/Feature/EventsMessagingIntegrationsTest.php index 30b79a1..d9d461c 100644 --- a/tests/Feature/EventsMessagingIntegrationsTest.php +++ b/tests/Feature/EventsMessagingIntegrationsTest.php @@ -39,7 +39,8 @@ class EventsMessagingIntegrationsTest extends TestCase ->assertOk() ->assertSee('SMS') ->assertSee('Email') - ->assertSee('credentials for outbound messaging'); + ->assertSee('work by default') + ->assertSee('optional'); } public function test_saving_sms_credentials_encrypts_key(): void diff --git a/tests/Feature/EventsSuiteMessagingTest.php b/tests/Feature/EventsSuiteMessagingTest.php index d571331..c08a89f 100644 --- a/tests/Feature/EventsSuiteMessagingTest.php +++ b/tests/Feature/EventsSuiteMessagingTest.php @@ -91,6 +91,34 @@ class EventsSuiteMessagingTest extends TestCase $this->assertTrue(app(EventMailer::class)->isConfiguredForOwner($user->public_id)); } + public function test_sms_service_uses_suite_when_platform_configured(): void + { + config([ + 'sms.platform_api_key' => 'events-sms-key', + 'sms.platform_api_url' => 'https://platform.test/api', + ]); + + Http::fake([ + 'platform.test/api/sms/messages/send' => Http::response(['success' => true, 'path' => 'suite']), + ]); + + $user = User::factory()->create(['public_id' => 'usr_sms_suite']); + $ok = app(\App\Services\Billing\SmsService::class)->send( + $user->public_id, + '0241234567', + 'Programme link', + ); + + $this->assertTrue($ok); + $this->assertTrue(app(\App\Services\Billing\SmsService::class)->isConfiguredForOwner($user->public_id)); + Http::assertSent(function ($request) { + $data = $request->data(); + + return str_contains($request->url(), '/sms/messages/send') + && ($data['channel'] ?? null) === 'suite'; + }); + } + public function test_billing_client_syncs_suite_entitlement(): void { config([