# Product Requirements Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Problem statement is specific and measurable
- [x] Problem is validated by evidence (not assumptions)
- [x] Context → Problem → Solution flow makes sense
- [x] Every persona has at least one user journey
- [x] All MoSCoW categories addressed (Must/Should/Could/Won't)
- [x] Every feature has testable acceptance criteria
- [x] Every metric has corresponding tracking events
- [x] No feature redundancy (check for duplicates)
- [x] No contradictions between sections
- [x] No technical implementation details included
- [x] A new team member could understand this PRD

---

## Product Overview

### Vision

Every SMS message sent or received through BuyerKiosk has full delivery visibility and accurate cost tracking, enabling store staff to see message status in real-time and the business to understand true SMS profitability per store, per message category, per billing period.

### Problem Statement

Today, BuyerKiosk sends thousands of SMS messages (buy completions, service notifications, surveys, chat messages, custom texts) but has **zero visibility** into whether those messages actually reach customers. When a buy completion text fails to deliver, the customer never shows up, and the store has no idea why. The system tracks segment counts for billing but doesn't know the **actual cost** charged by Twilio or Vonage, meaning we can't calculate our real profit margin on SMS — we're pricing blind.

Specific pain points:
- **No delivery confirmation**: Messages are fire-and-forget. Staff have no way to know if a customer received their text.
- **Silent failures**: If a carrier rejects a message (bad number, spam filter, etc.), nobody is notified. The customer just never shows up.
- **Unknown actual costs**: We charge stores a flat rate per message type but don't know what Twilio/Vonage actually charges us. International numbers, long messages (multi-segment), and carrier surcharges are invisible.
- **No profit analysis**: Without knowing actual costs, we can't determine if SMS is profitable per store, per category, or in aggregate.
- **Inbound cost blindness**: When customers text back through Chat, we absorb the inbound cost with no tracking at all.

### Value Proposition

This feature transforms SMS from a black box into a fully observable, profitable communication channel:
- **Store staff** see delivery status on every message, letting them follow up immediately when texts fail
- **Business operators** get real-time profit margin data per store and message category, enabling data-driven pricing decisions
- **Operations** can identify stores with poor delivery rates, flagging phone data quality issues before they impact customer experience

---

## User Personas

### Primary Persona: Store Staff Member
- **Demographics:** Store team members who manage daily buy queue operations, varying technical expertise
- **Goals:** Complete buys efficiently, ensure customers receive pickup notifications, manage customer communications via Chat
- **Pain Points:** No way to know if a customer got their text; when customers don't show up, they don't know if it's a messaging failure or the customer's choice; can't see which messages failed in Chat conversations

### Secondary Personas

#### Business Administrator (BuyerKiosk Internal)
- **Demographics:** Internal BuyerKiosk team managing the platform's billing and operations
- **Goals:** Understand true SMS costs across all stores, ensure SMS pricing covers costs and generates profit, identify stores with delivery issues
- **Pain Points:** Pricing SMS services without knowing actual costs; no way to compare Twilio vs Vonage costs; can't see which message categories are most expensive; no profit margin visibility

#### Store Owner/Manager
- **Demographics:** Store owners who review reports and manage their store's operations
- **Goals:** Understand their store's SMS usage, ensure communications are reaching customers
- **Pain Points:** Pay for SMS services with no visibility into delivery success rates; can't tell if they have customer phone data quality issues

---

## User Journey Maps

### Primary User Journey: Staff Monitoring Delivery Status

1. **Awareness:** Staff completes a buy and the system sends the customer a pickup text. Staff sees a "Sent" indicator next to the buy in the Completed Buys grid.
2. **Update:** Minutes later, the delivery receipt webhook fires. The status automatically updates to "Delivered" — no refresh needed on the Chat UI, and visible on next load of the Completed Buys page.
3. **Failure Detection:** If a message fails, the status updates to "Failed" with a red indicator. A toast notification appears on any Workbook SPA page alerting staff to the failure. In the Chat thread, a "Message Failed" inline status appears on the message timeline.
4. **Resolution:** Staff sees the failed message, can view the error reason (e.g., "Invalid phone number", "Carrier rejected"), and decides to call the customer instead or resend via a different channel.

### Secondary User Journey: Business Admin Analyzing SMS Profitability

1. **Access:** Admin navigates to the Billing module and opens the SMS Costs section.
2. **Overview:** Sees aggregate SMS cost vs revenue across all stores for the current billing period, with overall profit margin percentage.
3. **Drill-down:** Filters by specific store and date range. Views breakdown by message category (buy completion, service, survey, chat transactional, chat interactive, custom) showing: messages sent, actual provider cost, revenue charged, and profit per category.
4. **Insights:** Identifies that Chat Interactive messages at Store XY00 have high volume and low margin. Uses data to adjust pricing or discuss usage with the store.

### Tertiary User Journey: Ops Reviewing Store Delivery Health

1. **Access:** Admin views the Delivery Rate KPI per store in the Billing module.
2. **Overview:** Sees delivery rate percentage per store (e.g., "OU00: 96% delivered, 2% failed, 2% sent/unknown").
3. **Flag:** Notices Store PA00 has only 78% delivery rate — significantly below average.
4. **Action:** Investigates PA00's failed messages, discovers many are going to landline numbers. Works with store to improve phone number collection quality.

---

## Feature Requirements

### Must Have Features

#### Feature 1: Delivery Status Webhook Endpoints
- **User Story:** As a platform operator, I want the system to receive delivery status callbacks from Twilio and Vonage so that every outbound message has an accurate delivery status.
- **Acceptance Criteria:**
  - [ ] Vonage delivery receipt webhook endpoint exists and processes incoming status updates
  - [ ] Twilio status callback webhook endpoint exists and processes incoming status updates
  - [ ] Webhook endpoints use URL obscurity (secret token in path) for security
  - [ ] Webhooks map provider-specific statuses into our 3-state model using these rules:
    - **Sent**: Twilio `queued`, `accepted`, `sending`, `sent`; Vonage `submitted`, `buffered`
    - **Delivered**: Twilio `delivered`; Vonage `delivered`
    - **Failed**: Twilio `failed`, `undelivered`; Vonage `failed`, `rejected`, `expired`
  - [ ] Webhooks store the provider's original status code AND the mapped 3-state status
  - [ ] Webhooks store the error code/reason when a message fails (provider error code + human-readable description)
  - [ ] Webhooks are idempotent (processing the same receipt twice doesn't cause issues)
  - [ ] Webhooks always return 200 OK to prevent provider retries regardless of processing result
  - [ ] Failed webhook processing is logged with full payload for debugging (logged to error log, not silently swallowed)
  - [ ] Raw webhook payloads are logged for all incoming requests to enable troubleshooting

#### Feature 1a: Webhook Processing Monitoring
- **User Story:** As a platform operator, I want visibility into webhook processing health so that I can detect and diagnose data loss from failed webhook handling.
- **Acceptance Criteria:**
  - [ ] Every webhook request is logged (provider, type, timestamp, processing result)
  - [ ] Failed webhook processing generates an error log entry with full request payload
  - [ ] A `sms.webhook.processing_failed` tracking event is emitted when webhook processing fails
  - [ ] Webhook processing success/failure counts are queryable for a given time period

#### Feature 2: Delivery Status on Completed Buys Page
- **User Story:** As a store staff member, I want to see whether my buy completion texts were delivered so that I can follow up with customers whose messages failed.
- **Acceptance Criteria:**
  - [ ] Each completed buy row shows a 3-state delivery status: Sent, Delivered, Failed
  - [ ] Status icons: Sent = gray checkmark, Delivered = green double-checkmark, Failed = red exclamation/X
  - [ ] Status shows "Sent" immediately when the message is dispatched (on successful API response from provider)
  - [ ] Status updates to "Delivered" or "Failed" when the delivery receipt arrives
  - [ ] Failed status uses `text-danger` (red) color from design token system to ensure visibility
  - [ ] If no delivery receipt ever arrives, status remains "Sent" indefinitely (no timeout)
  - [ ] Status is visible as an icon/badge directly in the row, without clicking into the buy details

#### Feature 3: Delivery Status in Chat UI
- **User Story:** As a store staff member, I want to see delivery status on each outbound message in the Chat interface so that I know if my texts are reaching the customer.
- **Acceptance Criteria:**
  - [ ] Each outbound message in the Chat thread shows its delivery status using the same icon convention as Completed Buys (gray check / green double-check / red X)
  - [ ] Status updates in real-time via Ably (no refresh needed)
  - [ ] Failed messages show a "Message Failed" inline status in the chat timeline, using the same visual style as existing "New Buy" system messages (centered, muted background, system font)
  - [ ] Failed message error reason is viewable by clicking on the failed message (shows tooltip or expandable detail with human-readable error)
  - [ ] Error reasons are translated from provider codes to plain English (e.g., Twilio error 30003 → "Unreachable phone — the number may be disconnected or out of range")
  - [ ] A toast notification appears on ANY Workbook SPA page when a delivery failure occurs, not just when Chat is open
  - [ ] Toast includes the customer name (if available) and store, e.g., "SMS to John D. failed to deliver"

#### Feature 4: Provider Cost Capture (Outbound)
- **User Story:** As a business administrator, I want to know the actual cost charged by Twilio/Vonage for every outbound message so that I can calculate our true profit margin.
- **Acceptance Criteria:**
  - [ ] Actual cost from Twilio is captured per outbound message (via Twilio Message Resource API)
  - [ ] Actual cost from Vonage is captured per outbound message (from delivery receipt data)
  - [ ] Cost is stored in USD at per-message granularity
  - [ ] Cost data is linked to the existing `billingSmsUsage` record for the message
  - [ ] If cost data is unavailable from the provider, the record is flagged as "cost_unknown" (not estimated)
  - [ ] Failed cost lookups are retried up to 3 times over a 4-hour window via background processing before being permanently marked "cost_unknown"
  - [ ] Cost tracking covers ALL outbound send paths: buy completion, service completion, survey, chat transactional, chat interactive, custom

#### Feature 5: Provider Cost Capture (Inbound)
- **User Story:** As a business administrator, I want to know the cost of receiving inbound customer messages so that I have a complete picture of our SMS spend.
- **Acceptance Criteria:**
  - [ ] Vonage inbound: cost is extracted from the inbound webhook payload (Vonage includes price data)
  - [ ] Twilio inbound: cost is retrieved via a follow-up API call using the inbound message SID (Twilio inbound webhooks do NOT include cost data)
  - [ ] Cost lookup for Twilio inbound follows the same async pattern as outbound: queue a background API call using the message SID from the webhook
  - [ ] Inbound costs are tracked in the same billing usage system as outbound costs
  - [ ] Inbound cost is attributed to the correct store based on message routing (via ChatMatchingService)
  - [ ] Inbound messages are categorized as "inbound" in the billing usage system
  - [ ] If inbound cost lookup fails, the record is flagged as "cost_unknown" (same as outbound handling)

#### Feature 6: SMS Cost Report in Billing Module
- **User Story:** As a business administrator, I want to see SMS costs and profit margins inside the Billing module so that I can make data-driven pricing decisions.
- **Acceptance Criteria:**
  - [ ] An "SMS Costs" section exists within the Billing module
  - [ ] Shows aggregate cost vs revenue with profit margin for the selected period
  - [ ] Filterable by store (single store or all stores)
  - [ ] Filterable by date range
  - [ ] Breaks down by message category (buy completion, service, survey, chat transactional, chat interactive, custom, inbound)
  - [ ] Shows per-category: message count, actual provider cost, revenue charged (from billing config), profit/loss
  - [ ] Categories included in base plan (unlimited) show cost but $0 revenue with a "Base Plan" label
  - [ ] Categories charged per-message show actual cost, revenue, and profit margin

#### Feature 7: Outbound SMS Sender Configuration
- **User Story:** As a platform operator, I want all outbound SMS senders to include a status callback URL so that delivery receipts are routed to our webhook endpoints.
- **Acceptance Criteria:**
  - [ ] TwilioTextSender includes a `StatusCallback` URL when creating messages
  - [ ] VonageTextSender configures delivery receipt URL correctly (already partially exists)
  - [ ] Both senders capture and store the provider message ID on every send for later cost lookup
  - [ ] Both senders return the provider message ID in their response so downstream systems can correlate

### Should Have Features

#### Feature 8: Per-Store Delivery Rate KPI
- **User Story:** As a business administrator, I want to see the delivery success rate per store so that I can identify stores with phone data quality issues.
- **Acceptance Criteria:**
  - [ ] Delivery rate percentage is calculated per store (delivered / total sent * 100)
  - [ ] Visible in the Billing module's SMS Costs section
  - [ ] Shows breakdown: X% Delivered, Y% Failed, Z% Sent (no receipt)
  - [ ] Stores below 90% delivery rate are visually flagged
  - [ ] Rate is calculated for the selected date range

#### Feature 9: Demo Store Delivery Simulation
- **User Story:** As a user testing in demo mode, I want to see simulated delivery statuses so that I can experience the full feature flow without sending real SMS messages.
- **Acceptance Criteria:**
  - [ ] Demo stores auto-set delivery status to "Delivered" after a short simulated delay
  - [ ] Demo store messages are excluded from cost/profit calculations
  - [ ] Demo messages are clearly marked as simulated in any reporting

### Could Have Features

#### Feature 10: Delivery Failure Retry
- **User Story:** As a store staff member, I want to be able to retry a failed message directly from the UI.
- **Acceptance Criteria:**
  - [ ] Failed messages in Chat show a "Retry" button
  - [ ] Retry sends the same message content to the same phone number
  - [ ] Retry creates a new message record (preserves the original failed record)

#### Feature 11: Cost Trend Charts
- **User Story:** As a business administrator, I want to see SMS cost trends over time so that I can identify spending patterns.
- **Acceptance Criteria:**
  - [ ] Line chart showing daily/weekly/monthly SMS costs over a selected period
  - [ ] Compare cost trends across stores or message categories

### Won't Have (This Phase)

- **Provider signature validation on webhooks** — URL obscurity is sufficient for now; can add later if needed
- **Automated delivery failure alerts beyond toast notifications** — No email/SMS alerts to store managers for failed deliveries
- **Automatic phone number flagging** — Won't auto-flag numbers with repeated failures
- **Historical backfill** — No retrospective cost/status data for messages sent before deployment
- **International rate management** — All stores are US/Canada; no country-specific cost tracking
- **Legacy loyalty SMS tracking** — Legacy loyalty routes are no longer in active use
- **MMS cost tracking** — System only sends SMS; MMS is not supported
- **Provider cost comparison dashboard** — Comparing Twilio vs Vonage costs per message type (can be derived from per-message data later)

---

## Detailed Feature Specifications

### Feature: Provider Cost Capture (Outbound) — Most Complex Feature

**Description:** After every outbound SMS is sent, the system captures the actual cost charged by the provider. For Twilio, this requires a follow-up API call using the message SID. For Vonage, the cost is included in the delivery receipt webhook payload. The cost is stored per-message and linked to the billing usage record for aggregate reporting.

**User Flow:**
1. Staff or system triggers an outbound SMS (buy completion, chat message, etc.)
2. System sends the message via TwilioTextSender or VonageTextSender
3. System stores the provider message ID in both the billing usage record and (if applicable) the chat message record
4. **Twilio path:** When the delivery status webhook fires, system uses the message SID to query Twilio's API for the message cost, then stores it
5. **Vonage path:** The delivery receipt webhook payload includes the cost directly; system extracts and stores it
6. Cost is recorded in USD at per-message granularity in the billing usage table

**Business Rules:**
- Rule 1: Cost must be stored as actual provider cost in USD, not estimated
- Rule 2: If cost data is unavailable (API failure, missing from receipt), the record is flagged as "cost_unknown" — never estimated or defaulted to zero
- Rule 3: Demo store messages have $0.00 cost and are excluded from profit calculations
- Rule 4: Inbound messages also capture cost — Vonage includes price in the inbound webhook payload; Twilio requires an async API lookup using the inbound message SID (same retry pattern as outbound)
- Rule 5: Cost is always linked to a specific store via typeNum so that per-store profit can be calculated
- Rule 6: All message categories (buy completion, service, survey, chat transactional, chat interactive, custom) must have cost tracked — no exceptions
- Rule 7: The flat rate charged to the store is determined by the existing `billingSmsCategoryConfig` table — this feature does NOT change how stores are charged

**Edge Cases:**
- Provider API is down when cost lookup is attempted → Store the message with "cost_unknown" flag; implement a background retry mechanism to fill in cost later
- Message is sent but webhook never arrives → Cost may never be known for that message; it stays as "cost_unknown" and delivery status stays "Sent"
- Multi-segment message → Provider charges per segment; actual cost from provider already reflects this (no need to multiply)
- Store switches providers mid-billing-period → Cost data comes from whichever provider was used for each message; reporting handles mixed providers
- Vonage delivery receipt arrives before the outbound send is fully logged → Webhook must handle race condition gracefully (queue and retry matching, or accept orphaned receipts)
- Very high volume store sends 1000+ messages/day → Cost lookups must not block sending; all cost fetching is async
- Provider returns cost in non-USD currency → Convert to USD at time of receipt (US/Canada stores only, so this is unlikely but should be handled)

### Feature: Delivery Status in Chat UI

**Description:** When a delivery receipt webhook updates a message's status, the system publishes an Ably event. The Chat UI listens for these events and updates the message bubble in real-time. Failed messages display an inline "Message Failed" status in the timeline and trigger a toast notification on any Workbook SPA page.

**User Flow:**
1. Staff sends a message via Chat
2. Message bubble shows "Sent" status (checkmark or similar indicator)
3. Delivery receipt webhook fires → message status updates to "Delivered" (double-checkmark) or "Failed" (red X)
4. Ably event pushes the update to any connected browser
5. Chat UI updates the message status inline, no refresh needed
6. If failed: a toast notification appears on whatever Workbook page the user is on
7. Staff clicks the failed message to see the error reason

**Business Rules:**
- Rule 1: Toast notifications for failures must appear on ALL Workbook SPA pages, not just Chat
- Rule 2: The inline "Message Failed" status in Chat should be styled like existing system messages (e.g., "New Buy" messages)
- Rule 3: Error reason must be human-readable (translate provider error codes into plain English where possible)
- Rule 4: Only outbound messages show delivery status; inbound messages don't need it (they were obviously received)

**Edge Cases:**
- Staff closes their browser before the delivery receipt arrives → Status is stored in DB; visible next time they open the thread
- Multiple staff viewing the same thread → All see the real-time update simultaneously via Ably
- Message fails but no error reason is provided by the carrier → Display "Delivery failed — no additional details from carrier"
- Chat thread is closed/archived when delivery receipt arrives → Still update the message status; it's visible if the thread is reopened

---

## Success Metrics

### Key Performance Indicators

- **Delivery Visibility Coverage:** 100% of outbound messages sent after deployment have a tracked delivery status (Sent, Delivered, or Failed). Measured continuously.
- **Cost Capture Rate:** >95% of outbound messages have actual provider cost data within 4 hours of sending (allowing for retries). Measured weekly.
- **Inbound Cost Capture:** 100% of inbound messages processed through Chat webhooks have cost data within 4 hours. Measured weekly.
- **Delivery Status Timeliness:** >90% of delivery receipts processed within 5 minutes of provider sending them. Measured by webhook receipt timestamp vs. provider timestamp.
- **Delivery Rate Baseline:** Establish per-store delivery rate baselines within 30 days of deployment.
- **Profit Margin Visibility:** Business can see actual SMS profit margin per store, per category, per billing period within the first billing cycle after deployment.

### Tracking Requirements

| Event | Properties | Purpose |
|-------|------------|---------|
| `sms.outbound.sent` | typeNum, provider, messageId, category, segmentCount | Track every outbound send |
| `sms.outbound.delivered` | typeNum, provider, messageId, deliveredAt | Track successful deliveries |
| `sms.outbound.failed` | typeNum, provider, messageId, errorCode, errorReason | Track failures and reasons |
| `sms.outbound.cost_captured` | typeNum, provider, messageId, costUsd, currency | Track cost data capture |
| `sms.outbound.cost_unknown` | typeNum, provider, messageId, reason | Track cost capture failures |
| `sms.inbound.received` | typeNum, provider, messageId, costUsd | Track inbound messages and costs |
| `sms.webhook.received` | provider, webhookType, statusCode | Monitor webhook health |
| `sms.webhook.processing_failed` | provider, webhookType, errorMessage, rawPayload | Detect data loss from processing errors |
| `sms.cost.retry_attempted` | provider, messageId, attemptNumber, result | Track cost lookup retry health |
| `sms.delivery_rate.calculated` | typeNum, period, deliveredPct, failedPct, unknownPct | Periodic delivery rate calculation |
| `billing.sms_report.viewed` | userId, filters, dateRange | Track report usage |

---

## Constraints and Assumptions

### Constraints
- **Provider API rate limits:** Twilio's API has rate limits for message lookups; cost fetching must be throttled or batched to stay within limits
- **Webhook delivery:** Providers may retry webhooks if we don't return 200 OK quickly; endpoints must respond fast and process asynchronously
- **Existing billing infrastructure:** Must integrate with the existing `billingSmsUsage` and `billingSmsCategoryConfig` tables, not replace them
- **US/Canada only:** All stores operate in the US/Canada; no need for international rate handling or multi-currency support beyond USD
- **No legacy loyalty:** Legacy loyalty SMS routes (`/sms/inbound`, `/sms/delivery`) are deprecated and not in scope
- **TaskEngine dependency:** Background cost lookups and retries require the TaskEngine worker infrastructure to be running

### Assumptions
- Twilio's Message Resource API returns cost data reliably for recent messages (within minutes of sending)
- Vonage delivery receipts include price data in the webhook payload (per their documentation)
- The existing Ably infrastructure can handle additional delivery status events without significant load increase
- The Billing module UI (currently being built in spec-036) will be available to host the SMS Cost report; if not ready, the SMS Cost report can be deployed behind an admin-only feature flag and surfaced when the Billing module is complete
- Demo stores are identifiable programmatically (store config or typeNum pattern)
- The existing `provider_message_id` field in `chat_messages` and `billingSmsUsage` tables can be used to correlate webhook data with sent messages
- The TaskEngine worker pool has capacity for additional background jobs (cost lookups, retries)

---

## Risks and Mitigations

| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Twilio cost API lookup fails or times out | Medium — "cost_unknown" records accumulate | Low | Implement background retry queue; batch lookups during off-peak |
| Vonage delivery receipt doesn't include cost data | High — lose Vonage cost visibility | Low | Verify Vonage API docs; have fallback to Vonage pricing API if needed |
| High webhook volume overwhelms the server | High — lost delivery data | Low | Process webhooks asynchronously; queue for background processing |
| Race condition: webhook arrives before send is logged | Medium — orphaned webhook data | Medium | Queue incoming webhooks with short delay; or write webhook data first and match later |
| Demo store simulation confuses real reporting | Low — inaccurate metrics | Low | Clearly flag demo records; exclude from all aggregate calculations |
| Twilio rate limits hit during cost lookups | Medium — delayed cost data | Medium | Batch lookups; use exponential backoff; spread lookups over time |
| Provider changes webhook payload format | High — breaks status tracking | Low | Version webhook handlers; log raw payloads for debugging; monitor for parsing errors |

---

## Open Questions

- [x] What flat rate do we charge per message? → Per-store, per-category rates configured in `billingSmsCategoryConfig`
- [x] Should we validate webhook signatures? → No, URL obscurity is sufficient for v1
- [x] What about legacy loyalty SMS? → Deprecated, not in scope
- [x] International numbers? → US/Canada only
- [x] Historical backfill? → Forward-only
- [x] Where does the report live? → Inside Billing module

All open questions have been resolved through the discovery process.

---

## Supporting Research

### Competitive Analysis

Most SMS platforms (Twilio Console, Vonage Dashboard, MessageBird) provide delivery status and cost data natively in their dashboards. However, BuyerKiosk's value is bringing this data **into the operational workflow** — showing delivery status where staff are already working (Completed Buys, Chat) rather than requiring them to log into a separate provider dashboard. The profit margin analysis layer is unique to our multi-tenant model where we resell SMS at a markup.

### User Research

Based on operational experience:
- Store staff frequently call customers when they don't show up for pickups, without knowing if the text was delivered — this wastes time
- Store owners ask about SMS costs during onboarding but have no self-service way to see the data
- Business operations currently estimates SMS profitability using average per-segment rates, which is inaccurate for stores with different usage patterns

### Market Data

- SMS delivery failure rates in the US market average 3-5% due to invalid numbers, carrier filtering, and network issues
- Twilio charges $0.0079/segment for US outbound SMS; Vonage charges approximately $0.0068/segment
- Inbound SMS typically costs less than outbound (~$0.0075/segment for Twilio)
- Multi-segment messages (>160 chars GSM-7 or >70 chars UCS-2) are charged per segment, making actual costs 2-4x the apparent per-message rate for longer texts
