One send contract across all five providers, and stop dropping inbound texts

FixVOIPService
Shipped
September 6, 2026 at 5:05 PM UTC
Author
Kamo
Commit
de96099

The SMS abstraction layer had five providers disagreeing about how to fail and an inbound path that discarded real customer messages in silence. Sending - SendSmsResult -> SmsSendOutcome: a machine code to branch on (SmsFailureCode) and a finished sentence to show the member unchanged. handleOutbound read the old type's status and DROPPED errorMessage, so Teams' carefully written "add an SMS provider" explanation had never once reached a member. - New SmsGateway is the only way a text leaves this service. It refuses a provider whose ProviderCapabilities.sms is false -- a flag all five declared and nothing read -- and normalises anything a provider throws, returns null for, or times out on. - FreePBX threw **************** (a 500 with a stack trace) where Teams returned a sentence; both now answer NOT_SUPPORTED, with the wording still owned by the provider via smsUnavailableReason(). - KamoPBX had no try/catch at all, alone among the five: a timeout escaped as a 500 where every other provider returned FAILED. - RingCentral dereferenced response.getBody().get("id") twice unguarded on the SUCCESS path, reporting an accepted message as a failure, and leaked raw exception text as member-facing wording. - GET /sms/capability so the browser can gate the texting UI up front instead of letting a Teams org type a message and press send into nothing. Receiving - ingest() resolved the owning member with an exact string match on **************** RingCentral delivers +19495551212; an admin who typed (949) 555-1212 matched nothing and the text was dropped at a log.warn. New VOIP_NUMBER_OWNERSHIP inventory resolves canonically, refusing to guess when two orgs or two members claim one number. DDL is manual -- InitializerService is not deployed. Falls back to the old lookup, so rollback is dropping the table. - KamoPBX declared sms(true) and could send, but nothing in this service could receive on it, so those orgs never saw a single reply. New ON_PREMISE rail and /internal/voip/sms-inbound, a sibling of the existing /ring seam. Conversations - upsertConversation matched EXTERNAL_PHONE_NUMBER exactly, so each spelling of one number grew its own thread with its own history. Now canonical on read and E.164 on write, leaving genuinely international numbers untouched. Also: a failed send no longer links to a lead timeline as though it had gone, a never-transmitted attachment is no longer stored as a data: URL, and a HELP auto-reply the provider refuses is logged as refused rather than as answered. 440 tests pass, +20 new.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing