feat: add number library routing and cdr location support

This commit is contained in:
hectorzhao
2026-06-24 11:39:32 +08:00
parent 5fa1bd35e9
commit 7057fd3c42
63 changed files with 6361 additions and 566 deletions
+4
View File
@@ -5,6 +5,8 @@ export const PASSWORD_ALGO_ARGON2ID = 'argon2id';
export type PermissionKey =
| 'dashboard.view'
| 'active_calls.view'
| 'active_calls.manage'
| 'customers.view'
| 'customers.manage'
| 'customer_gateways.view'
@@ -15,6 +17,8 @@ export type PermissionKey =
| 'vendor_gateways.manage'
| 'line_groups.view'
| 'line_groups.manage'
| 'number_library.view'
| 'number_library.manage'
| 'recharges.view'
| 'recharges.manage'
| 'cdr.view'
+17 -1
View File
@@ -5,6 +5,7 @@ import { join } from 'node:path';
const schema = readFileSync(join(process.cwd(), 'prisma/schema.prisma'), 'utf8');
const migration = readFileSync(join(process.cwd(), 'prisma/migrations/20260621090000_init_v2_schema/migration.sql'), 'utf8');
const authMigration = readFileSync(join(process.cwd(), 'prisma/migrations/20260621093000_auth_sessions/migration.sql'), 'utf8');
const numberLibraryMigration = readFileSync(join(process.cwd(), 'prisma/migrations/20260624095000_number_library/migration.sql'), 'utf8');
describe('Prisma schema contract', () => {
it('defines every V2 core business table', () => {
@@ -16,11 +17,16 @@ describe('Prisma schema contract', () => {
'vendors',
'vendor_recharges',
'vendor_gateways',
'vendor_gateway_blocked_regions',
'vendor_gateway_forbidden_periods',
'vendor_gateway_codecs',
'vendor_gateway_prefix_rules',
'landing_line_groups',
'landing_line_group_items',
'geo_cities',
'phone_number_segments',
'phone_area_codes',
'carrier_prefix_rules',
'raw_cdrs',
'rated_cdrs',
'recordings',
@@ -39,7 +45,7 @@ describe('Prisma schema contract', () => {
for (const table of tables) {
expect(schema).toContain(`@@map("${table}")`);
expect(`${migration}\n${authMigration}`).toContain(`CREATE TABLE \`${table}\``);
expect(`${migration}\n${authMigration}\n${numberLibraryMigration}`).toContain(`CREATE TABLE \`${table}\``);
}
});
@@ -52,6 +58,16 @@ describe('Prisma schema contract', () => {
expect(migration).toContain('UNIQUE INDEX `idempotency_keys_key_key`');
});
it('captures number library and CDR callee attribution fields', () => {
expect(schema).toContain('model GeoCity');
expect(schema).toContain('model PhoneNumberSegment');
expect(schema).toContain('model CarrierPrefixRule');
expect(numberLibraryMigration).toContain('`segment7` CHAR(7) NOT NULL');
expect(numberLibraryMigration).toContain('`callee_city_code` VARCHAR(12) NULL');
expect(numberLibraryMigration).toContain('`callee_operator` ENUM');
expect(numberLibraryMigration).toContain('CREATE INDEX `raw_cdrs_callee_operator_started_at_idx`');
});
it('models audit columns and outbox publishing state', () => {
expect(schema).toContain('createdAt');
expect(schema).toContain('updatedAt');
+41
View File
@@ -142,6 +142,11 @@ function sampleEvent(overrides: Partial<CdrStreamPublishInput> = {}): CdrStreamP
sourceIp: '100.93.185.30',
caller: 's21-ip-1001',
callee: '13800138000',
calleeCityCode: '340100',
calleeCityName: '合肥市',
calleeProvinceName: '安徽省',
calleeOperator: 'MOBILE',
calleeNumberType: 'MOBILE',
sipCode: 503,
hangupReason: 'CONFIG_MISSING',
configVersion: '0',
@@ -162,9 +167,45 @@ describe('CDR Redis Stream contract', () => {
expect(parsed.node_id).toBe('a1');
expect(parsed.opensips_instance).toBe('opensips-a1');
expect(parsed.rtpengine_node).toBe('a1');
expect(parsed.callee_city_code).toBe('340100');
expect(parsed.callee_city_name).toBe('合肥市');
expect(parsed.callee_operator).toBe('MOBILE');
expect(parsed.sipCode).toBe(503);
});
it('keeps old stream entries compatible when number snapshot fields are absent', () => {
const parsed = parseCdrStreamEvent([
'schema_version',
'1',
'event_id',
'evt-old',
'idempotency_key',
'call-old:1',
'call_id',
'call-old',
'node_id',
'a1',
'opensips_instance',
'opensips-a1',
'ingress_a_ip',
'100.90.90.90',
'rtpengine_node',
'a1',
'source_ip',
'100.93.185.30',
'sip_code',
'503',
'hangup_reason',
'CONFIG_MISSING',
'created_at',
'2026-06-21T07:30:00.000Z'
]);
expect(parsed.callee_city_code).toBe('');
expect(parsed.callee_operator).toBe('UNKNOWN');
expect(parsed.callee_number_type).toBe('UNKNOWN');
});
it('creates the consumer group idempotently', async () => {
const redis = new FakeRedis();
+20
View File
@@ -36,6 +36,11 @@ export interface CdrStreamEvent {
source_ip: string;
caller: string;
callee: string;
callee_city_code: string;
callee_city_name: string;
callee_province_name: string;
callee_operator: string;
callee_number_type: string;
vendor_id: string;
vendor_gateway_id: string;
line_group_id: string;
@@ -69,6 +74,11 @@ export interface CdrStreamPublishInput {
sourceIp: string;
caller?: string;
callee?: string;
calleeCityCode?: string;
calleeCityName?: string;
calleeProvinceName?: string;
calleeOperator?: string;
calleeNumberType?: string;
vendorId?: string;
vendorGatewayId?: string;
lineGroupId?: string;
@@ -154,6 +164,11 @@ export function buildCdrStreamEvent(input: CdrStreamPublishInput): CdrStreamEven
source_ip: input.sourceIp,
caller: input.caller ?? '',
callee: input.callee ?? '',
callee_city_code: input.calleeCityCode ?? '',
callee_city_name: input.calleeCityName ?? '',
callee_province_name: input.calleeProvinceName ?? '',
callee_operator: input.calleeOperator ?? 'UNKNOWN',
callee_number_type: input.calleeNumberType ?? 'UNKNOWN',
vendor_id: input.vendorId ?? '',
vendor_gateway_id: input.vendorGatewayId ?? '',
line_group_id: input.lineGroupId ?? '',
@@ -220,6 +235,11 @@ export function parseCdrStreamEvent(fields: string[]): ParsedCdrStreamEvent {
source_ip: parsed.source_ip,
caller: parsed.caller ?? '',
callee: parsed.callee ?? '',
callee_city_code: parsed.callee_city_code ?? '',
callee_city_name: parsed.callee_city_name ?? '',
callee_province_name: parsed.callee_province_name ?? '',
callee_operator: parsed.callee_operator ?? 'UNKNOWN',
callee_number_type: parsed.callee_number_type ?? 'UNKNOWN',
vendor_id: parsed.vendor_id ?? '',
vendor_gateway_id: parsed.vendor_gateway_id ?? '',
line_group_id: parsed.line_group_id ?? '',