Files
lisglosips/apps/api/src/modules/cdrs/cdrs.repository.ts
T

435 lines
14 KiB
TypeScript

import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@lisglosips/database';
import { PrismaService } from '../database/prisma.service.js';
export type CdrCarrier = 'MOBILE' | 'UNICOM' | 'TELECOM' | 'BROADCAST' | 'MVNO' | 'UNKNOWN';
export interface CdrQuery {
caller?: string;
callee?: string;
customerGatewayId?: string;
vendorGatewayId?: string;
cityCode?: string;
carrier?: CdrCarrier;
startedFrom?: Date;
startedTo?: Date;
take: number;
skip: number;
}
export interface CdrPageMeta {
total: number;
take: number;
skip: number;
hasMore: boolean;
}
export interface CdrListItem {
id: string;
eventId: string;
callId: string;
sourceIp: string | null;
caller: string;
callee: string;
rawCallee: string | null;
businessPrefixId: string | null;
businessPrefix: string | null;
calleeCityCode: string | null;
calleeCityName: string | null;
calleeProvinceName: string | null;
calleeOperator: CdrCarrier;
calleeNumberType: string;
customerId: string | null;
customerName: string | null;
customerGatewayId: string | null;
customerGatewayName: string | null;
vendorId: string | null;
vendorName: string | null;
vendorGatewayId: string | null;
vendorGatewayName: string | null;
vendorGatewayHost: string | null;
vendorGatewayPort: number | null;
lineGroupId: string | null;
lineGroupName: string | null;
landingCaller: string | null;
landingCallee: string | null;
startedAt: Date;
answeredAt: Date | null;
endedAt: Date;
durationSec: number;
sipCode: number;
hangupReason: string | null;
recordingKey: string | null;
configVersion: number | null;
ratingStatus: string;
customerFee: string | null;
vendorCost: string | null;
grossProfit: string | null;
billSec: number | null;
hasRecording: boolean;
recordingId: string | null;
recordingStatus: string | null;
}
export interface CdrEntityRef {
id: string;
name: string;
status?: string;
}
export interface CdrGatewayRef extends CdrEntityRef {
authMode?: string;
sourceIp?: string | null;
host?: string;
port?: number;
transport?: string;
}
export interface CdrPolicyRef extends CdrEntityRef {
priority: number;
callerMode: string;
callerValue: string | null;
calleeMode: string;
calleeValue: string | null;
}
export interface CdrRatedDetail {
id: string;
billSec: number;
customerFee: string;
vendorCost: string;
grossProfit: string;
customerRate: Prisma.JsonValue | null;
vendorRate: Prisma.JsonValue | null;
ratedAt: Date;
}
export interface CdrRecordingDetail {
id: string;
storageKey: string;
sha256: string | null;
bytes: string;
durationSec: number;
status: string;
movedAt: Date | null;
createdAt: Date;
}
export interface CdrDetail extends CdrListItem {
customer: CdrEntityRef | null;
customerGateway: CdrGatewayRef | null;
customerGatewayPolicy: CdrPolicyRef | null;
businessPrefixRef: CdrEntityRef & { prefix: string; priority: number } | null;
vendor: CdrEntityRef | null;
vendorGateway: CdrGatewayRef | null;
lineGroup: CdrEntityRef | null;
rated: CdrRatedDetail | null;
recording: CdrRecordingDetail | null;
payload: Prisma.JsonValue | null;
receivedAt: Date;
createdAt: Date;
}
export interface CdrsRepository {
list(query: CdrQuery): Promise<{ items: CdrListItem[]; meta: CdrPageMeta }>;
get(id: string): Promise<CdrDetail>;
}
export const CDRS_REPOSITORY = Symbol('CDRS_REPOSITORY');
type RawCdrRecord = Prisma.RawCdrGetPayload<{
include: {
customer: { select: { name: true } };
customerGateway: { select: { name: true } };
vendor: { select: { name: true } };
vendorGateway: { select: { name: true; host: true; port: true } };
lineGroup: { select: { name: true } };
ratedCdr: { select: { customerFee: true; vendorCost: true; grossProfit: true; billSec: true } };
recording: { select: { id: true; status: true } };
};
}>;
type RawCdrDetailRecord = Prisma.RawCdrGetPayload<{
include: {
customer: { select: { id: true; name: true; status: true } };
customerGateway: { select: { id: true; name: true; status: true; authMode: true; sourceIp: true } };
customerGatewayPolicy: {
select: {
id: true;
name: true;
status: true;
priority: true;
callerMode: true;
callerValue: true;
calleeMode: true;
calleeValue: true;
};
};
businessPrefixRef: { select: { id: true; name: true; prefix: true; priority: true; status: true } };
vendor: { select: { id: true; name: true; status: true } };
vendorGateway: { select: { id: true; name: true; status: true; authMode: true; host: true; port: true; transport: true } };
lineGroup: { select: { id: true; name: true; status: true } };
ratedCdr: {
select: {
id: true;
billSec: true;
customerFee: true;
vendorCost: true;
grossProfit: true;
customerRate: true;
vendorRate: true;
ratedAt: true;
};
};
recording: {
select: {
id: true;
storageKey: true;
sha256: true;
bytes: true;
durationSec: true;
status: true;
movedAt: true;
createdAt: true;
};
};
};
}>;
@Injectable()
export class PrismaCdrsRepository implements CdrsRepository {
constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
async list(query: CdrQuery): Promise<{ items: CdrListItem[]; meta: CdrPageMeta }> {
const where = this.where(query);
const [items, total] = await Promise.all([
this.prisma.rawCdr.findMany({
where,
orderBy: [{ startedAt: 'desc' }, { id: 'desc' }],
take: query.take,
skip: query.skip,
include: this.includeCdr()
}),
this.prisma.rawCdr.count({ where })
]);
return {
items: items.map((item) => this.toItem(item)),
meta: {
total,
take: query.take,
skip: query.skip,
hasMore: query.skip + items.length < total
}
};
}
async get(id: string): Promise<CdrDetail> {
const item = await this.prisma.rawCdr.findUnique({
where: { id },
include: this.includeDetail()
});
if (!item) {
throw new NotFoundException({ code: 'CDR_NOT_FOUND', message: 'CDR not found.' });
}
return this.toDetail(item);
}
private where(query: CdrQuery): Prisma.RawCdrWhereInput {
return {
caller: query.caller ? { contains: query.caller } : undefined,
callee: query.callee ? { contains: query.callee } : undefined,
customerGatewayId: query.customerGatewayId,
vendorGatewayId: query.vendorGatewayId,
calleeCityCode: query.cityCode,
calleeOperator: query.carrier,
startedAt: query.startedFrom || query.startedTo ? { gte: query.startedFrom, lte: query.startedTo } : undefined
};
}
private includeCdr() {
return {
customer: { select: { name: true } },
customerGateway: { select: { name: true } },
vendor: { select: { name: true } },
vendorGateway: { select: { name: true, host: true, port: true } },
lineGroup: { select: { name: true } },
ratedCdr: { select: { customerFee: true, vendorCost: true, grossProfit: true, billSec: true } },
recording: { select: { id: true, status: true } }
} satisfies Prisma.RawCdrInclude;
}
private includeDetail() {
return {
customer: { select: { id: true, name: true, status: true } },
customerGateway: { select: { id: true, name: true, status: true, authMode: true, sourceIp: true } },
customerGatewayPolicy: {
select: {
id: true,
name: true,
status: true,
priority: true,
callerMode: true,
callerValue: true,
calleeMode: true,
calleeValue: true
}
},
businessPrefixRef: { select: { id: true, name: true, prefix: true, priority: true, status: true } },
vendor: { select: { id: true, name: true, status: true } },
vendorGateway: { select: { id: true, name: true, status: true, authMode: true, host: true, port: true, transport: true } },
lineGroup: { select: { id: true, name: true, status: true } },
ratedCdr: {
select: {
id: true,
billSec: true,
customerFee: true,
vendorCost: true,
grossProfit: true,
customerRate: true,
vendorRate: true,
ratedAt: true
}
},
recording: {
select: {
id: true,
storageKey: true,
sha256: true,
bytes: true,
durationSec: true,
status: true,
movedAt: true,
createdAt: true
}
}
} satisfies Prisma.RawCdrInclude;
}
private toItem(item: RawCdrRecord): CdrListItem {
return {
id: item.id,
eventId: item.eventId,
callId: item.callId,
sourceIp: item.sourceIp,
caller: item.caller,
callee: item.callee,
rawCallee: item.rawCallee,
businessPrefixId: item.businessPrefixId,
businessPrefix: item.businessPrefix,
calleeCityCode: item.calleeCityCode,
calleeCityName: item.calleeCityName,
calleeProvinceName: item.calleeProvinceName,
calleeOperator: item.calleeOperator,
calleeNumberType: item.calleeNumberType,
customerId: item.customerId,
customerName: item.customer?.name ?? null,
customerGatewayId: item.customerGatewayId,
customerGatewayName: item.customerGateway?.name ?? null,
vendorId: item.vendorId,
vendorName: item.vendor?.name ?? null,
vendorGatewayId: item.vendorGatewayId,
vendorGatewayName: item.vendorGateway?.name ?? null,
vendorGatewayHost: item.vendorGateway?.host ?? null,
vendorGatewayPort: item.vendorGateway?.port ?? null,
lineGroupId: item.lineGroupId,
lineGroupName: item.lineGroup?.name ?? null,
landingCaller: item.landingCaller,
landingCallee: item.landingCallee,
startedAt: item.startedAt,
answeredAt: item.answeredAt,
endedAt: item.endedAt,
durationSec: item.durationSec,
sipCode: item.sipCode,
hangupReason: item.hangupReason,
recordingKey: item.recordingKey,
configVersion: item.configVersion,
ratingStatus: item.ratingStatus,
customerFee: item.ratedCdr?.customerFee.toFixed(6) ?? null,
vendorCost: item.ratedCdr?.vendorCost.toFixed(6) ?? null,
grossProfit: item.ratedCdr?.grossProfit.toFixed(6) ?? null,
billSec: item.ratedCdr?.billSec ?? null,
hasRecording: Boolean(item.recording),
recordingId: item.recording?.id ?? null,
recordingStatus: item.recording?.status ?? null
};
}
private toDetail(item: RawCdrDetailRecord): CdrDetail {
const base = this.toItem(item);
return {
...base,
customer: item.customer ? { id: item.customer.id, name: item.customer.name, status: item.customer.status } : null,
customerGateway: item.customerGateway
? {
id: item.customerGateway.id,
name: item.customerGateway.name,
status: item.customerGateway.status,
authMode: item.customerGateway.authMode,
sourceIp: item.customerGateway.sourceIp
}
: null,
customerGatewayPolicy: item.customerGatewayPolicy
? {
id: item.customerGatewayPolicy.id,
name: item.customerGatewayPolicy.name,
status: item.customerGatewayPolicy.status,
priority: item.customerGatewayPolicy.priority,
callerMode: item.customerGatewayPolicy.callerMode,
callerValue: item.customerGatewayPolicy.callerValue,
calleeMode: item.customerGatewayPolicy.calleeMode,
calleeValue: item.customerGatewayPolicy.calleeValue
}
: null,
businessPrefixRef: item.businessPrefixRef
? {
id: item.businessPrefixRef.id,
name: item.businessPrefixRef.name,
prefix: item.businessPrefixRef.prefix,
priority: item.businessPrefixRef.priority,
status: item.businessPrefixRef.status
}
: null,
vendor: item.vendor ? { id: item.vendor.id, name: item.vendor.name, status: item.vendor.status } : null,
vendorGateway: item.vendorGateway
? {
id: item.vendorGateway.id,
name: item.vendorGateway.name,
status: item.vendorGateway.status,
authMode: item.vendorGateway.authMode,
host: item.vendorGateway.host,
port: item.vendorGateway.port,
transport: item.vendorGateway.transport
}
: null,
lineGroup: item.lineGroup ? { id: item.lineGroup.id, name: item.lineGroup.name, status: item.lineGroup.status } : null,
rated: item.ratedCdr
? {
id: item.ratedCdr.id,
billSec: item.ratedCdr.billSec,
customerFee: item.ratedCdr.customerFee.toFixed(6),
vendorCost: item.ratedCdr.vendorCost.toFixed(6),
grossProfit: item.ratedCdr.grossProfit.toFixed(6),
customerRate: item.ratedCdr.customerRate,
vendorRate: item.ratedCdr.vendorRate,
ratedAt: item.ratedCdr.ratedAt
}
: null,
recording: item.recording
? {
id: item.recording.id,
storageKey: item.recording.storageKey,
sha256: item.recording.sha256,
bytes: item.recording.bytes.toString(),
durationSec: item.recording.durationSec,
status: item.recording.status,
movedAt: item.recording.movedAt,
createdAt: item.recording.createdAt
}
: null,
payload: item.payload,
receivedAt: item.receivedAt,
createdAt: item.createdAt
};
}
}