final touches for beta skymoney (at least i think)
This commit is contained in:
338
api/tests/auto-payments.test.ts
Normal file
338
api/tests/auto-payments.test.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
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)
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user