## 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
- `statuscode` is the standard CRM/Dataverse Incident status field.
- `pinswg_casestage` is 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:
1. `searchResultsObj` via `getBasicSearch(...)`
2. `searchDetailsObj` via `getSearchDetails(searchResultsObj)`
Relevant CRM-derived inputs then get stored into `currentView.caseReference` using `setCurrentReference(...)`:
- `statuscode`
- `ticketnumber`
- `incidentid`
- `pinswg_appealcasetype` as `appealType`
- 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:
```js
normalizeSpecialistProcess(searchDetailsObj[0].value[0]);
```
`normalizeSpecialistProcess(...)` resolves specialist process by preferring:
1. `pinswg_specialistcaseprocess`
2. `pinswg_speacialistcaseprocess`
3. empty string fallback
Code references:
- `pages/case/[ticketnumber].js:225-227`
- `lib/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 ``
Code evidence:
- `components/case/summary.js` contains the selected tab state `whichTab == "case-status"`
- `components/case/summary.js` renders ``
#### 3.4 StatusDetails builds the stage journey
`components/case/status.js` reads from Redux/currentView and invokes:
```js
getLifecycleStagesForCase(
props.currentView?.caseReference?.appealType,
props.currentView?.caseReference?.statuscode,
specialistProcess
);
```
Important points:
- The second argument is named `currentStageId` in the lifecycle seam, but the actual caller passes **CRM `statuscode`**.
- The component does **not** use `pinswg_casestage`.
- `specialistProcess` is read again from `props.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:
```js
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`:
1. resolves a stage key using `getStageCaseTypeKey(...)`
2. loads the stage array with `getStagesForCaseTypeKey(...)`
3. finds the current stage index with `getLifecycleStageIndex(...)`
4. marks each stage `complete`, `in-progress`, or `not-started` with `getLifecycleStageStatus(...)`
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 `descriptionKey` in expandable details
- each stage gets a status tag:
- `complete`
- `in-progress`
- `not-started`
- `blocked` is defined in the map, though current lifecycle helper path does not produce it
Code references:
- `components/case/status.js:9-21`
- `components/case/status.js:80-137`
#### Rendering-flow summary
CRM data flow to rendered journey is:
```text
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.caseReference` on 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.js` as a summary row labelled `summary-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_appealcasetype` stored as `appealType` in `currentView.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].js` from canonical/misspelled CRM fields
- re-read in `components/case/status.js` from 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:
- `1000`
- `5`
- `6`
- `846040013`
- `846040060`
- `846040059`
**Purpose**
- fallback mechanism when the current numeric input does not exactly match a stage in the selected catalogue
**Display impact**
- if the supplied `statuscode` is considered closed, current stage resolves to the stage whose `titleKey === "case-closed"`
- otherwise unmatched values produce `currentIndex === -1`, making all stages render as `not-started`
---
### 5. Stage journey selection
#### 5.1 Appeal-type selection
The portal selects a stage journey first through `mapAppealType(...)` and `caseTypeKeyByAppealTypeId`.
Examples:
- `846040000` -> `PLANNING_S78`
- `846040010` -> `CALL_INS`
- `846040018` -> `ADVERTS`
- `846040019` -> `COMPULSORY_PURCHASE_ORDERS`
- `846040002` -> `SIP`
- `846040011` -> `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:
- `SIP` resolves by string indirection to `DNS`
- `CONDITIONS_73_79`, `LBCAC`, `LDCS`, `PLANNING_OBLIGATIONS_S106`, `PRIOR_NOTIFICATION` all share `PLANNING_S78`
- `ENFORCEMENT_LISTED_BUILDING` and `MAINTENANCE_OF_LAND` share `ENFORCEMENT`
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:
```js
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_ORDERS`
- `RIGHTS_OF_WAY_SCHEDULE_14`
- `REQUESTS_FOR_DIRECTION`
Code references:
- `lib/domain/case-lifecycle/getStageCaseTypeKey.js:9-25`
- `lib/domain/case-lifecycle/mapSpecialistProcessStageType.js:1-19`
#### 5.4 Fallback behaviour
Fallback order in `getStageCaseTypeKey(...)` is:
1. specialist-process-derived stage key
2. mapped appeal type key
3. alias lookup from normalized input
4. 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.
```js
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:
- `complete` when `index < currentIndex`
- `in-progress` when `index === currentIndex`
- `not-started` when `index > currentIndex`
- `not-started` for all stages when `currentIndex === -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.js` via `pinswg_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_casestage` directly 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.js` reads only `pinswg_specialistcaseprocess`
- **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 uses `pinswg_casestage`; status tab ignores `pinswg_casestage` and derives stage journey from `statuscode`.
- **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 `statuscode` does not match any stage ID and is not considered closed, every stage renders as `not-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
1. **Case page seeds status tab inputs correctly**
- protect that `statuscode`, appeal type, and normalized specialist process are placed into `currentView.caseReference`
2. **Stage catalogue selection by appeal type**
- representative coverage for shared and distinct catalogues (e.g. S78, Call-in, DNS, Advert, CPO)
3. **Specialist-process override selection**
- protect current Rights of Way specialist-process remapping into alternate stage journeys
4. **Exact status-to-stage matching**
- prove that matching `statuscode` selects the expected current stage index
5. **Closed-case fallback behaviour**
- prove unmatched closed values map to `case-closed`
6. **Unmatched non-closed status behaviour**
- prove all stages become `not-started`
7. **Status tab ignores `pinswg_casestage`**
- protect current separation between status-tab journey logic and custom case-stage summary display
#### Medium Priority
1. **String vs numeric appeal type handling in stage selection**
2. **Alias/indirection resolution**
- e.g. `SIP -> DNS`, `ENFORCEMENT_LISTED_BUILDING -> ENFORCEMENT`
3. **Specialist-process normalization divergence coverage**
- protect current canonical vs misspelled field handling at case-page input stage
4. **Representative summary/status/search comparison tests**
- prove summary row uses `pinswg_casestage`, search uses formatted `statuscode`, status tab uses derived journey
#### Low Priority
1. **Expand-all/default-open UI behaviour on the status tab**
2. **Presence/absence of description keys in journey rendering**
3. **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 `statuscode` labels.
- 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_casestage` is 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:
1. characterize **summary/details/search divergence** around:
- `statuscode`
- `pinswg_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