feat: complete phase2 baseline cdr quality rbac
This commit is contained in:
@@ -4,13 +4,14 @@ import { Prisma } from '@lisglosips/database';
|
||||
import type { ParsedCdrStreamEvent } from '@lisglosips/redis';
|
||||
|
||||
import { calculateCycleCharge } from './billing.js';
|
||||
import { CdrRatingService, type CdrRatingStore, type CdrRatingTransaction, type CreateRatedInput } from './rating.js';
|
||||
import { CdrRatingService, normalizeCdrNullableId, type CdrRatingStore, type CdrRatingTransaction, type CreateRatedInput } from './rating.js';
|
||||
|
||||
class MemoryStore implements CdrRatingStore, CdrRatingTransaction {
|
||||
rawByEventId = new Map<string, { id: string; eventId: string; ratingStatus: 'UNRATED' | 'RATED' | 'SKIPPED' | 'FAILED' }>();
|
||||
ratedByRawId = new Map<string, { id: string; rawCdrId: string; customerFee: Prisma.Decimal }>();
|
||||
vendorGateway = { id: 'vgw_1', vendorId: 'ven_1', billingCycleSec: 6, cycleRate: new Prisma.Decimal('0.012000') };
|
||||
customer = { id: 'cus_1', balance: new Prisma.Decimal('10.000000') };
|
||||
createdRawEvents: ParsedCdrStreamEvent[] = [];
|
||||
|
||||
async transaction<T>(operation: (tx: CdrRatingTransaction) => Promise<T>): Promise<T> {
|
||||
return operation(this);
|
||||
@@ -23,6 +24,7 @@ class MemoryStore implements CdrRatingStore, CdrRatingTransaction {
|
||||
async createRaw(event: ParsedCdrStreamEvent, rawCdrId: string) {
|
||||
const raw = { id: rawCdrId, eventId: event.event_id, ratingStatus: 'UNRATED' as const };
|
||||
this.rawByEventId.set(event.event_id, raw);
|
||||
this.createdRawEvents.push(event);
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -73,6 +75,12 @@ function event(overrides: Partial<ParsedCdrStreamEvent> = {}): ParsedCdrStreamEv
|
||||
source_ip: '100.93.185.30',
|
||||
caller: '1001',
|
||||
callee: '13800138000',
|
||||
raw_callee: '67113800138000',
|
||||
business_prefix_id: 'bp_seed',
|
||||
business_prefix: '671',
|
||||
landing_caller: '02160010001',
|
||||
landing_callee: '8613800138000',
|
||||
normalized_callee: '13800138000',
|
||||
callee_city_code: '340100',
|
||||
callee_city_name: '合肥市',
|
||||
callee_province_name: '安徽省',
|
||||
@@ -97,6 +105,15 @@ function event(overrides: Partial<ParsedCdrStreamEvent> = {}): ParsedCdrStreamEv
|
||||
}
|
||||
|
||||
describe('S23 CDR rating', () => {
|
||||
it('normalizes OpenSIPS placeholder ids before writing nullable foreign keys', () => {
|
||||
expect(normalizeCdrNullableId('none')).toBeNull();
|
||||
expect(normalizeCdrNullableId('unknown')).toBeNull();
|
||||
expect(normalizeCdrNullableId('no_active_version')).toBeNull();
|
||||
expect(normalizeCdrNullableId('no_policy_match')).toBeNull();
|
||||
expect(normalizeCdrNullableId('single_gateway')).toBeNull();
|
||||
expect(normalizeCdrNullableId('cgp_1')).toBe('cgp_1');
|
||||
});
|
||||
|
||||
it('calculates Decimal cycle charges with ceiling billing seconds', () => {
|
||||
const charge = calculateCycleCharge({ durationSec: 28, billingCycleSec: 6, cycleRate: '0.012000' });
|
||||
|
||||
@@ -121,6 +138,15 @@ describe('S23 CDR rating', () => {
|
||||
});
|
||||
expect(duplicate.outcome).toBe('duplicate');
|
||||
expect(store.customer.balance.toFixed(6)).toBe('9.940000');
|
||||
expect(store.createdRawEvents[0]).toMatchObject({
|
||||
callee: '13800138000',
|
||||
raw_callee: '67113800138000',
|
||||
business_prefix_id: 'bp_seed',
|
||||
business_prefix: '671',
|
||||
landing_caller: '02160010001',
|
||||
landing_callee: '8613800138000',
|
||||
normalized_callee: '13800138000'
|
||||
});
|
||||
});
|
||||
|
||||
it('skips failed or zero-duration CDRs without balance changes', async () => {
|
||||
|
||||
@@ -176,20 +176,25 @@ class PrismaCdrRatingTransaction implements CdrRatingTransaction {
|
||||
id: rawCdrId,
|
||||
eventId: storageEventId(event.event_id),
|
||||
callId: event.call_id,
|
||||
customerId: nullableId(event.customer_id),
|
||||
customerGatewayId: nullableId(event.customer_gateway_id),
|
||||
customerGatewayPolicyId: nullableId(event.customer_gateway_policy_id),
|
||||
customerId: normalizeCdrNullableId(event.customer_id),
|
||||
customerGatewayId: normalizeCdrNullableId(event.customer_gateway_id),
|
||||
customerGatewayPolicyId: normalizeCdrNullableId(event.customer_gateway_policy_id),
|
||||
sourceIp: emptyToNull(event.source_ip),
|
||||
caller: event.caller || 'unknown',
|
||||
callee: event.callee || 'unknown',
|
||||
rawCallee: emptyToNull(event.raw_callee),
|
||||
businessPrefixId: normalizeCdrNullableId(event.business_prefix_id),
|
||||
businessPrefix: emptyToNull(event.business_prefix),
|
||||
calleeCityCode: emptyToNull(event.callee_city_code),
|
||||
calleeCityName: emptyToNull(event.callee_city_name),
|
||||
calleeProvinceName: emptyToNull(event.callee_province_name),
|
||||
calleeOperator: numberCarrier(event.callee_operator),
|
||||
calleeNumberType: phoneNumberType(event.callee_number_type),
|
||||
vendorId: nullableId(event.vendor_id),
|
||||
vendorGatewayId: nullableId(event.vendor_gateway_id),
|
||||
lineGroupId: nullableId(event.line_group_id),
|
||||
vendorId: normalizeCdrNullableId(event.vendor_id),
|
||||
vendorGatewayId: normalizeCdrNullableId(event.vendor_gateway_id),
|
||||
lineGroupId: normalizeCdrNullableId(event.line_group_id),
|
||||
landingCaller: emptyToNull(event.landing_caller),
|
||||
landingCallee: emptyToNull(event.landing_callee),
|
||||
startedAt: parseCdrDate(event.started_at, event.created_at),
|
||||
answeredAt: parseOptionalCdrDate(event.answered_at),
|
||||
endedAt: parseCdrDate(event.ended_at, event.created_at),
|
||||
@@ -274,12 +279,19 @@ function prefixedId(prefix: string): string {
|
||||
}
|
||||
|
||||
function billableId(value: string): boolean {
|
||||
return Boolean(nullableId(value));
|
||||
return Boolean(normalizeCdrNullableId(value));
|
||||
}
|
||||
|
||||
function nullableId(value: string): string | null {
|
||||
export function normalizeCdrNullableId(value: string): string | null {
|
||||
const normalized = value.trim();
|
||||
if (!normalized || normalized === 'none' || normalized === 'unknown' || normalized === 'no_active_version') {
|
||||
if (
|
||||
!normalized ||
|
||||
normalized === 'none' ||
|
||||
normalized === 'unknown' ||
|
||||
normalized === 'no_active_version' ||
|
||||
normalized === 'no_policy_match' ||
|
||||
normalized === 'single_gateway'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
@@ -330,6 +342,12 @@ function eventToJson(event: ParsedCdrStreamEvent): Prisma.InputJsonValue {
|
||||
source_ip: event.source_ip,
|
||||
caller: event.caller,
|
||||
callee: event.callee,
|
||||
raw_callee: event.raw_callee,
|
||||
business_prefix_id: event.business_prefix_id,
|
||||
business_prefix: event.business_prefix,
|
||||
landing_caller: event.landing_caller,
|
||||
landing_callee: event.landing_callee,
|
||||
normalized_callee: event.normalized_callee,
|
||||
callee_city_code: event.callee_city_code,
|
||||
callee_city_name: event.callee_city_name,
|
||||
callee_province_name: event.callee_province_name,
|
||||
|
||||
Reference in New Issue
Block a user