fix(events): point Messaging settings at suite defaults, not API keys
Deploy Ladill Events / deploy (push) Successful in 48s

Mirror Care: suite platform keys count as ready for attendee email/SMS;
Account Messaging is primary, product Bird/SMS keys are advanced.
SmsService now prefers channel=suite before customer API keys.
This commit is contained in:
isaacclad
2026-07-16 11:36:43 +00:00
parent 3d5ffa593d
commit a5ebd1e414
11 changed files with 127 additions and 16 deletions
@@ -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'),
]);
}
@@ -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(),
]);
}
@@ -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'),
]);
}
+44 -2
View File
@@ -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;
}
+11 -4
View File
@@ -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;
@@ -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'] ?? '');
+6 -1
View File
@@ -54,7 +54,12 @@
</span>
<div>
<p class="text-sm font-semibold text-slate-900">Message attendees</p>
<p class="text-xs text-slate-500">Send programme links, virtual join links, or both. Requires SMS and email credentials. <a href="{{ route('account.integrations') }}" class="font-medium text-indigo-600 hover:text-indigo-800">Manage integrations</a></p>
<p class="text-xs text-slate-500">
Send programme links, virtual join links, or both. Uses suite messaging by default (no API keys).
<a href="{{ $messagingSettingsUrl ?? '#' }}" class="font-medium text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">Account messaging settings</a>
·
<a href="{{ route('account.integrations') }}" class="text-slate-500 hover:text-slate-700">Advanced keys</a>
</p>
<template x-if="preview">
<p class="mt-2 text-xs text-slate-600">
<span x-text="preview.recipients"></span> recipients ·
@@ -2,13 +2,17 @@
<x-slot name="title">Integrations</x-slot>
<div class="mx-auto max-w-2xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Messaging integrations</h1>
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Messaging (advanced)</h1>
<p class="mt-0.5 text-sm text-slate-500">
Configure
<strong class="font-semibold text-slate-800">Attendee email and SMS work by default</strong> via suite messaging
set a Ladill mailbox and optional sender ID under
<a href="{{ $messagingSettingsUrl ?? (function_exists('ladill_account_url') ? ladill_account_url('/account/settings/messaging') : (config('smtp.messaging_settings_url') ?? '#')) }}"
class="font-medium text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">Account Messaging</a>.
Product
<a href="https://sms.ladill.com" target="_blank" rel="noopener" class="font-medium text-indigo-600 hover:text-indigo-800">SMS</a>
and
<a href="https://bird.ladill.com" target="_blank" rel="noopener" class="font-medium text-indigo-600 hover:text-indigo-800">email</a>
credentials for outbound messaging.
<a href="https://bird.ladill.com" target="_blank" rel="noopener" class="font-medium text-indigo-600 hover:text-indigo-800">Bird</a>
API keys below are optional (power users / custom keys).
</p>
@if (session('success'))
+14 -3
View File
@@ -323,9 +323,20 @@
</form>
<div class="mt-4 rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Messaging integrations</h2>
<p class="mt-1 text-xs text-slate-500">Configure SMS and email credentials for outbound messages.</p>
<a href="{{ route('account.integrations') }}" class="mt-3 inline-block text-sm font-semibold text-indigo-600 hover:text-indigo-800">Open messaging integrations </a>
<h2 class="text-sm font-semibold text-slate-900">Messaging</h2>
<p class="mt-1 text-xs text-slate-500">
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.
</p>
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-sm font-semibold">
<a href="{{ $messagingSettingsUrl ?? '#' }}" class="text-indigo-600 hover:text-indigo-800" target="_blank" rel="noopener">
Account messaging settings
</a>
<a href="{{ route('account.integrations') }}" class="text-slate-500 hover:text-slate-700">
Advanced: optional product API keys
</a>
</div>
</div>
<p class="mt-6 text-xs text-slate-400">
@@ -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
@@ -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([