import { BadRequestException } from '@nestjs/common'; import { describe, expect, it } from 'vitest'; import { buildTrendBuckets, callMetrics, DashboardService, startOfShanghaiDayUtc } from './dashboard.service.js'; import type { DashboardRepository } from './dashboard.repository.js'; describe('dashboard service', () => { it('loads summary and trends together for the overview response', async () => { const repository = { summaryWindow: async () => ({ cdrs: [], ratedCdrs: [], failureCodes: [], abnormalGateways: [], activeCustomers: 0, activeCustomerGateways: 0, activeVendorGateways: 0, pendingQuality: 0 }), trendWindow: async () => ({ cdrs: [], ratedCdrs: [] }) } as unknown as DashboardRepository; const service = new DashboardService(repository); const result = await service.overview({ hours: 1, bucketMinutes: 60 }); expect(result.summary).toHaveProperty('calls.totalCalls', 0); expect(result.trends.buckets).toHaveLength(1); }); it('uses Asia/Shanghai day boundary for today summary', async () => { const calls: Array<{ start: Date; end: Date }> = []; const repository = { summaryWindow: async (start: Date, end: Date) => { calls.push({ start, end }); return { cdrs: [], ratedCdrs: [], failureCodes: [], abnormalGateways: [], activeCustomers: 0, activeCustomerGateways: 0, activeVendorGateways: 0, pendingQuality: 0 }; } } as unknown as DashboardRepository; const service = new DashboardService(repository); const now = new Date('2026-06-21T15:30:00.000Z'); await service.summary(now); expect(calls[0]?.start.toISOString()).toBe('2026-06-20T16:00:00.000Z'); expect(startOfShanghaiDayUtc(now).toISOString()).toBe('2026-06-20T16:00:00.000Z'); }); it('computes call metrics and answer rate without floating point money exposure', () => { expect( callMetrics([ { startedAt: new Date('2026-06-21T00:00:00.000Z'), durationSec: 10, sipCode: 200 }, { startedAt: new Date('2026-06-21T00:01:00.000Z'), durationSec: 0, sipCode: 486 }, { startedAt: new Date('2026-06-21T00:02:00.000Z'), durationSec: 0, sipCode: 503 } ]) ).toEqual({ totalCalls: 3, answeredCalls: 1, failedCalls: 2, answerRate: '0.3333', totalDurationSec: 10 }); }); it('builds fixed trend buckets', () => { const buckets = buildTrendBuckets( new Date('2026-06-21T00:00:00.000Z'), new Date('2026-06-21T01:00:00.000Z'), 30, [ { startedAt: new Date('2026-06-21T00:10:00.000Z'), durationSec: 30, sipCode: 200 }, { startedAt: new Date('2026-06-21T00:45:00.000Z'), durationSec: 0, sipCode: 480 } ], [ { startedAt: new Date('2026-06-21T00:10:00.000Z'), customerFee: '0.100000', vendorCost: '0.030000', grossProfit: '0.070000' } ] ); expect(buckets).toHaveLength(2); expect(buckets[0]).toMatchObject({ calls: { totalCalls: 1, answeredCalls: 1, failedCalls: 0 }, money: { customerFee: '0.100000', vendorCost: '0.030000', grossProfit: '0.070000' } }); expect(buckets[1]).toMatchObject({ calls: { totalCalls: 1, answeredCalls: 0, failedCalls: 1 } }); }); it('validates trend query bounds', async () => { const service = new DashboardService({} as DashboardRepository); await expect(service.trends({ hours: '0' })).rejects.toThrow(BadRequestException); await expect(service.trends({ bucketMinutes: '10' })).rejects.toThrow(BadRequestException); }); });