import { Injectable } from '@nestjs/common'; import { Prisma } from '@lisglosips/database'; import { PrismaService } from '../database/prisma.service.js'; export interface DashboardCdrRow { startedAt: Date; durationSec: number; sipCode: number; } export interface DashboardRatedRow { startedAt: Date; customerFee: string; vendorCost: string; grossProfit: string; } export interface DashboardFailureCodeRow { sipCode: number; count: number; } export interface DashboardGatewayFailureRow { vendorGatewayId: string; vendorGatewayName: string; vendorName: string | null; host: string; port: number; status: string; failedCalls: number; totalCalls: number; } export interface DashboardSummarySnapshot { cdrs: DashboardCdrRow[]; ratedCdrs: DashboardRatedRow[]; failureCodes: DashboardFailureCodeRow[]; abnormalGateways: DashboardGatewayFailureRow[]; activeCustomers: number; activeCustomerGateways: number; activeVendorGateways: number; pendingQuality: number; } export const DASHBOARD_REPOSITORY = Symbol('DASHBOARD_REPOSITORY'); export interface DashboardRepository { summaryWindow(start: Date, end: Date): Promise; trendWindow(start: Date, end: Date): Promise<{ cdrs: DashboardCdrRow[]; ratedCdrs: DashboardRatedRow[] }>; } @Injectable() export class PrismaDashboardRepository implements DashboardRepository { constructor(private readonly prisma: PrismaService) {} async summaryWindow(start: Date, end: Date): Promise { const [cdrs, ratedCdrs, activeCustomers, activeCustomerGateways, activeVendorGateways, pendingQuality] = await Promise.all([ this.prisma.rawCdr.findMany({ where: { startedAt: { gte: start, lt: end } }, select: { startedAt: true, durationSec: true, sipCode: true } }), this.prisma.ratedCdr.findMany({ where: { rawCdr: { startedAt: { gte: start, lt: end } } }, select: { customerFee: true, vendorCost: true, grossProfit: true, rawCdr: { select: { startedAt: true } } } }), this.prisma.customer.count({ where: { status: 'ENABLED', deletedAt: null } }), this.prisma.customerGateway.count({ where: { status: 'ENABLED', deletedAt: null } }), this.prisma.vendorGateway.count({ where: { status: 'ENABLED', deletedAt: null } }), this.prisma.recording.count({ where: { status: 'READY', reviews: { none: {} } } }) ]); const failureCodes = failureCodesFrom(cdrs); const abnormalGateways = await this.gatewayFailures(start, end); return { cdrs, ratedCdrs: ratedCdrs.map((row) => ({ startedAt: row.rawCdr.startedAt, customerFee: decimalString(row.customerFee), vendorCost: decimalString(row.vendorCost), grossProfit: decimalString(row.grossProfit) })), failureCodes, abnormalGateways, activeCustomers, activeCustomerGateways, activeVendorGateways, pendingQuality }; } async trendWindow(start: Date, end: Date): Promise<{ cdrs: DashboardCdrRow[]; ratedCdrs: DashboardRatedRow[] }> { const [cdrs, ratedCdrs] = await Promise.all([ this.prisma.rawCdr.findMany({ where: { startedAt: { gte: start, lt: end } }, select: { startedAt: true, durationSec: true, sipCode: true } }), this.prisma.ratedCdr.findMany({ where: { rawCdr: { startedAt: { gte: start, lt: end } } }, select: { customerFee: true, vendorCost: true, grossProfit: true, rawCdr: { select: { startedAt: true } } } }) ]); return { cdrs, ratedCdrs: ratedCdrs.map((row) => ({ startedAt: row.rawCdr.startedAt, customerFee: decimalString(row.customerFee), vendorCost: decimalString(row.vendorCost), grossProfit: decimalString(row.grossProfit) })) }; } private async gatewayFailures(start: Date, end: Date): Promise { const cdrs = await this.prisma.rawCdr.findMany({ where: { startedAt: { gte: start, lt: end }, vendorGatewayId: { not: null } }, select: { vendorGatewayId: true, sipCode: true, vendorGateway: { select: { id: true, name: true, host: true, port: true, status: true, vendor: { select: { name: true } } } } } }); const grouped = new Map }>(); for (const cdr of cdrs) { if (!cdr.vendorGatewayId || !cdr.vendorGateway) { continue; } const current = grouped.get(cdr.vendorGatewayId) ?? { failedCalls: 0, totalCalls: 0, gateway: cdr.vendorGateway }; current.totalCalls += 1; if (!isAnswered(cdr.sipCode)) { current.failedCalls += 1; } grouped.set(cdr.vendorGatewayId, current); } return [...grouped.entries()] .map(([vendorGatewayId, item]) => ({ vendorGatewayId, vendorGatewayName: item.gateway.name, vendorName: item.gateway.vendor?.name ?? null, host: item.gateway.host, port: item.gateway.port, status: item.gateway.status, failedCalls: item.failedCalls, totalCalls: item.totalCalls })) .filter((item) => item.failedCalls > 0) .sort((left, right) => right.failedCalls - left.failedCalls || right.totalCalls - left.totalCalls) .slice(0, 10); } } function failureCodesFrom(cdrs: DashboardCdrRow[]): DashboardFailureCodeRow[] { const counts = new Map(); for (const cdr of cdrs) { if (isAnswered(cdr.sipCode)) { continue; } counts.set(cdr.sipCode, (counts.get(cdr.sipCode) ?? 0) + 1); } return [...counts.entries()] .map(([sipCode, count]) => ({ sipCode, count })) .sort((left, right) => right.count - left.count || left.sipCode - right.sipCode) .slice(0, 10); } function decimalString(value: Prisma.Decimal): string { return value.toFixed(6); } function isAnswered(sipCode: number): boolean { return sipCode >= 200 && sipCode < 300; }