From 7277e60691f32f2e437910878e9e63da7efa12eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 07:52:40 +0200 Subject: [PATCH] test(api): widen resource capacity edge coverage --- .../api/src/__tests__/resource-router.test.ts | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) diff --git a/packages/api/src/__tests__/resource-router.test.ts b/packages/api/src/__tests__/resource-router.test.ts index 59ad664..fe6967c 100644 --- a/packages/api/src/__tests__/resource-router.test.ts +++ b/packages/api/src/__tests__/resource-router.test.ts @@ -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>); + + 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 () => { vi.useFakeTimers(); 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 () => { const resourceRecord = { id: "res_1",