import { BadRequestException } from "@nestjs/common"; import { AccountsService } from "../src/accounts/accounts.service"; import { createPrismaMock } from "./utils/mock-prisma"; const createService = () => { const prisma = createPrismaMock(); const plaid = { createLinkToken: jest.fn(), syncBalancesForUser: jest.fn() }; const teller = { syncBalancesForUser: jest.fn() }; const opaqueIds = { encode: jest.fn((_kind: string, _userId: string, id: string) => `opaque_account_${id}`), decode: jest.fn((_kind: string, _userId: string, handle: string) => handle.replace("opaque_account_", "")), }; const planLimits = { assertCanAddAccounts: jest.fn() }; return { prisma, service: new AccountsService(prisma as any, plaid as any, teller as any, opaqueIds as any, planLimits as any), }; }; describe("AccountsService ownership", () => { it("marks an account as joint for an active household member", async () => { const { prisma, service } = createService(); prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true }); prisma.householdMember.findFirst.mockResolvedValue({ householdId: "household_1", userId: "user_1", status: "active" }); prisma.account.update.mockResolvedValue({ id: "acct_1", institutionName: "Bank", accountType: "checking", mask: "1234", householdId: "household_1", ownerUserId: null, ownershipType: "joint", isActive: true, createdAt: new Date("2026-07-16T00:00:00.000Z"), }); prisma.auditLog.create.mockResolvedValue({}); const result = await service.updateOwnership("user_1", "opaque_account_acct_1", { ownershipType: "joint", householdId: "household_1", }); expect(result.id).toBe("opaque_account_acct_1"); expect(prisma.account.update).toHaveBeenCalledWith({ where: { id: "acct_1" }, data: { ownershipType: "joint", householdId: "household_1", ownerUserId: null, }, select: expect.any(Object), }); expect(prisma.auditLog.create).toHaveBeenCalledWith({ data: expect.objectContaining({ action: "account.ownership.update", metadata: expect.objectContaining({ ownershipType: "joint", householdId: "household_1" }), }), }); }); it("marks an account as theirs only when the owner is another household member", async () => { const { prisma, service } = createService(); prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true }); prisma.householdMember.findFirst .mockResolvedValueOnce({ householdId: "household_1", userId: "user_1", status: "active" }) .mockResolvedValueOnce({ householdId: "household_1", userId: "user_2", status: "active" }); prisma.account.update.mockResolvedValue({ id: "acct_1", householdId: "household_1", ownerUserId: "user_2", ownershipType: "theirs", }); prisma.auditLog.create.mockResolvedValue({}); await service.updateOwnership("user_1", "opaque_account_acct_1", { ownershipType: "theirs", householdId: "household_1", ownerUserId: "user_2", }); expect(prisma.account.update).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ ownershipType: "theirs", ownerUserId: "user_2" }), })); }); it("blocks shared ownership without active household membership", async () => { const { prisma, service } = createService(); prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true }); prisma.householdMember.findFirst.mockResolvedValue(null); await expect(service.updateOwnership("user_1", "opaque_account_acct_1", { ownershipType: "joint", householdId: "household_1", })).rejects.toBeInstanceOf(BadRequestException); expect(prisma.account.update).not.toHaveBeenCalled(); }); it("resets household ownership when marked mine", async () => { const { prisma, service } = createService(); prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true }); prisma.account.update.mockResolvedValue({ id: "acct_1", householdId: null, ownerUserId: "user_1", ownershipType: "mine", }); prisma.auditLog.create.mockResolvedValue({}); await service.updateOwnership("user_1", "opaque_account_acct_1", { ownershipType: "mine" }); expect(prisma.account.update).toHaveBeenCalledWith(expect.objectContaining({ data: { ownershipType: "mine", householdId: null, ownerUserId: "user_1" }, })); }); });