feat(05-05): billing workflow end-to-end test
- Subscriber registration -> invoice generation -> payment recording - Full payment marks invoice PAID with correct JE (DR Cash, CR AR) - Partial payment marks invoice PARTIAL with balanced JE - Trial balance verified balanced after all transactions - Dashboard reflects billing activity (revenue + subscriber metrics) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
463
src/lib/__tests__/integration/e2e-workflows.test.ts
Normal file
463
src/lib/__tests__/integration/e2e-workflows.test.ts
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
/**
|
||||||
|
* End-to-End Workflow Integration Tests
|
||||||
|
*
|
||||||
|
* Tests the three critical ISP business processes end-to-end, exercising
|
||||||
|
* multiple services in sequence to prove they integrate correctly and
|
||||||
|
* produce accurate accounting entries.
|
||||||
|
*
|
||||||
|
* INFRA-04: Prove the system works as a coherent whole, not just isolated units.
|
||||||
|
*
|
||||||
|
* Workflow 1: Subscriber Registration -> Invoice Generation -> Payment Recording
|
||||||
|
* Workflow 2: Collector Collection -> Remittance Verification
|
||||||
|
* Workflow 3: Ticket Creation -> Job Order -> Completion -> Auto-Resolve
|
||||||
|
*
|
||||||
|
* CLEANUP ORDER (comprehensive, covering all subsystems):
|
||||||
|
* ticketComments -> jobOrders -> tickets -> ticketCategories ->
|
||||||
|
* collectionAllocations -> collections -> remittances ->
|
||||||
|
* paymentAllocations -> payments -> invoiceLines -> invoices ->
|
||||||
|
* journalEntryLines -> null reversesEntryId -> journalEntries ->
|
||||||
|
* stockMovements -> inventoryItems -> expenses -> vendors ->
|
||||||
|
* expenseCategories (custom) -> subscribers -> servicePlans ->
|
||||||
|
* tenantSettings -> accountingPeriods -> accounts ->
|
||||||
|
* zoneAssignments -> zones -> technicianProfiles -> jobTypeRates ->
|
||||||
|
* expenseCategories (system) -> users -> tenant
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { withTenantContext } from "@/lib/prisma-tenant";
|
||||||
|
import { seedChartOfAccounts } from "@/lib/accounting/seed-coa";
|
||||||
|
import { JournalEntryService } from "@/lib/accounting/journal-entry-service";
|
||||||
|
import { createSubscriber } from "@/lib/services/subscriber-service";
|
||||||
|
import {
|
||||||
|
generateInvoiceForSubscriber,
|
||||||
|
computeBillingPeriod,
|
||||||
|
} from "@/lib/services/billing-service";
|
||||||
|
import { recordPayment } from "@/lib/services/payment-service";
|
||||||
|
import { recordCollection } from "@/lib/services/collector-service";
|
||||||
|
import {
|
||||||
|
createRemittance,
|
||||||
|
verifyRemittance,
|
||||||
|
} from "@/lib/services/remittance-service";
|
||||||
|
import { createTicket, transitionTicketStatus } from "@/lib/services/ticket-service";
|
||||||
|
import {
|
||||||
|
createJobOrder,
|
||||||
|
updateJobOrderStatus,
|
||||||
|
} from "@/lib/services/job-order-service";
|
||||||
|
import { DashboardService } from "@/lib/services/dashboard-service";
|
||||||
|
import {
|
||||||
|
Prisma,
|
||||||
|
Role,
|
||||||
|
TenantStatus,
|
||||||
|
BillingType,
|
||||||
|
PaymentMethod,
|
||||||
|
TicketPriority,
|
||||||
|
TicketSource,
|
||||||
|
TicketStatus,
|
||||||
|
JobOrderStatus,
|
||||||
|
} from "@prisma/client";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared test state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const TS = Date.now();
|
||||||
|
|
||||||
|
let tenantId: string;
|
||||||
|
let adminUserId: string;
|
||||||
|
let officeStaffUserId: string;
|
||||||
|
let collectorUserId: string;
|
||||||
|
let technicianUserId: string;
|
||||||
|
let servicePlanId: string;
|
||||||
|
let zoneId: string;
|
||||||
|
let ticketCategoryId: string;
|
||||||
|
|
||||||
|
// Billing workflow state
|
||||||
|
let subscriberId: string;
|
||||||
|
let subscriberAccountNumber: string;
|
||||||
|
let invoice1Id: string;
|
||||||
|
let invoice2Id: string;
|
||||||
|
|
||||||
|
// Collection workflow state
|
||||||
|
let collectionSubscriberId: string;
|
||||||
|
let collectionInvoiceId: string;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function tp() {
|
||||||
|
return withTenantContext(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let invoiceCounter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a raw invoice for tests that need one without going
|
||||||
|
* through the full billing service (e.g., collection workflow).
|
||||||
|
*/
|
||||||
|
async function createRawInvoice(
|
||||||
|
subscriberIdArg: string,
|
||||||
|
amount: number,
|
||||||
|
status = "SENT" as const,
|
||||||
|
) {
|
||||||
|
invoiceCounter++;
|
||||||
|
const periodStart = new Date(Date.UTC(2025, 0, invoiceCounter));
|
||||||
|
return prisma.invoice.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
invoiceNumber: `INV-E2E-${TS}-${invoiceCounter}`,
|
||||||
|
subscriberId: subscriberIdArg,
|
||||||
|
periodStart,
|
||||||
|
periodEnd: new Date(Date.UTC(2025, 0, invoiceCounter + 30)),
|
||||||
|
dueDate: new Date(Date.UTC(2025, 1, invoiceCounter)),
|
||||||
|
subtotal: new Prisma.Decimal(amount),
|
||||||
|
totalAmount: new Prisma.Decimal(amount),
|
||||||
|
amountPaid: new Prisma.Decimal(0),
|
||||||
|
status,
|
||||||
|
} as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Setup / Teardown
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// 1. Create tenant
|
||||||
|
const tenant = await prisma.tenant.create({
|
||||||
|
data: {
|
||||||
|
name: `E2E Workflow Test ${TS}`,
|
||||||
|
slug: `e2e-workflow-${TS}`,
|
||||||
|
ownerEmail: `e2e-${TS}@test.example`,
|
||||||
|
status: TenantStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tenantId = tenant.id;
|
||||||
|
|
||||||
|
// 2. Seed COA
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await seedChartOfAccounts(tx as any, tenantId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Seed ticket categories
|
||||||
|
const cat = await prisma.ticketCategory.create({
|
||||||
|
data: { name: "No Connection", tenantId, isActive: true },
|
||||||
|
});
|
||||||
|
ticketCategoryId = cat.id;
|
||||||
|
|
||||||
|
// 4. Create users
|
||||||
|
const admin = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `e2e-admin-${TS}@test.example`,
|
||||||
|
passwordHash: "hashed",
|
||||||
|
firstName: "E2E",
|
||||||
|
lastName: "Admin",
|
||||||
|
tenantId,
|
||||||
|
roles: [Role.ADMIN],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
adminUserId = admin.id;
|
||||||
|
|
||||||
|
const officeStaff = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `e2e-office-${TS}@test.example`,
|
||||||
|
passwordHash: "hashed",
|
||||||
|
firstName: "E2E",
|
||||||
|
lastName: "OfficeStaff",
|
||||||
|
tenantId,
|
||||||
|
roles: [Role.OFFICE_STAFF],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
officeStaffUserId = officeStaff.id;
|
||||||
|
|
||||||
|
const collector = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `e2e-collector-${TS}@test.example`,
|
||||||
|
passwordHash: "hashed",
|
||||||
|
firstName: "E2E",
|
||||||
|
lastName: "Collector",
|
||||||
|
tenantId,
|
||||||
|
roles: [Role.COLLECTOR],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
collectorUserId = collector.id;
|
||||||
|
|
||||||
|
const technician = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `e2e-tech-${TS}@test.example`,
|
||||||
|
passwordHash: "hashed",
|
||||||
|
firstName: "E2E",
|
||||||
|
lastName: "Technician",
|
||||||
|
tenantId,
|
||||||
|
roles: [Role.TECHNICIAN],
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
technicianUserId = technician.id;
|
||||||
|
|
||||||
|
// 5. Create service plan
|
||||||
|
const plan = await prisma.servicePlan.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
name: `Fiber 50 Mbps ${TS}`,
|
||||||
|
speed: "50 Mbps",
|
||||||
|
monthlyPrice: new Prisma.Decimal(1500),
|
||||||
|
billingType: BillingType.POSTPAID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
servicePlanId = plan.id;
|
||||||
|
|
||||||
|
// 6. Create zone and assign collector
|
||||||
|
const zone = await prisma.zone.create({
|
||||||
|
data: { tenantId, name: `Zone Alpha ${TS}` },
|
||||||
|
});
|
||||||
|
zoneId = zone.id;
|
||||||
|
|
||||||
|
await prisma.zoneAssignment.create({
|
||||||
|
data: { tenantId, userId: collectorUserId, zoneId },
|
||||||
|
});
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Comprehensive cleanup order covering all subsystems
|
||||||
|
await prisma.ticketComment.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.jobOrder.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.ticket.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.ticketCategory.deleteMany({ where: { tenantId } });
|
||||||
|
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.stockMovement.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.inventoryItem.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.expense.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.vendor.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.expenseCategory.deleteMany({
|
||||||
|
where: { tenantId, isSystemCategory: false },
|
||||||
|
});
|
||||||
|
await prisma.subscriber.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.servicePlan.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.tenantSettings.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.accountingPeriod.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.account.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.zoneAssignment.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.zone.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.technicianProfile.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.jobTypeRate.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.expenseCategory.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.user.deleteMany({ where: { tenantId } });
|
||||||
|
await prisma.tenant.deleteMany({ where: { id: tenantId } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// WORKFLOW 1: Subscriber Registration -> Invoice -> Payment -> Balanced Books
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
describe("E2E: Billing Workflow", () => {
|
||||||
|
test("1. Register a new subscriber", async () => {
|
||||||
|
const subscriber = await createSubscriber(tp(), {
|
||||||
|
firstName: "Juan",
|
||||||
|
lastName: "dela Cruz",
|
||||||
|
address: "123 Mango St, Barangay 1",
|
||||||
|
servicePlanId,
|
||||||
|
zoneId,
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriberId = subscriber.id;
|
||||||
|
subscriberAccountNumber = subscriber.accountNumber;
|
||||||
|
|
||||||
|
// Subscriber created with ACTIVE status
|
||||||
|
expect(subscriber.status).toBe("ACTIVE");
|
||||||
|
// Account number generated with SUB-NNNN format
|
||||||
|
expect(subscriber.accountNumber).toMatch(/^SUB-\d{4,}$/);
|
||||||
|
// Correct plan assigned
|
||||||
|
expect(subscriber.servicePlanId).toBe(servicePlanId);
|
||||||
|
expect(subscriber.servicePlan.monthlyPrice.toNumber()).toBe(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("2. Generate invoice for subscriber", async () => {
|
||||||
|
// Load subscriber with plan for billing service
|
||||||
|
const subscriber = await tp().subscriber.findFirst({
|
||||||
|
where: { id: subscriberId },
|
||||||
|
include: { servicePlan: true },
|
||||||
|
});
|
||||||
|
expect(subscriber).not.toBeNull();
|
||||||
|
|
||||||
|
const period = computeBillingPeriod(new Date(), subscriber!.billingDay);
|
||||||
|
|
||||||
|
const result = await generateInvoiceForSubscriber(
|
||||||
|
tp(),
|
||||||
|
tenantId,
|
||||||
|
subscriber!,
|
||||||
|
period,
|
||||||
|
adminUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const invoice = (result as any).invoice;
|
||||||
|
invoice1Id = invoice.id;
|
||||||
|
|
||||||
|
// Invoice created with correct amount matching plan price
|
||||||
|
expect(new Prisma.Decimal(invoice.totalAmount).toNumber()).toBe(1500);
|
||||||
|
// Invoice has SENT status (unpaid)
|
||||||
|
expect(invoice.status).toBe("SENT");
|
||||||
|
|
||||||
|
// Verify JE posted: DR 1100 AR, CR 4010 Service Revenue
|
||||||
|
const trialBalance = await JournalEntryService.getTrialBalance({
|
||||||
|
tenantPrisma: tp(),
|
||||||
|
});
|
||||||
|
const arLine = trialBalance.find((l) => l.accountCode === "1100");
|
||||||
|
const revLine = trialBalance.find((l) => l.accountCode === "4010");
|
||||||
|
// AR should have a debit balance of 1500
|
||||||
|
expect(arLine!.debitBalance.toNumber()).toBe(1500);
|
||||||
|
// Revenue should have a credit balance of 1500
|
||||||
|
expect(revLine!.creditBalance.toNumber()).toBe(1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("3. Record full payment against invoice", async () => {
|
||||||
|
const paymentResult = await recordPayment(tp(), tenantId, {
|
||||||
|
subscriberId,
|
||||||
|
amount: 1500,
|
||||||
|
paymentMethod: PaymentMethod.CASH,
|
||||||
|
paymentDate: new Date(),
|
||||||
|
idempotencyKey: `e2e-full-pay-${TS}`,
|
||||||
|
recordedById: adminUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Payment created with COMPLETED status
|
||||||
|
expect(paymentResult.payment.status).toBe("COMPLETED");
|
||||||
|
expect(paymentResult.idempotent).toBe(false);
|
||||||
|
// Full amount allocated
|
||||||
|
expect(paymentResult.allocations.length).toBe(1);
|
||||||
|
expect(paymentResult.allocations[0].amount.toNumber()).toBe(1500);
|
||||||
|
|
||||||
|
// Invoice status = PAID
|
||||||
|
const invoice = await tp().invoice.findFirst({
|
||||||
|
where: { id: invoice1Id },
|
||||||
|
});
|
||||||
|
expect(invoice!.status).toBe("PAID");
|
||||||
|
expect(new Prisma.Decimal(invoice!.amountPaid).toNumber()).toBe(1500);
|
||||||
|
|
||||||
|
// JE posted: DR 1010 Cash, CR 1100 AR
|
||||||
|
const trialBalance = await JournalEntryService.getTrialBalance({
|
||||||
|
tenantPrisma: tp(),
|
||||||
|
});
|
||||||
|
const cashLine = trialBalance.find((l) => l.accountCode === "1010");
|
||||||
|
// Cash should have debit balance of 1500
|
||||||
|
expect(cashLine!.debitBalance.toNumber()).toBe(1500);
|
||||||
|
// AR should now be zero (1500 debit from invoice - 1500 credit from payment)
|
||||||
|
const arLine = trialBalance.find((l) => l.accountCode === "1100");
|
||||||
|
expect(arLine!.debitBalance.toNumber()).toBe(0);
|
||||||
|
expect(arLine!.creditBalance.toNumber()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("4. Record partial payment on second invoice", async () => {
|
||||||
|
// Generate second invoice
|
||||||
|
const subscriber = await tp().subscriber.findFirst({
|
||||||
|
where: { id: subscriberId },
|
||||||
|
include: { servicePlan: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use a different period to avoid idempotency skip
|
||||||
|
const period = {
|
||||||
|
periodStart: new Date(Date.UTC(2025, 5, 15)),
|
||||||
|
periodEnd: new Date(Date.UTC(2025, 6, 14)),
|
||||||
|
dueDate: new Date(Date.UTC(2025, 7, 14)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result2 = await generateInvoiceForSubscriber(
|
||||||
|
tp(),
|
||||||
|
tenantId,
|
||||||
|
subscriber!,
|
||||||
|
period,
|
||||||
|
adminUserId,
|
||||||
|
);
|
||||||
|
expect(result2).not.toBeNull();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
invoice2Id = (result2 as any).invoice.id;
|
||||||
|
|
||||||
|
// Record partial payment (50%)
|
||||||
|
const paymentResult = await recordPayment(tp(), tenantId, {
|
||||||
|
subscriberId,
|
||||||
|
amount: 750,
|
||||||
|
paymentMethod: PaymentMethod.CASH,
|
||||||
|
paymentDate: new Date(),
|
||||||
|
idempotencyKey: `e2e-partial-pay-${TS}`,
|
||||||
|
recordedById: adminUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(paymentResult.payment.status).toBe("COMPLETED");
|
||||||
|
expect(paymentResult.allocations[0].amount.toNumber()).toBe(750);
|
||||||
|
|
||||||
|
// Invoice status = PARTIAL, amountPaid = 750
|
||||||
|
const invoice2 = await tp().invoice.findFirst({
|
||||||
|
where: { id: invoice2Id },
|
||||||
|
});
|
||||||
|
expect(invoice2!.status).toBe("PARTIAL");
|
||||||
|
expect(new Prisma.Decimal(invoice2!.amountPaid).toNumber()).toBe(750);
|
||||||
|
|
||||||
|
// JE for partial amount is balanced
|
||||||
|
const trialBalance = await JournalEntryService.getTrialBalance({
|
||||||
|
tenantPrisma: tp(),
|
||||||
|
});
|
||||||
|
const totalDebits = trialBalance.reduce(
|
||||||
|
(sum, l) => sum.plus(l.debitBalance),
|
||||||
|
new Prisma.Decimal(0),
|
||||||
|
);
|
||||||
|
const totalCredits = trialBalance.reduce(
|
||||||
|
(sum, l) => sum.plus(l.creditBalance),
|
||||||
|
new Prisma.Decimal(0),
|
||||||
|
);
|
||||||
|
expect(totalDebits.toNumber()).toBe(totalCredits.toNumber());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("5. Verify trial balance is balanced after all transactions", async () => {
|
||||||
|
const trialBalance = await JournalEntryService.getTrialBalance({
|
||||||
|
tenantPrisma: tp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalDebits = trialBalance.reduce(
|
||||||
|
(sum, l) => sum.plus(l.debitBalance),
|
||||||
|
new Prisma.Decimal(0),
|
||||||
|
);
|
||||||
|
const totalCredits = trialBalance.reduce(
|
||||||
|
(sum, l) => sum.plus(l.creditBalance),
|
||||||
|
new Prisma.Decimal(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Self-verifying books: debits === credits
|
||||||
|
expect(totalDebits.toNumber()).toBe(totalCredits.toNumber());
|
||||||
|
// Should have meaningful balances (not all zeros)
|
||||||
|
expect(totalDebits.toNumber()).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("6. Dashboard reflects billing activity", async () => {
|
||||||
|
// Revenue metrics
|
||||||
|
const revenue = await DashboardService.getRevenueMetrics(tp(), tenantId);
|
||||||
|
// revenueToday includes the payments made (1500 + 750 = 2250)
|
||||||
|
expect(revenue.revenueToday.toNumber()).toBeGreaterThanOrEqual(2250);
|
||||||
|
|
||||||
|
// Subscriber metrics
|
||||||
|
const subscribers = await DashboardService.getSubscriberMetrics(
|
||||||
|
tp(),
|
||||||
|
tenantId,
|
||||||
|
);
|
||||||
|
// Active count includes the registered subscriber
|
||||||
|
expect(subscribers.active).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(subscribers.total).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user