Skip to content

Phase 5: Telnyx and Vonage adapters, provider contract, Noticed - #5

Merged
timimsms merged 1 commit into
mainfrom
phase-5-breadth
Oct 16, 2026
Merged

timimsms merged 1 commit into
mainfrom
phase-5-breadth

Conversation

@timimsms

Copy link
Copy Markdown
Owner

Implements Phase 5 of docs/SPEC.md. Stacked on #4 (then #3, #2, #1). Merge in order.

What's included

  • Telnyx adapter. Uses the Messaging API v2 with a bearer key. A sender's messaging_service maps to messaging_profile_id, and media URLs and a per-message webhook URL are passed through. Webhooks are verified with Ed25519 over timestamp|raw body against the account public key, rejecting events more than five minutes old. Telnyx's STOP block, 40300, records an opt-out. Carrier throughput codes are retried.
  • Vonage adapter. Uses the SMS API, with unicode detection, TTL, and delivery-receipt callbacks. Webhook signatures follow the official Vonage Ruby SDK's algorithm, for both md5hash and the HMAC methods, and stale timestamps are rejected. Multipart sends keep the first part's id and total the price.
  • Provider contract. Smswire::ProviderContract is a Minitest module shipped in the gem. It checks receipts, the normalized status vocabulary, permanent error reasons, retryable errors, a full pipeline send, and, for declared capabilities, status and inbound parsing plus rejection of forged signatures.
  • Noticed integration. Noticed::DeliveryMethods::Smswire is loaded when Noticed is present. deliver_by :smswire with a messenger and action sends through the full pipeline using deliver_later. Each delivery stores the Noticed notification and event ids in its metadata.
  • One webhook controller. Both endpoints share it and accept either event kind, so a Telnyx messaging profile that posts everything to one URL is routed correctly.

Decisions

  • Vonage error 7 maps to :unroutable, not :opted_out. It means "number barred," which can include opt-outs but also other blocks, and treating it as an opt-out would write false consent records. The contract gained a contract_reports_opt_outs? hook so an adapter can say this explicitly instead of the suite accepting a guess.
  • The Noticed adapter uses deliver_later. Smswire's retries then apply, since Noticed's own delivery jobs don't retry. The delivery-to-notification join uses metadata instead of a callback write-back, so neither gem depends on the other's schema.
  • No currency is recorded for Vonage prices. Vonage's response doesn't name one.

Verification

  • 192 tests, 645 assertions, passing on Rails 7.1, 7.2, 8.0, and 8.1.
  • Eight consecutive randomized runs pass on Rails 8.1.
  • The three skipped tests belong to the generated provider skeleton, which declares no webhook capabilities.
  • The acceptance tests:
    • Twilio, Telnyx, and Vonage each pass the contract using the dummy app's OrderMessenger#shipped.
    • A Noticed notifier delivers through that messenger, records the recipient and Noticed ids, and respects opt-outs.
  • The provider generator's output is generated, loaded, and run against the contract inside the suite.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L5SuBiSMN4r9qr5CXbKkHV

…iced

Add Telnyx (Messaging API v2, Ed25519 webhook signatures with a
five-minute tolerance) and Vonage (SMS API, md5hash and HMAC webhook
signatures matching the official SDK). Vonage's "number barred" error
maps to :unroutable because it also covers blocks other than opt-outs.

Ship Smswire::ProviderContract, a Minitest suite covering receipts,
status vocabulary, error taxonomy, a pipeline send, and, for declared
capabilities, callback parsing and signature rejection. All three
adapters pass it with the dummy app's messenger, and the provider
generator now writes an adapter and test that pass it as generated.

Add Noticed::DeliveryMethods::Smswire, loaded when Noticed is present.
It delivers through the messenger with deliver_later and records the
Noticed notification and event ids in delivery metadata.

Webhook endpoints now share one controller and accept both event
kinds, so a provider that posts everything to one URL is routed by
event type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5SuBiSMN4r9qr5CXbKkHV
@timimsms
timimsms changed the base branch from phase-4-devx to main October 16, 2026 05:21
@timimsms
timimsms merged commit eb6e41f into main Oct 16, 2026
13 checks passed
@timimsms
timimsms deleted the phase-5-breadth branch October 16, 2026 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant