## 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