Outbound Dialplan Normalization¶
Reference for the number-format normalization rules that the dialplan generator emits at the top of every org_<prefix>__outbound context. These rules sit BEFORE the route-specific _X. pattern and rewrite incoming digit strings into the format the upstream trunk expects.
Why this exists¶
Customers dial numbers in many formats:
9944421125— bare 10-digit subscriber number919944421125— with India country code, no plus+919944421125— full E.16409944421125— with national-trunk prefix (rare in modern softphones, but legacy PSTN habit)
All four should reach the trunk identically, because:
- Tata's outbound termination accepts only the 10-digit form
- Softphones render incoming CallerID inconsistently (Zoiper uses 12-digit, native iOS Phone may use 10-digit)
- Tap-to-call from any of those formats should succeed without the agent retyping
Rules emitted today¶
Generator: api/src/services/asterisk/dialplanGenerator.js → generateOutboundContext(org). Rules emit only for orgs that have at least one active outbound route.
1. Strip leading + (E.164 normalization)¶
; Normalize E.164 leading '+'
exten => _+X.,1,NoOp(Stripping leading + from ${EXTEN})
exten => _+X.,n,Goto(${EXTEN:1},1)
Matches +<anything>. Goto(${EXTEN:1},1) re-enters the same context with the leading + removed, so subsequent rules pick up the digits-only form.
2. Strip Indian country code 91 (12-digit input)¶
; Normalize Indian country code '91' (Tata trunk expects 10-digit)
exten => _91XXXXXXXXXX,1,NoOp(Stripping 91 country code from ${EXTEN})
exten => _91XXXXXXXXXX,n,Goto(${EXTEN:2},1)
Matches exactly 12 digits starting with 91. Re-enters with the first two characters stripped (${EXTEN:2}), leaving the bare 10-digit number that _X. will catch.
3. Catch-all _X. route¶
exten => _X.,1,NoOp(Outbound call via Default Outbound)
exten => _X.,n,Set(__ORG_ID=...)
exten => _X.,n,Set(CALLERID(num)=...) ; org's outbound CID
...
exten => _X.,n,Dial(PJSIP/${EXTEN}@<trunk>,60)
_X. matches one or more digits. By the time we reach this rule, prior rules have normalised the input to bare digits.
How matches resolve (Asterisk rule precedence)¶
Asterisk picks the most specific pattern that matches. Specificity is roughly: literal characters > character ranges > wildcards.
| Input | Most specific match | Net effect |
|---|---|---|
9944421125 | _X. | Dialled directly (10-digit) |
919944421125 | _91XXXXXXXXXX (more specific than _X.) | Strip → re-enter as 9944421125 → _X. Dials |
+919944421125 | _+X. | Strip + → re-enter as 919944421125 → above row applies |
+9944421125 | _+X. | Strip + → re-enter as 9944421125 → _X. Dials |
1001 (extension) | (in _internal context, exact 1001 rule) | Calls user 1001, never reaches _outbound |
5001 (queue) | (in _queue context, exact 5001 rule) | Enters queue, never reaches _outbound |
Adding a new normalization rule¶
Use the Goto(${EXTEN:N},1) pattern — matches digits, strips, re-enters the context so a more general rule catches the result. Keep patterns narrow:
_91XXXXXXXXXX(12 digits, starts with 91) — safe_91X.(anything starting with 91) — would swallow legitimate extension dials starting with 91 if any existed_NXX.(anything starting with non-zero) — too broad, would conflict with_X.
Example — strip the legacy national-trunk prefix 0 from 11-digit dials¶
If a customer's softphone produces 09944421125, you'd add:
; Strip legacy national-trunk leading '0'
exten => _0XXXXXXXXXX,1,NoOp(Stripping leading 0 from ${EXTEN})
exten => _0XXXXXXXXXX,n,Goto(${EXTEN:1},1)
Place before the 91 strip and the _X. catch-all. Re-deploy + regenerate.
When the rules DON'T apply¶
These contexts run normalization independently:
org_<prefix>__internal— extension dials (1001-9999), conferences (_8XXX), call-forward (_*72.), etc. — different patterns; not affected.org_<prefix>__incoming— inbound trunk traffic; CallerID is what the carrier sent, not normalized further.org_<prefix>__queue— queue numbers (e.g., 5001); specific match, no normalization.tata-did-route/tata-inbound— gateway dispatcher contexts on PROD; carrier-side handling.
Trunk-specific format expectations¶
| Trunk type | Upstream | Expected dial format |
|---|---|---|
| Tata via NUC (all current customer trunks) | sip:10.10.10.2:5060 | 10-digit subscriber, no country code |
| Tata gateway (system) | sip:10.10.10.2:5060 | Same |
| (future) direct ITSP requiring E.164 | TBD | Would need a per-trunk preserve_country_code flag on OutboundRoute so generator skips the 91 strip for that route |
Today every customer routes through Tata, so the 91 strip is universally correct.
Re-deploying after a generator change¶
A change in dialplanGenerator.js only affects new generations — existing org config files on disk keep the old behaviour until re-deployed. After updating the generator on PROD:
ssh root@89.116.31.109
cd /opt/astrapbx
# Regenerate dialplans for all active orgs + reload Asterisk
node -e '
(async () => {
const ConfigDeploymentService = require("./src/services/asterisk/configDeploymentService");
const inst = new ConfigDeploymentService();
const { Organization } = require("./src/models");
const orgs = await Organization.findAll({ where: { status: "active" } });
console.log("orgs to regenerate:", orgs.length);
for (const o of orgs) {
process.stdout.write(" - " + o.name + " ... ");
try { await inst.deployOrganizationConfiguration(o.id, o.name); console.log("ok"); }
catch (e) { console.log("FAIL: " + e.message); }
}
await inst.reloadAsteriskConfiguration();
console.log("reloaded");
process.exit(0);
})().catch(e => { console.error(e.message); process.exit(1); });
'
Verify with dialplan show <number>@<org>__outbound afterward.
See also¶
- Error 54: Outbound dial fails for 12-digit Indian numbers — the bug that motivated the
91strip rule - Outbound Caller ID — how the CID presented to the recipient is decided (separate from the normalization above)