20 KiB
Case Status Tab Stage Display Characterization
1. Purpose
This report characterizes how the portal determines and renders the visual case progress journey shown on the Case Status tab.
Scope is limited to the case status tab and directly related stage-display behaviour.
This is discovery/documentation only. It does not propose implementation changes, refactoring, field normalization, or new domain policy.
2. Business context
statuscodeis the standard CRM/Dataverse Incident status field.pinswg_casestageis a PEDW custom field used to represent more detailed PEDW case-stage information than the standard CRM status model alone provides.- The portal presents visual case progression to users through the Case Status tab.
- The same broader area also exposes status/stage information elsewhere, including summary/details and search-result consumers.
- Current repository evidence shows these consumers do not all use the same source field or the same interpretation model.
3. Status tab rendering flow
3.1 CRM inputs fetched on case page load
The public case page loader in pages/case/[ticketnumber].js fetches:
searchResultsObjviagetBasicSearch(...)searchDetailsObjviagetSearchDetails(searchResultsObj)
Relevant CRM-derived inputs then get stored into currentView.caseReference using setCurrentReference(...):
statuscodeticketnumberincidentidpinswg_appealcasetypeasappealType- normalized specialist process from
searchDetailsObj[0].value[0]
Code reference:
pages/case/[ticketnumber].js:213-229
This means the status-tab journey is seeded primarily from:
- CRM
statuscode - CRM appeal type
- CRM specialist-process fields after portal-side normalization
3.2 Specialist-process normalization before rendering
pages/case/[ticketnumber].js calls:
normalizeSpecialistProcess(searchDetailsObj[0].value[0]);
normalizeSpecialistProcess(...) resolves specialist process by preferring:
pinswg_specialistcaseprocesspinswg_speacialistcaseprocess- empty string fallback
Code references:
pages/case/[ticketnumber].js:225-227lib/domain/case-lifecycle/normalizeSpecialistProcess.js:1-7
This is a CRM compatibility step, not a semantic reinterpretation step.
3.3 Case summary hosts the Case Status tab
components/case/summary.js owns the tab structure and mounts the status-tab component.
- The tab key is
case-status - The tab panel renders
<StatusDetails />
Code evidence:
components/case/summary.jscontains the selected tab statewhichTab == "case-status"components/case/summary.jsrenders<StatusDetails />
3.4 StatusDetails builds the stage journey
components/case/status.js reads from Redux/currentView and invokes:
getLifecycleStagesForCase(
props.currentView?.caseReference?.appealType,
props.currentView?.caseReference?.statuscode,
specialistProcess
);
Important points:
- The second argument is named
currentStageIdin the lifecycle seam, but the actual caller passes CRMstatuscode. - The component does not use
pinswg_casestage. specialistProcessis read again fromprops.searchResultsObj?.searchDetailsObj[0]?.value[0]?.pinswg_specialistcaseprocess, i.e. only the canonical field in this component.
Code reference:
components/case/status.js:26-36
3.5 Lifecycle seam delegates to legacy stage-catalogue helper
getLifecycleStagesForCase(...) is a thin wrapper:
return getStagesForAppealType(caseType, currentStageId, specialistProcess);
Code reference:
lib/domain/case-lifecycle/getLifecycleStagesForCase.js:1-9
This confirms the lifecycle seam is currently a read-only indirection layer, not an independently owned lifecycle engine.
3.6 Stage catalogue selection
getStagesForAppealType(...) in components/case/summary/utils/caseStagesByAppealType.js:
- resolves a stage key using
getStageCaseTypeKey(...) - loads the stage array with
getStagesForCaseTypeKey(...) - finds the current stage index with
getLifecycleStageIndex(...) - marks each stage
complete,in-progress, ornot-startedwithgetLifecycleStageStatus(...)
Code references:
components/case/summary/utils/caseStagesByAppealType.js:779-796
3.7 Stage journey rendering
components/case/status.js renders the returned stage array as a GOV.UK task-list style visual journey:
- each stage renders translated
titleKey - each stage can show
descriptionKeyin expandable details - each stage gets a status tag:
completein-progressnot-startedblockedis defined in the map, though current lifecycle helper path does not produce it
Code references:
components/case/status.js:9-21components/case/status.js:80-137
Rendering-flow summary
CRM data flow to rendered journey is:
CRM incident/search query
-> searchResultsObj.statuscode
-> searchResultsObj.pinswg_appealcasetype
-> searchDetailsObj specialist-process field(s)
-> setCurrentReference(...) on case page
-> StatusDetails reads currentView.caseReference
-> getLifecycleStagesForCase(...)
-> getStagesForAppealType(...)
-> getStageCaseTypeKey(...)
-> getStagesForCaseTypeKey(...)
-> getLifecycleStageIndex(...)
-> getLifecycleStageStatus(...)
-> rendered task-list stage journey
4. Field role assessment
statuscode
Where used
- stored into
currentView.caseReferenceon case page load (pages/case/[ticketnumber].js) - used as the current stage input for
getLifecycleStagesForCase(...)(components/case/status.js) - displayed directly as formatted status labels in search results, summary-type components, my portal, and admin/reporting consumers
- mapped in
components/utils/caseStagesObj.js
Purpose
- operational CRM status field
- primary input for status-tab current-stage resolution
- direct formatted label source for many non-status-tab consumers
Display impact
- determines which step in the visual stage journey is marked as current/completed/future
- also appears directly as human-readable status labels outside the journey UI
pinswg_casestage
Where used
- displayed in
components/case/summary.jsas a summary row labelledsummary-case-stage-label
Purpose
- PEDW custom case-stage field
- summary/details informational display field
Display impact
- affects the summary/details panel only in the reviewed path
- does not drive the Case Status tab journey in the current implementation
Appeal type fields
Where used
pinswg_appealcasetypestored asappealTypeincurrentView.caseReference- mapped through
mapAppealType(...) - used to choose the stage catalogue and, in some cases, specialist-process override logic
Purpose
- primary selector of the stage-journey family/catalogue
Display impact
- determines which stage list the user sees
- appeal types can share or diverge in stage journeys depending on mappings/aliases
Specialist process fields
Where used
- normalized in
pages/case/[ticketnumber].jsfrom canonical/misspelled CRM fields - re-read in
components/case/status.jsfrom canonical field only - passed to
getStageCaseTypeKey(...) - used by
mapSpecialistProcessStageType(...)for stage-journey override
Purpose
- refine stage-journey selection for selected appeal types, especially Rights of Way variants
Display impact
- can switch the status-tab journey from one stage catalogue to another
- affects selection, not just labels
Closed/terminal status fields
Where used
isClosedCaseStatus(...)treats these values as closed/terminal:100056846040013846040060846040059
Purpose
- fallback mechanism when the current numeric input does not exactly match a stage in the selected catalogue
Display impact
- if the supplied
statuscodeis considered closed, current stage resolves to the stage whosetitleKey === "case-closed" - otherwise unmatched values produce
currentIndex === -1, making all stages render asnot-started
5. Stage journey selection
5.1 Appeal-type selection
The portal selects a stage journey first through mapAppealType(...) and caseTypeKeyByAppealTypeId.
Examples:
846040000->PLANNING_S78846040010->CALL_INS846040018->ADVERTS846040019->COMPULSORY_PURCHASE_ORDERS846040002->SIP846040011->DNS
Code reference:
lib/domain/case-lifecycle/mapAppealType.js:35-80
5.2 Appeal-family / alias behaviour
Some stage journeys are shared by alias or indirection rather than unique arrays.
Examples:
SIPresolves by string indirection toDNSCONDITIONS_73_79,LBCAC,LDCS,PLANNING_OBLIGATIONS_S106,PRIOR_NOTIFICATIONall sharePLANNING_S78ENFORCEMENT_LISTED_BUILDINGandMAINTENANCE_OF_LANDshareENFORCEMENT
This is handled by:
- alias mapping in
mapAppealType.js - string indirection in
getStagesForCaseTypeKey.js
5.3 Specialist-process selection
getStageCaseTypeKey(...) only applies specialist-process override when the numeric appeal-type mapping is the one in play:
caseTypeKeyByAppealTypeId[appealTypeId] === mappedCaseType
? mapSpecialistProcessStageType(mappedCaseType, specialistProcess)
: undefined;
Current override table only exists for RIGHTS_OF_WAY_SCHEDULE_14, mapping specialist-process values into:
RIGHTS_OF_WAY_ORDERSRIGHTS_OF_WAY_SCHEDULE_14REQUESTS_FOR_DIRECTION
Code references:
lib/domain/case-lifecycle/getStageCaseTypeKey.js:9-25lib/domain/case-lifecycle/mapSpecialistProcessStageType.js:1-19
5.4 Fallback behaviour
Fallback order in getStageCaseTypeKey(...) is:
- specialist-process-derived stage key
- mapped appeal type key
- alias lookup from normalized input
- normalized raw input
Then getStagesForCaseTypeKey(...):
- returns
[]when no entry exists - resolves string indirection to another catalogue
- otherwise returns the array directly
Selection classification
Selection appears mixed:
- CRM-driven: because appeal type and specialist process are CRM inputs
- presentation-driven: because the output is selection of a visual journey catalogue with title/description text
- business-driven: partially, because different appeal types and specialist processes are clearly treated as meaningfully different journeys
- not purely CRM status display: because the portal constructs a catalogue-driven journey rather than simply displaying a single CRM status value
6. Current stage resolution
6.1 Exact status matching
getLifecycleStageIndex(...) converts the incoming current value to a number and tries to find a stage whose stageId exactly matches it.
const exactIndex = stages.findIndex(
(stage) => Number(stage.stageId) === currentStageNumber
);
This means the status-tab journey currently treats the passed statuscode as the numeric identifier to align with stage-catalogue stageId values.
6.2 Closed-stage fallback
If exact matching fails, isClosedCaseStatus(...) is consulted.
If true, current index becomes the index of the stage whose titleKey is case-closed.
This is a special fallback path for terminal/closed values rather than exact stage matching.
6.3 Unmatched status handling
If there is no exact match and the value is not recognized as closed, getLifecycleStageIndex(...) returns -1.
Then getLifecycleStageStatus(...) marks all stages as not-started.
That is important characterized behaviour.
6.4 Completed/current/future stage calculation
getLifecycleStageStatus(index, currentIndex) returns:
completewhenindex < currentIndexin-progresswhenindex === currentIndexnot-startedwhenindex > currentIndexnot-startedfor all stages whencurrentIndex === -1
6.5 Blocked behaviour
components/case/status.js includes blocked style/text mappings, but the lifecycle calculation path reviewed here never returns blocked.
This suggests either legacy/future intent or another non-reviewed path, but not active current-stage calculation from the status-tab helper chain.
7. Consumer comparison
Case Status Tab
Displays:
- stage journey: yes
- CRM status: indirectly, as the input driving current stage selection
- custom case stage (
pinswg_casestage): no - formatted labels: yes, but as translated stage titles/descriptions and progress tags, not CRM formatted status labels
- derived values: yes, complete/in-progress/not-started status is portal-derived
Primary inputs:
statuscode- appeal type
- specialist process
Case Details / Summary
Displays:
- CRM status: yes, via summary-type components using
statuscode@OData.Community.Display.V1.FormattedValue - custom case stage: yes, in
components/case/summary.jsviapinswg_casestage@OData.Community.Display.V1.FormattedValue - stage journey: yes, but only through the separate Case Status tab component
- formatted labels: yes
- derived values: limited; mostly direct display of CRM formatted values
Important distinction:
- summary/details panel uses
pinswg_casestagedirectly for one row - status tab uses
statuscode-driven stage-journey derivation
Search Results
Displays:
- CRM status: yes, direct formatted value via
statuscode@OData.Community.Display.V1.FormattedValue - custom case stage: no evidence in reviewed search results path
- stage journey: no
- formatted labels: yes
- derived values: no stage derivation; mostly direct CRM-formatted display
Additional role:
- clicking a search result seeds
currentView.caseReference.statuscode, appeal type, and specialist process for downstream case-page/status-tab behaviour
8. Divergence catalogue
Divergence 1: Status tab uses statuscode, summary row uses pinswg_casestage
- Behaviour: summary/details can display PEDW custom case stage while the Case Status tab derives its journey from
statuscode. - Likely reason: summary row is informational display of a custom PEDW field; status tab is a portal-constructed journey over a separate stage catalogue.
- Appears intentional? No / unclear. It is clearly implemented, but the repository does not prove this distinction is consciously documented as an intentional model boundary.
Divergence 2: Status tab renders derived journey; search results render direct formatted status label
- Behaviour: search results show
statuscode@OData.Community.Display.V1.FormattedValue, while the case status tab shows a multi-step journey. - Likely reason: search results need compact list display; case page gives richer progress visualization.
- Appears intentional? Yes, at presentation level.
Divergence 3: Specialist-process normalization differs between case page and status tab component
- Behaviour:
- case page uses
normalizeSpecialistProcess(...)with canonical + misspelled fallback components/case/status.jsreads onlypinswg_specialistcaseprocess
- case page uses
- Likely reason: partial refactor / mixed old-new access patterns.
- Appears intentional? No based on current evidence.
Divergence 4: Summary/status consumers display different concepts under similar “status/stage” area
- Behaviour: summary-type components use formatted
statuscode; summary row separately usespinswg_casestage; status tab ignorespinswg_casestageand derives stage journey fromstatuscode. - Likely reason: mixture of CRM operational status display and PEDW custom stage display without one unified presentation model.
- Appears intentional? Unclear.
Divergence 5: Closed-state handling is explicit in journey logic but not equivalent in direct status consumers
- Behaviour: status tab has a special closed-case fallback through
isClosedCaseStatus(...); direct status-label consumers simply display CRM formatted values. - Likely reason: journey UI requires a terminal visual state even when exact stage IDs do not match.
- Appears intentional? Probably yes for the journey component, though the exact closed-value set may be compatibility-driven.
Divergence 6: Unmatched status values collapse whole journey to not-started
- Behaviour: if a
statuscodedoes not match any stage ID and is not considered closed, every stage renders asnot-started. - Likely reason: simple index-based implementation with no fallback “unknown current stage” state.
- Appears intentional? No / unclear; more likely inherited behaviour.
9. Characterization test candidates
High Priority
-
Case page seeds status tab inputs correctly
- protect that
statuscode, appeal type, and normalized specialist process are placed intocurrentView.caseReference
- protect that
-
Stage catalogue selection by appeal type
- representative coverage for shared and distinct catalogues (e.g. S78, Call-in, DNS, Advert, CPO)
-
Specialist-process override selection
- protect current Rights of Way specialist-process remapping into alternate stage journeys
-
Exact status-to-stage matching
- prove that matching
statuscodeselects the expected current stage index
- prove that matching
-
Closed-case fallback behaviour
- prove unmatched closed values map to
case-closed
- prove unmatched closed values map to
-
Unmatched non-closed status behaviour
- prove all stages become
not-started
- prove all stages become
-
Status tab ignores
pinswg_casestage- protect current separation between status-tab journey logic and custom case-stage summary display
Medium Priority
- String vs numeric appeal type handling in stage selection
- Alias/indirection resolution
- e.g.
SIP -> DNS,ENFORCEMENT_LISTED_BUILDING -> ENFORCEMENT
- e.g.
- Specialist-process normalization divergence coverage
- protect current canonical vs misspelled field handling at case-page input stage
- Representative summary/status/search comparison tests
- prove summary row uses
pinswg_casestage, search uses formattedstatuscode, status tab uses derived journey
- prove summary row uses
Low Priority
- Expand-all/default-open UI behaviour on the status tab
- Presence/absence of description keys in journey rendering
- Translation-key coverage for representative stage catalogues
10. Initial conclusion
Answer: C. Portal-constructed visual journey based on CRM inputs
Supporting evidence
- The Case Status tab does not directly display
pinswg_casestage. - It does not simply render CRM formatted
statuscodelabels. - Instead, it uses CRM
statuscode, appeal type, and specialist process to select a portal-owned stage catalogue and then derive progress state across that catalogue. - The selected catalogue contains portal presentation metadata (
titleKey,descriptionKey) and is rendered as a visual journey/task list. pinswg_casestageis displayed elsewhere in summary/details as a separate PEDW custom field, which further shows that the Case Status tab is a constructed layer rather than a simple raw-field display.
Therefore the Case Status tab is best understood as:
a portal-constructed visual journey that uses CRM operational inputs and portal stage catalogues to present case progression to the user.
It is not a pure CRM status display, and it is not currently a pure PEDW custom case-stage display either.
11. Recommended next step
Keep the next slice narrow and characterization-focused.
The strongest follow-on is:
- characterize summary/details/search divergence around:
statuscodepinswg_casestage- formatted labels
- specialist-process field usage
Reason:
- this slice established how the Case Status tab works internally
- the remaining ambiguity now sits at the cross-consumer boundary, especially where summary/details and search display different lifecycle/status concepts from the status-tab journey