Refs #9 ## Summary - add a worker-side renewal invoice sweep that creates one invoice 72 hours before subscription expiry - expire elapsed pending invoices automatically and email users when an automatic renewal invoice is created - stop auto-recreating invoices for the same paid cycle once any invoice already exists for that cycle - document the current renewal-invoice and pending-invoice expiry behavior ## Testing - built `infra/docker/web.Dockerfile` - ran `pnpm --filter @nproxy/db test` inside the built container - verified `@nproxy/db build` and `@nproxy/web build` during the image build - built `infra/docker/worker.Dockerfile` Co-authored-by: sirily <sirily@git.shararam.party> Reviewed-on: #20
687 lines
22 KiB
TypeScript
687 lines
22 KiB
TypeScript
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<typeof createInvoiceFixture>;
|
|
updateManyCount?: number;
|
|
invoiceAfterFailedTransition?: ReturnType<typeof createInvoiceFixture>;
|
|
}) {
|
|
const calls = {
|
|
paymentInvoiceUpdateMany: [] as Array<Record<string, unknown>>,
|
|
paymentInvoiceTerminalUpdateMany: [] as Array<Record<string, unknown>>,
|
|
subscriptionUpdate: [] as Array<Record<string, unknown>>,
|
|
usageLedgerCreate: [] as Array<Record<string, unknown>>,
|
|
adminAuditCreate: [] as Array<Record<string, any>>,
|
|
};
|
|
|
|
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<string, unknown> }) => {
|
|
calls.subscriptionUpdate.push({ data });
|
|
return currentInvoice.subscription;
|
|
},
|
|
},
|
|
usageLedgerEntry: {
|
|
create: async ({ data }: { data: Record<string, unknown> }) => {
|
|
calls.usageLedgerCreate.push({ data });
|
|
return data;
|
|
},
|
|
},
|
|
adminAuditLog: {
|
|
create: async ({ data }: { data: Record<string, unknown> }) => {
|
|
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 <T>(callback: (tx: typeof transaction) => Promise<T>) => callback(transaction),
|
|
} as unknown as Parameters<typeof createPrismaBillingStore>[0];
|
|
|
|
return {
|
|
client,
|
|
calls,
|
|
};
|
|
}
|
|
|
|
function createInvoiceFixture(input: {
|
|
id?: string;
|
|
status: "pending" | "paid" | "expired" | "canceled";
|
|
paidAt: Date | null;
|
|
createdAt?: Date;
|
|
subscription: ReturnType<typeof createSubscriptionFixture> | 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<ReturnType<typeof createRenewalSubscriptionFixture>>;
|
|
existingInvoicesBySubscriptionId?: Record<string, ReturnType<typeof createInvoiceFixture>[]>;
|
|
expirePendingCount?: number;
|
|
}) {
|
|
const calls = {
|
|
paymentInvoiceCreate: [] as Array<Record<string, unknown>>,
|
|
paymentInvoiceFindFirst: [] as Array<Record<string, unknown>>,
|
|
paymentInvoiceExpireUpdateMany: [] as Array<Record<string, unknown>>,
|
|
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<string, unknown> }) => {
|
|
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 <T>(callback: (tx: typeof transaction) => Promise<T>) => callback(transaction),
|
|
} as unknown as Parameters<typeof createPrismaBillingStore>[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",
|
|
},
|
|
};
|
|
}
|