195 lines
6.1 KiB
TypeScript
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;
|
|
}
|