ledgerone_backend/test/planning.service.spec.ts

112 lines
4.0 KiB
TypeScript

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 })],
});
});
});