reports: total minutes, talk time, answered; outside minutes per day; voip.ms balance
Owner asked for total minutes, call time and missed calls in one place, and the money left on voip.ms. Reports now totals answered calls, talk time and outside minutes (in from arrival, out from the answer: roughly what the provider bills), charts outside minutes per day, and shows each voip.ms trunk's balance (getBalance; verify with a real account).
This commit is contained in:
1 parent
175d5a2c73
commit
338ab8d7f1
13 files changed
+380
-26
No files matched your search
@@ -24,20 +24,21 @@ describe.skipIf(!hasTestDb)('/api/reports', () => {
|
||||
direction: 'inbound' | 'outbound' | 'internal',
|
||||
status: 'answered' | 'missed' | 'voicemail' | 'busy',
|
||||
talkSeconds = 0,
|
||||
lineSeconds = talkSeconds, // arrival to hang-up
|
||||
) => ({
|
||||
linkedid,
|
||||
direction,
|
||||
status,
|
||||
startedAt: new Date(at),
|
||||
endedAt: new Date(at),
|
||||
endedAt: new Date(Date.parse(at) + lineSeconds * 1000),
|
||||
talkSeconds,
|
||||
});
|
||||
await t.db.insert(calls).values([
|
||||
call('a', '2026-05-04T14:00:00Z', 'inbound', 'answered', 60), // Mon 09:00 local
|
||||
call('b', '2026-05-04T14:30:00Z', 'inbound', 'missed'), // Mon 09:30
|
||||
call('c', '2026-05-04T15:00:00Z', 'outbound', 'answered', 120), // Mon 10:00
|
||||
call('d', '2026-05-05T03:00:00Z', 'inbound', 'voicemail'), // still Mon, 22:00
|
||||
call('e', '2026-05-06T13:00:00Z', 'internal', 'busy'), // Wed 08:00
|
||||
call('a', '2026-05-04T14:00:00Z', 'inbound', 'answered', 60, 75), // Mon 09:00 local
|
||||
call('b', '2026-05-04T14:30:00Z', 'inbound', 'missed', 0, 20), // Mon 09:30
|
||||
call('c', '2026-05-04T15:00:00Z', 'outbound', 'answered', 120, 130), // Mon 10:00
|
||||
call('d', '2026-05-05T03:00:00Z', 'inbound', 'voicemail', 0, 40), // still Mon, 22:00
|
||||
call('e', '2026-05-06T13:00:00Z', 'internal', 'busy', 0, 5), // Wed 08:00
|
||||
call('f', '2026-05-09T12:00:00Z', 'inbound', 'answered', 30), // outside the range
|
||||
]);
|
||||
const [q] = await t.db
|
||||
@@ -78,11 +79,19 @@ describe.skipIf(!hasTestDb)('/api/reports', () => {
|
||||
expect(r.statusCode, r.body).toBe(200);
|
||||
const s = r.json();
|
||||
expect(s.days).toEqual([
|
||||
{ date: '2026-05-04', inbound: 3, outbound: 1, internal: 0, missed: 2 },
|
||||
{ date: '2026-05-05', inbound: 0, outbound: 0, internal: 0, missed: 0 },
|
||||
{ date: '2026-05-06', inbound: 0, outbound: 0, internal: 1, missed: 0 },
|
||||
// outside: inbound 75 + 20 + 40 s from arrival, outbound 120 s talk = 255 s, 4 minutes
|
||||
{ date: '2026-05-04', inbound: 3, outbound: 1, internal: 0, missed: 2, outsideMinutes: 4 },
|
||||
{ date: '2026-05-05', inbound: 0, outbound: 0, internal: 0, missed: 0, outsideMinutes: 0 },
|
||||
{ date: '2026-05-06', inbound: 0, outbound: 0, internal: 1, missed: 0, outsideMinutes: 0 },
|
||||
]);
|
||||
expect(s.totals).toEqual({ calls: 5, missed: 2, avgTalkSeconds: 90 });
|
||||
expect(s.totals).toEqual({
|
||||
calls: 5,
|
||||
answered: 2,
|
||||
missed: 2,
|
||||
avgTalkSeconds: 90,
|
||||
talkSeconds: 180,
|
||||
outsideSeconds: { inbound: 135, outbound: 120 },
|
||||
});
|
||||
expect(s.hours).toHaveLength(7);
|
||||
expect(s.hours[1][9]).toBe(2);
|
||||
expect(s.hours[1][10]).toBe(1);
|
||||
|
||||
@@ -19,6 +19,10 @@ function fakeVoipms() {
|
||||
{ server_name: 'Chicago 1', server_hostname: 'chicago1.voip.ms', server_ip: '1.2.3.5' },
|
||||
],
|
||||
},
|
||||
getBalance: {
|
||||
status: 'success',
|
||||
balance: { current_balance: '12.3456', spent_total: '7.89', calls_total: '42' },
|
||||
},
|
||||
getDIDsInfo: {
|
||||
status: 'success',
|
||||
dids: [
|
||||
@@ -124,6 +128,15 @@ describe.skipIf(!hasTestDb)('/api/trunks/:id voip.ms routes', () => {
|
||||
const r = await req('GET', `/api/trunks/${trunkId}/voipms-servers`);
|
||||
expect(r.statusCode).toBe(400);
|
||||
expect(r.json().error.message).toMatch(/API username and password/);
|
||||
// The balance on Reports says so instead of failing (owner, 2026-10-21).
|
||||
expect((await req('GET', '/api/reports/balance')).json().items).toEqual([
|
||||
{
|
||||
trunkId,
|
||||
trunkName: 'voip.ms',
|
||||
balance: null,
|
||||
error: expect.stringMatching(/API username and password/),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('a wrong API password is a 502 with voip.ms’s reason', async () => {
|
||||
@@ -135,10 +148,20 @@ describe.skipIf(!hasTestDb)('/api/trunks/:id voip.ms routes', () => {
|
||||
expect(r.statusCode).toBe(502);
|
||||
expect(r.json().error.message).toBe('voip.ms rejected the API username or password');
|
||||
expect(r.body).not.toContain('nope');
|
||||
const bal = await req('GET', '/api/reports/balance');
|
||||
expect(bal.json().items[0]).toMatchObject({
|
||||
balance: null,
|
||||
error: 'voip.ms rejected the API username or password',
|
||||
});
|
||||
expect(bal.body).not.toContain('nope');
|
||||
});
|
||||
|
||||
it('lists servers, previews and imports numbers (not routed, label from voip.ms)', async () => {
|
||||
await req('PATCH', `/api/trunks/${trunkId}`, { voipmsApiPassword: 'api-pw' });
|
||||
expect((await req('GET', '/api/reports/balance')).json().items).toEqual([
|
||||
{ trunkId, trunkName: 'voip.ms', balance: 12.3456, error: null },
|
||||
]);
|
||||
expect(fake.seen.at(-1)!.get('method')).toBe('getBalance');
|
||||
expect((await req('GET', `/api/trunks/${trunkId}/voipms-servers`)).json()).toEqual({
|
||||
servers: ['chicago1.voip.ms', 'seattle1.voip.ms'],
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user