# PSTN phone fallback

When a user's data connection is too weak for WebRTC, the platform can ask the calling server to
bridge an accepted consultation over **regular phone calls** through a telephony provider. The
server-side meter (cap, warning, extend, hard stop, billing-grade end event) keeps running exactly
as for a WebRTC session; the provider only carries audio.

Code: `internal/pstn` (provider interface, `Mock`, `Twilio`), `internal/consult/external.go`
(`StartExternal`, `EndExternal`, `MeterSnapshot`), `internal/api/ext_pstn.go` (routes, bridge
state, persistence, webhook, meter watcher).

## Flow

```
 your backend                 calling server                       provider (Twilio)         phones
 ────────────                 ──────────────                       ─────────────────         ──────
 POST /consultations ──────►  requested
 POST /{id}/accept   ──────►  accepted (room + tokens as usual)
 user app reports "network too weak"
 POST /{id}/phone    ──────►  bridge: dialing ───── Dial ────────►  call user ─────────────► user rings
     ◄── 201 {bridge_id}                                            <Dial><Number> ────────► consultant rings
                              ◄── webhook leg=user answered ──────
                              ◄── webhook leg=consultant answered ─
                              both answered → StartExternal
                              consultation: active, meter starts
                              event consultation.started {source:"pstn"}, pstn.answered
                              ... meter ticks / warning / extend as usual ...
     either party hangs up    ◄── webhook leg=X completed ─────────
                              EndExternal(user_hangup | consultant_hangup)
                              Hangup(bridge) ─────────────────────►  drop the other leg
                              consultation: ended {billed_seconds, ended_by}, pstn.ended
     or the meter ends it     (balance_exhausted / POST /end / cancel)
                              watcher (every 5 s, or next provider event)
                              Hangup(bridge) ─────────────────────►  both legs dropped
```

Bridge states: `dialing` → `answered` → `ended`; `failed` when a leg fails/busy/no-answer before both
answered (consultation ends with reason `phone_failed`, `billed_seconds: 0`).

The consultation's `connect_timeout_seconds` (default 60 s) still applies while the phones ring: if
both legs have not answered by then the consultation ends with `connect_timeout` and the bridge is hung up.
Pass a larger `connect_timeout_seconds` on `POST /v1/consultations` if you expect slow pickups.

## API (application key)

### `POST /v1/consultations/{id}/phone`
```json
{ "user_number": "+919876543210", "consultant_number": "+919812345678", "caller_id": "+1855…" }
```
`caller_id` is optional (defaults to `TWILIO_CALLER_ID`). Numbers must be E.164. The consultation must be
`accepted` or `active` (an active WebRTC session can be moved to the phone; the meter simply continues).
→ `201 { "bridge_id", "status": "dialing", "bridge": {…} }` · `409` if not accepted/active or a bridge is already live ·
`502` if the provider refused the call · `501` if `PSTN_PROVIDER` is misconfigured.

### `GET /v1/consultations/{id}/phone`
→ `200 { "bridge_id", "status", "bridge": { "legs": {"user": "answered", "consultant": "ringing"}, "end_reason", … }, "meter": { "state", "elapsed_seconds", "remaining_seconds", … } }`
`404` when the consultation never had a bridge. `meter` is the same payload WebRTC peers receive on the socket
(`state`: `pending` | `active` | `ended`) so a phone-only session can still show a countdown in your app.

### `POST /v1/pstn/webhook/{provider}` (public, signed)
Provider status callbacks. Twilio: `X-Twilio-Signature` is verified against `PUBLIC_URL` + request path.
Mock: JSON `{"bridge_id","leg","status"}` with `X-Mock-Signature` = hex HMAC-SHA256 of the body using
`PSTN_MOCK_SECRET` (unsigned when the secret is empty). Responses: `200`, `401` bad signature, `404` unknown bridge/provider.

### Events (webhooks + analytics)
`pstn.dialing` (`leg: "both"`), `pstn.answered` (per leg), `pstn.ended` (`leg` = the leg that ended it, or `both` when the meter/API did) with
`data: { bridge_id, leg, status, consultation_id, provider }`, plus the usual `consultation.started`
(with `data.source: "pstn"`) and `consultation.ended` (`reason`: `user_hangup`, `consultant_hangup`,
`phone_failed`, `balance_exhausted`, `closed_by_api`). `GET /v1/consultations/{id}` shows `metadata.source: "pstn"`.

## Configuration (environment)

| Variable | Purpose |
|----------|---------|
| `PSTN_PROVIDER` | `mock` (default) or `twilio` |
| `TWILIO_ACCOUNT_SID` | Twilio account SID (`AC…`) |
| `TWILIO_AUTH_TOKEN` | Twilio auth token; used for REST auth and webhook signature checks, never logged |
| `TWILIO_CALLER_ID` | Twilio number shown to both parties, E.164 |
| `PUBLIC_URL` | Public base URL of the API; callbacks are `PUBLIC_URL/v1/pstn/webhook/twilio` and signatures are checked against it |
| `PSTN_MOCK_SECRET` | Optional HMAC secret for simulated mock webhooks |
| `DATA_DIR` | Bridges are persisted in `DATA_DIR/pstn_bridges.json` (last 2000 kept) |

