feat: add advisor household roles

This commit is contained in:
MOHAN 2026-07-16 18:11:04 +05:30
parent aeb23530cd
commit cce14fa5c6
3 changed files with 61 additions and 2 deletions

View File

@ -1,6 +1,6 @@
import { IsIn, IsOptional } from "class-validator";
export const HOUSEHOLD_ROLES = ["owner", "admin", "member", "viewer"] as const;
export const HOUSEHOLD_ROLES = ["owner", "admin", "member", "viewer", "accountant", "advisor"] as const;
export const HOUSEHOLD_MEMBER_STATUSES = ["active", "inactive", "removed"] as const;
export type HouseholdRole = typeof HOUSEHOLD_ROLES[number];

View File

@ -212,6 +212,7 @@ export class HouseholdsService {
id: member.id,
userId: member.userId,
role: member.role,
status: member.status,
joinedAt: member.joinedAt,
user: member.user,
})),
@ -464,7 +465,7 @@ export class HouseholdsService {
const email = payload.email.trim().toLowerCase();
const role = payload.role ?? "member";
if (role === "owner") {
throw new BadRequestException("Invite partners as admin, member, or viewer. Promote owners after acceptance.");
throw new BadRequestException("Invite collaborators as admin, member, viewer, accountant, or advisor. Promote owners after acceptance.");
}
const token = crypto.randomBytes(32).toString("base64url");

View File

@ -239,6 +239,29 @@ describe("HouseholdsService", () => {
});
});
it("allows managers to assign accountant and advisor collaborator roles", async () => {
const { prisma, service } = createService();
prisma.householdMember.findFirst
.mockResolvedValueOnce({ id: "owner_member", userId: "user_1", role: "owner", status: "active" })
.mockResolvedValueOnce({ id: "member_2", userId: "user_2", role: "viewer", status: "active" });
prisma.householdMember.update.mockResolvedValue({
id: "member_2",
userId: "user_2",
role: "advisor",
status: "active",
});
prisma.auditLog.create.mockResolvedValue({});
const result = await service.updateMember("user_1", "household_1", "member_2", { role: "advisor" });
expect(result.role).toBe("advisor");
expect(prisma.householdMember.update).toHaveBeenCalledWith({
where: { id: "member_2" },
data: { role: "advisor" },
include: expect.any(Object),
});
});
it("lists shared household goals for active members", async () => {
const { prisma, service } = createService();
prisma.householdMember.findFirst.mockResolvedValue({ id: "member_1", userId: "user_1", role: "member", status: "active" });
@ -444,6 +467,41 @@ describe("HouseholdsService", () => {
});
});
it("creates accountant collaborator invites for household managers", async () => {
const { prisma, email, service } = createService();
prisma.householdMember.findFirst.mockResolvedValue({ id: "owner_member", userId: "user_1", role: "owner", status: "active" });
prisma.household.findFirst.mockResolvedValue({ id: "household_1", name: "Mohan Household" });
prisma.user.findUnique.mockResolvedValue({ email: "owner@example.com", fullName: "Owner User" });
prisma.householdInvite.create.mockResolvedValue({
id: "invite_2",
email: "cpa@example.com",
role: "accountant",
status: "pending",
expiresAt: new Date("2026-07-23T00:00:00.000Z"),
createdAt: new Date("2026-07-16T00:00:00.000Z"),
});
prisma.auditLog.create.mockResolvedValue({});
const result = await service.invite("user_1", "household_1", {
email: " CPA@Example.com ",
role: "accountant",
});
expect(result.role).toBe("accountant");
expect(prisma.householdInvite.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
email: "cpa@example.com",
role: "accountant",
}),
}));
expect(email.sendHouseholdInviteEmail).toHaveBeenCalledWith(
"cpa@example.com",
"Mohan Household",
"Owner User",
expect.any(String),
);
});
it("accepts a pending invite for the matching email", async () => {
const { prisma, service } = createService();
const expiresAt = new Date(Date.now() + 60_000);