import { BadRequestException } from "@nestjs/common"; import { PlanningService } from "../src/planning/planning.service"; const model = () => ({ findMany: jest.fn(), findFirst: jest.fn(), create: jest.fn(), update: jest.fn(), }); const createService = () => { const prisma = { householdMember: model(), householdBudget: model(), personalGoal: model(), investmentHolding: model(), netWorthSnapshot: model(), recurringTransaction: model(), account: model(), transactionRaw: model(), auditLog: { create: jest.fn() }, }; const notifications = { notifyUser: jest.fn() }; const service = new PlanningService(prisma as any, notifications as any); return { service, prisma, notifications }; }; describe("PlanningService", () => { it("creates shared household budgets for household managers", async () => { const { service, prisma } = createService(); prisma.householdMember.findFirst.mockResolvedValue({ role: "owner" }); prisma.householdBudget.create.mockResolvedValue({ id: "budget_1", householdId: "household_1", limitAmount: 1000, spentAmount: 250, }); await expect(service.createBudget("user_1", { householdId: "household_1", name: "Groceries", limitAmount: 1000, spentAmount: 250, })).resolves.toMatchObject({ id: "budget_1", progressPercent: 25, remainingAmount: 750, }); expect(prisma.householdBudget.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ householdId: "household_1", createdByUserId: "user_1" }), })); }); it("blocks budget management for non-managers", async () => { const { service, prisma } = createService(); prisma.householdMember.findFirst.mockResolvedValue({ role: "member" }); await expect(service.createBudget("user_1", { householdId: "household_1", name: "Groceries", limitAmount: 1000, })).rejects.toBeInstanceOf(BadRequestException); }); it("creates personal goals with progress", async () => { const { service, prisma } = createService(); prisma.personalGoal.create.mockResolvedValue({ id: "goal_1", targetAmount: 5000, currentAmount: 1250, }); await expect(service.createPersonalGoal("user_1", { name: "Emergency fund", targetAmount: 5000, currentAmount: 1250, })).resolves.toMatchObject({ progressPercent: 25, remainingAmount: 3750 }); }); it("creates investment holdings and includes them in net worth", async () => { const { service, prisma } = createService(); prisma.investmentHolding.create.mockResolvedValue({ id: "holding_1", symbol: "VOO", marketValue: 1200 }); prisma.netWorthSnapshot.findMany.mockResolvedValue([]); prisma.account.findMany.mockResolvedValue([{ currentBalance: 800 }, { currentBalance: -200 }]); prisma.investmentHolding.findMany.mockResolvedValue([{ marketValue: 1200 }]); await service.createInvestment("user_1", { symbol: "voo", name: "Vanguard S&P 500", quantity: 4, price: 300 }); await expect(service.netWorthSummary("user_1")).resolves.toMatchObject({ computed: { assets: 2000, liabilities: 200, netWorth: 1800, }, }); }); it("detects monthly recurring transactions", async () => { const { service, prisma } = createService(); prisma.transactionRaw.findMany.mockResolvedValue([ { date: new Date("2026-01-01"), amount: -20, description: "Netflix", account: { isoCurrencyCode: "USD" } }, { date: new Date("2026-02-01"), amount: -20, description: "Netflix", account: { isoCurrencyCode: "USD" } }, { date: new Date("2026-03-01"), amount: -22, description: "Netflix", account: { isoCurrencyCode: "USD" } }, ]); prisma.recurringTransaction.create.mockImplementation(async ({ data }) => ({ id: "rec_1", ...data })); await expect(service.detectRecurring("user_1")).resolves.toMatchObject({ detected: 1, recurring: [expect.objectContaining({ merchant: "netflix", cadence: "monthly", occurrenceCount: 3 })], }); }); });