cdr: store Cdr events, aggregate calls, fixtures from real calls

services/cdr.ts stores every AMI Cdr event in cdr and, 5 s after a
call's last event, builds its calls row (direction, status, from, to,
DID, answered by, talk time) and call_extensions (caller, rang,
answered) per 04 §5.4; linkedids without a calls row are caught up on
AMI connect. Aggregation is idempotent.

test/asterisk/cdr-record.ts records 8 scenarios with our config, the
fake voip.ms (now shared in provider.ts) and baresip phones; each has a
fixture and a test with the expected calls row. Queue abandon and park
come with M8.

Found while recording: Asterisk 24's Cdr event has no LinkedID unless
cdr_manager maps it, its times carry no zone (now usegmtime), and the
mappings read CDR variables, so the dialplan sets CDR(SP_DID) and
CDR(SP_DIRECTION) next to the channel variables. The Asterisk image
needs a rebuild for the cdr_manager.conf change.

Task: M7-T1
This commit is contained in:
iamdoubz committed 2026-10-18 14:37:14 -05:00
1 parent 8a763ff130
commit 71440ac85d
31 files changed
+1332 -48

No files matched your search

+4 -47
View File
@@ -16,6 +16,8 @@ import { AmiClient } from '../../src/ami/client.ts';
import { GENERATED_FILES, render } from '../../src/generator/index.ts';
import type { Snapshot } from '../../src/generator/load.ts';
import { providerDialplan, providerPjsip } from './provider.ts';
const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk';
const RECORD = process.argv.includes('--record')
? process.argv[process.argv.indexOf('--record') + 1]
@@ -152,51 +154,6 @@ function phoneFiles(dir: string) {
);
}
/** The fake voip.ms: one sub-account that may register. */
const PROVIDER_PJSIP = `[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
[${SUB.user}]
type = endpoint
context = provider-in
disallow = all
allow = ulaw
auth = ${SUB.user}-auth
aors = ${SUB.user}
[${SUB.user}-auth]
type = auth
auth_type = userpass
username = ${SUB.user}
password = ${SUB.pass}
[${SUB.user}]
type = aor
max_contacts = 1
remove_existing = yes
`;
/**
* provider-out: a mobile phone calls one of our DIDs. Like voip.ms, the Request-URI user is the
* 10-digit DID (06 §3), sent to the sub-account's registered contact. Echo() returns the audio.
*/
const PROVIDER_DIALPLAN = `[provider-out]
exten => _X.,1,Set(CALLERID(all)="Mobile" <${MOBILE}>)
same => n,Dial(PJSIP/\${EXTEN}@${SUB.user},30)
[provider-echo]
exten => s,1,Answer()
same => n,Echo()
; Our outbound calls arrive here: remember what the provider saw, then echo the audio.
[provider-in]
exten => _X.,1,Set(GLOBAL(LAST_IN)=to=\${EXTEN} from=\${CALLERID(num)} rpid=\${PJSIP_HEADER(read,Remote-Party-ID)})
same => n,Answer()
same => n,Echo()
`;
async function waitFor(what: string, fn: () => Promise<boolean>, seconds = 60) {
for (let i = 0; i < seconds; i++) {
if (await fn()) return;
@@ -217,9 +174,9 @@ async function main() {
writeFileSync(
join(provConf, f),
f === 'pjsip_selfphoneng.conf'
? PROVIDER_PJSIP
? providerPjsip(SUB)
: f === 'extensions_selfphoneng.conf'
? PROVIDER_DIALPLAN
? providerDialplan(MOBILE, SUB)
: '',
);
}