Merged PR 2374: addeding domain layer extrraction
# Summary This PR introduces a **Case Lifecycle Domain Boundary** to centralize lifecycle decision logic and reduce coupling within the appeals application. The work is **behaviour-preserving** and introduces no intentional changes to business rules, CRM integrations, translations, dashboards, API routes, or user-facing functionality. ## What was added New lifecycle boundary: ```text lib/domain/case-lifecycle/ ``` Key responsibilities extracted: - Specialist process normalization - Appeal type mapping - Specialist process stage override mapping - Stage case-type resolution - Stage catalogue lookup - Closed-case status recognition - Lifecycle stage index resolution - Lifecycle stage status assignment ## Behaviour preserved Characterization tests were added before each extraction to preserve: - Appeal type mapping and aliases - Specialist process handling - Lifecycle stage progression - Closed-case handling - Status assignment (`complete`, `in-progress`, `not-started`) - Existing ROW behaviour - Existing `statuscode` lifecycle semantics Closed-case recognition remains unchanged for: ```text 1000 5 6 846040013 846040059 846040060 ``` ## Documentation Added: ```text lib/domain/case-lifecycle/README.md ``` Documenting: - Boundary ownership - Non-goals - Lifecycle invariants - Known architectural constraints - Future extraction roadmap ## Testing Added lifecycle characterization coverage for: - Stage wrapper behaviour - Specialist process normalization - Appeal type mapping - Specialist process stage mapping - Stage case-type resolution - Stage catalogue lookup - Progress behaviour - Closed-case status handling - Stage index resolution - Stage status assignment ## Validation - Lifecycle characterization tests passed - `npm run lint` passed with no errors ## Out of Scope No changes to: - Stage catalogue ownership - Representation eligibility - Dashboard calculations - CRM/OData queries - API routes - Redux state - EN/CY translations - Event visibility logic ## Risk **Low risk** The refactor was delivered through small, characterization-first slices with no functional changes intended. Related work items: #23527
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# Case Lifecycle Domain Boundary
|
||||
|
||||
## Purpose
|
||||
|
||||
This directory contains a narrow **case lifecycle decision boundary**, not a full domain model.
|
||||
|
||||
Its current purpose is to centralise **behaviour-preserving interpretation logic** that was previously embedded in UI-adjacent helpers, while keeping live behaviour unchanged during refactor.
|
||||
|
||||
This boundary should be treated as an incremental extraction seam for read-only lifecycle interpretation, not as a place to redesign lifecycle behaviour.
|
||||
|
||||
## Current ownership
|
||||
|
||||
### `getLifecycleStagesForCase(...)`
|
||||
|
||||
- thin wrapper around the existing stage/progress helper
|
||||
- preserves existing output shape for current consumers
|
||||
- does not define new lifecycle semantics
|
||||
|
||||
### `normalizeSpecialistProcess(...)`
|
||||
|
||||
- raw CRM field fallback only
|
||||
- preserves current canonical and misspelled field fallback behaviour
|
||||
- does not validate or reinterpret CRM values
|
||||
|
||||
### `mapAppealType(...)`
|
||||
|
||||
- current appeal-type-to-case-type mapping
|
||||
- current alias handling
|
||||
- current unknown input behaviour
|
||||
|
||||
### `mapSpecialistProcessStageType(...)`
|
||||
|
||||
- specialist-process stage override mapping
|
||||
- preserves current placeholder-like CRM specialist process values
|
||||
- preserves current override target keys and fallback behaviour
|
||||
|
||||
### `getStageCaseTypeKey(...)`
|
||||
|
||||
- current stage case-type resolution decision
|
||||
- combines:
|
||||
- appeal type mapping
|
||||
- specialist-process stage override mapping
|
||||
- alias normalization
|
||||
- current fallback behaviour
|
||||
|
||||
### `getStagesForCaseTypeKey(...)`
|
||||
|
||||
- current stage catalogue lookup only
|
||||
- preserves:
|
||||
- direct key lookup
|
||||
- string indirection resolution
|
||||
- fallback to `[]`
|
||||
- does not own stage catalogue contents
|
||||
|
||||
## Stage catalogue ownership and lookup
|
||||
|
||||
Stage catalogue arrays still live in:
|
||||
|
||||
- `components/case/summary/utils/caseStagesByAppealType.js`
|
||||
|
||||
The domain boundary currently owns **lookup behaviour only**:
|
||||
|
||||
- direct key lookup
|
||||
- string indirection resolution
|
||||
- fallback to `[]`
|
||||
|
||||
`getStagesForCaseTypeKey(...)` must not be treated as stage catalogue ownership yet.
|
||||
|
||||
Lazy access in `getStagesForCaseTypeKey(...)` is intentional:
|
||||
|
||||
- it avoids circular dependency issues while stage arrays remain in the legacy helper
|
||||
- the domain helper must not eagerly import catalogue contents while ownership remains outside the domain boundary
|
||||
|
||||
Any future catalogue migration must be a separate, explicitly scoped slice.
|
||||
|
||||
Until catalogue ownership moves:
|
||||
|
||||
- stage array contents remain the source of truth in the legacy helper
|
||||
- the domain lookup helper must preserve reference identity
|
||||
- the domain lookup helper must preserve current fallback behaviour
|
||||
|
||||
## Explicit non-goals
|
||||
|
||||
This boundary does **not** currently own:
|
||||
|
||||
- stage catalogue arrays
|
||||
- progress/status calculation
|
||||
- closed-case handling
|
||||
- representation eligibility
|
||||
- dashboard layout or calculations
|
||||
- translations / EN-CY labels
|
||||
- CRM/OData query construction
|
||||
- API routes
|
||||
- Redux state shape
|
||||
- upload, submit, finalisation, or notification logic
|
||||
|
||||
## Behaviour-preservation invariants
|
||||
|
||||
Future slices must preserve:
|
||||
|
||||
- current stage IDs
|
||||
- translation keys
|
||||
- alias behaviour
|
||||
- unknown input behaviour
|
||||
- current fallback semantics
|
||||
- current specialist-process override semantics
|
||||
- status-vs-stage ambiguity
|
||||
- current closed-case behaviour
|
||||
|
||||
## Known oddities to preserve
|
||||
|
||||
1. `statuscode` is currently passed into stage/progress logic.
|
||||
- Do not rename or reinterpret this without a separate behaviour-change decision.
|
||||
|
||||
2. Specialist-process overrides are intentionally narrow.
|
||||
- They currently apply only through the existing appeal-type-based resolution path.
|
||||
- Do not broaden this behaviour during refactor.
|
||||
|
||||
3. Some specialist process values appear placeholder-like.
|
||||
- Do not “correct” them without CRM/product confirmation.
|
||||
|
||||
4. Some ROW inputs currently produce non-intuitive stage output.
|
||||
- Preserve characterized behaviour unless explicitly changing business rules.
|
||||
|
||||
5. The test harnesses are VM-based and import-sensitive.
|
||||
- Future extractions must update harness injections carefully and avoid broad rewrites.
|
||||
|
||||
## Testing expectations
|
||||
|
||||
Future slices should run:
|
||||
|
||||
```bash
|
||||
node tests/phase22/case-lifecycle-stage-case-type-resolution.test.cjs
|
||||
node tests/phase22/case-lifecycle-specialist-process-stage-mapping.test.cjs
|
||||
node tests/phase22/case-lifecycle-appeal-type-mapping.test.cjs
|
||||
node tests/phase22/case-lifecycle-specialist-process-normalization.test.cjs
|
||||
node tests/phase22/case-lifecycle-stage-wrapper.test.cjs
|
||||
node tests/phase22/representation-build-reps-arr-rules.test.cjs
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Future extraction order
|
||||
|
||||
Recommended future order:
|
||||
|
||||
1. stage catalogue lookup helper
|
||||
2. stage catalogue ownership only after enough characterization coverage
|
||||
3. progress/status calculation extraction
|
||||
4. additional read-only consumers
|
||||
5. representation eligibility only as a separate policy slice after characterization
|
||||
@@ -0,0 +1,17 @@
|
||||
import { isClosedCaseStatus } from "./isClosedCaseStatus";
|
||||
|
||||
export function getLifecycleStageIndex(stages, currentStageId) {
|
||||
const currentStageNumber = Number(currentStageId);
|
||||
|
||||
const exactIndex = stages.findIndex(
|
||||
(stage) => Number(stage.stageId) === currentStageNumber
|
||||
);
|
||||
|
||||
if (exactIndex !== -1) return exactIndex;
|
||||
|
||||
if (isClosedCaseStatus(currentStageNumber)) {
|
||||
return stages.findIndex((stage) => stage.titleKey === "case-closed");
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function getLifecycleStageStatus(index, currentIndex) {
|
||||
return currentIndex === -1
|
||||
? "not-started"
|
||||
: index < currentIndex
|
||||
? "complete"
|
||||
: index === currentIndex
|
||||
? "in-progress"
|
||||
: "not-started";
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { getStagesForAppealType } from "../../../components/case/summary/utils/caseStagesByAppealType";
|
||||
|
||||
export function getLifecycleStagesForCase(
|
||||
caseType,
|
||||
currentStageId,
|
||||
specialistProcess
|
||||
) {
|
||||
return getStagesForAppealType(caseType, currentStageId, specialistProcess);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
caseTypeAliases,
|
||||
caseTypeKeyByAppealTypeId,
|
||||
mapAppealType,
|
||||
normaliseCaseType
|
||||
} from "./mapAppealType";
|
||||
import { mapSpecialistProcessStageType } from "./mapSpecialistProcessStageType";
|
||||
|
||||
export function getStageCaseTypeKey(caseTypeOrAppealTypeId, specialistProcess) {
|
||||
const appealTypeId = Number(caseTypeOrAppealTypeId);
|
||||
const mappedCaseType = mapAppealType(caseTypeOrAppealTypeId);
|
||||
|
||||
const specialistProcessKey =
|
||||
caseTypeKeyByAppealTypeId[appealTypeId] === mappedCaseType
|
||||
? mapSpecialistProcessStageType(mappedCaseType, specialistProcess)
|
||||
: undefined;
|
||||
|
||||
const normalised = normaliseCaseType(caseTypeOrAppealTypeId);
|
||||
|
||||
return (
|
||||
specialistProcessKey ||
|
||||
mappedCaseType ||
|
||||
caseTypeAliases[normalised] ||
|
||||
normalised
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
const getStagesByCaseType = () =>
|
||||
require("../../../components/case/summary/utils/caseStagesByAppealType")
|
||||
.stagesByCaseType;
|
||||
|
||||
export function getStagesForCaseTypeKey(stageCaseTypeKey) {
|
||||
const stagesByCaseType = getStagesByCaseType();
|
||||
const entry = stagesByCaseType[stageCaseTypeKey];
|
||||
|
||||
if (!entry) return [];
|
||||
|
||||
if (typeof entry === "string") {
|
||||
return stagesByCaseType[entry] || [];
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { getLifecycleStagesForCase } from "./getLifecycleStagesForCase";
|
||||
export { isClosedCaseStatus } from "./isClosedCaseStatus";
|
||||
export { getLifecycleStageIndex } from "./getLifecycleStageIndex";
|
||||
export { getLifecycleStageStatus } from "./getLifecycleStageStatus";
|
||||
export { getStageCaseTypeKey } from "./getStageCaseTypeKey";
|
||||
export { getStagesForCaseTypeKey } from "./getStagesForCaseTypeKey";
|
||||
export { mapAppealType } from "./mapAppealType";
|
||||
export { mapSpecialistProcessStageType } from "./mapSpecialistProcessStageType";
|
||||
export { normalizeSpecialistProcess } from "./normalizeSpecialistProcess";
|
||||
@@ -0,0 +1,5 @@
|
||||
const closedCaseStatusIds = [1000, 5, 6, 846040013, 846040060, 846040059];
|
||||
|
||||
export function isClosedCaseStatus(statusOrStageId) {
|
||||
return closedCaseStatusIds.includes(Number(statusOrStageId));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export const caseTypeAliases = {
|
||||
// Planning S78 group
|
||||
S78: "PLANNING_S78",
|
||||
PLANNING_78: "PLANNING_S78",
|
||||
PLANNING_S78: "PLANNING_S78",
|
||||
CONDITIONS_73_79: "CONDITIONS_73_79",
|
||||
LBCAC: "LBCAC",
|
||||
LDCS: "LDCS",
|
||||
PLANNING_OBLIGATIONS_S106: "PLANNING_OBLIGATIONS_S106",
|
||||
PRIOR_NOTIFICATION: "PRIOR_NOTIFICATION",
|
||||
|
||||
// Other types
|
||||
HAS: "HAS",
|
||||
CALL_INS: "CALL_INS",
|
||||
CALL_IN: "CALL_INS",
|
||||
ENFORCEMENT: "ENFORCEMENT",
|
||||
ENFORCEMENT_LISTED_BUILDING: "ENFORCEMENT_LISTED_BUILDING",
|
||||
MAINTENANCE_OF_LAND: "MAINTENANCE_OF_LAND",
|
||||
RIGHTS_OF_WAY_SCHEDULE_14: "RIGHTS_OF_WAY_SCHEDULE_14",
|
||||
RIGHTS_OF_WAY_ORDERS: "RIGHTS_OF_WAY_ORDERS",
|
||||
REQUESTS_FOR_DIRECTION: "REQUESTS_FOR_DIRECTION",
|
||||
ADVERTS: "ADVERTS",
|
||||
HEDGEROW_TPO: "HEDGEROW_TPO",
|
||||
COMMON_LAND: "COMMON_LAND",
|
||||
COMPULSORY_PURCHASE_ORDERS: "COMPULSORY_PURCHASE_ORDERS",
|
||||
ELECTRICITY_ACT: "ELECTRICITY_ACT",
|
||||
HARBOUR_REVISION_ORDER: "HARBOUR_REVISION_ORDER",
|
||||
TRANSPORT_WORKS: "TRANSPORT_WORKS",
|
||||
WAYLEAVE: "WAYLEAVE",
|
||||
NON_VALIDATION: "NON_VALIDATION",
|
||||
DNS: "DNS",
|
||||
SIP: "SIP"
|
||||
};
|
||||
|
||||
export const caseTypeKeyByAppealTypeId = {
|
||||
846040000: "PLANNING_S78",
|
||||
846040001: "CONDITIONS_73_79",
|
||||
846040002: "SIP",
|
||||
846040003: "PLANNING_OBLIGATIONS_S106",
|
||||
846040004: "HAS",
|
||||
846040005: "ENFORCEMENT",
|
||||
846040006: "ENFORCEMENT_LISTED_BUILDING",
|
||||
846040007: "MAINTENANCE_OF_LAND",
|
||||
846040008: "LDCS",
|
||||
846040009: "PLANNING_S78",
|
||||
846040010: "CALL_INS",
|
||||
846040011: "DNS",
|
||||
846040012: "ELECTRICITY_ACT",
|
||||
846040013: "TRANSPORT_WORKS",
|
||||
846040014: "HARBOUR_REVISION_ORDER",
|
||||
846040015: "RIGHTS_OF_WAY_SCHEDULE_14",
|
||||
846040016: "COMMON_LAND",
|
||||
846040017: "HEDGEROW_TPO",
|
||||
846040018: "ADVERTS",
|
||||
846040019: "COMPULSORY_PURCHASE_ORDERS",
|
||||
846040020: "PLANNING_S78",
|
||||
846040021: "WAYLEAVE",
|
||||
846040024: "NON_VALIDATION",
|
||||
846040025: "LBCAC"
|
||||
};
|
||||
|
||||
export const normaliseCaseType = (caseType) =>
|
||||
caseType
|
||||
?.toString()
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.replace(/&/g, "AND")
|
||||
.replace(/[^A-Z0-9]+/g, "_")
|
||||
.replace(/^_|_$/g, "");
|
||||
|
||||
export function mapAppealType(appealTypeId) {
|
||||
const mappedCaseType = caseTypeKeyByAppealTypeId[Number(appealTypeId)];
|
||||
|
||||
if (mappedCaseType) {
|
||||
return mappedCaseType;
|
||||
}
|
||||
|
||||
const normalised = normaliseCaseType(appealTypeId);
|
||||
|
||||
return caseTypeAliases[normalised] || normalised;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const specialistProcessStageTypeByCaseType = {
|
||||
RIGHTS_OF_WAY_SCHEDULE_14: {
|
||||
// TODO: replace these with the real CRM option-set values
|
||||
846040000: "RIGHTS_OF_WAY_ORDERS",
|
||||
846040100: "RIGHTS_OF_WAY_ORDERS",
|
||||
846040001: "RIGHTS_OF_WAY_ORDERS",
|
||||
846040101: "RIGHTS_OF_WAY_ORDERS",
|
||||
846040002: "RIGHTS_OF_WAY_SCHEDULE_14",
|
||||
846040102: "RIGHTS_OF_WAY_SCHEDULE_14",
|
||||
846040003: "REQUESTS_FOR_DIRECTION",
|
||||
846040103: "REQUESTS_FOR_DIRECTION"
|
||||
}
|
||||
};
|
||||
|
||||
export function mapSpecialistProcessStageType(caseTypeKey, specialistProcess) {
|
||||
return specialistProcessStageTypeByCaseType[caseTypeKey]?.[
|
||||
specialistProcess
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function normalizeSpecialistProcess(source) {
|
||||
return (
|
||||
source?.pinswg_specialistcaseprocess ||
|
||||
source?.pinswg_speacialistcaseprocess ||
|
||||
""
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { getRepsFromBlob } from "../../actions/services/documentService";
|
||||
import { getBasicSearch } from "../../actions/services/searchService";
|
||||
import { consoleLogger } from "../../actions/core/logger";
|
||||
import { normalizeSpecialistProcess } from "../domain/case-lifecycle";
|
||||
import {
|
||||
getFormCollectionByID,
|
||||
getSearchDetails
|
||||
@@ -280,16 +281,9 @@ export const loadNewRepresentation = async ({ store, ctx, bootstrap }) => {
|
||||
"currentType": "myRepresentations",
|
||||
"incidentid": searchResultsObj?.value?.[0]?.incidentid,
|
||||
"appealType": searchResultsObj?.value?.[0]?.pinswg_appealcasetype,
|
||||
"specialistProcess":
|
||||
"specialistProcess": normalizeSpecialistProcess(
|
||||
searchDetailsObj?.[0]?.value?.[0]
|
||||
?.pinswg_speacialistcaseprocess != null ||
|
||||
searchDetailsObj?.[0]?.value?.[0]
|
||||
?.pinswg_specialistcaseprocess != null
|
||||
? searchDetailsObj?.[0]?.value?.[0]
|
||||
.pinswg_speacialistcaseprocess ||
|
||||
searchDetailsObj?.[0]?.value?.[0]
|
||||
.pinswg_specialistcaseprocess
|
||||
: ""
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user