import { afterAll, beforeEach, describe, expect, it } from "vitest"; import { prisma, ensureUser, resetUser, U, closePrisma } from "./helpers"; import { processAutoPayments, type PaymentSchedule } from "../src/jobs/auto-payments"; describe("processAutoPayments", () => { beforeEach(async () => { await resetUser(U); await ensureUser(U); }); afterAll(async () => { await closePrisma(); }); it("processes auto-payment for fully funded monthly plan", async () => { const paymentSchedule: PaymentSchedule = { frequency: "monthly", dayOfMonth: 1, minFundingPercent: 100, }; const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Rent Auto-Pay", totalCents: 120000n, // $1,200 fundedCents: 120000n, // Fully funded currentFundedCents: 120000n, // Set current funding priority: 10, cycleStart: new Date("2025-01-01T00:00:00Z"), dueOn: new Date("2025-01-01T00:00:00Z"), periodDays: 30, autoPayEnabled: true, paymentSchedule, nextPaymentDate: new Date("2025-01-01T09:00:00Z"), // Payment due }, select: { id: true }, }); // Process payments as of payment date const reports = await processAutoPayments(prisma, "2025-01-01T10:00:00Z", { dryRun: false }); expect(reports).toHaveLength(1); expect(reports[0].success).toBe(true); expect(reports[0].paymentAmountCents).toBe(120000); expect(reports[0].planId).toBe(plan.id); // Verify payment transaction was created const transaction = await prisma.transaction.findFirst({ where: { planId: plan.id, kind: "fixed_payment" }, }); expect(transaction).toBeTruthy(); expect(Number(transaction?.amountCents)).toBe(120000); expect(transaction?.note).toBe("Auto-payment (monthly)"); // Verify plan funding was reduced const updatedPlan = await prisma.fixedPlan.findUniqueOrThrow({ where: { id: plan.id }, }); expect(Number(updatedPlan.fundedCents)).toBe(0); expect(updatedPlan.lastAutoPayment).toBeTruthy(); expect(updatedPlan.nextPaymentDate).toBeTruthy(); // Next payment should be February 1st const nextPayment = updatedPlan.nextPaymentDate!; expect(nextPayment.getMonth()).toBe(1); // February (0-indexed) expect(nextPayment.getDate()).toBe(1); }); it("skips payment when funding is below minimum threshold", async () => { const paymentSchedule: PaymentSchedule = { frequency: "monthly", dayOfMonth: 1, minFundingPercent: 100, }; await prisma.fixedPlan.create({ data: { userId: U, name: "Under-funded Plan", totalCents: 100000n, // $1,000 fundedCents: 50000n, // Only 50% funded currentFundedCents: 50000n, // Set current funding priority: 10, cycleStart: new Date("2025-01-01T00:00:00Z"), dueOn: new Date("2025-01-01T00:00:00Z"), periodDays: 30, autoPayEnabled: true, paymentSchedule, nextPaymentDate: new Date("2025-01-01T09:00:00Z"), }, }); const reports = await processAutoPayments(prisma, "2025-01-01T10:00:00Z", { dryRun: false }); expect(reports).toHaveLength(1); expect(reports[0].success).toBe(false); expect(reports[0].error).toContain("Insufficient funding: 50.0% < 100%"); expect(reports[0].paymentAmountCents).toBe(0); // Verify no transaction was created const transaction = await prisma.transaction.findFirst({ where: { kind: "fixed_payment" }, }); expect(transaction).toBeNull(); }); it("processes weekly auto-payment", async () => { const paymentSchedule: PaymentSchedule = { frequency: "weekly", dayOfWeek: 1, // Monday minFundingPercent: 50, }; const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "Weekly Payment", totalCents: 50000n, // $500 fundedCents: 30000n, // 60% funded (above 50% minimum) currentFundedCents: 30000n, // Set current funding priority: 10, cycleStart: new Date("2025-01-06T00:00:00Z"), // Monday dueOn: new Date("2025-01-06T00:00:00Z"), periodDays: 7, autoPayEnabled: true, paymentSchedule, nextPaymentDate: new Date("2025-01-06T09:00:00Z"), // Monday 9 AM }, select: { id: true }, }); const reports = await processAutoPayments(prisma, "2025-01-06T10:00:00Z", { dryRun: false }); expect(reports).toHaveLength(1); expect(reports[0].success).toBe(true); expect(reports[0].paymentAmountCents).toBe(30000); // Full funded amount // Verify next payment is scheduled for next Monday const updatedPlan = await prisma.fixedPlan.findUniqueOrThrow({ where: { id: plan.id }, }); const nextPayment = updatedPlan.nextPaymentDate!; expect(nextPayment.getDay()).toBe(1); // Monday expect(nextPayment.getDate()).toBe(13); // Next Monday (Jan 13) }); it("processes daily auto-payment", async () => { const paymentSchedule: PaymentSchedule = { frequency: "daily", minFundingPercent: 25, }; await prisma.fixedPlan.create({ data: { userId: U, name: "Daily Payment", totalCents: 10000n, // $100 fundedCents: 3000n, // 30% funded (above 25% minimum) currentFundedCents: 3000n, // Set current funding priority: 10, cycleStart: new Date("2025-01-01T00:00:00Z"), dueOn: new Date("2025-01-01T00:00:00Z"), periodDays: 1, autoPayEnabled: true, paymentSchedule, nextPaymentDate: new Date("2025-01-01T09:00:00Z"), }, }); const reports = await processAutoPayments(prisma, "2025-01-01T10:00:00Z", { dryRun: false }); expect(reports).toHaveLength(1); expect(reports[0].success).toBe(true); expect(reports[0].paymentAmountCents).toBe(3000); }); it("handles multiple plans with different schedules", async () => { // Plan 1: Ready for payment await prisma.fixedPlan.create({ data: { userId: U, name: "Ready Plan", totalCents: 50000n, fundedCents: 50000n, currentFundedCents: 50000n, priority: 10, cycleStart: new Date("2025-01-01T00:00:00Z"), dueOn: new Date("2025-01-01T00:00:00Z"), periodDays: 30, autoPayEnabled: true, paymentSchedule: { frequency: "monthly", minFundingPercent: 100 }, nextPaymentDate: new Date("2025-01-01T09:00:00Z"), }, }); // Plan 2: Not ready (insufficient funding) await prisma.fixedPlan.create({ data: { userId: U, name: "Not Ready Plan", totalCents: 100000n, fundedCents: 30000n, // Only 30% funded currentFundedCents: 30000n, priority: 20, cycleStart: new Date("2025-01-01T00:00:00Z"), dueOn: new Date("2025-01-01T00:00:00Z"), periodDays: 30, autoPayEnabled: true, paymentSchedule: { frequency: "monthly", minFundingPercent: 100 }, nextPaymentDate: new Date("2025-01-01T09:00:00Z"), }, }); // Plan 3: Auto-pay disabled await prisma.fixedPlan.create({ data: { userId: U, name: "Disabled Plan", totalCents: 75000n, fundedCents: 75000n, currentFundedCents: 75000n, priority: 30, cycleStart: new Date("2025-01-01T00:00:00Z"), dueOn: new Date("2025-01-01T00:00:00Z"), periodDays: 30, autoPayEnabled: false, // Disabled nextPaymentDate: new Date("2025-01-01T09:00:00Z"), }, }); const reports = await processAutoPayments(prisma, "2025-01-01T10:00:00Z", { dryRun: false }); // Should only process the first two plans (third is disabled) expect(reports).toHaveLength(2); const successfulPayments = reports.filter(r => r.success); const failedPayments = reports.filter(r => !r.success); expect(successfulPayments).toHaveLength(1); expect(failedPayments).toHaveLength(1); expect(successfulPayments[0].name).toBe("Ready Plan"); expect(failedPayments[0].name).toBe("Not Ready Plan"); // Verify only one transaction was created const transactions = await prisma.transaction.findMany({ where: { kind: "fixed_payment" }, }); expect(transactions).toHaveLength(1); }); it("handles dry run mode", async () => { await prisma.fixedPlan.create({ data: { userId: U, name: "Dry Run Plan", totalCents: 100000n, fundedCents: 100000n, currentFundedCents: 100000n, priority: 10, cycleStart: new Date("2025-01-01T00:00:00Z"), dueOn: new Date("2025-01-01T00:00:00Z"), periodDays: 30, autoPayEnabled: true, paymentSchedule: { frequency: "monthly", minFundingPercent: 100 }, nextPaymentDate: new Date("2025-01-01T09:00:00Z"), }, }); // Run in dry-run mode const reports = await processAutoPayments(prisma, "2025-01-01T10:00:00Z", { dryRun: true }); expect(reports).toHaveLength(1); expect(reports[0].success).toBe(true); expect(reports[0].paymentAmountCents).toBe(100000); // Verify no transaction was created in dry-run mode const transaction = await prisma.transaction.findFirst({ where: { kind: "fixed_payment" }, }); expect(transaction).toBeNull(); // Verify plan was not modified in dry-run mode const plan = await prisma.fixedPlan.findFirst({ where: { name: "Dry Run Plan" }, }); expect(Number(plan?.fundedCents)).toBe(100000); // Still fully funded expect(plan?.lastAutoPayment).toBeNull(); // Not updated }); it("calculates next payment dates correctly for end-of-month scenarios", async () => { const paymentSchedule: PaymentSchedule = { frequency: "monthly", dayOfMonth: 31, // End of month minFundingPercent: 100, }; const plan = await prisma.fixedPlan.create({ data: { userId: U, name: "End of Month Plan", totalCents: 100000n, fundedCents: 100000n, currentFundedCents: 100000n, priority: 10, cycleStart: new Date("2025-01-31T00:00:00Z"), // January 31st dueOn: new Date("2025-01-31T00:00:00Z"), periodDays: 30, autoPayEnabled: true, paymentSchedule, nextPaymentDate: new Date("2025-01-31T09:00:00Z"), }, select: { id: true }, }); const reports = await processAutoPayments(prisma, "2025-01-31T10:00:00Z", { dryRun: false }); expect(reports).toHaveLength(1); expect(reports[0].success).toBe(true); // Verify next payment is February 28th (since Feb doesn't have 31 days) const updatedPlan = await prisma.fixedPlan.findUniqueOrThrow({ where: { id: plan.id }, }); const nextPayment = updatedPlan.nextPaymentDate!; expect(nextPayment.getMonth()).toBe(1); // February expect(nextPayment.getDate()).toBe(28); // February 28th (not 31st) }); });