const fs = require("fs"); const path = require("path"); const vm = require("vm"); const assert = require("assert"); const rootDir = path.resolve(__dirname, "..", ".."); const loadBuildRepsArrRulesModule = () => { const filePath = path.join( rootDir, "components", "case", "representation", "utils", "buildRepsArrRules.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace( /const\s+\{\s*getAvailableRepresentationTypes\s*\}\s*=\s*require\("\.\.\.\.\/\.\.\.\/\.\.\.\/lib\/domain\/representation-type-policy\/getAvailableRepresentationTypes"\);/, 'const { getAvailableRepresentationTypes } = require("../../../../lib/domain/representation-type-policy/getAvailableRepresentationTypes");' ); source = source.replace(/export const\s+/g, "const "); source += ` module.exports = { APPEAL_TYPES, SPECIALIST_PROCESS, INVOLVEMENT_TYPES, buildRepresentationContext, buildRepsArrFromContext }; `; const context = { module: { exports: {} }, exports: {}, require: (modulePath) => { if ( modulePath === "../../../../lib/domain/representation-type-policy/getAvailableRepresentationTypes" ) { const domainFilePath = path.join( rootDir, "lib", "domain", "representation-type-policy", "getAvailableRepresentationTypes.js" ); let domainSource = fs.readFileSync(domainFilePath, "utf8"); domainSource = domainSource.replace( /import\s+\{\s*getAppealTypeFamily\s*\}\s+from\s+"\.\.\/appeal-type-policy";/, 'const { getAppealTypeFamily } = require("../appeal-type-policy");' ); domainSource = domainSource.replace( /export const\s+getAvailableRepresentationTypes\s*=/g, "const getAvailableRepresentationTypes =" ); domainSource += "\nmodule.exports = { getAvailableRepresentationTypes };\n"; const domainContext = { module: { exports: {} }, exports: {}, require: (nestedModulePath) => { if (nestedModulePath === "../appeal-type-policy") { const appealTypePolicyPath = path.join( rootDir, "lib", "domain", "appeal-type-policy", "index.js" ); let appealTypePolicySource = fs.readFileSync( appealTypePolicyPath, "utf8" ); appealTypePolicySource = appealTypePolicySource.replace( /export\s+\{\s*getAppealTypeFamily\s*\}\s+from\s+"\.\/getAppealTypeFamily";/, 'const { getAppealTypeFamily } = require("./getAppealTypeFamily");\nmodule.exports = { getAppealTypeFamily };' ); const appealTypePolicyContext = { module: { exports: {} }, exports: {}, require: (appealModulePath) => { if ( appealModulePath === "./getAppealTypeFamily" ) { const getAppealTypeFamilyPath = path.join( rootDir, "lib", "domain", "appeal-type-policy", "getAppealTypeFamily.js" ); let getAppealTypeFamilySource = fs.readFileSync( getAppealTypeFamilyPath, "utf8" ); getAppealTypeFamilySource = getAppealTypeFamilySource.replace( /import\s+\{\s*mapAppealType\s*\}\s+from\s+"\.\.\/case-lifecycle\/mapAppealType";/, 'const { mapAppealType } = require("../case-lifecycle/mapAppealType");' ); getAppealTypeFamilySource = getAppealTypeFamilySource.replace( /export function\s+getAppealTypeFamily\s*\(/, "function getAppealTypeFamily(" ); getAppealTypeFamilySource += "\nmodule.exports = { getAppealTypeFamily };\n"; const getAppealTypeFamilyContext = { module: { exports: {} }, exports: {}, require: (caseLifecyclePath) => { if ( caseLifecyclePath === "../case-lifecycle/mapAppealType" ) { const mapAppealTypePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "mapAppealType.js" ); let mapAppealTypeSource = fs.readFileSync( mapAppealTypePath, "utf8" ); mapAppealTypeSource = mapAppealTypeSource.replace( /export const\s+/g, "const " ); mapAppealTypeSource = mapAppealTypeSource.replace( /export function\s+mapAppealType\s*\(/, "function mapAppealType(" ); mapAppealTypeSource += "\nmodule.exports = { caseTypeAliases, caseTypeKeyByAppealTypeId, normaliseCaseType, mapAppealType };\n"; const mapAppealTypeContext = { module: { exports: {} }, exports: {} }; vm.runInNewContext( mapAppealTypeSource, mapAppealTypeContext, { filename: mapAppealTypePath } ); return mapAppealTypeContext .module.exports; } return require( caseLifecyclePath ); } }; vm.runInNewContext( getAppealTypeFamilySource, getAppealTypeFamilyContext, { filename: getAppealTypeFamilyPath } ); return getAppealTypeFamilyContext.module .exports; } return require(appealModulePath); } }; vm.runInNewContext( appealTypePolicySource, appealTypePolicyContext, { filename: appealTypePolicyPath } ); return appealTypePolicyContext.module.exports; } return require(nestedModulePath); }, Date }; vm.runInNewContext(domainSource, domainContext, { filename: domainFilePath }); return domainContext.module.exports; } return require(modulePath); }, Date }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const tests = []; const test = (name, fn) => tests.push({ name, fn }); const mod = loadBuildRepsArrRulesModule(); const { APPEAL_TYPES, SPECIALIST_PROCESS, INVOLVEMENT_TYPES, buildRepresentationContext, buildRepsArrFromContext } = mod; const BASE_DETAILS = { pinswg_startdate: "2026-04-01T00:00:00.000Z", pinswg_statementduedate: "2026-04-30T00:00:00.000Z", pinswg_finalcommentsduedate: "2026-05-25T00:00:00.000Z" }; const baseInput = (overrides = {}) => ({ detailsObj: BASE_DETAILS, appealType: 846040000, specialistProcess: SPECIALIST_PROCESS.WRITTEN_REPS, involvementType: 999, selectedCapacity: "appellant", isCaseOwner: false, isNRW: false, now: new Date("2026-04-10T10:00:00.000Z"), ...overrides }); const normalizeForAssertion = (value) => JSON.parse(JSON.stringify(value ?? null)); const buildOptions = (overrides = {}) => normalizeForAssertion( buildRepsArrFromContext( buildRepresentationContext(baseInput(overrides)) ) ); test("baseline truth-table: non-DNS non-SIPS across windows/capacities/ownership", () => { const capacities = [ { value: "appellant", hasFinalComments: true }, { value: "agent", hasFinalComments: true }, { value: "interestedparty", hasFinalComments: true }, { value: false, hasFinalComments: false }, { value: "", hasFinalComments: false }, { value: "unknown", hasFinalComments: false } ]; const cases = [ { label: "before start", now: "2026-03-31T10:00:00.000Z", expected: (c, isCaseOwner) => [] }, { label: "statement period", now: "2026-04-15T10:00:00.000Z", expected: (c, isCaseOwner) => (isCaseOwner ? [] : ["Statement"]) }, { label: "final comments period", now: "2026-05-10T10:00:00.000Z", expected: (c) => (c.hasFinalComments ? ["Final comments"] : []) }, { label: "after final comments due", now: "2026-05-26T10:00:00.000Z", expected: () => [] } ]; for (const capacity of capacities) { for (const scenario of cases) { for (const isCaseOwner of [true, false]) { const result = buildOptions({ selectedCapacity: capacity.value, isCaseOwner, now: new Date(scenario.now) }); assert.deepStrictEqual( result, scenario.expected(capacity, isCaseOwner), `Failed for ${scenario.label}, capacity=${capacity.value}, isCaseOwner=${isCaseOwner}` ); } } } }); test("LPA involvement: questionnaire and final comments windows", () => { assert.deepStrictEqual( buildOptions({ involvementType: INVOLVEMENT_TYPES.LPA, selectedCapacity: "unknown", now: new Date("2026-04-10T10:00:00.000Z") }), ["Questionnaire", "Statement"] ); assert.deepStrictEqual( buildOptions({ involvementType: INVOLVEMENT_TYPES.LPA, selectedCapacity: "unknown", now: new Date("2026-05-10T10:00:00.000Z") }), ["Questionnaire", "Final comments"] ); assert.deepStrictEqual( buildOptions({ involvementType: INVOLVEMENT_TYPES.LPA, selectedCapacity: "unknown", now: new Date("2026-05-26T10:00:00.000Z") }), [] ); }); test("questionnaire excluded appeal types for LPA", () => { const excluded = [846040019, 846040011, 846040015, 846040016]; for (const appealType of excluded) { const result = buildOptions({ appealType, involvementType: INVOLVEMENT_TYPES.LPA, now: new Date("2026-04-10T10:00:00.000Z") }); assert.strictEqual( result.includes("Questionnaire"), false, `Questionnaire should be excluded for ${appealType}` ); } }); test("householder: statements blocked; final comments depend on specialist process", () => { assert.deepStrictEqual( buildOptions({ appealType: APPEAL_TYPES.HOUSEHOLDER, selectedCapacity: "appellant", specialistProcess: SPECIALIST_PROCESS.HEARING, now: new Date("2026-04-15T10:00:00.000Z") }), [] ); assert.deepStrictEqual( buildOptions({ appealType: APPEAL_TYPES.HOUSEHOLDER, selectedCapacity: "appellant", specialistProcess: SPECIALIST_PROCESS.HEARING, now: new Date("2026-05-10T10:00:00.000Z") }), [] ); assert.deepStrictEqual( buildOptions({ appealType: APPEAL_TYPES.HOUSEHOLDER, selectedCapacity: "appellant", specialistProcess: SPECIALIST_PROCESS.WRITTEN_REPS, now: new Date("2026-05-10T10:00:00.000Z") }), ["Final comments"] ); }); test("advert appeal type: LPA written reps blocks statement, non-LPA non-owner can still submit statement", () => { assert.deepStrictEqual( buildOptions({ appealType: APPEAL_TYPES.ADVERT, involvementType: INVOLVEMENT_TYPES.LPA, specialistProcess: SPECIALIST_PROCESS.WRITTEN_REPS, now: new Date("2026-04-15T10:00:00.000Z") }), ["Questionnaire"] ); assert.deepStrictEqual( buildOptions({ appealType: APPEAL_TYPES.ADVERT, involvementType: 999, isCaseOwner: false, now: new Date("2026-04-15T10:00:00.000Z") }), ["Statement"] ); }); test("DNS rules: statement/LIR ownership behaviour and questionnaire exclusion", () => { const dnsDetails = { pinswg_applicationacceptedasvalid: "2026-04-01T00:00:00.000Z", pinswg_endofrepresentationperiod: "2026-05-25T00:00:00.000Z" }; const dnsOptions = (overrides = {}) => normalizeForAssertion( buildRepsArrFromContext( buildRepresentationContext( baseInput({ appealType: APPEAL_TYPES.DNS, detailsObj: dnsDetails, ...overrides }) ) ) ); assert.deepStrictEqual( dnsOptions({ involvementType: 999, isCaseOwner: false, now: new Date("2026-05-01T10:00:00.000Z") }), ["Statement"] ); assert.deepStrictEqual( dnsOptions({ involvementType: 999, isCaseOwner: true, now: new Date("2026-05-01T10:00:00.000Z") }), [] ); assert.deepStrictEqual( dnsOptions({ involvementType: INVOLVEMENT_TYPES.LPA, isCaseOwner: false, now: new Date("2026-05-01T10:00:00.000Z") }), ["Local Impact Report", "Statement", "Final comments"] ); assert.deepStrictEqual( dnsOptions({ involvementType: INVOLVEMENT_TYPES.LPA, isCaseOwner: true, now: new Date("2026-05-01T10:00:00.000Z") }), ["Final comments"] ); assert.strictEqual( dnsOptions({ involvementType: INVOLVEMENT_TYPES.LPA, isCaseOwner: false, now: new Date("2026-05-01T10:00:00.000Z") }).includes("Questionnaire"), false ); }); test("SIPS consultation behaviour is not date-gated", () => { const sips = (overrides = {}) => buildOptions({ appealType: APPEAL_TYPES.SIPS, ...overrides }); assert.deepStrictEqual( sips({ selectedCapacity: "appellant", isNRW: false }), ["Statement", "Consultation Response"] ); assert.deepStrictEqual(sips({ selectedCapacity: "agent", isNRW: false }), [ "Statement", "Consultation Response", "Local Impact Report" ]); assert.deepStrictEqual( sips({ selectedCapacity: "interestedparty", isNRW: true }), [ "Statement", "Consultation Response", "Local Impact Report", "Marine Impact Report" ] ); assert.deepStrictEqual( sips({ selectedCapacity: "appellant", now: new Date("2026-06-01T10:00:00.000Z") }), ["Consultation Response"] ); }); test("deduplication and ordering are stable", () => { const result = buildOptions({ appealType: APPEAL_TYPES.SIPS, involvementType: INVOLVEMENT_TYPES.LPA, selectedCapacity: "agent", isNRW: true, isCaseOwner: false, now: new Date("2026-05-10T10:00:00.000Z") }); assert.deepStrictEqual(result, [ "Questionnaire", "Final comments", "Consultation Response", "Local Impact Report", "Marine Impact Report" ]); assert.strictEqual(new Set(result).size, result.length); }); test("invalid or missing dates suppress date-gated options but SIPS consultation persists", () => { assert.deepStrictEqual( normalizeForAssertion( buildRepsArrFromContext( buildRepresentationContext( baseInput({ detailsObj: { pinswg_statementduedate: "2026-04-30T00:00:00.000Z", pinswg_finalcommentsduedate: "2026-05-25T00:00:00.000Z" } }) ) ) ), [] ); assert.deepStrictEqual( normalizeForAssertion( buildRepsArrFromContext( buildRepresentationContext( baseInput({ detailsObj: { pinswg_startdate: "2026-04-01T00:00:00.000Z", pinswg_finalcommentsduedate: "2026-05-25T00:00:00.000Z" } }) ) ) ), [] ); assert.deepStrictEqual( normalizeForAssertion( buildRepsArrFromContext( buildRepresentationContext( baseInput({ detailsObj: { pinswg_startdate: "2026-04-01T00:00:00.000Z", pinswg_statementduedate: "2026-04-30T00:00:00.000Z" }, involvementType: INVOLVEMENT_TYPES.LPA, now: new Date("2026-05-10T10:00:00.000Z") }) ) ) ), [] ); assert.deepStrictEqual( normalizeForAssertion( buildRepsArrFromContext( buildRepresentationContext( baseInput({ appealType: APPEAL_TYPES.SIPS, detailsObj: {}, selectedCapacity: "agent", isNRW: true, now: new Date("2026-06-20T10:00:00.000Z") }) ) ) ), ["Consultation Response", "Local Impact Report", "Marine Impact Report"] ); }); test("non-DNS startdate plural fallback preserves successful statement availability and current output order", () => { assert.deepStrictEqual( buildOptions({ detailsObj: { pinswg_startdate: null, pinswg_startdates: "2026-04-01T00:00:00.000Z", pinswg_statementduedate: "2026-04-30T00:00:00.000Z", pinswg_finalcommentsduedate: "2026-05-25T00:00:00.000Z" }, selectedCapacity: "appellant", isCaseOwner: false, now: new Date("2026-04-15T10:00:00.000Z") }), ["Statement"] ); assert.deepStrictEqual( buildOptions({ detailsObj: { pinswg_startdate: null, pinswg_startdates: "2026-04-01T00:00:00.000Z", pinswg_statementduedate: "2026-04-30T00:00:00.000Z", pinswg_finalcommentsduedate: "2026-05-25T00:00:00.000Z" }, selectedCapacity: "agent", involvementType: INVOLVEMENT_TYPES.LPA, isCaseOwner: false, isNRW: true, appealType: APPEAL_TYPES.SIPS, now: new Date("2026-05-10T10:00:00.000Z") }), [ "Questionnaire", "Final comments", "Consultation Response", "Local Impact Report", "Marine Impact Report" ] ); }); test("non-DNS statementsduedate plural fallback preserves successful final comments availability", () => { assert.deepStrictEqual( buildOptions({ detailsObj: { pinswg_startdate: "2026-04-01T00:00:00.000Z", pinswg_statementduedate: null, pinswg_statementsduedate: "2026-04-30T00:00:00.000Z", pinswg_finalcommentsduedate: "2026-05-25T00:00:00.000Z" }, selectedCapacity: "interestedparty", isCaseOwner: false, now: new Date("2026-05-10T10:00:00.000Z") }), ["Final comments"] ); }); test("combined plural date fallbacks preserve successful statement and final comments availability across windows", () => { const detailsObj = { pinswg_startdate: null, pinswg_startdates: "2026-04-01T00:00:00.000Z", pinswg_statementduedate: null, pinswg_statementsduedate: "2026-04-30T00:00:00.000Z", pinswg_finalcommentsduedate: "2026-05-25T00:00:00.000Z" }; assert.deepStrictEqual( buildOptions({ detailsObj, selectedCapacity: "appellant", isCaseOwner: false, now: new Date("2026-04-15T10:00:00.000Z") }), ["Statement"] ); assert.deepStrictEqual( buildOptions({ detailsObj, selectedCapacity: "appellant", isCaseOwner: false, now: new Date("2026-05-10T10:00:00.000Z") }), ["Final comments"] ); }); test("capacity normalization coverage", () => { const inFinalWindow = { now: new Date("2026-05-10T10:00:00.000Z") }; assert.deepStrictEqual( buildOptions({ selectedCapacity: "appellant", ...inFinalWindow }), ["Final comments"] ); assert.deepStrictEqual( buildOptions({ selectedCapacity: "Appellant", ...inFinalWindow }), ["Final comments"] ); assert.deepStrictEqual( buildOptions({ selectedCapacity: "Interested Party", ...inFinalWindow }), ["Final comments"] ); assert.deepStrictEqual( buildOptions({ selectedCapacity: "interested-party", ...inFinalWindow }), ["Final comments"] ); assert.deepStrictEqual( buildOptions({ selectedCapacity: "agent", ...inFinalWindow }), ["Final comments"] ); }); const run = async () => { let passed = 0; for (const currentTest of tests) { await currentTest.fn(); passed += 1; } console.log( `Phase 22 representation buildRepsArr rules tests passed (${passed}/${tests.length}).` ); }; module.exports = run; if (require.main === module) { run().catch((error) => { console.error(error); process.exit(1); }); }