feat(05-01): add Dashboard API route and integration tests
- GET /api/dashboard with read Report permission (ADMIN, OFFICE_STAFF) - Optional startDate/endDate query params for cash flow date range - 6 integration tests verifying all metric methods against seeded data - Tests cover revenue, overdue, subscriber status, cash flow, collectors - Comprehensive cleanup order for dashboard test data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
49
src/app/api/dashboard/route.ts
Normal file
49
src/app/api/dashboard/route.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* GET /api/dashboard
|
||||||
|
*
|
||||||
|
* Returns all dashboard metrics for the ISP owner's business health overview.
|
||||||
|
* Aggregates revenue, overdue, subscriber status, cash flow, and collector metrics.
|
||||||
|
*
|
||||||
|
* Query params:
|
||||||
|
* startDate? - ISO date string for cash flow start (default: start of current month)
|
||||||
|
* endDate? - ISO date string for cash flow end (default: end of current month)
|
||||||
|
*
|
||||||
|
* Access: ADMIN and OFFICE_STAFF (read Report permission).
|
||||||
|
*/
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { withPermission } from "@/lib/middleware/authorize";
|
||||||
|
import { withTenantContext } from "@/lib/prisma-tenant";
|
||||||
|
import { DashboardService } from "@/lib/services/dashboard-service";
|
||||||
|
|
||||||
|
export const GET = withPermission("read", "Report")(
|
||||||
|
async (req: NextRequest, { user }) => {
|
||||||
|
if (!user.tenantId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "No tenant context -- super-admins must use the admin API" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const startDateParam = searchParams.get("startDate");
|
||||||
|
const endDateParam = searchParams.get("endDate");
|
||||||
|
|
||||||
|
const tenantPrisma = withTenantContext(user.tenantId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const summary = await DashboardService.getDashboardSummary(
|
||||||
|
tenantPrisma,
|
||||||
|
user.tenantId,
|
||||||
|
{
|
||||||
|
startDate: startDateParam ? new Date(startDateParam) : undefined,
|
||||||
|
endDate: endDateParam ? new Date(endDateParam) : undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return NextResponse.json(summary);
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : "Failed to load dashboard";
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
445
src/lib/__tests__/dashboard-service.test.ts
Normal file
445
src/lib/__tests__/dashboard-service.test.ts
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard Service Integration Tests
|
||||||
|
*
|
||||||
|
* Tests the DashboardService metric aggregation methods against a live
|
||||||
|
* PostgreSQL database with seeded data.
|
||||||
|
*
|
||||||
|
* Setup:
|
||||||
|
* - Create tenant, admin user, collector user
|
||||||
|
* - Seed COA and expense categories
|
||||||
|
* - Create subscriber (active), suspended subscriber
|
||||||
|
* - Generate invoice (overdue), record payment
|
||||||
|
* - Create collection via collector, create PENDING remittance
|
||||||
|
* - Create expense (POSTED, creates JE lines for cash flow)
|
||||||
|
*
|
||||||
|
* CLEANUP ORDER:
|
||||||
|
* expenses -> vendors -> expenseCategories (custom) -> collectionAllocations ->
|
||||||
|
* collections -> remittances -> paymentAllocations -> payments ->
|
||||||
|
* invoiceLines -> invoices -> journalEntryLines ->
|
||||||
|
* null reversesEntryId -> journalEntries -> zoneAssignments ->
|
||||||
|
* subscribers -> zones -> servicePlans -> accountingPeriods -> accounts ->
|
||||||
|
* ticketCategories -> expenseCategories (system) -> users -> tenant
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { withTenantContext } from "@/lib/prisma-tenant";
|
||||||
|
import { seedChartOfAccounts } from "@/lib/accounting/seed-coa";
|
||||||
|
import { DashboardService } from "@/lib/services/dashboard-service";
|
||||||
|
import { JournalEntryService } from "@/lib/accounting/journal-entry-service";
|
||||||
|
import {
|
||||||
|
Prisma,
|
||||||
|
Role,
|
||||||
|
TenantStatus,
|
||||||
|
BillingType,
|
||||||
|
InvoiceStatus,
|
||||||
|
PaymentMethod,
|
||||||
|
PaymentStatus,
|
||||||
|
JournalEntrySource,
|
||||||
|
CollectionStatus,
|
||||||
|
RemittanceStatus,
|
||||||
|
SubscriberStatus,
|
||||||
|
ExpensePaymentMethod,
|
||||||
|
} from "@prisma/client";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared test state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const TS = Date.now();
|
||||||
|
|
||||||
|
let tenantId: string;
|
||||||
|
let adminUserId: string;
|
||||||
|
let collectorUserId: string;
|
||||||
|
|
||||||
|
// Entity IDs
|
||||||
|
let subscriberActiveId: string;
|
||||||
|
let subscriberSuspendedId: string;
|
||||||
|
let servicePlanId: string;
|
||||||
|
let zoneId: string;
|
||||||
|
let invoiceId: string;
|
||||||
|
let paymentId: string;
|
||||||
|
let collectionId: string;
|
||||||
|
|
||||||
|
// Account IDs
|
||||||
|
let cashAccountId: string; // 1010
|
||||||
|
let bankAccountId: string; // 1020
|
||||||
|
let cashInTransitId: string; // 1030
|
||||||
|
let arAccountId: string; // 1100
|
||||||
|
let revenueAccountId: string; // 4010
|
||||||
|
let expenseAccountId: string; // 5040
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function tp() {
|
||||||
|
return withTenantContext(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Setup / Teardown
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Create tenant
|
||||||
|
const tenant = await prisma.tenant.create({
|
||||||
|
data: {
|
||||||
|
name: `Dashboard Test ${TS}`,
|
||||||
|
slug: `dashboard-test-${TS}`,
|
||||||
|
ownerEmail: `dashboard-${TS}@test.example`,
|
||||||
|
status: TenantStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tenantId = tenant.id;
|
||||||
|
|
||||||
|
// Seed COA
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await seedChartOfAccounts(tx as any, tenantId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed ticket categories (required for cleanup)
|
||||||
|
await prisma.ticketCategory.createMany({
|
||||||
|
data: [{ name: "Test Category", tenantId }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed expense categories
|
||||||
|
const defaultExpenseCategories = [
|
||||||
|
{ name: "Internet Bandwidth", accountCode: "5040", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Equipment & Supplies", accountCode: "5030", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Salary & Wages", accountCode: "5010", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Technician Compensation", accountCode: "5020", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Office Supplies", accountCode: "5050", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Utilities", accountCode: "5060", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Fuel & Transportation", accountCode: "5080", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Rent", accountCode: "5085", isSystemCategory: true, tenantId },
|
||||||
|
{ name: "Other", accountCode: "5090", isSystemCategory: true, tenantId },
|
||||||
|
];
|
||||||
|
await prisma.expenseCategory.createMany({ data: defaultExpenseCategories });
|
||||||
|
|
||||||
|
// Look up account IDs
|
||||||
|
const accounts = await prisma.account.findMany({
|
||||||
|
where: { tenantId, code: { in: ["1010", "1020", "1030", "1100", "4010", "5040"] } },
|
||||||
|
select: { id: true, code: true },
|
||||||
|
});
|
||||||
|
const accountMap = new Map(accounts.map((a) => [a.code, a.id]));
|
||||||
|
cashAccountId = accountMap.get("1010")!;
|
||||||
|
bankAccountId = accountMap.get("1020")!;
|
||||||
|
cashInTransitId = accountMap.get("1030")!;
|
||||||
|
arAccountId = accountMap.get("1100")!;
|
||||||
|
revenueAccountId = accountMap.get("4010")!;
|
||||||
|
expenseAccountId = accountMap.get("5040")!;
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
const admin = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `dash-admin-${TS}@test.example`,
|
||||||
|
passwordHash: "hashed",
|
||||||
|
firstName: "Dash",
|
||||||
|
lastName: "Admin",
|
||||||
|
tenantId,
|
||||||
|
roles: [Role.ADMIN],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
adminUserId = admin.id;
|
||||||
|
|
||||||
|
// Create collector user
|
||||||
|
const collector = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `dash-collector-${TS}@test.example`,
|
||||||
|
passwordHash: "hashed",
|
||||||
|
firstName: "Dash",
|
||||||
|
lastName: "Collector",
|
||||||
|
tenantId,
|
||||||
|
roles: [Role.COLLECTOR],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
collectorUserId = collector.id;
|
||||||
|
|
||||||
|
// Create zone + zone assignment for collector
|
||||||
|
const zone = await prisma.zone.create({
|
||||||
|
data: { tenantId, name: `Dash Zone ${TS}` },
|
||||||
|
});
|
||||||
|
zoneId = zone.id;
|
||||||
|
|
||||||
|
await prisma.zoneAssignment.create({
|
||||||
|
data: { tenantId, userId: collectorUserId, zoneId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create service plan
|
||||||
|
const plan = await prisma.servicePlan.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
name: `Dash Plan ${TS}`,
|
||||||
|
speed: "50 Mbps",
|
||||||
|
monthlyPrice: new Prisma.Decimal(1500),
|
||||||
|
billingType: BillingType.POSTPAID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
servicePlanId = plan.id;
|
||||||
|
|
||||||
|
// Create active subscriber in the zone
|
||||||
|
const activeSub = await prisma.subscriber.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
accountNumber: `SUB-DASH-A-${TS}`,
|
||||||
|
firstName: "Active",
|
||||||
|
lastName: "Subscriber",
|
||||||
|
address: "123 Active St",
|
||||||
|
servicePlanId,
|
||||||
|
zoneId,
|
||||||
|
billingDay: 15,
|
||||||
|
status: SubscriberStatus.ACTIVE,
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
subscriberActiveId = activeSub.id;
|
||||||
|
|
||||||
|
// Create suspended subscriber
|
||||||
|
const suspSub = await prisma.subscriber.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
accountNumber: `SUB-DASH-S-${TS}`,
|
||||||
|
firstName: "Suspended",
|
||||||
|
lastName: "Subscriber",
|
||||||
|
address: "456 Suspended St",
|
||||||
|
servicePlanId,
|
||||||
|
billingDay: 15,
|
||||||
|
status: SubscriberStatus.SUSPENDED,
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
subscriberSuspendedId = suspSub.id;
|
||||||
|
|
||||||
|
// Create overdue invoice for the active subscriber (past due date)
|
||||||
|
const invoice = await prisma.invoice.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
invoiceNumber: `INV-DASH-${TS}`,
|
||||||
|
subscriberId: subscriberActiveId,
|
||||||
|
periodStart: new Date("2026-01-01"),
|
||||||
|
periodEnd: new Date("2026-01-31"),
|
||||||
|
dueDate: new Date("2026-02-15"),
|
||||||
|
subtotal: new Prisma.Decimal(1500),
|
||||||
|
totalAmount: new Prisma.Decimal(1500),
|
||||||
|
amountPaid: new Prisma.Decimal(0),
|
||||||
|
status: InvoiceStatus.OVERDUE,
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
invoiceId = invoice.id;
|
||||||
|
|
||||||
|
// Record a payment today (for revenue metrics)
|
||||||
|
const payment = await prisma.payment.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
subscriberId: subscriberActiveId,
|
||||||
|
amount: new Prisma.Decimal(500),
|
||||||
|
paymentMethod: PaymentMethod.CASH,
|
||||||
|
paymentDate: new Date(),
|
||||||
|
idempotencyKey: `pay-dash-${TS}`,
|
||||||
|
recordedById: adminUserId,
|
||||||
|
status: PaymentStatus.COMPLETED,
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
paymentId = payment.id;
|
||||||
|
|
||||||
|
// Create a payment JE (for revenue: DR Cash, CR AR)
|
||||||
|
await JournalEntryService.createEntry({
|
||||||
|
tenantPrisma: tp(),
|
||||||
|
tenantId,
|
||||||
|
date: new Date(),
|
||||||
|
description: "Payment received for dashboard test",
|
||||||
|
source: JournalEntrySource.SYSTEM,
|
||||||
|
referenceType: "Payment",
|
||||||
|
referenceId: paymentId,
|
||||||
|
createdById: adminUserId,
|
||||||
|
lines: [
|
||||||
|
{ accountId: cashAccountId, debit: 500, credit: 0, description: "Cash received" },
|
||||||
|
{ accountId: arAccountId, debit: 0, credit: 500, description: "AR reduced" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a revenue JE (for cash flow: DR Cash, CR Revenue)
|
||||||
|
await JournalEntryService.createEntry({
|
||||||
|
tenantPrisma: tp(),
|
||||||
|
tenantId,
|
||||||
|
date: new Date(),
|
||||||
|
description: "Monthly subscription revenue",
|
||||||
|
source: JournalEntrySource.SYSTEM,
|
||||||
|
referenceType: "Invoice",
|
||||||
|
referenceId: invoiceId,
|
||||||
|
createdById: adminUserId,
|
||||||
|
lines: [
|
||||||
|
{ accountId: arAccountId, debit: 1500, credit: 0, description: "AR for invoice" },
|
||||||
|
{ accountId: revenueAccountId, debit: 0, credit: 1500, description: "Subscription revenue" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an expense JE (for cash flow money out: DR Expense, CR Bank)
|
||||||
|
await JournalEntryService.createEntry({
|
||||||
|
tenantPrisma: tp(),
|
||||||
|
tenantId,
|
||||||
|
date: new Date(),
|
||||||
|
description: "Bandwidth expense for dashboard test",
|
||||||
|
source: JournalEntrySource.SYSTEM,
|
||||||
|
referenceType: "Expense",
|
||||||
|
referenceId: "expense-dash-" + TS,
|
||||||
|
createdById: adminUserId,
|
||||||
|
lines: [
|
||||||
|
{ accountId: expenseAccountId, debit: 800, credit: 0, description: "Bandwidth cost" },
|
||||||
|
{ accountId: bankAccountId, debit: 0, credit: 800, description: "Bank payment" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a collection today (for collector summary)
|
||||||
|
const collection = await prisma.collection.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
collectorId: collectorUserId,
|
||||||
|
subscriberId: subscriberActiveId,
|
||||||
|
amount: new Prisma.Decimal(300),
|
||||||
|
collectionDate: new Date(),
|
||||||
|
status: CollectionStatus.COMPLETED,
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
collectionId = collection.id;
|
||||||
|
|
||||||
|
// Create a PENDING remittance (for unverified count)
|
||||||
|
await prisma.remittance.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
collectorId: collectorUserId,
|
||||||
|
remittanceDate: new Date(),
|
||||||
|
collectedTotal: new Prisma.Decimal(300),
|
||||||
|
status: RemittanceStatus.PENDING,
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Cleanup in comprehensive order
|
||||||
|
await prisma.expense.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.vendor.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.expenseCategory.deleteMany({ where: { tenantId, isSystemCategory: false } });
|
||||||
|
await prisma.collectionAllocation.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.collection.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.remittance.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.paymentAllocation.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.payment.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.invoiceLine.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.invoice.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.journalEntryLine.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.journalEntry.updateMany({
|
||||||
|
where: { tenantId, reversesEntryId: { not: null } },
|
||||||
|
data: { reversesEntryId: null },
|
||||||
|
});
|
||||||
|
await prisma.journalEntry.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.zoneAssignment.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.subscriber.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.zone.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.servicePlan.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.accountingPeriod.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.account.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.ticketCategory.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.expenseCategory.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.user.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.tenant.deleteMany({ where: { id: tenantId } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("getRevenueMetrics", () => {
|
||||||
|
it("returns today's revenue from COMPLETED payments", async () => {
|
||||||
|
const result = await DashboardService.getRevenueMetrics(tp(), tenantId);
|
||||||
|
|
||||||
|
// We created a 500 payment today
|
||||||
|
expect(result.revenueToday.toNumber()).toBeGreaterThanOrEqual(500);
|
||||||
|
expect(result.revenueThisMonth.toNumber()).toBeGreaterThanOrEqual(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOverdueMetrics", () => {
|
||||||
|
it("counts overdue invoices and outstanding amount", async () => {
|
||||||
|
const result = await DashboardService.getOverdueMetrics(tp(), tenantId);
|
||||||
|
|
||||||
|
// We created 1 overdue invoice with 1500 total, 0 amountPaid
|
||||||
|
expect(result.overdueCount).toBe(1);
|
||||||
|
expect(result.totalOutstanding.toNumber()).toBe(1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getSubscriberMetrics", () => {
|
||||||
|
it("returns correct status breakdown", async () => {
|
||||||
|
const result = await DashboardService.getSubscriberMetrics(tp(), tenantId);
|
||||||
|
|
||||||
|
expect(result.active).toBe(1);
|
||||||
|
expect(result.suspended).toBe(1);
|
||||||
|
expect(result.cancelled).toBe(0);
|
||||||
|
expect(result.total).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getCashFlowSummary", () => {
|
||||||
|
it("computes money in and money out from JE lines", async () => {
|
||||||
|
const now = new Date();
|
||||||
|
const startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
||||||
|
const endDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0, 23, 59, 59, 999));
|
||||||
|
|
||||||
|
const result = await DashboardService.getCashFlowSummary(
|
||||||
|
tp(),
|
||||||
|
tenantId,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
// Revenue JE: 1500 credit to 4010 (money in)
|
||||||
|
// Expense JE: 800 debit to 5040 (money out)
|
||||||
|
expect(result.moneyIn.toNumber()).toBe(1500);
|
||||||
|
expect(result.moneyOut.toNumber()).toBe(800);
|
||||||
|
expect(result.netCashFlow.toNumber()).toBe(700);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getCollectorSummary", () => {
|
||||||
|
it("returns today's collections and unverified remittance count", async () => {
|
||||||
|
const result = await DashboardService.getCollectorSummary(tp(), tenantId);
|
||||||
|
|
||||||
|
// We created 1 collection of 300 today
|
||||||
|
expect(result.collectionsToday.toNumber()).toBeGreaterThanOrEqual(300);
|
||||||
|
// We created 1 PENDING remittance
|
||||||
|
expect(result.unverifiedRemittances).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDashboardSummary", () => {
|
||||||
|
it("aggregates all metrics into a single object", async () => {
|
||||||
|
const result = await DashboardService.getDashboardSummary(tp(), tenantId);
|
||||||
|
|
||||||
|
// Revenue
|
||||||
|
expect(result.revenue).toBeDefined();
|
||||||
|
expect(result.revenue.revenueToday).toBeDefined();
|
||||||
|
expect(result.revenue.revenueThisMonth).toBeDefined();
|
||||||
|
|
||||||
|
// Overdue
|
||||||
|
expect(result.overdue).toBeDefined();
|
||||||
|
expect(result.overdue.overdueCount).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(result.overdue.totalOutstanding.toNumber()).toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
|
// Subscribers
|
||||||
|
expect(result.subscribers).toBeDefined();
|
||||||
|
expect(result.subscribers.total).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
// Cash Flow
|
||||||
|
expect(result.cashFlow).toBeDefined();
|
||||||
|
expect(result.cashFlow.moneyIn).toBeDefined();
|
||||||
|
expect(result.cashFlow.moneyOut).toBeDefined();
|
||||||
|
expect(result.cashFlow.netCashFlow).toBeDefined();
|
||||||
|
|
||||||
|
// Collectors
|
||||||
|
expect(result.collectors).toBeDefined();
|
||||||
|
expect(result.collectors.collectionsToday).toBeDefined();
|
||||||
|
expect(typeof result.collectors.unverifiedRemittances).toBe("number");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user