Files
CapaKraken/packages/api/src/__tests__/resource-router.test.ts
T

1626 lines
46 KiB
TypeScript

import { PermissionKey, SystemRole } from "@capakraken/shared";
import { ResourceType } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@capakraken/application", async (importOriginal) => {
const actual = await importOriginal<typeof import("@capakraken/application")>();
return {
...actual,
isChargeabilityActualBooking: actual.isChargeabilityActualBooking,
isChargeabilityRelevantProject: actual.isChargeabilityRelevantProject,
listAssignmentBookings: vi.fn(),
recomputeResourceValueScores: vi.fn(),
};
});
import { listAssignmentBookings } from "@capakraken/application";
import { resourceRouter } from "../router/resource.js";
import { createCallerFactory } from "../trpc.js";
const createCaller = createCallerFactory(resourceRouter);
function createControllerCaller(db: Record<string, unknown>) {
return createCaller({
session: {
user: { email: "controller@example.com", name: "Controller", image: null, role: "CONTROLLER" },
expires: "2026-03-14T00:00:00.000Z",
},
db: db as never,
dbUser: {
id: "user_controller",
systemRole: SystemRole.CONTROLLER,
permissionOverrides: null,
},
});
}
function createProtectedCaller(db: Record<string, unknown>) {
return createCaller({
session: {
user: { email: "user@example.com", name: "User", image: null, role: "USER" },
expires: "2026-03-14T00:00:00.000Z",
},
db: db as never,
dbUser: {
id: "user_1",
systemRole: SystemRole.USER,
permissionOverrides: null,
},
});
}
function createProtectedCallerWithOverrides(
db: Record<string, unknown>,
overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null,
) {
return createCaller({
session: {
user: { email: "user@example.com", name: "User", image: null, role: "USER" },
expires: "2026-03-14T00:00:00.000Z",
},
db: db as never,
dbUser: {
id: "user_1",
systemRole: SystemRole.USER,
permissionOverrides: overrides,
},
});
}
describe("resource router", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("filters proposed utilization rows unless explicitly requested", async () => {
const resource = {
id: "resource_1",
eid: "E-001",
displayName: "Alice",
email: "alice@example.com",
chapter: "CGI",
lcrCents: 5000,
ucrCents: 9000,
currency: "EUR",
chargeabilityTarget: 80,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
skills: [],
dynamicFields: {},
blueprintId: null,
isActive: true,
createdAt: new Date("2026-03-01"),
updatedAt: new Date("2026-03-01"),
roleId: null,
portfolioUrl: null,
postalCode: null,
federalState: null,
valueScore: null,
valueScoreBreakdown: null,
valueScoreUpdatedAt: null,
userId: null,
countryId: "country_de",
metroCityId: null,
country: { code: "DE" },
metroCity: null,
};
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([resource]),
},
};
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_confirmed",
projectId: "project_1",
resourceId: "resource_1",
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-06T00:00:00.000Z"),
hoursPerDay: 4,
dailyCostCents: 0,
status: "CONFIRMED",
project: {
id: "project_1",
name: "Project 1",
shortCode: "P1",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" },
},
{
id: "assignment_proposed",
projectId: "project_2",
resourceId: "resource_1",
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-06T00:00:00.000Z"),
hoursPerDay: 4,
dailyCostCents: 0,
status: "PROPOSED",
project: {
id: "project_2",
name: "Project 2",
shortCode: "P2",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" },
},
]);
const caller = createControllerCaller(db);
const strict = await caller.listWithUtilization({
startDate: "2026-03-02T00:00:00.000Z",
endDate: "2026-03-08T00:00:00.000Z",
});
const withProposed = await caller.listWithUtilization({
startDate: "2026-03-02T00:00:00.000Z",
endDate: "2026-03-08T00:00:00.000Z",
includeProposed: true,
});
expect(strict[0]).toMatchObject({
bookingCount: 1,
bookedHours: 20,
utilizationPercent: 50,
});
expect(withProposed[0]).toMatchObject({
bookingCount: 2,
bookedHours: 40,
utilizationPercent: 100,
});
});
it("calculates utilization with regional holidays removed from available hours", async () => {
const resource = {
id: "resource_1",
eid: "E-001",
displayName: "Alice",
email: "alice@example.com",
chapter: "CGI",
lcrCents: 5000,
ucrCents: 9000,
currency: "EUR",
chargeabilityTarget: 80,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
skills: [],
dynamicFields: {},
blueprintId: null,
isActive: true,
createdAt: new Date("2026-03-01"),
updatedAt: new Date("2026-03-01"),
roleId: null,
portfolioUrl: null,
postalCode: null,
federalState: "BY",
countryId: "country_de",
metroCityId: null,
valueScore: null,
valueScoreBreakdown: null,
valueScoreUpdatedAt: null,
userId: null,
country: { code: "DE" },
metroCity: null,
};
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([resource]),
},
};
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_confirmed",
projectId: "project_1",
resourceId: "resource_1",
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-06T00:00:00.000Z"),
hoursPerDay: 8,
dailyCostCents: 0,
status: "CONFIRMED",
project: {
id: "project_1",
name: "Project 1",
shortCode: "P1",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" },
},
]);
const caller = createControllerCaller(db);
const result = await caller.listWithUtilization({
startDate: "2026-01-05T00:00:00.000Z",
endDate: "2026-01-06T00:00:00.000Z",
});
expect(result[0]).toMatchObject({
bookingCount: 1,
bookedHours: 8,
availableHours: 8,
utilizationPercent: 100,
});
});
it("shifts marketplace availability when a local holiday blocks today", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-06T10:00:00.000Z"));
try {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "resource_by",
displayName: "Bavaria Artist",
eid: "E-BY",
chapter: "CGI",
skills: [{ skill: "Houdini", proficiency: 5 }],
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "BY",
metroCityId: null,
country: { code: "DE" },
metroCity: null,
},
{
id: "resource_hh",
displayName: "Hamburg Artist",
eid: "E-HH",
chapter: "CGI",
skills: [{ skill: "Houdini", proficiency: 5 }],
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "HH",
metroCityId: null,
country: { code: "DE" },
metroCity: null,
},
]),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
demandRequirement: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const caller = createControllerCaller(db);
const result = await caller.getSkillMarketplace({
searchSkill: "houdini",
availableOnly: true,
});
const bavaria = result.searchResults.find((resource) => resource.id === "resource_by");
const hamburg = result.searchResults.find((resource) => resource.id === "resource_hh");
expect(bavaria?.availableFrom).toBe("2026-01-07T00:00:00.000Z");
expect(hamburg?.availableFrom).toBe("2026-01-06T00:00:00.000Z");
} finally {
vi.useRealTimers();
}
});
it("uses a composite displayName/id cursor for stable pagination", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{ id: "2", displayName: "Alex", eid: "E-002", email: "alex2@example.com" },
{ id: "3", displayName: "Bea", eid: "E-003", email: "bea@example.com" },
]),
count: vi.fn().mockResolvedValue(3),
},
};
const caller = createControllerCaller(db);
const result = await caller.listStaff({
limit: 1,
cursor: JSON.stringify({ displayName: "Alex", id: "1" }),
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
AND: expect.arrayContaining([
{
OR: [
{ displayName: { gt: "Alex" } },
{ displayName: "Alex", id: { gt: "1" } },
],
},
]),
}),
orderBy: [{ displayName: "asc" }, { id: "asc" }],
take: 2,
}),
);
expect(result.nextCursor).toBe(JSON.stringify({ displayName: "Alex", id: "2" }));
});
it("resolves resource ownership server-side without exposing linked user email", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "resource_1" }),
findUnique: vi.fn().mockResolvedValue({
id: "resource_1",
eid: "E-001",
displayName: "Alice",
email: "alice@example.com",
chapter: "CGI",
lcrCents: 5000,
ucrCents: 9000,
currency: "EUR",
chargeabilityTarget: 80,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
skills: [],
dynamicFields: {},
blueprint: null,
blueprintId: null,
isActive: true,
createdAt: new Date("2026-03-01"),
updatedAt: new Date("2026-03-01"),
resourceRoles: [],
areaRole: null,
portfolioUrl: null,
roleId: null,
aiSummary: null,
aiSummaryUpdatedAt: null,
skillMatrixUpdatedAt: null,
valueScore: null,
valueScoreBreakdown: null,
valueScoreUpdatedAt: null,
userId: "user_1",
}),
findMany: vi.fn().mockResolvedValue([]),
},
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getById({ id: "resource_1" });
expect(result).toMatchObject({
id: "resource_1",
displayName: "Alice",
eid: "E-001",
email: "alice@example.com",
isOwnedByCurrentUser: true,
});
expect(result).not.toHaveProperty("user");
expect(db.resource.findUnique).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.not.objectContaining({
user: expect.anything(),
}),
}),
);
});
it("counts imported TBD draft projects in chargeability stats only when proposed work is enabled", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "resource_1",
eid: "E-001",
displayName: "Alice",
chapter: "CGI",
chargeabilityTarget: 80,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
},
]),
},
};
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_tbd",
projectId: "project_tbd",
resourceId: "resource_1",
startDate: new Date("2026-03-02T00:00:00.000Z"),
endDate: new Date("2026-03-06T00:00:00.000Z"),
hoursPerDay: 4,
dailyCostCents: 0,
status: "PROPOSED",
project: {
id: "project_tbd",
name: "TBD Project",
shortCode: "TBD-P1",
status: "DRAFT",
orderType: "CLIENT",
dynamicFields: { dispoImport: { isTbd: true } },
},
resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" },
},
]);
const caller = createControllerCaller(db);
const strict = await caller.getChargeabilityStats({});
const withProposed = await caller.getChargeabilityStats({ includeProposed: true });
expect(strict[0]?.actualChargeability).toBe(0);
expect(strict[0]?.expectedChargeability).toBeGreaterThan(0);
expect(withProposed[0]?.actualChargeability).toBeGreaterThan(strict[0]?.actualChargeability ?? 0);
expect(withProposed[0]?.expectedChargeability).toBe(strict[0]?.expectedChargeability);
});
it("excludes regional public holidays from chargeability stats", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "resource_by",
eid: "E-BY",
displayName: "Bavaria",
chapter: "CGI",
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "BY",
metroCityId: "city_munich",
country: { code: "DE" },
metroCity: { name: "Munich" },
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
},
]),
},
};
vi.mocked(listAssignmentBookings).mockResolvedValue([
{
id: "assignment_holiday",
projectId: "project_1",
resourceId: "resource_by",
startDate: new Date("2026-01-06T00:00:00.000Z"),
endDate: new Date("2026-01-06T00:00:00.000Z"),
hoursPerDay: 8,
dailyCostCents: 0,
status: "CONFIRMED",
project: {
id: "project_1",
name: "Project 1",
shortCode: "P1",
status: "ACTIVE",
orderType: "CLIENT",
dynamicFields: null,
},
resource: { id: "resource_by", displayName: "Bavaria", chapter: "CGI" },
},
]);
const RealDate = Date;
class MockDate extends Date {
constructor(...args: ConstructorParameters<typeof Date>) {
if (args.length === 0) {
super("2026-01-15T00:00:00.000Z");
return;
}
super(...args);
}
static now() {
return new RealDate("2026-01-15T00:00:00.000Z").getTime();
}
}
vi.stubGlobal("Date", MockDate);
try {
const caller = createControllerCaller(db);
const result = await caller.getChargeabilityStats({});
expect(result[0]).toMatchObject({
actualChargeability: 0,
expectedChargeability: 0,
availableHours: 168,
});
} finally {
vi.unstubAllGlobals();
}
});
it("returns a holiday-aware chargeability summary readmodel", async () => {
const resourceRecord = {
id: "res_1",
displayName: "Bruce Banner",
eid: "bruce.banner",
fte: 1,
lcrCents: 5000,
chargeabilityTarget: 80,
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 },
countryId: "country_de",
federalState: "BY",
metroCityId: null,
country: { id: "country_de", code: "DE", name: "Deutschland", dailyWorkingHours: 8, scheduleRules: null },
metroCity: null,
managementLevelGroup: null,
};
const db = {
resource: {
findUniqueOrThrow: vi.fn().mockResolvedValue(resourceRecord),
},
assignment: {
findMany: vi.fn().mockResolvedValue([
{
id: "assign_1",
hoursPerDay: 8,
startDate: new Date("2026-01-05T00:00:00.000Z"),
endDate: new Date("2026-01-06T00:00:00.000Z"),
dailyCostCents: 40000,
status: "CONFIRMED",
project: {
id: "project_gamma",
name: "Gamma",
shortCode: "GAM",
orderType: "CLIENT",
utilizationCategory: { code: "Chg" },
},
},
]),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([]),
},
calculationRule: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const caller = createControllerCaller(db);
const result = await caller.getChargeabilitySummary({ resourceId: "res_1", month: "2026-01" });
expect(result.bookedHours).toBe(8);
expect(result.allocations).toEqual([expect.objectContaining({ hours: 8, project: "Gamma", code: "GAM" })]);
expect(result.baseWorkingDays).toBe(22);
expect(result.baseAvailableHours).toBe(176);
expect(result.availableHours).toBe(160);
expect(result.workingDays).toBe(20);
expect(result.targetHours).toBe(128);
expect(result.unassignedHours).toBe(152);
expect(result.locationContext.federalState).toBe("BY");
expect(result.holidaySummary).toEqual(
expect.objectContaining({
count: 2,
workdayCount: 2,
hoursDeduction: 16,
}),
);
expect(result.capacityBreakdown).toEqual(
expect.objectContaining({
formula: "baseAvailableHours - holidayHoursDeduction - absenceHoursDeduction = availableHours",
holidayHoursDeduction: 16,
absenceHoursDeduction: 0,
}),
);
});
it("allows regular users to read their own chargeability summary", async () => {
const resourceRecord = {
id: "res_own",
displayName: "Bruce Banner",
eid: "bruce.banner",
fte: 1,
lcrCents: 5000,
chargeabilityTarget: 80,
availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0 },
countryId: "country_de",
federalState: "BY",
metroCityId: null,
country: { id: "country_de", code: "DE", name: "Deutschland", dailyWorkingHours: 8, scheduleRules: null },
metroCity: null,
managementLevelGroup: null,
};
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
findUniqueOrThrow: vi.fn().mockResolvedValue(resourceRecord),
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([]),
},
calculationRule: {
findMany: vi.fn().mockResolvedValue([]),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getChargeabilitySummary({ resourceId: "res_own", month: "2026-01" });
expect(result.resource).toBe("Bruce Banner");
expect(db.resource.findFirst).toHaveBeenCalledWith({
where: { userId: "user_1" },
select: { id: true },
});
});
it("rejects chargeability summary access for foreign resources", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
findUniqueOrThrow: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.getChargeabilitySummary({ resourceId: "res_other", month: "2026-01" }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "You can only view chargeability details for your own resource unless you have staff access",
});
expect(db.resource.findUniqueOrThrow).not.toHaveBeenCalled();
});
it("requires explicit score permission for value scores", async () => {
const db = {
resource: {
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(caller.getValueScores({})).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Permission required: viewScores",
});
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("returns value scores when the caller has explicit score permission", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
eid: "E-001",
displayName: "Alice",
chapter: "CGI",
lcrCents: 5000,
valueScore: 93,
valueScoreBreakdown: { delivery: 50, scarcity: 43 },
valueScoreUpdatedAt: new Date("2026-03-01T00:00:00.000Z"),
},
]),
},
};
const caller = createProtectedCallerWithOverrides(db, { granted: [PermissionKey.VIEW_SCORES] });
const result = await caller.getValueScores({ limit: 25 });
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: "res_1",
displayName: "Alice",
valueScore: 93,
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { isActive: true },
take: 25,
}),
);
});
it("rejects resource summary searches for regular users", async () => {
const db = {
resource: {
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.listSummaries({ search: "Alice", limit: 10 }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Resource overview access required",
});
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("allows resource summary searches with broad resource read permission", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
eid: "E-001",
displayName: "Alice",
chapter: "CGI",
isActive: true,
areaRole: { name: "Developer" },
country: { code: "DE", name: "Germany" },
metroCity: { name: "Munich" },
orgUnit: { name: "Studio A" },
},
]),
},
};
const caller = createProtectedCallerWithOverrides(db, { granted: [PermissionKey.VIEW_ALL_RESOURCES] });
const result = await caller.listSummaries({ search: "Ali", limit: 10 });
expect(result).toEqual([
{
id: "res_1",
eid: "E-001",
name: "Alice",
chapter: "CGI",
role: "Developer",
country: "Germany",
countryCode: "DE",
metroCity: "Munich",
orgUnit: "Studio A",
active: true,
},
]);
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: expect.arrayContaining([
{ displayName: { contains: "Ali", mode: "insensitive" } },
{ eid: { contains: "Ali", mode: "insensitive" } },
]),
}),
select: {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
areaRole: { select: { name: true } },
country: { select: { code: true, name: true } },
metroCity: { select: { name: true } },
orgUnit: { select: { name: true } },
},
take: 10,
}),
);
});
it("returns assistant-facing resource summary details from the canonical router", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
eid: "E-001",
displayName: "Alice",
chapter: "CGI",
fte: 1,
lcrCents: 5000,
chargeabilityTarget: 80,
isActive: true,
areaRole: { name: "Developer" },
country: { code: "DE", name: "Germany" },
metroCity: { name: "Munich" },
orgUnit: { name: "Studio A" },
},
]),
},
};
const caller = createProtectedCallerWithOverrides(db, { granted: [PermissionKey.VIEW_ALL_RESOURCES] });
const result = await caller.listSummariesDetail({ search: "Ali", limit: 10 });
expect(result).toEqual([
{
id: "res_1",
eid: "E-001",
name: "Alice",
chapter: "CGI",
role: "Developer",
country: "Germany",
countryCode: "DE",
metroCity: "Munich",
orgUnit: "Studio A",
fte: 1,
lcr: "50,00 EUR",
chargeabilityTarget: "80%",
active: true,
},
]);
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
select: expect.objectContaining({
fte: true,
lcrCents: true,
chargeabilityTarget: true,
}),
}),
);
});
it("returns only safe directory fields for generic resource lookups", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_1",
eid: "E-001",
displayName: "Alice",
chapter: "CGI",
isActive: true,
},
]),
count: vi.fn().mockResolvedValue(1),
},
};
const caller = createProtectedCaller(db);
const result = await caller.directory({ limit: 10 });
expect(result.resources).toHaveLength(1);
expect(result.resources[0]).toEqual({
id: "res_1",
eid: "E-001",
displayName: "Alice",
chapter: "CGI",
isActive: true,
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
select: {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
},
}),
);
});
it("does not include email search in the safe resource directory", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createProtectedCaller(db);
await caller.directory({ search: "alice@example.com", limit: 25 });
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{
OR: [
{ displayName: { contains: "alice@example.com", mode: "insensitive" } },
{ eid: { contains: "alice@example.com", mode: "insensitive" } },
],
},
]),
},
}),
);
});
it("rejects staff resource lists for regular users", async () => {
const db = {
resource: {
findMany: vi.fn(),
count: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.listStaff({ limit: 10 }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Resource overview access required",
});
expect(db.resource.findMany).not.toHaveBeenCalled();
expect(db.resource.count).not.toHaveBeenCalled();
});
it("includes email search in explicit staff resource lists", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({ search: "alice@example.com", limit: 25 });
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{
OR: [
{ displayName: { contains: "alice@example.com", mode: "insensitive" } },
{ eid: { contains: "alice@example.com", mode: "insensitive" } },
{ email: { contains: "alice@example.com", mode: "insensitive" } },
],
},
]),
},
}),
);
});
it("returns sensitive fields and optional roles in explicit staff resource lists", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_other",
eid: "E-OTHER",
displayName: "Bob",
email: "bob@example.com",
chapter: "CGI",
lcrCents: 7000,
ucrCents: 12000,
currency: "EUR",
roleId: "role_artist",
federalState: "BY",
dynamicFields: { level: "senior" },
resourceRoles: [
{
roleId: "role_artist",
isPrimary: true,
role: { id: "role_artist", name: "Artist", color: "#123456" },
},
],
},
]),
count: vi.fn().mockResolvedValue(1),
},
};
const caller = createControllerCaller(db);
const result = await caller.listStaff({ limit: 10, includeRoles: true });
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.objectContaining({
resourceRoles: expect.objectContaining({
include: expect.any(Object),
}),
}),
}),
);
expect(result.resources[0]).toMatchObject({
id: "res_other",
email: "bob@example.com",
lcrCents: 7000,
ucrCents: 12000,
currency: "EUR",
roleId: "role_artist",
federalState: "BY",
dynamicFields: { level: "senior" },
resourceRoles: [
expect.objectContaining({
roleId: "role_artist",
isPrimary: true,
role: expect.objectContaining({ id: "role_artist", name: "Artist" }),
}),
],
});
});
it("allows exact self lookup via getByIdentifier without broad search", async () => {
const db = {
resource: {
findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(null),
findFirst: vi.fn()
.mockResolvedValueOnce({
id: "res_own",
eid: "E-OWN",
displayName: "Alice Example",
chapter: "CGI",
isActive: true,
})
.mockResolvedValueOnce({ id: "res_own" }),
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getByIdentifier({ identifier: "Alice Example" });
expect(result).toEqual({
id: "res_own",
eid: "E-OWN",
displayName: "Alice Example",
chapter: "CGI",
isActive: true,
});
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("returns assistant-facing resource details from the canonical router", async () => {
const db = {
resource: {
findUnique: vi.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: "res_own",
eid: "E-OWN",
displayName: "Alice Example",
chapter: "CGI",
isActive: true,
})
.mockResolvedValueOnce({
id: "res_own",
eid: "E-OWN",
displayName: "Alice Example",
email: "alice@example.com",
chapter: "CGI",
fte: 1,
lcrCents: 5000,
ucrCents: 9000,
chargeabilityTarget: 80,
isActive: true,
availability: {},
skills: [{ name: "Houdini", level: 5 }],
postalCode: "80331",
federalState: "BY",
areaRole: { name: "Developer", color: "#123456" },
country: { code: "DE", name: "Germany", dailyWorkingHours: 8 },
metroCity: { name: "Munich" },
managementLevelGroup: { name: "Senior", targetPercentage: 75 },
orgUnit: { name: "Studio A", level: 5 },
_count: { assignments: 4, vacations: 2 },
}),
findFirst: vi.fn().mockResolvedValueOnce({ id: "res_own" }),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getByIdentifierDetail({ identifier: "E-OWN" });
expect(result).toEqual({
id: "res_own",
eid: "E-OWN",
name: "Alice Example",
email: "alice@example.com",
chapter: "CGI",
role: "Developer",
country: "Germany",
countryCode: "DE",
countryHours: 8,
metroCity: "Munich",
fte: 1,
lcr: "50,00 EUR",
ucr: "90,00 EUR",
chargeabilityTarget: "80%",
managementLevel: "Senior",
orgUnit: "Studio A",
postalCode: "80331",
federalState: "BY",
active: true,
totalAssignments: 4,
totalVacations: 2,
skillCount: 1,
topSkills: ["Houdini (5)"],
});
});
it("rejects foreign identifier lookups for regular users", async () => {
const db = {
resource: {
findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(null),
findFirst: vi.fn()
.mockResolvedValueOnce({
id: "res_other",
eid: "E-OTHER",
displayName: "Bob Other",
chapter: "CGI",
isActive: true,
})
.mockResolvedValueOnce({ id: "res_own" }),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.getByIdentifier({ identifier: "Bob Other" }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "You can only view your own resource unless you have staff access",
});
});
it("does not return fuzzy identifier suggestions to regular users", async () => {
const db = {
resource: {
findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(null),
findFirst: vi.fn().mockResolvedValueOnce(null),
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getByIdentifier({ identifier: "Ali" });
expect(result).toEqual({ error: "Resource not found: Ali" });
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("rejects foreign EID lookups for regular users", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValue({ id: "res_own" }),
findUnique: vi.fn().mockResolvedValue({
id: "res_other",
eid: "E-OTHER",
displayName: "Bob Other",
}),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.getByEid({ eid: "E-OTHER" }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "You can only view your own resource unless you have staff access",
});
});
it("rejects foreign resolveByIdentifier lookups for regular users", async () => {
const db = {
resource: {
findFirst: vi.fn()
.mockResolvedValueOnce({
id: "res_other",
eid: "E-OTHER",
displayName: "Bob Other",
chapter: "CGI",
isActive: true,
})
.mockResolvedValueOnce({ id: "res_own" }),
findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(null),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.resolveByIdentifier({ identifier: "Bob Other" }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "You can only resolve your own resource unless you have staff access",
});
});
it("returns only identity-safe fields from resolveByIdentifier", async () => {
const db = {
resource: {
findUnique: vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({
id: "res_own",
eid: "E-OWN",
displayName: "Alice Example",
chapter: "CGI",
isActive: true,
}),
findFirst: vi.fn().mockResolvedValueOnce({ id: "res_own" }),
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
const result = await caller.resolveByIdentifier({ identifier: "E-OWN" });
expect(result).toEqual({
id: "res_own",
eid: "E-OWN",
displayName: "Alice Example",
chapter: "CGI",
isActive: true,
});
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("resolves responsible person names through the canonical resource search boundary", async () => {
const db = {
resource: {
findFirst: vi.fn().mockResolvedValueOnce(null),
findMany: vi.fn().mockResolvedValue([
{ displayName: "Peter Parker", eid: "EMP-001" },
]),
},
};
const caller = createProtectedCallerWithOverrides(db, { granted: [PermissionKey.VIEW_ALL_RESOURCES] });
const result = await caller.resolveResponsiblePersonName({ name: "Peter" });
expect(result).toEqual({
status: "resolved",
displayName: "Peter Parker",
});
});
it("rejects responsible-person resolution for regular users without resource overview access", async () => {
const db = {
resource: {
findFirst: vi.fn(),
findMany: vi.fn(),
},
};
const caller = createProtectedCaller(db);
await expect(
caller.resolveResponsiblePersonName({ name: "Peter" }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Resource overview access required",
});
expect(db.resource.findFirst).not.toHaveBeenCalled();
expect(db.resource.findMany).not.toHaveBeenCalled();
});
it("applies country filters on the staff list including explicit no-country toggle", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({
countryIds: ["country_de", "country_us"],
includeWithoutCountry: false,
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{ countryId: { in: ["country_de", "country_us"] } },
]),
},
}),
);
});
it("excludes disabled countries on the staff list while leaving all others visible", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({
excludedCountryIds: ["country_fr"],
includeWithoutCountry: true,
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{ NOT: { countryId: { in: ["country_fr"] } } },
]),
},
}),
);
});
it("applies resource type filters on the staff list while keeping unspecified rows when requested", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({
resourceTypes: [ResourceType.EMPLOYEE, ResourceType.INTERN],
includeWithoutResourceType: true,
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{
OR: [
{ resourceType: { in: [ResourceType.EMPLOYEE, ResourceType.INTERN] } },
{ resourceType: null },
],
},
]),
},
}),
);
});
it("excludes disabled resource types on the staff list while leaving all others visible", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({
excludedResourceTypes: [ResourceType.FREELANCER],
includeWithoutResourceType: true,
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{ NOT: { resourceType: { in: [ResourceType.FREELANCER] } } },
]),
},
}),
);
});
it("applies rolled-off and departed filters on the staff list", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({
rolledOff: true,
departed: false,
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{ rolledOff: true },
{ departed: false },
]),
},
}),
);
});
it("applies multi-select chapter filters on the staff list", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({
chapters: ["Art Direction", "Project Management"],
});
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{ chapter: { in: ["Art Direction", "Project Management"] } },
]),
},
}),
);
});
it("supports stable anonymized identities and alias-based filtering on the staff list", async () => {
const resource = {
id: "resource_anon_1",
eid: "h.noerenberg",
displayName: "Hartmut Noerenberg",
email: "h.noerenberg@accenture.com",
chapter: "Art Direction",
lcrCents: 15000,
isActive: true,
createdAt: new Date("2026-03-01"),
updatedAt: new Date("2026-03-01"),
};
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue({
anonymizationEnabled: true,
anonymizationDomain: "superhartmut.de",
anonymizationSeed: null,
anonymizationMode: "global",
}),
},
resource: {
findMany: vi.fn().mockResolvedValue([resource]),
count: vi.fn(),
},
};
const caller = createControllerCaller(db);
const first = await caller.listStaff({ limit: 10 });
const alias = first.resources[0];
expect(alias).toBeDefined();
expect(alias?.displayName).not.toBe(resource.displayName);
expect(alias?.eid).not.toBe(resource.eid);
expect(alias?.email).toBe(`${alias?.eid}@superhartmut.de`);
const byAlias = await caller.listStaff({ eids: [alias!.eid], limit: 10 });
const byAliasSearch = await caller.listStaff({ search: alias!.displayName.slice(0, 4), limit: 10 });
expect(byAlias.resources).toHaveLength(1);
expect(byAlias.resources[0]?.id).toBe(resource.id);
expect(byAliasSearch.resources).toHaveLength(1);
expect(byAliasSearch.resources[0]?.id).toBe(resource.id);
});
it("includes email search for staff resource list lookups", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({ search: "alice@example.com", limit: 25 });
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
AND: expect.arrayContaining([
{ isActive: true },
{
OR: [
{ displayName: { contains: "alice@example.com", mode: "insensitive" } },
{ eid: { contains: "alice@example.com", mode: "insensitive" } },
{ email: { contains: "alice@example.com", mode: "insensitive" } },
],
},
]),
},
}),
);
});
it("keeps contact and cost fields intact in staff resource lists", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([
{
id: "res_other",
eid: "E-OTHER",
displayName: "Bob",
email: "bob@example.com",
lcrCents: 7000,
ucrCents: 12000,
chargeabilityTarget: 85,
valueScore: 88,
valueScoreBreakdown: { scarcity: 40 },
valueScoreUpdatedAt: new Date("2026-03-02T00:00:00.000Z"),
},
]),
count: vi.fn().mockResolvedValue(1),
},
};
const caller = createControllerCaller(db);
const result = await caller.listStaff({ limit: 10 });
expect(result.resources[0]).toMatchObject({
id: "res_other",
email: "bob@example.com",
lcrCents: 7000,
ucrCents: 12000,
chargeabilityTarget: 85,
valueScore: 88,
valueScoreBreakdown: { scarcity: 40 },
});
expect(result.resources[0]?.valueScoreUpdatedAt).toBeInstanceOf(Date);
});
it("keeps includeRoles available for the staff route", async () => {
const db = {
resource: {
findMany: vi.fn().mockResolvedValue([]),
count: vi.fn().mockResolvedValue(0),
},
};
const caller = createControllerCaller(db);
await caller.listStaff({ limit: 10, includeRoles: true });
expect(db.resource.findMany).toHaveBeenCalledWith(
expect.objectContaining({
include: expect.objectContaining({
resourceRoles: expect.objectContaining({
include: expect.any(Object),
}),
}),
}),
);
});
});