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

195 lines
6.1 KiB
TypeScript

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<DashboardSummarySnapshot>;
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<DashboardSummarySnapshot> {
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<DashboardGatewayFailureRow[]> {
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<string, { failedCalls: number; totalCalls: number; gateway: NonNullable<(typeof cdrs)[number]['vendorGateway']> }>();
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<number, number>();
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;
}