feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -86,6 +86,10 @@ describe("resource router", () => {
|
||||
valueScoreBreakdown: null,
|
||||
valueScoreUpdatedAt: null,
|
||||
userId: null,
|
||||
countryId: "country_de",
|
||||
metroCityId: null,
|
||||
country: { code: "DE" },
|
||||
metroCity: null,
|
||||
};
|
||||
const db = {
|
||||
resource: {
|
||||
@@ -158,6 +162,165 @@ describe("resource router", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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: {
|
||||
@@ -314,6 +477,84 @@ describe("resource router", () => {
|
||||
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("applies country filters including explicit no-country toggle", async () => {
|
||||
const db = {
|
||||
resource: {
|
||||
|
||||
Reference in New Issue
Block a user