const fs = require("fs"); const path = require("path"); const vm = require("vm"); const assert = require("assert"); const rootDir = path.resolve(__dirname, "..", ".."); const loadClosedCaseStatusModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "isClosedCaseStatus.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace( /export function\s+isClosedCaseStatus/, "function isClosedCaseStatus" ); source += "\nmodule.exports = { isClosedCaseStatus };\n"; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetLifecycleStageIndexModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getLifecycleStageIndex.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace( /export function\s+getLifecycleStageIndex/, "function getLifecycleStageIndex" ); source += "\nmodule.exports = { getLifecycleStageIndex };\n"; const context = { module: { exports: {} }, exports: {}, require, isClosedCaseStatus: closedCaseStatusModule.isClosedCaseStatus }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetLifecycleStageStatusModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getLifecycleStageStatus.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace( /export function\s+getLifecycleStageStatus/, "function getLifecycleStageStatus" ); source += "\nmodule.exports = { getLifecycleStageStatus };\n"; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadMapAppealTypeModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "mapAppealType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/export const\s+/g, "const "); source = source.replace( /export function\s+mapAppealType/, "function mapAppealType" ); source += ` module.exports = { caseTypeAliases, caseTypeKeyByAppealTypeId, normaliseCaseType, mapAppealType }; `; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadMapSpecialistProcessStageTypeModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "mapSpecialistProcessStageType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/export const\s+/g, "const "); source = source.replace( /export function\s+mapSpecialistProcessStageType/, "function mapSpecialistProcessStageType" ); source += ` module.exports = { specialistProcessStageTypeByCaseType, mapSpecialistProcessStageType }; `; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetStageCaseTypeKeyModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getStageCaseTypeKey.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace( /export function\s+getStageCaseTypeKey/, "function getStageCaseTypeKey" ); source += "\nmodule.exports = { getStageCaseTypeKey };\n"; const context = { module: { exports: {} }, exports: {}, require, caseTypeAliases: mapAppealTypeModule.caseTypeAliases, caseTypeKeyByAppealTypeId: mapAppealTypeModule.caseTypeKeyByAppealTypeId, mapAppealType: mapAppealTypeModule.mapAppealType, normaliseCaseType: mapAppealTypeModule.normaliseCaseType, mapSpecialistProcessStageType: mapSpecialistProcessStageTypeModule.mapSpecialistProcessStageType }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadStageCatalogueModule = () => { const filePath = path.join( rootDir, "components", "case", "summary", "utils", "caseStagesByAppealType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace(/export const\s+/g, "const "); source += "\nmodule.exports = { stagesByCaseType };\n"; const context = { module: { exports: {} }, exports: {}, require, getStageCaseTypeKey: getStageCaseTypeKeyModule.getStageCaseTypeKey, getStagesForCaseTypeKey: () => [], isClosedCaseStatus: closedCaseStatusModule.isClosedCaseStatus, getLifecycleStageIndex: getLifecycleStageIndexModule.getLifecycleStageIndex, getLifecycleStageStatus: getLifecycleStageStatusModule.getLifecycleStageStatus }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetStagesForCaseTypeKeyModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getStagesForCaseTypeKey.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace( /export function\s+getStagesForCaseTypeKey/, "function getStagesForCaseTypeKey" ); source += "\nmodule.exports = { getStagesForCaseTypeKey };\n"; const context = { module: { exports: {} }, exports: {}, require: (modulePath) => { if ( modulePath === "../../../components/case/summary/utils/caseStagesByAppealType" ) { return stageCatalogueModule; } return require(modulePath); } }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadStageHelperModule = () => { const filePath = path.join( rootDir, "components", "case", "summary", "utils", "caseStagesByAppealType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace(/export const\s+/g, "const "); source += "\nmodule.exports = { getStagesForAppealType, stagesByCaseType };\n"; const context = { module: { exports: {} }, exports: {}, require, getStageCaseTypeKey: getStageCaseTypeKeyModule.getStageCaseTypeKey, getStagesForCaseTypeKey: getStagesForCaseTypeKeyModule.getStagesForCaseTypeKey, isClosedCaseStatus: closedCaseStatusModule.isClosedCaseStatus, getLifecycleStageIndex: getLifecycleStageIndexModule.getLifecycleStageIndex, getLifecycleStageStatus: getLifecycleStageStatusModule.getLifecycleStageStatus }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const tests = []; const test = (name, fn) => tests.push({ name, fn }); const normalizeForAssertion = (value) => JSON.parse(JSON.stringify(value ?? null)); const closedCaseStatusModule = loadClosedCaseStatusModule(); const getLifecycleStageIndexModule = loadGetLifecycleStageIndexModule(); const getLifecycleStageStatusModule = loadGetLifecycleStageStatusModule(); const mapAppealTypeModule = loadMapAppealTypeModule(); const mapSpecialistProcessStageTypeModule = loadMapSpecialistProcessStageTypeModule(); const getStageCaseTypeKeyModule = loadGetStageCaseTypeKeyModule(); const stageCatalogueModule = loadStageCatalogueModule(); const getStagesForCaseTypeKeyModule = loadGetStagesForCaseTypeKeyModule(); const stageHelperModule = loadStageHelperModule(); const { getLifecycleStageIndex } = getLifecycleStageIndexModule; const { getStagesForAppealType, stagesByCaseType } = stageHelperModule; test("direct stage ID match returns the correct index", () => { assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, 846040014), 0 ); assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.DNS, 846040006), 5 ); }); test("numeric and string stage ID forms preserve the same current index", () => { assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, 846040018), 5 ); assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, "846040018"), 5 ); }); test("unknown stage or status preserves -1 fallback", () => { assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, 999999999), -1 ); assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, "UNKNOWN"), -1 ); }); test("closed-case statuses resolve to the case-closed index where present", () => { const closedStatuses = [1000, 5, 6, 846040013, 846040059, 846040060]; for (const closedStatus of closedStatuses) { assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, closedStatus), 10 ); } }); test("missing case-closed stage preserves current -1 fallback for closed statuses", () => { const stagesWithoutClosed = stagesByCaseType.PLANNING_S78.filter( (stage) => stage.titleKey !== "case-closed" ); assert.strictEqual(getLifecycleStageIndex(stagesWithoutClosed, 1000), -1); assert.strictEqual( getLifecycleStageIndex(stagesWithoutClosed, 846040059), -1 ); }); test("null and undefined current stage input preserve current -1 fallback", () => { assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, null), -1 ); assert.strictEqual( getLifecycleStageIndex(stagesByCaseType.PLANNING_S78, undefined), -1 ); }); test("integration parity preserves existing lifecycle outputs for representative inputs", () => { const scenarios = [ [846040000, 846040018, undefined], [846040011, 846040006, undefined], [846040004, 846040021, undefined], [846040010, 846040049, undefined], [846040000, 846040059, undefined], [846040000, 999999999, undefined] ]; const baselineGetStageIndex = (stages, currentStageId) => { const currentStageNumber = Number(currentStageId); const exactIndex = stages.findIndex( (stage) => Number(stage.stageId) === currentStageNumber ); if (exactIndex !== -1) return exactIndex; if (closedCaseStatusModule.isClosedCaseStatus(currentStageNumber)) { return stages.findIndex( (stage) => stage.titleKey === "case-closed" ); } return -1; }; const baselineGetStagesForAppealType = ( caseType, currentStageId, specialistProcess ) => { const stages = normalizeForAssertion( getStagesForAppealType(caseType, currentStageId, specialistProcess) ).map(({ status, ...stage }) => stage); const freshStages = normalizeForAssertion( getStagesForAppealType(caseType, currentStageId, specialistProcess) ).map(({ status, ...stage }) => stage); const currentIndex = baselineGetStageIndex(freshStages, currentStageId); return freshStages.map((stage, index) => ({ ...stage, status: currentIndex === -1 ? "not-started" : index < currentIndex ? "complete" : index === currentIndex ? "in-progress" : "not-started" })); }; for (const [caseType, currentStageId, specialistProcess] of scenarios) { assert.deepStrictEqual( normalizeForAssertion( getStagesForAppealType( caseType, currentStageId, specialistProcess ) ), normalizeForAssertion( baselineGetStagesForAppealType( caseType, currentStageId, specialistProcess ) ) ); } }); const run = async () => { let passed = 0; for (const currentTest of tests) { await currentTest.fn(); passed += 1; } console.log( `Phase 22 case-lifecycle-stage-index tests passed (${passed}/${tests.length}).` ); }; module.exports = run; if (require.main === module) { run().catch((error) => { console.error(error); process.exit(1); }); }