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; } 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 { 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 }; } }