test(api): widen resource capacity edge coverage
This commit is contained in:
@@ -261,6 +261,118 @@ describe("resource router", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns an empty utilization result when no active resources match the chapter filter", async () => {
|
||||||
|
const db = {
|
||||||
|
resource: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(listAssignmentBookings).mockResolvedValue([]);
|
||||||
|
|
||||||
|
const caller = createControllerCaller(db);
|
||||||
|
const result = await caller.listWithUtilization({
|
||||||
|
chapter: "Rigging",
|
||||||
|
startDate: "2026-03-01T00:00:00.000Z",
|
||||||
|
endDate: "2026-03-31T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(db.resource.findMany).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
chapter: "Rigging",
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
orderBy: { displayName: "asc" },
|
||||||
|
}));
|
||||||
|
expect(listAssignmentBookings).toHaveBeenCalledWith(db, {
|
||||||
|
startDate: new Date("2026-03-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-03-31T00:00:00.000Z"),
|
||||||
|
resourceIds: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores orphaned utilization bookings without a resourceId", 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_orphaned",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: null,
|
||||||
|
startDate: new Date("2026-03-02T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-03-06T00:00:00.000Z"),
|
||||||
|
hoursPerDay: 8,
|
||||||
|
dailyCostCents: 0,
|
||||||
|
status: "CONFIRMED",
|
||||||
|
project: {
|
||||||
|
id: "project_1",
|
||||||
|
name: "Project 1",
|
||||||
|
shortCode: "P1",
|
||||||
|
status: "ACTIVE",
|
||||||
|
orderType: "CLIENT",
|
||||||
|
clientId: null,
|
||||||
|
dynamicFields: null,
|
||||||
|
},
|
||||||
|
resource: null,
|
||||||
|
},
|
||||||
|
] satisfies Awaited<ReturnType<typeof listAssignmentBookings>>);
|
||||||
|
|
||||||
|
const caller = createControllerCaller(db);
|
||||||
|
const result = await caller.listWithUtilization({
|
||||||
|
startDate: "2026-03-02T00:00:00.000Z",
|
||||||
|
endDate: "2026-03-08T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
id: "resource_1",
|
||||||
|
bookingCount: 0,
|
||||||
|
bookedHours: 0,
|
||||||
|
utilizationPercent: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("shifts marketplace availability when a local holiday blocks today", async () => {
|
it("shifts marketplace availability when a local holiday blocks today", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.setSystemTime(new Date("2026-01-06T10:00:00.000Z"));
|
vi.setSystemTime(new Date("2026-01-06T10:00:00.000Z"));
|
||||||
@@ -581,6 +693,190 @@ describe("resource router", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns zero chargeability when a resource has no working availability", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-03-15T12:00:00.000Z"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = {
|
||||||
|
resource: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "resource_zero",
|
||||||
|
eid: "E-000",
|
||||||
|
displayName: "No Capacity",
|
||||||
|
chapter: "CGI",
|
||||||
|
chargeabilityTarget: 80,
|
||||||
|
availability: {
|
||||||
|
monday: 0,
|
||||||
|
tuesday: 0,
|
||||||
|
wednesday: 0,
|
||||||
|
thursday: 0,
|
||||||
|
friday: 0,
|
||||||
|
saturday: 0,
|
||||||
|
sunday: 0,
|
||||||
|
},
|
||||||
|
countryId: "country_de",
|
||||||
|
federalState: null,
|
||||||
|
metroCityId: null,
|
||||||
|
country: { code: "DE" },
|
||||||
|
metroCity: null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(listAssignmentBookings).mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "assignment_zero",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_zero",
|
||||||
|
startDate: new Date("2026-03-02T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-03-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_zero", displayName: "No Capacity", chapter: "CGI" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const caller = createControllerCaller(db);
|
||||||
|
const result = await caller.getChargeabilityStats({});
|
||||||
|
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
id: "resource_zero",
|
||||||
|
actualChargeability: 0,
|
||||||
|
expectedChargeability: 0,
|
||||||
|
availableHours: 0,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty chargeability stats result when no active resources match", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-03-15T12:00:00.000Z"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = {
|
||||||
|
resource: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(listAssignmentBookings).mockResolvedValue([]);
|
||||||
|
|
||||||
|
const caller = createControllerCaller(db);
|
||||||
|
const result = await caller.getChargeabilityStats({ resourceId: "missing_resource" });
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(db.resource.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
id: "missing_resource",
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
eid: true,
|
||||||
|
displayName: true,
|
||||||
|
chapter: true,
|
||||||
|
chargeabilityTarget: true,
|
||||||
|
availability: true,
|
||||||
|
countryId: true,
|
||||||
|
federalState: true,
|
||||||
|
metroCityId: true,
|
||||||
|
country: { select: { code: true } },
|
||||||
|
metroCity: { select: { name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listAssignmentBookings).toHaveBeenCalledWith(db, expect.objectContaining({
|
||||||
|
resourceIds: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [, bookingQuery] = vi.mocked(listAssignmentBookings).mock.calls.at(-1)!;
|
||||||
|
expect(bookingQuery.startDate).toEqual(new Date(2026, 2, 1));
|
||||||
|
expect(bookingQuery.endDate).toEqual(new Date(2026, 3, 0));
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes the explicit resource filter through to chargeability stats reads", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-03-15T12:00:00.000Z"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = {
|
||||||
|
resource: {
|
||||||
|
findMany: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "resource_target",
|
||||||
|
eid: "E-TARGET",
|
||||||
|
displayName: "Target",
|
||||||
|
chapter: "CGI",
|
||||||
|
chargeabilityTarget: 80,
|
||||||
|
availability: {
|
||||||
|
monday: 8,
|
||||||
|
tuesday: 8,
|
||||||
|
wednesday: 8,
|
||||||
|
thursday: 8,
|
||||||
|
friday: 8,
|
||||||
|
},
|
||||||
|
countryId: "country_de",
|
||||||
|
federalState: null,
|
||||||
|
metroCityId: null,
|
||||||
|
country: { code: "DE" },
|
||||||
|
metroCity: null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(listAssignmentBookings).mockResolvedValue([]);
|
||||||
|
|
||||||
|
const caller = createControllerCaller(db);
|
||||||
|
await caller.getChargeabilityStats({ resourceId: "resource_target" });
|
||||||
|
|
||||||
|
expect(db.resource.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
id: "resource_target",
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
eid: true,
|
||||||
|
displayName: true,
|
||||||
|
chapter: true,
|
||||||
|
chargeabilityTarget: true,
|
||||||
|
availability: true,
|
||||||
|
countryId: true,
|
||||||
|
federalState: true,
|
||||||
|
metroCityId: true,
|
||||||
|
country: { select: { code: true } },
|
||||||
|
metroCity: { select: { name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(listAssignmentBookings).toHaveBeenCalledWith(db, expect.objectContaining({
|
||||||
|
resourceIds: ["resource_target"],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [, bookingQuery] = vi.mocked(listAssignmentBookings).mock.calls.at(-1)!;
|
||||||
|
expect(bookingQuery.startDate).toEqual(new Date(2026, 2, 1));
|
||||||
|
expect(bookingQuery.endDate).toEqual(new Date(2026, 3, 0));
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("returns a holiday-aware chargeability summary readmodel", async () => {
|
it("returns a holiday-aware chargeability summary readmodel", async () => {
|
||||||
const resourceRecord = {
|
const resourceRecord = {
|
||||||
id: "res_1",
|
id: "res_1",
|
||||||
|
|||||||
Reference in New Issue
Block a user