dids: phone numbers, destinations, inbound dialplan, Phone numbers page

DIDs CRUD (any input format, normalized; 409 on duplicates; a number
that is an emergency or default caller ID can't be removed). Shared
destination service: assertDestinationExists, describeDestination
("Ring Dan Doe (101)") and findReferences over every destination
column in 02 §1 plus the unmatched-number setting; deleting an
extension a destination points at is now a 409 naming the places.

Generator: destApp (user, external, hangup for now), sp-from-trunk-*
per enabled trunk, sp-did-exists (left out when empty) and sp-inbound
with caller-name prefix. Golden case voipms-basic. Phone numbers page
with a minimal DestinationPicker (user, outside number, hang up).

trunk-test.ts: the fake provider calls the DID with a 10-digit
Request-URI like voip.ms; baresip 101 answers, caller ID arrives
normalized, 500 packets each way on both legs. scripts/e2e/m3-t2.mjs
checks the UI → Asterisk path. Still to do with real voip.ms: the
mobile-to-DID call and recording which header carries the DID (06 §3).

Task: M3-T2
This commit is contained in:
iamdoubz committed 2026-10-17 19:52:49 -05:00
1 parent 1b7f60b5f2
commit c117d8bb1d
28 files changed
+1371 -21

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Phone numbers (DIDs) as the API accepts and returns them (02, 04 §7.3, 05 §2).
import { z } from 'zod';
import { DestinationSchema } from './destination.ts';
import { normalizeNumber } from './phone.ts';
/** Any way of writing a number → its normalized digits (00 §8). */
export const DidNumber = z
.string()
.trim()
.transform((v, ctx) => {
try {
const n = normalizeNumber(v);
if (/^\d{7,15}$/.test(n)) return n;
} catch {
// reported below
}
ctx.addIssue({ code: 'custom', message: 'A phone number like (312) 555-1234' });
return z.NEVER;
});
const DidFields = z.object({
number: DidNumber,
trunkId: z.uuid('Pick a trunk'),
label: z.string().trim().max(60).nullable(),
destination: DestinationSchema,
callerIdNamePrefix: z
.string()
.trim()
.max(12)
.regex(/^[A-Za-z0-9 :_.#-]*$/, 'Letters, digits, spaces and : _ . # - only')
.nullable(),
e911Address: z.string().trim().max(200).nullable(),
});
export const DidCreateSchema = DidFields.partial().required({
number: true,
trunkId: true,
destination: true,
});
export const DidUpdateSchema = DidFields.partial();
export type DidCreate = z.input<typeof DidCreateSchema>;
export type DidUpdate = z.input<typeof DidUpdateSchema>;
export type DidDto = z.output<typeof DidFields> & {
id: string;
/** "Ring Dan Doe (101)", for lists. */
destinationSummary: string;
createdAt: string;
};
+1
View File
@@ -8,3 +8,4 @@ export * from './users.ts';
export * from './extensions.ts';
export * from './live.ts';
export * from './trunks.ts';
export * from './dids.ts';