feat: add advisor household roles
This commit is contained in:
parent
aeb23530cd
commit
cce14fa5c6
@ -1,6 +1,6 @@
|
|||||||
import { IsIn, IsOptional } from "class-validator";
|
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 const HOUSEHOLD_MEMBER_STATUSES = ["active", "inactive", "removed"] as const;
|
||||||
|
|
||||||
export type HouseholdRole = typeof HOUSEHOLD_ROLES[number];
|
export type HouseholdRole = typeof HOUSEHOLD_ROLES[number];
|
||||||
|
|||||||
@ -212,6 +212,7 @@ export class HouseholdsService {
|
|||||||
id: member.id,
|
id: member.id,
|
||||||
userId: member.userId,
|
userId: member.userId,
|
||||||
role: member.role,
|
role: member.role,
|
||||||
|
status: member.status,
|
||||||
joinedAt: member.joinedAt,
|
joinedAt: member.joinedAt,
|
||||||
user: member.user,
|
user: member.user,
|
||||||
})),
|
})),
|
||||||
@ -464,7 +465,7 @@ export class HouseholdsService {
|
|||||||
const email = payload.email.trim().toLowerCase();
|
const email = payload.email.trim().toLowerCase();
|
||||||
const role = payload.role ?? "member";
|
const role = payload.role ?? "member";
|
||||||
if (role === "owner") {
|
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");
|
const token = crypto.randomBytes(32).toString("base64url");
|
||||||
|
|||||||
@ -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 () => {
|
it("lists shared household goals for active members", async () => {
|
||||||
const { prisma, service } = createService();
|
const { prisma, service } = createService();
|
||||||
prisma.householdMember.findFirst.mockResolvedValue({ id: "member_1", userId: "user_1", role: "member", status: "active" });
|
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 () => {
|
it("accepts a pending invite for the matching email", async () => {
|
||||||
const { prisma, service } = createService();
|
const { prisma, service } = createService();
|
||||||
const expiresAt = new Date(Date.now() + 60_000);
|
const expiresAt = new Date(Date.now() + 60_000);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user