outbound: routes with failover and caller ID, test box, Outbound rules page
Outbound routes CRUD, reorder and POST /test (mirrors the dialplan:
911 and internal numbers first, then the first matching enabled route,
class check, usable trunks, caller ID priority route > user > default).
The first trunk creates the default rules: toll-free first (because
1NXXNXXXXXX also matches 1800...), US/Canada, international disabled.
Generator: Asterisk orders patterns in one context by specificity, not
by file order, so each route gets its own sp-or-{id} context and
sp-outbound-routes includes them in position order (spec 03 §6.12
updated). sp-route-{id}: class check -> ss-noservice, caller ID
priority, per-trunk GROUP channel limit, failover on CHANUNAVAIL or
CONGESTION, all-circuits-busy-now when every trunk is full or off.
UI: Outbound rules page (↑/↓ reorder, pattern rows with help, ordered
trunk list, caller ID override, test box) and a Calling tab on users
(calling permission, outbound caller ID).
trunk-test.ts: 101 dials 3125550000 -> the fake provider receives
13125550000 with the DID in Remote-Party-ID (From user is the
sub-account), two-way audio; a caller without a class hears
ss-noservice. scripts/e2e/m3-t3.mjs covers the UI. With real voip.ms
still to do: whether per-call caller ID is honored (06 §4).
Task: M3-T3
This commit is contained in:
1 parent
c117d8bb1d
commit
4496f6b89a
33 files changed
+1686
-70
No files matched your search
@@ -3,10 +3,10 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { alias } from 'drizzle-orm/pg-core';
|
||||
import type { Db } from '../db/client.ts';
|
||||
import { dids, extensions, trunks, users } from '../db/schema.ts';
|
||||
import { dids, extensions, outboundRoutes, trunks, users } from '../db/schema.ts';
|
||||
import { decrypt } from '../crypto/secrets.ts';
|
||||
import { getAllSettings } from '../services/settings.ts';
|
||||
import type { Destination } from '@selfphoneng/shared';
|
||||
import type { Destination, OutboundClass, RoutePattern } from '@selfphoneng/shared';
|
||||
|
||||
export type SnapExtension = {
|
||||
id: string;
|
||||
@@ -36,6 +36,17 @@ export type SnapDid = {
|
||||
callerIdNamePrefix: string | null;
|
||||
};
|
||||
|
||||
export type SnapRoute = {
|
||||
id: string;
|
||||
name: string;
|
||||
position: number;
|
||||
enabled: boolean;
|
||||
patterns: RoutePattern[];
|
||||
requiredClass: OutboundClass;
|
||||
trunkIds: string[];
|
||||
callerId: string | null; // the override DID's number
|
||||
};
|
||||
|
||||
export type SnapTrunk = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -60,6 +71,7 @@ export type Snapshot = {
|
||||
extensions: SnapExtension[];
|
||||
dids: SnapDid[];
|
||||
trunks: SnapTrunk[];
|
||||
routes: SnapRoute[];
|
||||
};
|
||||
|
||||
export function todayIn(timezone: string, now = new Date()): string {
|
||||
@@ -94,10 +106,22 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
|
||||
})
|
||||
.from(dids);
|
||||
const trunkRows = await db.select().from(trunks);
|
||||
const routeRows = await db.select().from(outboundRoutes);
|
||||
const didNumber = new Map(didRows.map((d) => [d.id, d.number]));
|
||||
return {
|
||||
today: todayIn(settings['general.timezone'], now),
|
||||
settings,
|
||||
dids: didRows.map((d) => ({ ...d, destination: d.destination as Destination })),
|
||||
routes: routeRows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
position: r.position,
|
||||
enabled: r.enabled,
|
||||
patterns: r.patterns as RoutePattern[],
|
||||
requiredClass: r.requiredClass,
|
||||
trunkIds: r.trunkIds,
|
||||
callerId: r.callerIdDidId ? (didNumber.get(r.callerIdDidId) ?? null) : null,
|
||||
})),
|
||||
trunks: trunkRows.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
|
||||
Reference in new issue
Block a user