435 lines
14 KiB
TypeScript
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
|
|
};
|
|
}
|
|
}
|