import test from "node:test"; import assert from "node:assert/strict"; import { Prisma } from "@prisma/client"; import { BillingError, createPrismaBillingStore } from "./billing-store.js"; test("createUpcomingRenewalInvoices creates one invoice for subscriptions entering the 72h renewal window", async () => { const database = createRenewalBillingDatabase({ subscriptions: [ createRenewalSubscriptionFixture({ id: "subscription_1", currentPeriodStart: new Date("2026-03-01T12:00:00.000Z"), currentPeriodEnd: new Date("2026-03-12T11:00:00.000Z"), }), ], }); const store = createPrismaBillingStore(database.client); const notifications = await store.createUpcomingRenewalInvoices({ paymentProvider: "nowpayments", paymentProviderAdapter: { createInvoice: async () => ({ providerInvoiceId: "provider_invoice_renewal_1", paymentAddress: "wallet_renewal_1", amountCrypto: 29, amountUsd: 29, currency: "USDT", expiresAt: new Date("2026-03-10T13:00:00.000Z"), }), getInvoiceStatus: async (providerInvoiceId) => ({ providerInvoiceId, status: "pending", }), }, renewalLeadTimeHours: 72, now: new Date("2026-03-09T12:00:00.000Z"), }); assert.equal(notifications.length, 1); assert.equal(notifications[0]?.email, "user_subscription_1@example.com"); assert.equal(notifications[0]?.subscriptionId, "subscription_1"); assert.equal(database.calls.paymentInvoiceCreate.length, 1); }); test("createUpcomingRenewalInvoices does not auto-create another invoice after one already exists in the current cycle", async () => { const currentPeriodStart = new Date("2026-03-01T12:00:00.000Z"); const database = createRenewalBillingDatabase({ subscriptions: [ createRenewalSubscriptionFixture({ id: "subscription_1", currentPeriodStart, currentPeriodEnd: new Date("2026-03-12T11:00:00.000Z"), }), ], existingInvoicesBySubscriptionId: { subscription_1: [ createInvoiceFixture({ id: "invoice_existing", status: "expired", createdAt: new Date("2026-03-09T09:00:00.000Z"), paidAt: null, subscription: createSubscriptionFixture({ id: "subscription_1", currentPeriodStart, currentPeriodEnd: new Date("2026-03-12T11:00:00.000Z"), status: "active", }), }), ], }, }); const store = createPrismaBillingStore(database.client); const notifications = await store.createUpcomingRenewalInvoices({ paymentProvider: "nowpayments", paymentProviderAdapter: { createInvoice: async () => { throw new Error("createInvoice should not be called"); }, getInvoiceStatus: async (providerInvoiceId) => ({ providerInvoiceId, status: "pending", }), }, renewalLeadTimeHours: 72, now: new Date("2026-03-09T12:00:00.000Z"), }); assert.equal(notifications.length, 0); assert.equal(database.calls.paymentInvoiceCreate.length, 0); }); test("createUpcomingRenewalInvoices acquires a subscription billing lock before creating an invoice", async () => { const database = createRenewalBillingDatabase({ subscriptions: [ createRenewalSubscriptionFixture({ id: "subscription_1", currentPeriodStart: new Date("2026-03-01T12:00:00.000Z"), currentPeriodEnd: new Date("2026-03-12T11:00:00.000Z"), }), ], }); const store = createPrismaBillingStore(database.client); const notifications = await store.createUpcomingRenewalInvoices({ paymentProvider: "nowpayments", paymentProviderAdapter: { createInvoice: async () => ({ providerInvoiceId: "provider_invoice_renewal_1", paymentAddress: "wallet_renewal_1", amountCrypto: 29, amountUsd: 29, currency: "USDT", expiresAt: new Date("2026-03-10T13:00:00.000Z"), }), getInvoiceStatus: async (providerInvoiceId) => ({ providerInvoiceId, status: "pending", }), }, renewalLeadTimeHours: 72, now: new Date("2026-03-09T12:00:00.000Z"), }); assert.equal(notifications.length, 1); assert.equal(database.calls.paymentInvoiceCreate.length, 1); assert.deepEqual(database.calls.subscriptionBillingLocks, ["subscription_1"]); }); test("expireElapsedPendingInvoices marks pending invoices expired", async () => { const database = createRenewalBillingDatabase({ subscriptions: [], expirePendingCount: 2, }); const store = createPrismaBillingStore(database.client); const result = await store.expireElapsedPendingInvoices(new Date("2026-03-10T12:00:00.000Z")); assert.equal(result.expiredCount, 2); assert.equal(database.calls.paymentInvoiceExpireUpdateMany.length, 1); }); test("markInvoicePaid activates a pending invoice once and writes an admin audit log", async () => { const invoice = createInvoiceFixture({ status: "pending", paidAt: null, subscription: createSubscriptionFixture(), }); const database = createBillingDatabase({ invoice, }); const store = createPrismaBillingStore(database.client); const result = await store.markInvoicePaid({ invoiceId: invoice.id, actor: { type: "web_admin", ref: "admin_user_1", }, }); assert.equal(result.status, "paid"); assert.ok(result.paidAt instanceof Date); assert.equal(database.calls.paymentInvoiceUpdateMany.length, 1); assert.equal(database.calls.subscriptionUpdate.length, 1); assert.equal(database.calls.usageLedgerCreate.length, 1); assert.equal(database.calls.adminAuditCreate.length, 1); const paymentUpdate = database.calls.paymentInvoiceUpdateMany[0] as ({ data: { status: "paid"; paidAt: Date }; } | undefined); const auditEntry = database.calls.adminAuditCreate[0]; assert.ok(paymentUpdate); assert.ok(auditEntry); assert.equal(paymentUpdate.data.status, "paid"); assert.equal(result.paidAt?.toISOString(), paymentUpdate.data.paidAt.toISOString()); assert.equal(auditEntry.actorType, "web_admin"); assert.equal(auditEntry.actorRef, "admin_user_1"); assert.equal(auditEntry.action, "invoice_mark_paid"); assert.equal(auditEntry.targetType, "payment_invoice"); assert.equal(auditEntry.targetId, invoice.id); assert.deepEqual(auditEntry.metadata, { invoiceId: invoice.id, subscriptionId: invoice.subscriptionId, provider: invoice.provider, providerInvoiceId: invoice.providerInvoiceId, status: "paid", paidAt: paymentUpdate.data.paidAt.toISOString(), replayed: false, }); }); test("markInvoicePaid is idempotent for already paid invoices", async () => { const paidAt = new Date("2026-03-10T12:00:00.000Z"); const invoice = createInvoiceFixture({ status: "paid", paidAt, subscription: createSubscriptionFixture(), }); const database = createBillingDatabase({ invoice, }); const store = createPrismaBillingStore(database.client); const result = await store.markInvoicePaid({ invoiceId: invoice.id, actor: { type: "web_admin", ref: "admin_user_1", }, }); assert.equal(result.status, "paid"); assert.equal(result.paidAt?.toISOString(), paidAt.toISOString()); assert.equal(database.calls.paymentInvoiceUpdateMany.length, 0); assert.equal(database.calls.subscriptionUpdate.length, 0); assert.equal(database.calls.usageLedgerCreate.length, 0); assert.equal(database.calls.adminAuditCreate.length, 1); assert.equal(database.calls.adminAuditCreate[0]?.action, "invoice_mark_paid_replayed"); assert.equal(database.calls.adminAuditCreate[0]?.metadata?.replayed, true); }); test("markInvoicePaid rejects invalid terminal invoice transitions", async () => { const invoice = createInvoiceFixture({ status: "expired", paidAt: null, subscription: createSubscriptionFixture(), }); const database = createBillingDatabase({ invoice, }); const store = createPrismaBillingStore(database.client); await assert.rejects( store.markInvoicePaid({ invoiceId: invoice.id, actor: { type: "web_admin", ref: "admin_user_1", }, }), (error: unknown) => error instanceof BillingError && error.code === "invoice_transition_not_allowed" && error.message === 'Invoice in status "expired" cannot be marked paid.', ); assert.equal(database.calls.paymentInvoiceUpdateMany.length, 0); assert.equal(database.calls.subscriptionUpdate.length, 0); assert.equal(database.calls.usageLedgerCreate.length, 0); assert.equal(database.calls.adminAuditCreate.length, 0); }); test("markInvoicePaid treats a concurrent pending->paid race as a replay without duplicate side effects", async () => { const paidAt = new Date("2026-03-10T12:00:00.000Z"); const database = createBillingDatabase({ invoice: createInvoiceFixture({ status: "pending", paidAt: null, subscription: createSubscriptionFixture(), }), updateManyCount: 0, invoiceAfterFailedTransition: createInvoiceFixture({ status: "paid", paidAt, subscription: createSubscriptionFixture(), }), }); const store = createPrismaBillingStore(database.client); const result = await store.markInvoicePaid({ invoiceId: "invoice_1", actor: { type: "web_admin", ref: "admin_user_1", }, }); assert.equal(result.status, "paid"); assert.equal(result.paidAt?.toISOString(), paidAt.toISOString()); assert.equal(database.calls.paymentInvoiceUpdateMany.length, 1); assert.equal(database.calls.subscriptionUpdate.length, 0); assert.equal(database.calls.usageLedgerCreate.length, 0); assert.equal(database.calls.adminAuditCreate.length, 1); assert.equal(database.calls.adminAuditCreate[0]?.action, "invoice_mark_paid_replayed"); assert.equal(database.calls.adminAuditCreate[0]?.metadata?.replayed, true); }); test("reconcilePendingInvoice marks a pending invoice paid using provider paidAt", async () => { const providerPaidAt = new Date("2026-03-10T12:34:56.000Z"); const database = createBillingDatabase({ invoice: createInvoiceFixture({ status: "pending", paidAt: null, subscription: createSubscriptionFixture(), }), }); const store = createPrismaBillingStore(database.client); const result = await store.reconcilePendingInvoice({ invoiceId: "invoice_1", providerStatus: "paid", paidAt: providerPaidAt, actor: { type: "system", ref: "invoice_reconciliation", }, }); assert.equal(result.outcome, "marked_paid"); assert.equal(result.invoice.paidAt?.toISOString(), providerPaidAt.toISOString()); assert.equal(database.calls.paymentInvoiceUpdateMany.length, 1); assert.equal( ( database.calls.paymentInvoiceUpdateMany[0] as { data: { paidAt: Date }; } ).data.paidAt.toISOString(), providerPaidAt.toISOString(), ); assert.equal( ( database.calls.subscriptionUpdate[0] as { data: { currentPeriodStart: Date }; } ).data.currentPeriodStart.toISOString(), providerPaidAt.toISOString(), ); }); test("reconcilePendingInvoice marks a pending invoice expired", async () => { const database = createBillingDatabase({ invoice: createInvoiceFixture({ status: "pending", paidAt: null, subscription: createSubscriptionFixture(), }), }); const store = createPrismaBillingStore(database.client); const result = await store.reconcilePendingInvoice({ invoiceId: "invoice_1", providerStatus: "expired", }); assert.equal(result.outcome, "marked_expired"); assert.equal(result.invoice.status, "expired"); assert.equal(database.calls.paymentInvoiceTerminalUpdateMany.length, 1); assert.equal(database.calls.subscriptionUpdate.length, 0); assert.equal(database.calls.usageLedgerCreate.length, 0); }); test("reconcilePendingInvoice does not override an already paid invoice with expired status", async () => { const paidAt = new Date("2026-03-10T12:00:00.000Z"); const database = createBillingDatabase({ invoice: createInvoiceFixture({ status: "paid", paidAt, subscription: createSubscriptionFixture(), }), }); const store = createPrismaBillingStore(database.client); const result = await store.reconcilePendingInvoice({ invoiceId: "invoice_1", providerStatus: "expired", }); assert.equal(result.outcome, "ignored_terminal_state"); assert.equal(result.invoice.status, "paid"); assert.equal(database.calls.paymentInvoiceTerminalUpdateMany.length, 0); }); test("reconcilePendingInvoice accepts provider paid for a locally expired invoice", async () => { const providerPaidAt = new Date("2026-03-10T12:34:56.000Z"); const database = createBillingDatabase({ invoice: createInvoiceFixture({ status: "expired", paidAt: null, subscription: createSubscriptionFixture(), }), }); const store = createPrismaBillingStore(database.client); const result = await store.reconcilePendingInvoice({ invoiceId: "invoice_1", providerStatus: "paid", paidAt: providerPaidAt, actor: { type: "system", ref: "invoice_reconciliation", }, }); assert.equal(result.outcome, "marked_paid"); assert.equal(result.invoice.status, "paid"); assert.equal(result.invoice.paidAt?.toISOString(), providerPaidAt.toISOString()); assert.equal(database.calls.paymentInvoiceUpdateMany.length, 1); }); function createBillingDatabase(input: { invoice: ReturnType; updateManyCount?: number; invoiceAfterFailedTransition?: ReturnType; }) { const calls = { paymentInvoiceUpdateMany: [] as Array>, paymentInvoiceTerminalUpdateMany: [] as Array>, subscriptionUpdate: [] as Array>, usageLedgerCreate: [] as Array>, adminAuditCreate: [] as Array>, }; let currentInvoice = input.invoice; let findUniqueCallCount = 0; const transaction = { paymentInvoice: { findUnique: async () => { findUniqueCallCount += 1; if ( input.invoiceAfterFailedTransition && input.updateManyCount === 0 && findUniqueCallCount > 1 ) { currentInvoice = input.invoiceAfterFailedTransition; } return currentInvoice; }, updateMany: async ({ where, data, }: { where: { id: string; status: "pending" | { in: Array<"pending" | "expired" | "canceled"> } }; data: { status: "paid"; paidAt: Date }; }) => { calls.paymentInvoiceUpdateMany.push({ where, data }); const allowedStatuses = (typeof where.status === "string" ? [where.status] : where.status.in) as Array< "pending" | "expired" | "canceled" | "paid" >; const count = input.updateManyCount ?? (currentInvoice.id === where.id && allowedStatuses.includes(currentInvoice.status) ? 1 : 0); if (count > 0) { currentInvoice = { ...currentInvoice, status: data.status, paidAt: data.paidAt, }; } return { count }; }, }, subscription: { update: async ({ data }: { data: Record }) => { calls.subscriptionUpdate.push({ data }); return currentInvoice.subscription; }, }, usageLedgerEntry: { create: async ({ data }: { data: Record }) => { calls.usageLedgerCreate.push({ data }); return data; }, }, adminAuditLog: { create: async ({ data }: { data: Record }) => { calls.adminAuditCreate.push(data); return data; }, }, }; const client = { paymentInvoice: { findUnique: async () => currentInvoice, updateMany: async ({ where, data, }: { where: { id: string; status: "pending" }; data: { status: "expired" | "canceled" }; }) => { calls.paymentInvoiceTerminalUpdateMany.push({ where, data }); const count = currentInvoice.id === where.id && currentInvoice.status === where.status ? 1 : 0; if (count > 0) { currentInvoice = { ...currentInvoice, status: data.status, }; } return { count }; }, }, $transaction: async (callback: (tx: typeof transaction) => Promise) => callback(transaction), } as unknown as Parameters[0]; return { client, calls, }; } function createInvoiceFixture(input: { id?: string; status: "pending" | "paid" | "expired" | "canceled"; paidAt: Date | null; createdAt?: Date; subscription: ReturnType | null; }) { return { id: input.id ?? "invoice_1", userId: "user_1", subscriptionId: input.subscription?.id ?? null, provider: "nowpayments", providerInvoiceId: "provider_invoice_1", status: input.status, currency: "USDT", amountCrypto: new Prisma.Decimal("29"), amountUsd: new Prisma.Decimal("29"), paymentAddress: "wallet_1", expiresAt: new Date("2026-03-11T12:00:00.000Z"), paidAt: input.paidAt, createdAt: input.createdAt ?? new Date("2026-03-10T11:00:00.000Z"), updatedAt: input.createdAt ?? new Date("2026-03-10T11:00:00.000Z"), subscription: input.subscription, }; } function createSubscriptionFixture( overrides?: Partial<{ id: string; status: "pending_activation" | "active" | "expired"; currentPeriodStart: Date | null; currentPeriodEnd: Date | null; activatedAt: Date | null; }>, ) { return { id: overrides?.id ?? "subscription_1", userId: "user_1", planId: "plan_1", status: overrides?.status ?? ("pending_activation" as const), renewsManually: true, activatedAt: overrides?.activatedAt ?? null, currentPeriodStart: overrides?.currentPeriodStart ?? null, currentPeriodEnd: overrides?.currentPeriodEnd ?? null, canceledAt: null, createdAt: new Date("2026-03-10T11:00:00.000Z"), updatedAt: new Date("2026-03-10T11:00:00.000Z"), plan: { id: "plan_1", code: "basic", displayName: "Basic", monthlyRequestLimit: 100, monthlyPriceUsd: new Prisma.Decimal("29"), billingCurrency: "USDT", isActive: true, createdAt: new Date("2026-03-10T11:00:00.000Z"), updatedAt: new Date("2026-03-10T11:00:00.000Z"), }, }; } function createRenewalBillingDatabase(input: { subscriptions: Array>; existingInvoicesBySubscriptionId?: Record[]>; expirePendingCount?: number; }) { const calls = { paymentInvoiceCreate: [] as Array>, paymentInvoiceFindFirst: [] as Array>, paymentInvoiceExpireUpdateMany: [] as Array>, subscriptionBillingLocks: [] as string[], }; const transaction = { $queryRaw: async (strings: TemplateStringsArray, subscriptionId: string) => { if (strings[0]?.includes('FROM "Subscription"')) { calls.subscriptionBillingLocks.push(subscriptionId); } return []; }, subscription: { findMany: async () => input.subscriptions, }, paymentInvoice: { findFirst: async ({ where, }: { where: { subscriptionId: string; createdAt: { gte: Date }; }; }) => { calls.paymentInvoiceFindFirst.push({ where }); const invoices = input.existingInvoicesBySubscriptionId?.[where.subscriptionId] ?? []; return ( invoices .filter((invoice) => invoice.createdAt >= where.createdAt.gte) .sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())[0] ?? null ); }, create: async ({ data }: { data: Record }) => { calls.paymentInvoiceCreate.push({ data }); return { id: "invoice_created_1", subscriptionId: data.subscriptionId as string, provider: data.provider as string, providerInvoiceId: data.providerInvoiceId as string, status: "pending" as const, currency: data.currency as string, amountCrypto: new Prisma.Decimal(String(data.amountCrypto)), amountUsd: new Prisma.Decimal(String(data.amountUsd)), paymentAddress: data.paymentAddress as string, expiresAt: data.expiresAt as Date, paidAt: null, createdAt: new Date("2026-03-09T12:00:00.000Z"), updatedAt: new Date("2026-03-09T12:00:00.000Z"), }; }, updateMany: async ({ where, data, }: { where: { status: "pending"; expiresAt: { lte: Date } }; data: { status: "expired" }; }) => { calls.paymentInvoiceExpireUpdateMany.push({ where, data }); return { count: input.expirePendingCount ?? 0 }; }, }, }; const client = { subscription: transaction.subscription, paymentInvoice: transaction.paymentInvoice, $transaction: async (callback: (tx: typeof transaction) => Promise) => callback(transaction), } as unknown as Parameters[0]; return { client, calls, }; } function createRenewalSubscriptionFixture(input: { id: string; currentPeriodStart: Date; currentPeriodEnd: Date; }) { return { id: input.id, userId: `user_${input.id}`, planId: "plan_1", status: "active" as const, renewsManually: true, activatedAt: input.currentPeriodStart, currentPeriodStart: input.currentPeriodStart, currentPeriodEnd: input.currentPeriodEnd, canceledAt: null, createdAt: input.currentPeriodStart, updatedAt: input.currentPeriodStart, user: { id: `user_${input.id}`, email: `user_${input.id}@example.com`, }, plan: { id: "plan_1", code: "monthly", displayName: "Monthly", monthlyRequestLimit: 100, monthlyPriceUsd: new Prisma.Decimal("29"), billingCurrency: "USDT", }, }; }