## Twilio setup

1. Buy (or verify) a voice-capable number in the Twilio console; put it in `TWILIO_CALLER_ID`.
   For outbound calls to each country check Twilio's geographic permissions (Voice → Settings → Geo permissions).
2. Copy the Account SID and Auth Token into `TWILIO_ACCOUNT_SID` / `TWILIO_AUTH_TOKEN`, set `PSTN_PROVIDER=twilio`,
   make sure `PUBLIC_URL` is the exact `https://` origin Twilio will call (same scheme/host/port as seen by Twilio; the
   signature covers the full URL, so a wrong `PUBLIC_URL` behind nginx yields `401 invalid signature`).
3. No number configuration is needed in the console: the server creates the call with inline TwiML and passes
   `StatusCallback` URLs on the API call (`…/v1/pstn/webhook/twilio?leg=user` for the parent call and
   `?leg=consultant` on the `<Number>` child call). Allow Twilio's IPs through your firewall/WAF to `/v1/pstn/webhook/`.
4. Restart the API and trial a call. Twilio's call log (Monitor → Calls) shows both legs; the parent call SID is the `bridge_id`.

Design: one outbound call to the user whose TwiML is `<Dial callerId answerOnBridge><Number statusCallback=…>consultant</Number></Dial>`.
The user hears ringing until the consultant answers, both legs live in one Twilio call, the parent SID is the bridge id,
so `Hangup` (`POST Calls/{sid}.json Status=completed`) needs no local state and survives restarts.

## Cost

Telephony minutes are billed by the provider (two legs for the whole duration, from the moment each phone is
answered, plus any ringing time Twilio charges for the outbound attempt). Our meter bills **relay minutes** as it
does for WebRTC: `billed_seconds` runs from both legs answered until the end, capped at `max_seconds`. Platform
accounting therefore sees both numbers; the provider invoice is not surfaced by this server.

## Limitations

- No DTMF, no recording, no IVR prompts ("you are being connected to…"), no call transfer.
- One live bridge per consultation; dial again after the previous bridge ended.
- End attribution relies on webhook order: the child (consultant) leg's `completed` is normally generated before the
  parent call's, but Twilio does not guarantee delivery order, so a consultant hangup can occasionally be recorded as
  `user_hangup`. Billing is identical either way.
- The meter stop polls every 5 s, so after `balance_exhausted` the phones may stay connected for up to ~5 s
  (plus provider teardown). Billing is unaffected (capped at `max_seconds`).
- The `connect_timeout_seconds` of the consultation bounds the ringing time (see above).
- Twilio signature validation assumes `PUBLIC_URL` is what Twilio dialled; non-standard ports or path rewrites in
  front of the API break it.
- Provider callbacks share the public rate limiter (per IP).

## Try it with the mock provider

The mock dials nothing and lets you inject leg events through the webhook. With the API running locally
(`PSTN_PROVIDER` unset → mock; set `PSTN_MOCK_SECRET` to require signed events):

```bash
API=http://localhost:8080; KEY=ck_live_…   # from make create-app
H="Authorization: Bearer $KEY"
curl -s -X PUT  $API/v1/consultants/astro-1 -H "$H" -d '{"name":"Pandit","modes":["audio"]}'
curl -s -X POST $API/v1/consultants/astro-1/status -H "$H" -d '{"status":"online"}'
ID=$(curl -s -X POST $API/v1/consultations -H "$H" -d '{"consultant_id":"astro-1","user_id":"user-42","max_seconds":120}' | jq -r .consultation.id)
curl -s -X POST $API/v1/consultations/$ID/accept -H "$H"
B=$(curl -s -X POST $API/v1/consultations/$ID/phone -H "$H" -d '{"user_number":"+15550001111","consultant_number":"+15550002222"}' | jq -r .bridge_id)
ev() { body="{\"bridge_id\":\"$B\",\"leg\":\"$1\",\"status\":\"$2\"}"
      sig=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "${PSTN_MOCK_SECRET:-}" | awk '{print $NF}')
      curl -s -X POST $API/v1/pstn/webhook/mock -H "X-Mock-Signature: $sig" -d "$body"; echo; }
ev user answered; ev consultant answered          # -> consultation active, meter running
curl -s $API/v1/consultations/$ID/phone -H "$H" | jq .meter
ev user completed                                 # -> ended, end_reason user_hangup
curl -s $API/v1/consultations/$ID -H "$H" | jq .consultation
```

## Untested without credentials

`internal/pstn/twilio_test.go` covers call creation parameters, TwiML, hangup tolerance of finished calls, callback
parsing and signature validation (including Twilio's documented test vector) against a fake Twilio API. Not exercised
live: real call placement, Twilio's actual callback ordering/timing, geo permissions, and `answerOnBridge` audio
behaviour. Run one real call before relying on it in production.
