import { fileURLToPath } from "node:url"; import { describe, expect, it, vi } from "vitest"; import { assessDispoImportReadiness, parseDispoChargeabilityWorkbook, parseDispoPlanningWorkbook, parseResourceRosterMasterWorkbook, parseDispoRosterWorkbook, parseMandatoryDispoReferenceWorkbook, stageDispoImportBatch, stageDispoChargeabilityResources, stageDispoPlanningData, stageDispoProjects, stageDispoRosterResources, stageDispoReferenceData, } from "../index.js"; const mandatoryWorkbookPath = fileURLToPath( new URL("../../../../samples/Dispov2/MandatoryDispoCategories_V3.xlsx", import.meta.url), ); const chargeabilityWorkbookPath = fileURLToPath( new URL( "../../../../samples/Dispov2/20260309_Bi-Weekly_Chargeability_Reporting_Content_Production_V0.943_4Hartmut.xlsx", import.meta.url, ), ); const planningWorkbookPath = fileURLToPath( new URL("../../../../samples/Dispov2/DISPO_2026.xlsx", import.meta.url), ); const rosterWorkbookPath = fileURLToPath( new URL("../../../../samples/Dispov2/MV_DispoRoster.xlsx", import.meta.url), ); const costWorkbookPath = fileURLToPath( new URL( "../../../../samples/Dispov2/Resource Roster_MASTER_FY26_CJ_20251201.xlsx", import.meta.url, ), ); describe("dispo import", () => { it("parses the mandatory reference workbook into normalized master data", async () => { const parsed = await parseMandatoryDispoReferenceWorkbook(mandatoryWorkbookPath); expect(parsed.countries).toEqual( expect.arrayContaining([ expect.objectContaining({ countryCode: "DE", name: "Germany", metroCities: expect.arrayContaining(["Munich", "Stuttgart"]), }), expect.objectContaining({ countryCode: "ES", name: "Spain", dailyWorkingHours: 8, }), ]), ); expect(parsed.orgUnits).toEqual( expect.arrayContaining([ expect.objectContaining({ level: 5, name: "Content Production", parentName: null }), expect.objectContaining({ level: 6, name: "CGI Content", parentName: "Content Production", }), expect.objectContaining({ level: 7, name: "Art Direction", parentName: "CGI Content" }), ]), ); expect(parsed.managementLevelGroups).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "Consultant", targetPercentage: 0.808, levels: expect.arrayContaining(["8-Associate Manager", "9-Team Lead/Consultant"]), }), ]), ); expect(parsed.clients).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "BMW", clientCode: "BMW", parentName: null }), expect.objectContaining({ name: "BMW AG", parentName: "BMW", parentClientCode: "BMW" }), ]), ); }); it("parses the chargeability workbook into deduplicated staged resources", async () => { const parsed = await parseDispoChargeabilityWorkbook(chargeabilityWorkbookPath); expect(parsed.resources.length).toBeGreaterThan(300); expect(parsed.unresolved).toEqual([]); expect(parsed.resources).toEqual( expect.arrayContaining([ expect.objectContaining({ canonicalExternalId: "a.kasperovich", chapter: "CGI Development", countryCode: "DE", chargeabilityTarget: 74.7, resourceType: "EMPLOYEE", }), expect.objectContaining({ canonicalExternalId: "alexander.broeckel", chapter: "Digital Content Production", chapterCode: "3D", roleTokens: ["3D"], }), ]), ); }); it("parses the planning workbook into staged assignments, vacations, availability rules, and unresolved project references", async () => { const parsed = await parseDispoPlanningWorkbook(planningWorkbookPath); expect(parsed.assignments.length).toBeGreaterThan(1000); expect(parsed.vacations.length).toBeGreaterThan(1000); expect(parsed.availabilityRules.length).toBeGreaterThan(0); expect(parsed.unresolved.length).toBeGreaterThan(0); expect(parsed.assignments).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceExternalId: "a.d.singh.sandhu", assignmentDate: new Date("2025-12-22T00:00:00.000Z"), hoursPerDay: 8, isInternal: false, isTbd: true, projectKey: null, utilizationCategoryCode: "Chg", winProbability: 80, }), ]), ); expect(parsed.vacations).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceExternalId: "samuel.bubat", startDate: new Date("2025-12-22T00:00:00.000Z"), endDate: new Date("2025-12-22T00:00:00.000Z"), vacationType: "ANNUAL", isHalfDay: false, }), expect.objectContaining({ resourceExternalId: "samuel.bubat", startDate: new Date("2025-12-24T00:00:00.000Z"), endDate: new Date("2025-12-24T00:00:00.000Z"), vacationType: "PUBLIC_HOLIDAY", isPublicHoliday: true, }), ]), ); expect(parsed.availabilityRules).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceExternalId: "marina.hechler", effectiveStartDate: new Date("2025-12-22T00:00:00.000Z"), availableHours: 6, percentage: 75, ruleType: "PART_TIME", }), ]), ); }); it("parses the roster workbook into merged resource master rows", async () => { const parsed = await parseDispoRosterWorkbook(rosterWorkbookPath, { costWorkbookPath }); expect(parsed.resources.length).toBeGreaterThan(500); expect(parsed.ignoredPseudoDemandRows).toBeGreaterThan(100); expect(parsed.excludedCanonicalExternalIds).toEqual( expect.arrayContaining(["antonia.melzer", "placeholder.hamburg"]), ); expect(parsed.resources).toEqual( expect.arrayContaining([ expect.objectContaining({ canonicalExternalId: "a.kasperovich", displayName: "Alexander Kasperovich", email: "a.kasperovich@accenture.com", lcrCents: 10892, rateResolution: "EXACT", ucrCents: 7261, chapter: "CGI-Dev", clientUnitName: "Cross-Unit", sourceSheet: "DispoRoster", }), expect.objectContaining({ canonicalExternalId: "alexander.broeckel", displayName: "Alex Bröckel", email: "alexander.broeckel@accenture.com", sourceSheet: "SAP_data", }), expect.objectContaining({ canonicalExternalId: "a.appelt", email: "a.appelt@accenture.com", rateResolution: "LEVEL_AVERAGE", rateResolutionLevel: "10-Senior Analyst", resourceType: "FREELANCER", roleTokens: ["2D"], }), ]), ); expect(parsed.resources.find((resource) => resource.canonicalExternalId === "antonia.melzer")).toBeUndefined(); }); it("parses the cost workbook into exact rates and level averages", async () => { const parsed = await parseResourceRosterMasterWorkbook(costWorkbookPath); expect(parsed.rates.get("a.kasperovich")).toEqual( expect.objectContaining({ canonicalExternalId: "a.kasperovich", lcrCents: 10892, ucrCents: 7261, level: "7-Manager", }), ); expect(parsed.levelAverages.get("10-Senior Analyst")).toEqual( expect.objectContaining({ level: "10-Senior Analyst", lcrCents: expect.any(Number), ucrCents: expect.any(Number), sampleCount: expect.any(Number), }), ); }); it("assesses import readiness against the merged workbook constraints", async () => { const report = await assessDispoImportReadiness({ referenceWorkbookPath: mandatoryWorkbookPath, chargeabilityWorkbookPath, planningWorkbookPath, rosterWorkbookPath, costWorkbookPath, }); expect(report.resourceCount).toBeGreaterThan(500); expect(report.canCommitWithStrictSourceData).toBe(false); expect(report.canCommitWithFallbacks).toBe(false); expect(report.issues.find((issue) => issue.code === "FALLBACK_EMAIL_REQUIRED")).toBeUndefined(); expect(report.issues.find((issue) => issue.code === "FALLBACK_LCR_REQUIRED")).toBeUndefined(); expect(report.issues.find((issue) => issue.code === "FALLBACK_UCR_REQUIRED")).toBeUndefined(); expect( report.issues.find((issue) => issue.code === "PLANNING_RESOURCE_MISSING_FROM_ROSTER"), ).toBeUndefined(); expect(report.issues).not.toEqual( expect.arrayContaining([ expect.objectContaining({ code: "REFERENCE_RESOURCE_MASTER_MISSING", }), ]), ); expect(report.issues).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "PUBLIC_HOLIDAY_IMPORT_REQUIRES_CALENDAR_SYNC", severity: "blocker", }), expect.objectContaining({ code: "UNRESOLVED_RECORDS_PRESENT", severity: "warning", }), ]), ); }); it("stages reference workbook clients and upserts master data", async () => { const db = { importBatch: { create: vi.fn().mockResolvedValue({ id: "batch_1", summary: {} }), findUnique: vi.fn(), update: vi.fn().mockResolvedValue({ id: "batch_1", summary: {} }), }, country: { upsert: vi.fn().mockResolvedValue({ id: "country_1" }), }, metroCity: { upsert: vi.fn().mockResolvedValue({ id: "city_1" }), }, orgUnit: { findFirst: vi.fn().mockResolvedValue(null), create: vi .fn() .mockResolvedValueOnce({ id: "org_root" }) .mockResolvedValue({ id: "org_child" }), update: vi.fn(), upsert: vi.fn().mockResolvedValue({ id: "org_upserted" }), }, managementLevelGroup: { upsert: vi.fn().mockResolvedValue({ id: "group_1" }), }, managementLevel: { upsert: vi.fn().mockResolvedValue({ id: "level_1" }), }, client: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({ id: "client_1" }), update: vi.fn(), }, stagedClient: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 10 }), }, stagedResource: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedUnresolvedRecord: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn(), }, }; const result = await stageDispoReferenceData(db as never, { referenceWorkbookPath: mandatoryWorkbookPath, }); expect(result.batchId).toBe("batch_1"); expect(result.counts.countries).toBeGreaterThan(0); expect(db.country.upsert).toHaveBeenCalled(); expect(db.stagedClient.createMany).toHaveBeenCalled(); expect(db.importBatch.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "batch_1" }, data: expect.objectContaining({ status: "STAGED", summary: expect.objectContaining({ reference: expect.objectContaining({ stagedClients: expect.any(Number), }), }), }), }), ); }); it("stages chargeability roster resources into staged resource rows", async () => { const db = { importBatch: { create: vi.fn().mockResolvedValue({ id: "batch_2", summary: {} }), findUnique: vi.fn(), update: vi.fn().mockResolvedValue({ id: "batch_2", summary: {} }), }, client: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), }, country: { upsert: vi.fn(), }, metroCity: { upsert: vi.fn(), }, orgUnit: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), upsert: vi.fn(), }, managementLevelGroup: { upsert: vi.fn(), }, managementLevel: { upsert: vi.fn(), }, stagedClient: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedResource: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 100 }), }, stagedUnresolvedRecord: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 0 }), }, }; const result = await stageDispoChargeabilityResources(db as never, { chargeabilityWorkbookPath, }); expect(result.batchId).toBe("batch_2"); expect(result.counts.stagedResources).toBeGreaterThan(300); expect(db.stagedResource.createMany).toHaveBeenCalled(); expect(db.importBatch.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: "STAGED", summary: expect.objectContaining({ chargeability: expect.objectContaining({ stagedResources: expect.any(Number), }), }), }), }), ); }); it("stages roster and SAP workbook rows into staged resource rows", async () => { const db = { importBatch: { create: vi.fn().mockResolvedValue({ id: "batch_roster", summary: {} }), findUnique: vi.fn(), update: vi.fn().mockResolvedValue({ id: "batch_roster", summary: {} }), }, client: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), }, country: { upsert: vi.fn(), }, metroCity: { upsert: vi.fn(), }, orgUnit: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), upsert: vi.fn(), }, managementLevelGroup: { upsert: vi.fn(), }, managementLevel: { upsert: vi.fn(), }, stagedClient: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedResource: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 100 }), }, stagedUnresolvedRecord: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 0 }), }, }; const result = await stageDispoRosterResources(db as never, { rosterWorkbookPath, costWorkbookPath, }); expect(result.batchId).toBe("batch_roster"); expect(result.counts.stagedResources).toBeGreaterThan(500); expect(result.counts.ignoredPseudoDemandRows).toBeGreaterThan(100); expect(result.counts.excludedResources).toBeGreaterThan(0); expect(db.stagedResource.createMany).toHaveBeenCalled(); expect(db.stagedResource.createMany).toHaveBeenCalledWith( expect.objectContaining({ data: expect.arrayContaining([ expect.objectContaining({ lcrCents: expect.any(Number), ucrCents: expect.any(Number), normalizedData: expect.objectContaining({ rateResolution: expect.any(String), }), }), ]), }), ); expect(db.importBatch.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: "STAGED", summary: expect.objectContaining({ roster: expect.objectContaining({ stagedResources: expect.any(Number), ignoredPseudoDemandRows: expect.any(Number), }), }), }), }), ); }); it("stages planning workbook rows into staged planning records", async () => { const db = { importBatch: { create: vi.fn().mockResolvedValue({ id: "batch_3", summary: {} }), findUnique: vi.fn(), update: vi.fn().mockResolvedValue({ id: "batch_3", summary: {} }), }, client: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), }, country: { upsert: vi.fn(), }, metroCity: { upsert: vi.fn(), }, orgUnit: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), upsert: vi.fn(), }, managementLevelGroup: { upsert: vi.fn(), }, managementLevel: { upsert: vi.fn(), }, stagedClient: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedResource: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedAssignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 1000 }), }, stagedVacation: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 1000 }), }, stagedAvailabilityRule: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 100 }), }, stagedUnresolvedRecord: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 100 }), }, }; const result = await stageDispoPlanningData(db as never, { planningWorkbookPath, }); expect(result.batchId).toBe("batch_3"); expect(result.counts.stagedAssignments).toBeGreaterThan(1000); expect(result.counts.stagedVacations).toBeGreaterThan(1000); expect(result.counts.stagedAvailabilityRules).toBeGreaterThan(0); expect(result.counts.unresolved).toBeGreaterThan(0); expect(db.stagedAssignment.createMany).toHaveBeenCalled(); expect(db.stagedVacation.createMany).toHaveBeenCalled(); expect(db.stagedAvailabilityRule.createMany).toHaveBeenCalled(); expect(db.stagedUnresolvedRecord.createMany).toHaveBeenCalled(); expect(db.importBatch.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: "STAGED", summary: expect.objectContaining({ planning: expect.objectContaining({ stagedAssignments: expect.any(Number), stagedVacations: expect.any(Number), }), }), }), }), ); }); it("resolves staged projects from planning workbook assignments", async () => { const db = { importBatch: { create: vi.fn().mockResolvedValue({ id: "batch_4", summary: {} }), findUnique: vi.fn(), update: vi.fn().mockResolvedValue({ id: "batch_4", summary: {} }), }, client: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), }, country: { upsert: vi.fn(), }, metroCity: { upsert: vi.fn(), }, orgUnit: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), upsert: vi.fn(), }, managementLevelGroup: { upsert: vi.fn(), }, managementLevel: { upsert: vi.fn(), }, stagedClient: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedResource: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedAssignment: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedVacation: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedAvailabilityRule: { deleteMany: vi.fn(), createMany: vi.fn(), }, stagedProject: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 100 }), }, stagedUnresolvedRecord: { deleteMany: vi.fn(), createMany: vi.fn(), }, }; const result = await stageDispoProjects(db as never, { planningWorkbookPath, }); expect(result.batchId).toBe("batch_4"); expect(result.counts.stagedProjects).toBeGreaterThan(10); expect(db.stagedProject.createMany).toHaveBeenCalled(); expect(db.stagedProject.createMany).toHaveBeenCalledWith( expect.objectContaining({ data: expect.arrayContaining([ expect.objectContaining({ projectKey: "INT-MO", shortCode: "INT-MO", name: "Management & Operations", isInternal: true, utilizationCategoryCode: "M&O", }), expect.objectContaining({ projectKey: "11035763", shortCode: "11035763", clientCode: "BMW", isInternal: false, utilizationCategoryCode: "Chg", }), expect.objectContaining({ isTbd: true, isInternal: false, shortCode: expect.stringMatching(/^TBD-/), }), ]), }), ); expect(db.importBatch.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: "STAGED", summary: expect.objectContaining({ projectResolution: expect.objectContaining({ stagedProjects: expect.any(Number), }), }), }), }), ); }); it("stages all dispo workbooks into one import batch and persists readiness", async () => { const db = { importBatch: { create: vi.fn().mockResolvedValue({ id: "batch_5", summary: {} }), findUnique: vi.fn().mockResolvedValue({ id: "batch_5", summary: {} }), update: vi.fn().mockResolvedValue({ id: "batch_5", summary: {} }), }, client: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({ id: "client_1" }), update: vi.fn(), }, country: { upsert: vi.fn().mockResolvedValue({ id: "country_1" }), }, metroCity: { upsert: vi.fn().mockResolvedValue({ id: "metro_1" }), }, orgUnit: { findFirst: vi.fn().mockResolvedValue(null), create: vi .fn() .mockResolvedValueOnce({ id: "org_root" }) .mockResolvedValue({ id: "org_child" }), update: vi.fn(), upsert: vi.fn().mockResolvedValue({ id: "org_upserted" }), }, managementLevelGroup: { upsert: vi.fn().mockResolvedValue({ id: "group_1" }), }, managementLevel: { upsert: vi.fn().mockResolvedValue({ id: "level_1" }), }, stagedClient: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 10 }), }, stagedResource: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi .fn() .mockResolvedValueOnce({ count: 100 }) .mockResolvedValueOnce({ count: 100 }), }, stagedProject: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 50 }), }, stagedAssignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 1000 }), }, stagedVacation: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 1000 }), }, stagedAvailabilityRule: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 100 }), }, stagedUnresolvedRecord: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }), createMany: vi.fn().mockResolvedValue({ count: 100 }), }, }; const result = await stageDispoImportBatch(db as never, { referenceWorkbookPath: mandatoryWorkbookPath, chargeabilityWorkbookPath, planningWorkbookPath, rosterWorkbookPath, costWorkbookPath, }); expect(result.batchId).toBe("batch_5"); expect(result.counts.stagedResources).toBeGreaterThan(800); expect(result.counts.stagedRosterResources).toBeGreaterThan(500); expect(result.counts.stagedAssignments).toBeGreaterThan(1000); expect(result.readiness.canCommitWithStrictSourceData).toBe(false); expect(result.readiness.issues).not.toEqual( expect.arrayContaining([ expect.objectContaining({ code: "REFERENCE_RESOURCE_MASTER_MISSING", }), ]), ); expect(db.importBatch.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "batch_5" }, data: expect.objectContaining({ summary: expect.objectContaining({ readiness: expect.objectContaining({ canCommitWithStrictSourceData: false, }), }), }), }), ); }, 15_000); });