208 KiB
Journey Architecture Map
This document is a maintainability-focused architecture map for two PEDW journeys:
- Public Search → Case Details
- My Portal Dashboard
It is a discovery-only slice.
It does not recommend refactor, implementation, API, state, or folder changes.
1. Public Search → Case Details
Purpose
Supports anonymous public discovery of published cases and transition from result browsing into a single case detail view.
For maintainers, this journey also includes the optional signed-in enrichment path where case detail pages preload account context for watch/portal-adjacent interactions.
Primary Entry Points
pages/searchresults.jscomponents/search/searchresults.jspages/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.js- adjacent breadcrumb/state context:
components/breadcrumbs.jsstore/currentView/*
Loaders / Initialisation
Search results page
pages/searchresults.jsgetServerSidePropsdoes light bootstrap only- captures request IP via
getIP(req) - derives linked-case mode from
query.lk - reads feature flags:
SHOWLOGINSHOWREPRESENTATIONS
- dispatches:
setShowReps(showReps, showLoginCheck)setSearch(query?.q || "")
Important maintainer note:
- This loader does not fully hydrate search results itself.
- Result retrieval is substantially driven in the client/component layer by
components/search/searchresults.jsvia paged service calls.
Search result data bootstrap
components/search/searchresults.js- reads
searchResultsObjandsearchDetailsObjfrom Redux - derives pagination from
@odata.nextLink - for paging/sorting calls:
getBasicSearchPaged(...)getAdvancedSearchPaged(...)
- after each result fetch, hydrates detail data with:
getSearchDetailsPaged(...)
- dispatches:
setSearchResults(...)setSearchDetails(...)setCurrentPage(...)
- reads
Case details page
pages/case/[ticketnumber].jsgetServerSidePropsis the main loader for the detail page- captures request IP via
getIP(req) - optionally resolves signed-in portal context:
getSession(ctx)getPortalLogin(session.user.email)getPersonalAccount(contactid)setAccountDetails(accountDetails)
- normalises the case identifier from route param to search form
- retrieves the case through the search family, not a standalone case-by-ticket API:
getBasicSearch(developmentQuery)getSearchDetails(searchResultsObj)
- conditionally retrieves SIPS-specific enrichments:
getSIPSEvents(...)getSIPSMedia(...)
- retrieves messages:
getCaseMessage(incidentid)
- dispatches:
setSearch(developmentQuery)setSearchResults(searchResultsObj)setSearchDetails(searchDetailsObj)setEventDetails(eventsObj)when relevantsetMediaDetails(mediaObj)when relevantsetCurrentReference({...})
- redirects to
/404if the search resolves to zero or multiple matches
State Ownership
Primary state slices
-
store/search/reducer.js- owns
searchString - used as the retained current search input across search/case navigation
- owns
-
store/searchOutput/reducer.js- owns:
searchResultsObjsearchDetailsObjdocumentDetailsObjrepresentationsObjeventDetailsObjmediaDetailsObj
- this is the main read model for both results and case-detail rendering
- owns:
-
store/currentView/reducer.js- owns:
caseReferencecurrentPageshowRepsshowLoginlinkedCaseReferenceslocale
caseReferenceis the key bridge from result selection into detail context
- owns:
-
store/accountDetails/reducer.js- only participates when a user is signed in on the case detail route
- owns signed-in account context:
accountDetailsloggedinUserIdcontainerID
-
store/watchedCases/reducer.js- participates when signed-in users watch/unwatch or manage email notifications from results
- owns:
watchedCaseswatchedCasesDetails
currentView usage
-
components/search/searchresults.js- sets
currentReferenceon case link click - updates
currentPageduring pagination/sort
- sets
-
pages/case/[ticketnumber].js- sets canonical case reference context for the detail page
-
components/breadcrumbs.js- uses
currentView.caseReferenceto reconstruct breadcrumb state and origin context
- uses
accountDetails usage
- Not required for anonymous search or basic case reading
- Used for optional signed-in enrichment on case pages and watchlist interactions in result views
Service Layer
Primary services
-
actions/services/searchDirectService.jsgetBasicSearch(...)getBasicSearchPaged(...)getAdvancedSearchPaged(...)getBasicSearchDetails(...)getBasicSearchDetailsPaged(...)getSearchDocumentDetails(...)getLinkedCases(...)
-
actions/services/caseDirectService.jsgetCaseMessage(...)getCase(...)getCaseByID(...)getSIPSEvents(...)getSIPSMedia(...)getPortalModuleDetails(...)for adjacent case-detail enrichment patterns
-
actions/services/accountDirectService.jsgetPortalLogin(...)getPersonalAccount(...)- only used on the optional signed-in branch of case detail bootstrap
Supporting maintainability helper
components/utils/index.jsgetSearchDetails(searchResultsObj)- expands result records into case-type-specific detail queries using
collections.json - this is a key maintainability join point because it converts generic search rows into richer case detail payload lookups
API Layer
Principal route families
-
Public search reads
pages/api/endpoint/getbasicsearch_api.jspages/api/endpoint/getbasicsearchpaged_api.jspages/api/endpoint/getadvancedsearch_api.jspages/api/endpoint/getadvancedsearchpaged_api.js
-
Search detail expansion / supporting reads
pages/api/endpoint/getbasicsearchdetails_api.jspages/api/endpoint/getbasicsearchdetailspaged_api.jspages/api/endpoint/getlinkedcases_api.js
-
Case-specific reads
pages/api/endpoint/getcase_api.jspages/api/endpoint/getcasebyid_api.jspages/api/endpoint/getcasemessage_api.jspages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.js
-
Signed-in account bootstrap on the case page
pages/api/endpoint/getportallogin_api.jspages/api/endpoint/getpersonalaccount_api.js
Route-family characteristics
-
getbasicsearchpaged_api.js- helper-oriented CRM relay read
- validates
searchString,orderby,fieldSort,showNumberOfRecords - uses
relayGet(...) - uses
RELAY_POLICY_SEARCH_PAGED - normalises
@odata.nextLink
-
getcase_api.js- narrow CRM relay read by
incidentID - used as a supporting case lookup shape, though the public ticketnumber route primarily bootstraps via search
- narrow CRM relay read by
Integration Boundaries
-
CRM via Azure Relay
- primary data source for search results, search detail expansion, case messages, case records, SIPS events, and SIPS media
- touched because the journey is fundamentally a public case-discovery/read flow
-
NextAuth
- touched only on the optional signed-in branch of
pages/case/[ticketnumber].js - used to derive current session and then CRM contact context
- touched only on the optional signed-in branch of
-
Azure Storage
- not part of the core public search → case details path in this slice
-
Azure Queue
- not touched
-
GOV.UK Notify
- not touched by the core read path
-
Local-only processing
- Redux hydration and page state transitions
- breadcrumb/view-state persistence
- search result highlighting, sorting state, pagination state, and result/detail joining logic in the frontend
Architectural Flow
Public search results
User
→ pages/searchresults.js
→ Redux bootstrap (setSearch, setShowReps)
→ components/search/searchresults.js
→ searchDirectService.getBasicSearchPaged / getAdvancedSearchPaged
→ pages/api/endpoint/getbasicsearchpaged_api.js / related search routes
→ relayGet(...)
→ Azure Relay
→ Dynamics 365 CRM
Transition to case details
User
→ case link click in components/search/searchresults.js
→ Redux setCurrentReference(...)
→ pages/case/[ticketnumber].js SSR loader
→ searchDirectService.getBasicSearch(...) + components/utils.getSearchDetails(...)
→ supporting caseDirectService calls for messages / events / media
→ endpoint route family
→ Azure Relay
→ Dynamics 365 CRM
Optional signed-in enrichment
User session
→ getSession(ctx)
→ accountDirectService.getPortalLogin(email)
→ accountDirectService.getPersonalAccount(contactid)
→ Redux accountDetails
→ watchlist/account-aware UI behavior
Change Entry Set
First files to inspect
pages/searchresults.jscomponents/search/searchresults.jspages/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jsactions/services/searchDirectService.jsactions/services/caseDirectService.jscomponents/utils/index.jsstore/search/reducer.jsstore/searchOutput/reducer.jsstore/currentView/reducer.js
Likely adjacent files
pages/api/endpoint/getbasicsearch_api.jspages/api/endpoint/getbasicsearchpaged_api.jspages/api/endpoint/getbasicsearchdetails_api.jspages/api/endpoint/getbasicsearchdetailspaged_api.jspages/api/endpoint/getcase_api.jspages/api/endpoint/getcasemessage_api.jspages/api/endpoint/getlinkedcases_api.jspages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.jsstore/accountDetails/reducer.jsstore/watchedCases/reducer.jscomponents/breadcrumbs.js
Highest-risk areas
- Search contract shape and pagination assumptions (
searchResultsObj,@odata.nextLink) - Result-to-detail expansion in
components/utils/index.js currentView.caseReferenceas the navigation/breadcrumb handoff- Case loader assumption that ticketnumber resolves uniquely through the search family
- Optional signed-in account bootstrap on a nominally public page
- Watched-case side interactions embedded in search results
Risk Classification
High
Reasoning:
- public-facing and contract-sensitive
- spans multiple read families rather than a single dedicated case-by-route loader
- combines SSR and client-driven hydration patterns
- includes subtle state handoff through Redux rather than only route params
- optional signed-in behavior adds a second identity/bootstrap branch maintainers must understand
2. My Portal Dashboard
Purpose
Supports the authenticated portal landing experience for a signed-in user or LPA user by presenting:
- my cases
- watched cases
- draft/awaiting-submission items
- representation draft lists
- submitted representation-related cards
- account-contextual portal entry actions
This journey is the main authenticated dashboard bootstrap for portal-owned and draft-owned work.
Primary Entry Points
pages/myportal/index.jscomponents/myportal.jscomponents/myportal/mycases.jscomponents/myportal/watchedcases.jscomponents/myportal/myrepresentations.jscomponents/myportal/mysubmittedrepresentations.jscomponents/myportal/awaitingsubmissionfromblob.jscomponents/myportal/topthree.jscomponents/myportal/viewall.js
Loaders / Initialisation
Dashboard page loader
pages/myportal/index.js- is the principal authenticated bootstrap for the dashboard
- requires
getSession(ctx) - redirects to
/auth/signinwhen the session or session identity is absent - resolves CRM contact identity via:
getPortalLogin(thisSession.user.email)
- resolves account record via:
getPersonalAccount(loggedInUser)
- creates or ensures user storage container via:
createContainerProxy(thisSession.user.id)
- branches between user and LPA case retrieval:
getMyCases(loggedInUser)getMyLPACases(lpaId)
- retrieves mixed-source dashboard datasets:
getRepsFromBlob(thisSession.user.id)getWatchedCases(loggedInUser)getAwaitingSubmissionFromBlob(thisSession.user.id)
- classifies watchlist output using:
splitWatchedCasesBySubmissionState(watchedCases.value)
- derives detail cards for multiple lists through bounded parallel
getPortalModuleDetails(...)calls - stores locale and feature flags:
setLocale(locale)setShowReps(showReps, showLoginCheck)
Dashboard detail expansion
- inside
pages/myportal/index.js, localgetDetails(...)- determines per-list case reference form
- maps each dashboard record to
getPortalModuleDetails(collectionName, caseID) - this is a key aggregation step because it turns list rows into card/detail-ready case-specific data
Client-side dashboard navigation state
-
components/myportal/topthree.js- sets current case reference before navigating into detail or resume flows
- refreshes watched cases and awaiting-submission lists after deletion actions
-
components/myportal/viewall.js- derives active list from
currentView.currentView.viewKeyorrouter.query.key - sets
currentViewandcurrentReferencebefore navigating to case detail or representation edit flows
- derives active list from
State Ownership
Primary state slices
-
store/accountDetails/reducer.js- owns:
accountDetailsloggedinUserIdcontainerID
- this slice is the main ownership root for:
- CRM contact identity
- storage container identity
- user display/account context
- owns:
-
store/currentView/reducer.js- owns:
currentViewcaseReferencecurrentPageshowRepsshowLoginlocale
- this slice drives which dashboard sub-view is active and what downstream case/representation context should be used
- owns:
-
store/myCases/reducer.js- owns:
myCasesmyCasesDetails
- owns:
-
store/watchedCases/reducer.js- owns:
watchedCaseswatchedCasesDetails
- owns:
-
store/awaitingSubmission/reducer.js- owns:
awaitingSubmissionawaitingSubmissionDetailsawaitingSubmissionFromBlob
- owns:
-
store/myRepresentations/*- not re-read in full for this slice, but used by
pages/myportal/index.jsas a primary journey state owner for:myRepresentationsmyRepresentationsDetailsmySubmittedRepsmySubmittedRepsDetails
- not re-read in full for this slice, but used by
currentView usage
-
components/myportal/topthree.js- sets
currentReferencebefore opening case/resume routes
- sets
-
components/myportal/viewall.js- uses
currentView.viewKeyto determine whether the page is showing:- my cases
- watched cases
- awaiting submission
- my representations
- submitted reps
- updates
currentViewafter list mutations to keep the dashboard sub-view stable
- uses
-
components/breadcrumbs.js- depends on
currentViewandcaseReferenceto reconstruct myportal-origin breadcrumbs
- depends on
accountDetails usage
-
pages/myportal/index.js- populates it at bootstrap time
-
components/myportal.js- uses it to determine LPA vs non-LPA rendering
- uses user name, involvement type, and associated LPA display
-
components/myportal/topthree.jsandcomponents/myportal/viewall.js- use
loggedinUserIdfor watched-case mutations and refreshes - use
containerIDfor draft/blob deletion and resume pathways
- use
Service Layer
Primary services
-
actions/services/accountDirectService.jsgetPortalLogin(...)getPersonalAccount(...)
-
actions/services/portalDirectService.jsgetMyCases(...)getMyLPACases(...)getWatchedCases(...)getWatchedCasesProxy(...)getAwaitingSubmission(...)getAwaitingSubmissionProxy(...)createWatchedCases(...)deleteWatchedCases(...)
-
actions/services/documentDirectService.jscreateContainerProxy(...)getRepsFromBlob(...)getRepsFromBlobProxy(...)getAwaitingSubmissionFromBlob(...)getAwaitingSubmissionFromBlobProxy(...)deleteAwaitingSubmissionsFromBlob(...)deleteMyRepresentationsFromBlob(...)
-
actions/services/caseDirectService.jsgetPortalModuleDetails(...)getPortalModuleDetailsProxy(...)- used as the detail enrichment layer for dashboard cards and lists
Supporting domain helper
lib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.js- classifies watchlist records into watched cases vs submitted representations
- maintainers should treat this as a journey-shaping policy boundary rather than just display logic
API Layer
Principal CRM-backed routes
pages/api/endpoint/getmycases_api.jspages/api/endpoint/getmylpacases_api.jspages/api/endpoint/getwatchedcases_api.jspages/api/endpoint/getmyrepresentations_api.jspages/api/endpoint/getawaitingsubmission_api.jspages/api/endpoint/getportalmoduledetails_api.jspages/api/endpoint/getpersonalaccount_api.jspages/api/endpoint/getportallogin_api.js
Principal storage-backed routes
pages/api/file/setupcontainer.jspages/api/file/getrepsblob.jspages/api/file/getawaitingsubmissionfromblob.jspages/api/file/getrepsblobproxy.jspages/api/file/getawaitingsubmissionfromblobproxy.jspages/api/file/deleteblobcase.jspages/api/file/deleteblobrep.js
Route-family characteristics
-
getmycases_api.js- helper-oriented CRM relay read
- requires
loggedInUserId - filters incidents by CRM customer/contact ownership
- transforms
titleintopinswg_titlefor downstream consumers
-
getwatchedcases_api.js- helper-oriented CRM relay read
- requires
loggedInUserId - reads watchlist rows and expands watched-case metadata
- flattens nested watched case values into dashboard-friendly fields
-
getawaitingsubmissionfromblob.js- storage/blob read route
- requires
containerandhash - validates signed hash before blob enumeration
- reads draft progress files from Azure Storage
Integration Boundaries
-
NextAuth
- required entry boundary for the dashboard
- used to establish
session.user.emailandsession.user.id
-
CRM via Azure Relay
- used for:
- portal login/contact resolution
- account details
- my cases
- LPA cases
- watched cases
- portal module details
- touched because the dashboard mixes user-owned and relationship-owned business records
- used for:
-
Azure Storage
- used for:
- storage container creation/ensuring
- representation draft blob lists
- awaiting-submission draft lists
- deletion of draft case/representation blobs
- touched because dashboard content includes pre-submission work that is storage-owned rather than CRM-owned
- used for:
-
Azure Queue
- not directly touched by the dashboard landing slice reviewed here
-
GOV.UK Notify
- not part of the dashboard landing bootstrap itself
-
Local-only processing
- Redux hydration for all dashboard slices
- current-view selection
- watched-case classification
- card/list sorting, list merges, and view transitions
Architectural Flow
User
→ pages/myportal/index.js
→ getSession(ctx)
→ accountDirectService.getPortalLogin(email)
→ accountDirectService.getPersonalAccount(contactid)
→ documentDirectService.createContainerProxy(session.user.id)
→ portal/document services fetch CRM-owned and storage-owned lists
→ caseDirectService.getPortalModuleDetails(...) for detail enrichment
→ Redux slices (accountDetails, myCases, watchedCases, myRepresentations, awaitingSubmission, currentView)
→ components/myportal.js and card/list components
→ endpoint/file routes
→ Azure Relay / Azure Storage
Change Entry Set
First files to inspect
pages/myportal/index.jscomponents/myportal.jscomponents/myportal/topthree.jscomponents/myportal/viewall.jsactions/services/accountDirectService.jsactions/services/portalDirectService.jsactions/services/documentDirectService.jsactions/services/caseDirectService.jsstore/accountDetails/reducer.jsstore/currentView/reducer.jsstore/myCases/reducer.jsstore/watchedCases/reducer.jsstore/awaitingSubmission/reducer.js
Likely adjacent files
pages/api/endpoint/getmycases_api.jspages/api/endpoint/getmylpacases_api.jspages/api/endpoint/getwatchedcases_api.jspages/api/endpoint/getmyrepresentations_api.jspages/api/endpoint/getportalmoduledetails_api.jspages/api/endpoint/getportallogin_api.jspages/api/endpoint/getpersonalaccount_api.jspages/api/file/getawaitingsubmissionfromblob.jspages/api/file/getrepsblob.jspages/api/file/setupcontainer.jspages/api/file/deleteblobcase.jspages/api/file/deleteblobrep.jslib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.jscomponents/myportal/mycases.jscomponents/myportal/watchedcases.jscomponents/myportal/myrepresentations.jscomponents/myportal/mysubmittedrepresentations.js
Highest-risk areas
- Session → CRM contact bootstrap via
getPortalLogin(...) - Mixed ownership model:
- CRM-owned lists
- Azure-storage-owned draft lists
- LPA vs non-LPA branching in the page loader
currentView-driven list/view routing assumptions inviewall.js- Watchlist mutation/refresh behavior embedded in dashboard components
- Container identity usage for draft deletion/resume paths
- Detail enrichment fan-out using
getPortalModuleDetails(...)
Risk Classification
High
Reasoning:
- authenticated portal-critical journey
- depends on both identity bootstrap and mixed integration data sources
- mixes CRM-owned and blob-owned records in one page-level loader
- multiple Redux slices must stay aligned for correct downstream navigation
- list cards and view-all pages reuse the same state in several slightly different ways
Investigation Method
Files reviewed
Required context:
context/architecture.mdcontext/api-route-map.mdcontext/integration-map.mdcontext/portal-api-platform-assessment.mdmemory-bank/change-log.md
Guardrails/context discipline:
.clinerules/refactor-branch-rules.mdGUARDRAILS.md
Journey pages and major components:
pages/searchresults.jspages/case/[ticketnumber].jspages/myportal/index.jscomponents/search/searchresults.jscomponents/case.jscomponents/myportal.jscomponents/myportal/topthree.jscomponents/myportal/viewall.js
Supporting services/helpers:
actions/services/searchDirectService.jsactions/services/caseDirectService.jsactions/services/portalDirectService.jsactions/services/documentDirectService.jsactions/services/accountDirectService.jscomponents/utils/index.js
API handlers:
pages/api/endpoint/getbasicsearchpaged_api.jspages/api/endpoint/getcase_api.jspages/api/endpoint/getmycases_api.jspages/api/endpoint/getwatchedcases_api.jspages/api/file/getawaitingsubmissionfromblob.js
Redux ownership files:
store/accountDetails/reducer.jsstore/currentView/reducer.jsstore/search/reducer.jsstore/searchOutput/reducer.jsstore/watchedCases/reducer.jsstore/myCases/reducer.jsstore/awaitingSubmission/reducer.js
Searches performed
pages:getServerSideProps|getInitialPropsstore:currentView|accountDetails|search|myportal|watchedCasesactions/services:getBasicSearchPaged|getAdvancedSearchPaged|getCase\(|getCaseByID|getMyCases|getMyRepresentations|getAwaitingSubmission|getWatchedCaseslib:loadMyPortal|resolveMyPortalAuthContext|search|casecomponents:currentView|accountDetails|getBasicSearch|getCase|getMyCases|getWatchedCases|pinsUser
Limitations
- This slice was intentionally limited to the two requested journeys.
- It did not trace unrelated journeys such as new appeal, representation submission, auth-only flows, or document-download deep paths beyond adjacent references.
- It did not execute the application or produce runtime traces.
- It did not generate a full route inventory.
- It did not deeply inspect every nested component under case detail or myportal once the primary ownership and integration boundaries were established.
- Some adjacent state slices, especially
myRepresentations, were confirmed by usage in the page loader/component layer rather than fully re-read in this slice.
Recommendations
Documentation and understanding only:
- Treat this map as the maintainer-first companion to
context/api-route-map.md. - When changing either journey, start from the journey entry page and confirm the owning Redux slices before reading deeper API files.
- Preserve awareness that both journeys use aggregation patterns rather than single-source page loaders:
- Public Search → Case Details uses search-family reads plus detail expansion.
- My Portal Dashboard uses session/bootstrap plus mixed CRM and Azure Storage sources.
- Keep identity bootstrap and state ownership explicitly documented in future journey maps, because they are as important to maintainability as the page/component structure.
- If this document is extended later, continue documenting by journey and by ownership flow rather than by folder alone.
Slice 2 — Draft Appeal Creation and Appeal Submission / Finalisation
Files Modified
context/journey-architecture-map.mdmemory-bank/change-log.md
Findings
- The appeal lifecycle is split across two closely linked but distinct maintainability shapes:
- Draft Appeal Creation is primarily a storage-owned journey rooted in
session.user.id -> container identity. - Appeal Submission / Finalisation is an orchestration-owned transition from blob-backed draft state into queue-backed and CRM-backed submitted state.
- Draft Appeal Creation is primarily a storage-owned journey rooted in
- The entry pages for both new and resumed appeals converge on the same page shell and flow components:
pages/newappeal/[appealtypes].jspages/myportal/[appealtypes].jscomponents/newappeal/newAppealFlow.js
- The strongest visible ownership model remains:
NextAuth session.user.id -> Azure Storage containerpinsUser cookie / CRM contact -> account context and submitted-case ownership context
- Submission is not a single direct CRM write from the page layer.
It visibly passes through:
- check answers
- PDF generation / finalisation prep
- appeal-complete message route
- Azure Queue message creation
- downstream submitted-record processing
Draft Appeal Journey Map
Purpose
Business purpose:
- allows an authenticated user to begin an appeal, build it over multiple sections, upload supporting files, save progress, exit, and later resume without immediate submission.
Maintainer purpose:
- this journey is the main blob-backed draft lifecycle for appeals and is the clearest place to understand how PEDW uses
session.user.idas storage/container ownership before CRM submission occurs.
Primary Entry Points
pages/newappeal/index.jscomponents/newappeal/createCase.jspages/newappeal/[appealtypes].jspages/myportal/[appealtypes].jscomponents/newappeal/newAppealFlow.jscomponents/newappeal/buildsection.js
Loaders / Initialisation
Draft creation entry
pages/newappeal/index.js- session-gated via
getSession(ctx) - uses
pinsUsercookie as CRM contact identity for account lookup - loads reference data for starting a draft:
getAppealsTypesForNewAppeal()getLPA()getPersonalAccount(loggedInUser)
- dispatches:
setAppealType(...)setLPA(...)setLoggedInUserId(loggedInUser)setAccountDetails(accountDetails)setContainerID(thisSession.user.id)
- session-gated via
Draft section loader
-
lib/newappeal/loadNewAppealPage.js- validates required query params:
appealtypesaptid
- requires
getSession(ctx) - requires
session.user.idandsession.user.email - requires
pinsUsercookie presence - fetches in parallel:
- appeal type reference data
- mandatory fields
- pick lists
getProgressFromBlob(session.user.id, query.id)getPersonalAccount(pinsUser)
- reads form XML through
readFormXml(query.appealtypes)
- validates required query params:
-
pages/newappeal/[appealtypes].js- delegates SSR loading to
loadNewAppealPage(ctx) - hydrates Redux using
hydrateNewAppealStore(...)
- delegates SSR loading to
Draft resume loader
-
lib/myportal/loadMyPortalAppealPage.js- validates required query params:
appealtypesaptcasereference
- requires
getSession(ctx) - requires
pinsUsercookie - fetches in parallel:
- appeal type reference data
- mandatory fields
- pick lists
getFilesFromBlob(session.user.id, query.casereference)getProgressFromBlob(session.user.id, query.casereference)getPersonalAccount(pinsUser)
- conditionally fetches
getAwaitingSubmissionFromBlob(session.user.id)whenquery.keyis present - reads form XML through
readFormXml(query.appealtypes)
- validates required query params:
-
pages/myportal/[appealtypes].js- delegates SSR loading to
loadMyPortalAppealPage(ctx) - hydrates Redux using
hydrateMyPortalAppealStore(...)
- delegates SSR loading to
Draft bootstrap logic
-
lib/newappeal/hydrateNewAppealStore.js- constructs
appealType.caseReferenceas:ticketnumber: query.idincidentid: query.idcaseDetails: blobProgress
- dispatches:
setLoggedInUserId(...)setLoggedInUserEmail(...)setAppealLPA(query.lpa)setAppealTypeID(query.apt)setCaseReference(...)setForm(xmlStr, mandatoryFieldsData, pickListData)setAppealType(appealTypeData)setContainerID(session.user.id)setAccountDetails(accountDetails)
- constructs
-
lib/myportal/hydrateMyPortalAppealStore.js- builds equivalent case reference state for resume mode
- additionally dispatches:
setFilesForAppeal(blobList)setAwaitingSubmissionFromBlob(...)setAwaitingSubmissionDetails(...)setCurrentView({ viewName: "Awaiting Submission", viewKey: "awaitingSubmissionDetails" })when resume is entered from that list context
State Ownership
Primary slices
-
store/appealType/reducer.js- main appeal-journey owner for:
appealTypeOptionsappealTypeIDcurrentSectionappealLPAcaseReferenceformCompletedocumentListfileListfileCountprogress
- main appeal-journey owner for:
-
store/formData/reducer.js- owns:
formDatamandatoryFieldsDatapickListData
- owns:
-
store/accountDetails/reducer.js- owns:
accountDetailsloggedinUserIdloggedinUserEmailcontainerID
containerIDis the strongest visible draft-ownership identifier in the UI/store layer
- owns:
-
store/currentView/reducer.js- supports locale and resume-entry view state
- used less as the main draft owner than in dashboard journeys, but still participates in:
locale- resume-origin context
-
store/awaitingSubmission/reducer.js- participates when a saved draft is resumed from myportal context
currentView usage
hydrateMyPortalAppealStore(...)setscurrentViewwhen the draft resume path originates from awaiting-submission UI- component flows use current section progression through
appealType.currentSectionrather thancurrentView
accountDetails usage
- provides:
- CRM contact identity (
loggedinUserId) - email for partial-save/completion emails (
loggedinUserEmail) - container ownership (
containerID)
- CRM contact identity (
components/newappeal/buildsection.jsandbuildchecksection.jsrely onaccountDetails.containerIDto persist and finalise draft material
Draft ownership state
- draft ownership is represented across:
accountDetails.containerIDappealType.caseReference.ticketnumberappealType.caseReference.caseDetailsappealType.fileList- blob-backed
progress/ file objects retrieved from storage
Service Layer
Primary service modules
-
actions/services/documentDirectService.jscreateContainerProxy(...)getProgressFromBlob(...)getFilesFromBlob(...)uploadFiles(...)generateAppealPDF(...)deleteAwaitingSubmissionsFromBlob(...)
-
actions/services/accountDirectService.jsgetPersonalAccount(...)
-
actions/services/referenceDataService.jsgetAppealsTypesForNewAppeal(...)getMandatoryFields(...)getPickLists(...)getLPA(...)
-
lib/newappeal/journeyEffects.jsuploadAppealFilesEffect(...)sendPartialSaveEmailEffect(...)generateAppealPDFEffect(...)sendCaseCompleteMessageEffect(...)
Major component/service interaction points
components/newappeal/buildsection.js- orchestrates section progression, progress persistence, partial-save email composition, and upload trigger behavior
components/newappeal/newAppealFlow.js- switches between section form, check answers, and completion views based on
appealType.currentSection
- switches between section form, check answers, and completion views based on
API Layer
Principal routes
- Azure Storage / draft persistence
pages/api/file/setupcontainer.jspages/api/file/getprogressobjblob.jspages/api/file/getbloblist.jspages/api/file/upload.jspages/api/file/uploadsinglefile.jspages/api/file/deleteblobcase.js
Route-family classification
-
setupcontainer.js- Azure Storage
- creates or ensures the user-owned container using a signed hash-protected path
-
getprogressobjblob.js- Azure Storage
- retrieves the most recent draft appeal JSON for a case reference within the container
-
getbloblist.js- Azure Storage
- lists uploaded files under a case folder
-
upload.js- Azure Storage
- stores draft appeal or representation payload/file material into blob storage
-
deleteblobcase.js- Azure Storage
- deletes all blobs under a draft case prefix
Integration Boundaries
-
NextAuth
- required because draft ownership begins with
session.user.id
- required because draft ownership begins with
-
CRM
- touched during account bootstrap and reference/account lookup, but not yet as the primary owner of the draft itself
-
Azure Storage
- primary persistence boundary for draft progress, files, case JSON, and generated PDFs before submission
-
Azure Queue
- not part of draft creation itself
-
GOV.UK Notify
- touched for partial-save and completion email helper paths in the UI/service layer
-
Local processing
- form XML parsing
- progress derivation
- payload cleanup
- section/state transitions
Ownership Model
NextAuth session.user.id
→ accountDetails.containerID
→ Azure Storage container
→ caseReference ticketnumber / casefolderID
→ draft JSON + uploaded files + case blob
Visible characteristics:
- storage ownership is strongest at
session.user.id -> containerID - draft identity is then refined by
caseReference/ casefolder prefix inside the container - CRM contact identity (
pinsUser) supports account/bootstrap context but is not the main draft storage key
Architectural Flows
Draft creation / save
User
→ pages/newappeal/index.js
→ account/reference bootstrap
→ pages/newappeal/[appealtypes].js
→ loadNewAppealPage()
→ hydrateNewAppealStore()
→ Redux (appealType, formData, accountDetails)
→ components/newappeal/buildsection.js
→ uploadAppealFilesEffect() / progress persistence behavior
→ pages/api/file/upload.js + getprogressobjblob.js + getbloblist.js
→ Azure Storage
Draft resume
User
→ pages/myportal/[appealtypes].js
→ loadMyPortalAppealPage()
→ getFilesFromBlob() + getProgressFromBlob()
→ hydrateMyPortalAppealStore()
→ Redux hydration with blob progress and file list
→ components/newappeal/newAppealFlow.js
Change Entry Set
First files to inspect
pages/newappeal/index.jspages/newappeal/[appealtypes].jspages/myportal/[appealtypes].jslib/newappeal/loadNewAppealPage.jslib/myportal/loadMyPortalAppealPage.jslib/newappeal/hydrateNewAppealStore.jslib/myportal/hydrateMyPortalAppealStore.jscomponents/newappeal/buildsection.jsactions/services/documentDirectService.jsactions/azurestorage.jsstore/appealType/reducer.jsstore/formData/reducer.jsstore/accountDetails/reducer.js
Adjacent files
pages/api/file/setupcontainer.jspages/api/file/getprogressobjblob.jspages/api/file/getbloblist.jspages/api/file/upload.jspages/api/file/uploadsinglefile.jspages/api/file/deleteblobcase.jscomponents/newappeal/newAppealFlow.jslib/newappeal/journeyEffects.js
Highest-risk areas
- session/container ownership assumptions
- caseReference prefix assumptions in blob naming
- progress JSON shape versus form XML expectations
- save/resume state handoff between storage and Redux hydration
- file-list merging/deduplication in section progress
Risk Classification
High
Reasoning:
- user-critical draft persistence journey
- strong dependence on storage naming/path conventions
- loader/bootstrap and hydration behavior must stay aligned
- save/resume integrity depends on both storage and Redux state consistency
Appeal Submission / Finalisation Journey Map
Purpose
Business purpose:
- converts a complete draft appeal into a submitted appeal and confirmation outcome.
Maintainer purpose:
- this journey is the clearest orchestration boundary where PEDW transitions from storage-owned draft material into queue-backed submitted processing and CRM-backed case records.
Primary Entry Points
components/newappeal/buildchecksection.jscomponents/newappeal/complete.jscomponents/newappeal/newAppealFlow.js- resumed-entry shell:
pages/myportal/[appealtypes].js
Loaders / Initialisation
- submission uses the same draft loader/hydration paths described above
- no separate SSR loader exists just for finalisation
- the submission preconditions are established by:
- hydrated
appealType.caseReference - hydrated
accountDetails.containerID - hydrated
formData - hydrated uploaded file list / draft progress state
- hydrated
Check answers bootstrap
-
components/newappeal/newAppealFlow.js- routes to
BuildCheckSectionwhencurrentSection === sectionCount + 1
- routes to
-
components/newappeal/buildchecksection.js- assembles final review payload from:
legacyFormState.appealForm.valuesappealType.fileListaccountDetails.containerIDappealType.caseReference.ticketnumber
- deduplicates file list before finalisation
- requires explicit user confirmation before submission button becomes active
- assembles final review payload from:
State Ownership
Primary slices
-
store/appealType/reducer.js- controls finalisation stage through:
currentSectioncaseReferencefileListformComplete
- controls finalisation stage through:
-
store/accountDetails/reducer.js- provides:
containerIDloggedinUserIdloggedinUserEmailaccountDetails.pinswg_typeofinvolvement
- provides:
-
store/formData/reducer.js- provides mandatory fields/picklist/form shape used to render and validate final answers
Completion-state transition
BuildCheckSection.finaliseAppeal()setssetCurrentSection(9999)after PDF generation and finalisation message triggercomponents/newappeal/newAppealFlow.jsthen switches toCompleteAppealSECTION_COMPLETEtherefore acts as the visible client-side completion-state boundary
Service Layer
Primary services
-
actions/services/documentDirectService.jsgenerateAppealPDF(...)
-
actions/services/portalDirectService.jssendCaseCompleteMessage(...)- constructs signed URL to
createappealcompletemessage_api
-
actions/services/caseDirectService.jscreateNewCase(...)updateCase(...)patchCase(...)- these are the visible CRM mutation helpers adjacent to finalisation ownership transition
-
lib/newappeal/journeyEffects.jsgenerateAppealPDFEffect(...)sendCaseCompleteMessageEffect(...)sendCompletionEmailEffect(...)
Major finalisation component behavior
-
components/newappeal/buildchecksection.js- generates appeal PDF
- triggers case-complete message effect
- advances to completion state
-
components/newappeal/complete.js- sends completion email on mount
- reconstructs submitted payload context for display/side-effect continuity
- visibly represents post-submission confirmation state
API Layer
Principal routes
-
queue/finalisation/orchestration
pages/api/file/createappealcompletemessage_api.jspages/api/file/createappealcompletemessageproxy_api.js
-
CRM mutation support
pages/api/endpoint/createcase_api.jspages/api/endpoint/updatecase_api.jspages/api/endpoint/patchcase_api.js
-
draft storage dependencies used during finalisation
pages/api/file/getprogressobjblob.jspages/api/file/getbloblist.jspages/api/file/upload.js
Route-family classification
-
createappealcompletemessage_api.js- queue/finalisation
- orchestration
- reads draft progress blob and case blob
- rewrites progress blob with
appealComplete - updates case blob fields
- may trigger account-type side effect
- creates queue message for submitted application processing
-
createcase_api.js- CRM relay write
- creates incident in CRM and writes case blob to storage
-
updatecase_api.js- CRM relay write
- patches appeal-form-specific CRM entity by object ID
-
patchcase_api.js- CRM relay write
- patches the incident
servicestage
Integration Boundaries
-
NextAuth
- still the root for container identity and authenticated access to the stored draft
-
CRM
- visible submitted-record target via
createcase_api,updatecase_api,patchcase_api - visible account update side effect in
createappealcompletemessage_api
- visible submitted-record target via
-
Azure Storage
- source of truth for draft payload, uploaded files, and case blob before submission completes
- also persists rewritten completion marker state
-
Azure Queue
- explicit transition boundary through
createCaseCompleteMessage(...) - carries message containing
appealpath,casepath, andfilespath
- explicit transition boundary through
-
GOV.UK Notify
- used for completion email from
components/newappeal/complete.js
- used for completion email from
-
Local processing
- deduplication, payload cleanup, confirmation-state UI, and client-side section completion transition
Ownership Model
Session identity
→ container identity
→ draft ownership
→ finalisation route reads blob-backed draft state
→ queue message points to storage artifacts
→ downstream submitted appeal ownership transitions toward CRM-backed records
Visible ownership transition:
Draft ownership (session.user.id container)
→ appealComplete marker written to draft blob
→ Azure Queue message created with storage paths
→ CRM case creation/update pathways become the submitted-record boundary
Important maintainer note:
- the reviewed code makes the queue handoff explicit, but downstream consumer processing is out of scope of this frontend repository slice.
- the visible transition point is therefore the queue message plus the adjacent CRM mutation routes, not a fully local end-to-end submitted pipeline implementation inside one file.
Architectural Flows
Check answers to finalisation
User
→ components/newappeal/buildchecksection.js
→ assemble final payload + files list
→ generateAppealPDFEffect()
→ documentDirectService.generateAppealPDF()
→ storage/PDF path
→ sendCaseCompleteMessageEffect()
→ portalDirectService.sendCaseCompleteMessage()
→ pages/api/file/createappealcompletemessage_api.js
Finalisation orchestration
createappealcompletemessage_api.js
→ read draft appeal blob
→ read case blob
→ rewrite completion state
→ persist updated blob state
→ build queue payload
→ actions/azurestorage.createCaseCompleteMessage()
→ Azure Queue
→ downstream submitted processing / CRM transition
Completion state
User
→ finalise action succeeds
→ setCurrentSection(9999)
→ components/newappeal/complete.js
→ completion email effect
→ confirmation UI
Change Entry Set
First files to inspect
components/newappeal/buildchecksection.jscomponents/newappeal/complete.jslib/newappeal/journeyEffects.jsactions/services/portalDirectService.jsactions/services/documentDirectService.jsactions/services/caseDirectService.jspages/api/file/createappealcompletemessage_api.jsactions/azurestorage.js
Adjacent files
pages/api/file/createappealcompletemessageproxy_api.jspages/api/endpoint/createcase_api.jspages/api/endpoint/updatecase_api.jspages/api/endpoint/patchcase_api.jscomponents/newappeal/newAppealFlow.jsstore/appealType/reducer.jsstore/accountDetails/reducer.js
Highest-risk areas
- queue payload creation and storage path assumptions
- draft blob rewrite before submission handoff
- case blob update logic
- involvement/account side effects during finalisation
- coordination between completion UI state and actual backend handoff
- mixed storage + queue + CRM orchestration boundary
Risk Classification
Very High
Reasoning:
- it is the core ownership transition boundary in the appeal lifecycle
- it crosses storage, queue, and CRM concerns
- subtle regressions can break submission without obviously breaking draft editing
- completion UI state is near, but not identical to, true backend workflow completion
Ownership Model
Draft ownership root
NextAuth session.user.id
→ setContainerID(session.user.id)
→ storage container identity
→ casefolderID / ticketnumber prefix
→ draft appeal JSON
→ case blob
→ uploaded files
Submitted ownership transition
Draft blob state
→ finalisation route
→ queue message with storage artifact paths
→ submitted processing boundary
→ CRM case / appeal entity ownership context
Visible split:
- before submission: ownership is primarily storage/container scoped
- after submission boundary: ownership becomes increasingly CRM-scoped, with queue handoff as the clearest transition marker visible here
Architectural Flows
Draft Appeal Creation
User
→ New Appeal page
→ SSR loader
→ Redux hydration
→ BuildSection
→ document services
→ file APIs
→ Azure Storage
Draft Resume
User
→ My Portal resume page
→ SSR loader
→ getProgressFromBlob + getFilesFromBlob
→ Redux hydration
→ NewAppealFlow
Appeal Submission / Finalisation
User
→ Check Answers
→ generateAppealPDF
→ createappealcompletemessage_api
→ Azure Storage draft read/write
→ Azure Queue
→ CRM transition boundary
Change Entry Sets
Draft Appeal Creation
- start with:
pages/newappeal/index.jspages/newappeal/[appealtypes].jslib/newappeal/loadNewAppealPage.jscomponents/newappeal/buildsection.jsactions/services/documentDirectService.jspages/api/file/{setupcontainer,getprogressobjblob,getbloblist,upload,deleteblobcase}.js
Appeal Submission / Finalisation
- start with:
components/newappeal/buildchecksection.jscomponents/newappeal/complete.jslib/newappeal/journeyEffects.jspages/api/file/createappealcompletemessage_api.jsactions/azurestorage.jspages/api/endpoint/{createcase_api,updatecase_api,patchcase_api}.js
Risk Classification
- Draft Appeal Creation: High
- Appeal Submission / Finalisation: Very High
Investigation Method
Files reviewed for Slice 2
Required context re-read:
context/journey-architecture-map.mdcontext/api-route-map.mdcontext/architecture.mdcontext/integration-map.mdmemory-bank/change-log.md
Journey pages / loaders / hydration:
pages/newappeal/index.jspages/newappeal/[appealtypes].jspages/myportal/[appealtypes].jslib/newappeal/loadNewAppealPage.jslib/myportal/loadMyPortalAppealPage.jslib/newappeal/hydrateNewAppealStore.jslib/myportal/hydrateMyPortalAppealStore.js
Journey components / effects:
components/newappeal/newAppealFlow.jscomponents/newappeal/buildsection.jscomponents/newappeal/buildchecksection.jscomponents/newappeal/complete.jslib/newappeal/journeyEffects.js
State / service files:
store/appealType/reducer.jsstore/formData/reducer.jsstore/accountDetails/reducer.jsstore/currentView/reducer.jsstore/awaitingSubmission/reducer.jsactions/services/documentDirectService.jsactions/services/portalDirectService.jsactions/services/caseDirectService.jsactions/services/accountDirectService.jsactions/azurestorage.js
API handlers:
pages/api/file/setupcontainer.jspages/api/file/getprogressobjblob.jspages/api/file/getbloblist.jspages/api/file/upload.jspages/api/file/deleteblobcase.jspages/api/file/createappealcompletemessage_api.jspages/api/endpoint/createcase_api.jspages/api/endpoint/updatecase_api.jspages/api/endpoint/patchcase_api.js
Searches performed for Slice 2
pages:newappeal|createappeal|checkanswers|appealtypes|casereferencelib:loadNewAppealPage|loadMyPortalAppealPage|hydrateNewAppealStore|hydrateMyPortalAppealStore|journeyEffects|session.user.id|containerIDactions/services:getProgressFromBlob|getFilesFromBlob|createContainerProxy|sendCaseCompleteMessage|createNewCase|updateCase|patchCase|uploadFiles|generateAppealPDFpages/api:createappealcompletemessage_api|setupcontainer|getprogressobjblob|getbloblist|uploadsinglefile|upload\.js|deleteblobcase|createcase_api|updatecase_api|patchcase_apistore:appealType|containerID|caseReference|formData|currentView|accountDetailscomponents/newappeal:check|complete|upload|save|submit|partial|generateAppealPDF|sendCaseCompleteMessageactions:createCaseCompleteMessage|getCaseBlob|downloadProgressFile|getProgressBlobs|createBlob\(|deleteBlobCase\(|createQueue|QueueClient|sendMessage
Limitations for Slice 2
- This slice was intentionally limited to draft appeals and appeal finalisation only.
- Representation journeys were not traced.
- Downstream queue consumers outside the reviewed frontend repository were not inspected.
- No runtime execution or queue-processing verification was performed.
- No implementation changes, tests, or scripts were run because this remained documentation-only discovery.
Risks / Cautions
- The visible draft ownership model is strong at the loader/bootstrap level, but several storage APIs still accept caller-supplied container/casefolder values and rely on signed-path integrity.
- Finalisation is an orchestration boundary, not a simple page submit. Maintainers should expect interactions across UI state, storage state, queue handoff, and CRM write helpers.
- Completion UI state (
currentSection = 9999) should not be treated as equivalent to a fully independently verified downstream submitted-record outcome. - Queue creation is visible in this slice; downstream processing behavior is not.
Validation Performed
- Re-read all required Slice 2 context files before investigation.
- Performed non-destructive code reading and targeted searches only.
- Traced the visible ownership model from:
- session
- container
- draft blob state
- queue handoff
- CRM mutation boundary
- No runtime code changed.
- No lint/tests run because this was documentation-only work.
Recommendation
Next journey slice only:
- Representation Draft Creation and Representation Submission / Finalisation
This would complete the parallel maintainability map for the other major storage-backed and queue-backed submission lifecycle without widening into implementation work.
Slice 3 — Draft Representation Creation and Representation Submission / Finalisation
Files Modified
context/journey-architecture-map.mdmemory-bank/change-log.md
Findings
- The representation lifecycle reuses much of the same platform shape as the appeal lifecycle:
- authenticated session bootstrap
- blob-backed draft persistence in a
session.user.idcontainer - signed storage routes
- queue-backed completion handoff
- The most important structural difference is that representation creation is centred on a case-linked representation draft rather than a standalone appeal draft keyed by a new appeal case reference from the start.
- Representation draft and submission flows rely more heavily on
currentViewrepresentation-specific state than the appeal flow, especially for:representationCapacityrepresentationSubmitrepresentationSubmitConfirmationrepresentationMessageSent- representation file list state
- The representation completion flow visibly includes more portal-side side effects than the appeal completion flow:
- representation involvement creation
- completion queue message
- email send
- watched-case/representation-submitted update
Draft Representation Journey Map
Purpose
Business purpose:
- allows an authenticated portal user to start a representation against a case, save it as a draft, upload supporting files, leave, and later return to continue editing before final submission.
Maintainer purpose:
- this journey is the clearest representation-specific example of storage-backed draft ownership using the user container, but with a stronger dependency on case context and representation metadata than the appeal draft flow.
Primary Entry Points
pages/myportal/representation.jslib/representation/pageLoaders.jscomponents/representation.jscomponents/case/representation/*- adjacent dashboard/list entry points that navigate into it:
components/myportal/viewall.jscomponents/myportal/topthree_reps.js
Loaders / Initialisation
Primary page loader
pages/myportal/representation.js- uses
wrapper.getServerSideProps - delegates SSR bootstrap to
loadRepresentationPage({ store, ctx })
- uses
Shared representation bootstrap
lib/representation/pageLoaders.jsloadRepresentationBootstrap({ ctx })- requires
getSession(ctx) - resolves CRM contact via:
getPortalLogin(session.user.email)
- loads account details via:
getPersonalAccount(contactid)
- loads blob-backed representation drafts via:
getRepsFromBlob(session.user.id)
- derives detail payloads for existing drafts with:
getDetails(myRepresentations, "myRepresentations")- which calls
getCase(...)andgetPortalModuleDetails(...)
- requires
New representation bootstrap
loadNewRepresentation({ store, ctx, bootstrap })- requires
query.case - resolves case context via:
getBasicSearch(query.case)getSearchDetails(searchResultsObj)
- dispatches:
setContainerID(session.user.id)setSearchResults(...)setSearchDetails(...)setSearch(query.case)setCurrentView({ viewName: "My Representations", viewKey: "myRepresentations" })setCurrentReference({...})setAccountDetails(accountDetails)
- requires
Existing representation draft bootstrap
loadExistingRepresentation({ store, ctx, bootstrap })- requires:
query.casequery.created
- resolves case context via:
getBasicSearch(query.case)getSearchDetails(searchResultsObj)
- finds the existing representation draft by matching
repfile_name === query.created - loads supporting representation files from storage via:
getRepsFilesBlobs(containerID, ticketnumber/caseRef, repfile_name)
- dispatches:
setSearchResults(...)setSearchDetails(...)setContainerID(session.user.id)setMyRepresentations(...)setMyRepresentationsDetails(...)setCurrentReference({... repDetails, filesList ...})setRepresentationCapacity(result.representationCapacity)setAccountDetails(accountDetails)setFilesForRepresentations(repsFileListObj)
- requires:
State Ownership
Primary slices
-
store/currentView/reducer.js- main representation journey owner for:
caseReferencerepresentationCapacityrepresentationSubmitrepresentationSubmitConfirmationrepresentationMessageSentfileListlocale
- main representation journey owner for:
-
store/myRepresentations/reducer.js- owns:
myRepresentationsmyRepresentationsDetailsmySubmittedRepsmySubmittedRepsDetails
- owns:
-
store/accountDetails/reducer.js- owns:
accountDetailsloggedinUserIdcontainerID
containerIDis the strongest visible draft-representation storage owner
- owns:
-
store/searchOutput/reducer.js- provides case context to the representation journey through:
searchResultsObjsearchDetailsObj
- provides case context to the representation journey through:
currentView usage
- representation draft lifecycle uses
currentViewmore directly than the appeal lifecycle for journey state:caseReference.repDetailsrepresentationCapacityrepresentationSubmitrepresentationSubmitConfirmationrepresentationMessageSent- representation file list
- breadcrumb/back-link behaviour also depends on these representation flags and questionnaire state
accountDetails usage
- provides:
- CRM contact identity
- email address for completion notifications
- storage container identity for draft retrieval/deletion/completion
Service Layer
Primary service modules
-
actions/services/documentDirectService.jsgetRepsFromBlob(...)getRepsFromBlobProxy(...)deleteMyRepresentationsFromBlob(...)generateRepPDF(...)
-
actions/services/portalDirectService.jsgetMyRepresentations(...)getMyRepresentationsProxy(...)getRepresentations(...)getRepresentationsProxy(...)sendRepCompleteMessage(...)setRepInvolvment(...)
-
actions/services/accountDirectService.jsgetPortalLogin(...)getPersonalAccount(...)
-
actions/services/caseDirectService.jsgetCase(...)getPortalModuleDetails(...)
-
actions/services/searchDirectService.jsgetBasicSearch(...)
API Layer
Principal routes
-
storage-backed draft routes
pages/api/file/getrepsblob.jspages/api/file/getrepsblobproxy.jspages/api/file/deleteblobrep.js- adjacent representation draft JSON update route:
pages/api/file/editRepJson.js
-
CRM-backed representation retrieval routes
pages/api/endpoint/getmyrepresentations_api.jspages/api/endpoint/getmyrepresentationsproxy_api.jspages/api/endpoint/getrepresentations_api.jspages/api/endpoint/getrepresentationsproxy_api.js
Route-family classification
-
getrepsblob.js- Azure Storage
- reads all representation draft JSON blobs in the user container via signed hash validation
-
deleteblobrep.js- Azure Storage
- deletes a representation draft subtree using container + casefolder + repfile identifier
-
getmyrepresentations_api.js- CRM relay
- retrieves CRM representation records filtered by contact ownership
-
getrepresentations_api.js- CRM relay
- retrieves published representations for a case by incident ID
Integration Boundaries
-
NextAuth
- required for session bootstrap and the storage container owner
-
CRM via Azure Relay
- used for contact resolution, account details, case lookup, portal module detail lookup, and representation retrieval
-
Azure Storage
- primary persistence layer for representation drafts and representation files before submission
-
Azure Queue
- not part of the draft creation phase itself
-
GOV.UK Notify
- not a core part of draft creation itself
-
Local-only processing
- representation draft selection
- questionnaire/back-link view state
- derived case/search context joining
Ownership Model
NextAuth session.user.id
→ accountDetails.containerID
→ Azure Storage container
→ rep draft JSON blobs
→ representation file subtree
Visible distinction from appeals:
- appeal drafts are keyed around an appeal case reference being created/progressed
- representation drafts are keyed around an existing case context plus a
repfile_namedraft identity inside the container
Architectural Flow
New representation draft
User
→ pages/myportal/representation.js
→ loadRepresentationPage()
→ loadRepresentationBootstrap()
→ loadNewRepresentation()
→ Redux hydration (currentView, searchResultsObj, accountDetails)
→ representation components
→ representation draft persisted to Azure Storage-backed routes
Existing representation draft resume
User
→ pages/myportal/representation.js?case=...&state=edit&created=...
→ loadExistingRepresentation()
→ blob draft match by repfile_name
→ getRepsFilesBlobs(...)
→ Redux hydration with repDetails, representationCapacity, and representation file list
Change Entry Set
First files to inspect
pages/myportal/representation.jslib/representation/pageLoaders.jscomponents/representation.jsactions/services/documentDirectService.jsactions/services/portalDirectService.jsstore/currentView/reducer.jsstore/myRepresentations/reducer.js
Adjacent files
pages/api/file/getrepsblob.jspages/api/file/getrepsblobproxy.jspages/api/file/deleteblobrep.jspages/api/file/editRepJson.jspages/api/endpoint/getmyrepresentations_api.jspages/api/endpoint/getrepresentations_api.jscomponents/case/representation/*
Highest-risk areas
- draft identity through
repfile_name - combined use of case search context and storage-backed representation context
currentViewrepresentation-specific flags- container-scoped file tree handling for representation subfolders
Risk Classification
High
Reasoning:
- sensitive user draft flow
- depends on both storage ownership and case-linked context
- relies on several representation-specific state flags that can drift from generic appeal behaviour
Representation Submission / Finalisation Journey Map
Purpose
Business purpose:
- converts a representation draft or newly entered representation into a submitted representation outcome for a case.
Maintainer purpose:
- this journey shows how representation submission differs from appeal submission by centring on involvement creation, representation queue handoff, and watched-case/submission side effects rather than case creation.
Primary Entry Points
components/case/representation/representationComplete.jspages/myportal/representation.jscomponents/representation.js
Loaders / Initialisation
- uses the same
loadRepresentationPage()bootstrap as draft creation - finalisation-specific client state is carried through
currentViewrather than a separate SSR loader - representation completion depends on hydrated:
currentView.caseReferencecurrentView.representationCapacitycurrentView.representationMessageSentaccountDetails.containerIDrepFormData.repfile_name
State Ownership
Primary slices
-
store/currentView/reducer.js- main submission-state owner for:
representationSubmitrepresentationSubmitConfirmationrepresentationMessageSentrepresentationCapacitycaseReference.repDetails
- main submission-state owner for:
-
store/accountDetails/reducer.js- provides:
- CRM contact identity
- email address
- container identity
- provides:
-
store/myRepresentations/reducer.js- stores list-level representation state before and after submission refreshes
Service Layer
Primary services
-
actions/services/portalDirectService.jssendRepCompleteMessage(...)setRepInvolvment(...)createWatchedCases(...)via portal service export path used in the completion component
-
actions/services/notifyDirectService.js- send email helper path used by completion flow
-
actions/services/documentDirectService.jsgenerateRepPDF(...)where applicable in representation flows
-
actions/azurestorage.jscreateRepCompleteMessage(...)
API Layer
Principal routes
-
finalisation/orchestration
pages/api/file/createrepcompletemessage_api.jspages/api/file/createrepinvolvement_api.js
-
adjacent representation mutation/deletion route
pages/api/endpoint/deletemyrepresentations_api.js
Route-family classification
-
createrepcompletemessage_api.js- queue/finalisation
- triggers queue-backed representation completion handoff using container, case reference, and rep draft id
-
createrepinvolvement_api.js- CRM relay write / orchestration
- ensures the contact-to-case involvement relationship exists before/around representation completion
-
deletemyrepresentations_api.js- CRM relay delete
- deletes representation records by CRM representation ID
Integration Boundaries
-
NextAuth
- still the root of container ownership and authenticated portal identity bootstrap
-
CRM via Azure Relay
- used for representation visibility, involvement creation, and watched/submitted representation side effects
-
Azure Storage
- source of representation draft JSON/files prior to completion
-
Azure Queue
- explicit transition boundary through
createRepCompleteMessage(...)
- explicit transition boundary through
-
GOV.UK Notify
- explicit part of the completion flow via completion email send
-
Local-only processing
- completion-state guards, questionnaire navigation, and one-time message-sent state handling
Ownership Model
Session identity
→ container identity
→ representation draft ownership
→ completion message route
→ queue handoff
→ CRM involvement / representation side effects
Visible submitted transition:
Representation draft blob
→ create rep complete message
→ Azure Queue message
→ CRM representation/involvement boundary
→ watched/submitted status updates
Architectural Flow
User
→ representation completion component
→ setRepInvolvment(...)
→ sendRepCompleteMessage(...)
→ pages/api/file/createrepcompletemessage_api.js
→ actions/azurestorage.createRepCompleteMessage(...)
→ Azure Queue
→ email send
→ watched-case/submitted side effect
→ completion UI
Change Entry Set
First files to inspect
components/case/representation/representationComplete.jsactions/services/portalDirectService.jspages/api/file/createrepcompletemessage_api.jspages/api/file/createrepinvolvement_api.jsstore/currentView/reducer.js
Adjacent files
pages/api/endpoint/deletemyrepresentations_api.jspages/api/endpoint/getmyrepresentations_api.jspages/api/endpoint/getrepresentations_api.jsactions/azurestorage.jscomponents/case/representation/*
Highest-risk areas
- representation involvement sequencing
- queue handoff for representation completion
- side effects combined in one completion component
representationMessageSentguarding versus repeated side effects- watched/submitted representation state mutation after completion
Risk Classification
Very High
Reasoning:
- multi-integration workflow
- combines storage, queue, CRM, and notify concerns
- more client-side side-effect concentration than the appeal completion flow
Appeal vs Representation Lifecycle Comparison
Ownership comparison
-
Shared
- both begin from
NextAuth session.user.id -> container identity - both also carry CRM contact identity via account/bootstrap flows
- both begin from
-
Different
- appeal draft ownership centres on the appeal case reference and draft case blob
- representation draft ownership centres on an existing case plus a representation draft identity (
repfile_name) under the container
Storage comparison
-
Shared
- both use Azure Storage for pre-submission draft persistence
- both use signed storage routes and blob helper utilities
-
Different
- appeal draft flow uses casefolder-based appeal progress blob + case blob + files
- representation draft flow uses representation draft JSON blobs plus nested representation file subtrees
Queue comparison
-
Shared
- both use queue-backed completion handoff
- both have explicit completion-message file routes
-
Different
- appeals queue handoff packages appeal/case/files paths for submitted application processing
- representations queue handoff packages representation-specific path and file subtree for submitted representation processing
CRM comparison
-
Shared
- both eventually transition toward CRM-owned submitted-record outcomes
- both rely on relay-backed mutation/support routes
-
Different
- appeal lifecycle is more closely aligned to case creation/update transitions
- representation lifecycle is more closely aligned to involvement creation and representation submission side effects rather than creating a new appeal case
Maintainability comparison
-
Shared mechanisms
- session bootstrap
- storage container ownership
- signed file routes
- queue completion message pattern
-
Different maintainability assumptions
- appeal lifecycle has a clearer draft → submitted application path anchored by case creation/finalisation
- representation lifecycle has denser client-side state and more completion-side effect coupling in one component
- representation lifecycle therefore has slightly higher local workflow complexity even though the platform primitives are shared
Ownership Model
Representation draft ownership root
NextAuth session.user.id
→ accountDetails.containerID
→ Azure Storage container
→ representation draft blob set
→ representation file subtree
Representation submitted transition
Representation draft ownership
→ completion message route
→ Azure Queue
→ CRM involvement / representation boundary
Architectural Flows
Draft Representation Creation
User
→ Session
→ Representation loader
→ Redux currentView/accountDetails/myRepresentations
→ Storage draft retrieval / save
→ Azure Storage
Representation Submission / Finalisation
User
→ Representation complete flow
→ involvement creation
→ completion message creation
→ Azure Queue
→ CRM representation boundary
→ Notify / completion UI
Change Entry Sets
Draft Representation Creation
- start with:
pages/myportal/representation.jslib/representation/pageLoaders.jsactions/services/documentDirectService.jspages/api/file/{getrepsblob,getrepsblobproxy,deleteblobrep}.jsstore/currentView/reducer.jsstore/myRepresentations/reducer.js
Representation Submission / Finalisation
- start with:
components/case/representation/representationComplete.jsactions/services/portalDirectService.jspages/api/file/{createrepcompletemessage_api,createrepinvolvement_api}.jspages/api/endpoint/deletemyrepresentations_api.jsactions/azurestorage.js
Risk Classification
- Draft Representation Creation: High
- Representation Submission / Finalisation: Very High
Investigation Method
Files reviewed for Slice 3
Required context re-read:
context/journey-architecture-map.mdcontext/api-route-map.mdcontext/architecture.mdcontext/integration-map.mdmemory-bank/change-log.md
Journey pages / loaders / components:
pages/myportal/representation.jslib/representation/pageLoaders.jscomponents/representation.jscomponents/case/representation/representationComplete.js
State / service / API files:
store/currentView/reducer.jsstore/myRepresentations/reducer.jsactions/services/documentDirectService.jsactions/services/portalDirectService.jspages/api/file/getrepsblob.jspages/api/file/deleteblobrep.jspages/api/file/createrepcompletemessage_api.jspages/api/file/createrepinvolvement_api.jspages/api/endpoint/getmyrepresentations_api.jspages/api/endpoint/getrepresentations_api.jspages/api/endpoint/deletemyrepresentations_api.js
Searches performed for Slice 3
pages:representation|repsblob|repcompletemessage|created=|case=|state=editlib:loadRepresentationPage|loadExistingRepresentation|loadNewRepresentation|representation|getRepsFromBlob|createRepCompleteMessage|questionnaire|showQuestionnaireSectionactions/services:getRepsFromBlob|getRepsFromBlobProxy|sendRepCompleteMessage|setRepInvolvment|getMyRepresentations|getRepresentations|deleteMyRepresentationsFromBlob|generateRepPDFpages/api:getrepsblob|editRepJson|deleteblobrep|createrepcompletemessage_api|createrepinvolvement_api|deletemyrepresentations_api|getmyrepresentations_api|getrepresentations_apistore:myRepresentations|representationCapacity|representationSubmit|representationMessageSent|fileList|currentView|accountDetails
Limitations for Slice 3
- This slice was intentionally limited to representation draft and representation finalisation only.
- It did not reopen appeal implementation details except where comparison was required.
- Downstream queue consumers outside this frontend repository were not inspected.
- No runtime execution or queue-processing verification was performed.
- No implementation changes, tests, or scripts were run because this remained documentation-only discovery.
Risks / Cautions
- Representation draft state is more distributed across
currentViewflags than the appeal draft lifecycle. - Representation completion currently concentrates several side effects in one completion component, which raises maintenance sensitivity even when behaviour is stable.
- Queue creation is visible, but downstream representation processing is not visible in this repository slice.
- Comparison conclusions are based on frontend-visible lifecycle behaviour and adjacent API boundaries only.
Validation Performed
- Re-read all required Slice 3 context files before investigation.
- Performed non-destructive code reading and targeted searches only.
- Traced representation lifecycle from:
- session
- blob-backed draft ownership
- completion queue handoff
- CRM representation/involvement boundary
- Compared that lifecycle back to the already documented appeal lifecycle.
- No runtime code changed.
- No lint/tests run because this was documentation-only work.
Recommendation
Next journey slice only:
- Account Registration and Personal Details / Account Management
This would extend the journey map into the identity/bootstrap side of PEDW and complement the already documented draft/submission lifecycles without widening into implementation work.
Slice 4 — Account Registration and Personal Details / Account Management
Files Modified
context/journey-architecture-map.mdmemory-bank/change-log.md
Findings
- These two journeys are the clearest maintainer-facing view of the PEDW identity bootstrap model:
NextAuth session
→ session.user.email
→ getPortalLogin(email)
→ CRM Contact
→ portal access / dashboard access
- Account Registration exists to bridge the gap when a valid NextAuth session exists but
getPortalLogin(session.user.email)returns no CRM contact. - Personal Details / Account Management depends on that bridge having already succeeded and then operates primarily against a CRM contact identifier stored in Redux.
- The account journeys therefore strengthen the already documented ownership model by making the
session.user.email -> CRM Contacttransition explicit, rather than only implicit through dashboard or draft loaders. - The reviewed account APIs fit a future journey-based grouping model reasonably well at the architecture level, but several routes still carry historical naming/behaviour patterns that should be treated as future grouping candidates only, not implementation recommendations.
Account Registration Journey Map
Purpose
Business purpose:
- allows an authenticated user who has signed in successfully but does not yet have a CRM-backed portal account/contact to create that account and become eligible for portal access.
Maintainer purpose:
- this journey is the clearest place to understand how PEDW turns an authenticated email identity into a CRM Contact record and then into portal eligibility.
Primary Entry Points
pages/index.jspages/account/register.jscomponents/account/registerform.jscomponents/account/registerCheck.jscomponents/account/registerComplete.js- adjacent auth route:
pages/api/auth/[...nextauth].js
Loaders / Initialisation
Signed-in / no-CRM-contact detection
pages/index.js- uses
getServerSideProps - requires
getSession(ctx)for signed-in portal bootstrap - if a session exists:
- dispatches
setContainerID(session.user.id) - calls
getPortalLogin(session.user.email)
- dispatches
- client-side branch then decides:
- if
loggedInUserId.valueis empty -> redirect to/account/register?id=<encoded email> - if CRM contact exists -> set
pinsUsercookie and redirect to/myportal
- if
- uses
Registration page loader
pages/account/register.js- requires
getSession(ctx) - redirects to
/auth/signinif missing - passes
loggedInUserEmail: session.user.emailinto the page props
- requires
Registration form bootstrap
components/account/registerform.js- uses Redux Form with
enableReinitialize - seeds
initialValues.emailaddress1fromloggedInUserEmail - keeps email field disabled, so the current signed-in email remains the registration identity source in the reviewed flow
- uses Redux Form with
Post-registration bootstrap
- registration completion does not itself grant portal access directly
- visible post-registration bootstrap remains:
User returns through signed-in homepage flow
→ pages/index.js
→ getPortalLogin(session.user.email)
→ CRM contact now exists
→ pinsUser cookie set
→ redirect to /myportal
State Ownership
Primary slices
store/accountDetails/reducer.js- owns:
accountDetailsloggedinUserIdloggedinUserEmailaccCrcontainerID
- owns:
Registration-specific state
accCr- used as the registration completion state marker:
falsecreatedexists
- used as the registration completion state marker:
components/account/register.jsalso uses local component state for form/check/complete progression:registerFormCompleteaccountCreatedComplete
accountDetails usage
- registration completion writes account-creation outcome into Redux through
setAccCr(...) - homepage bootstrap later uses the CRM lookup result, not the registration component state itself, as the source of portal eligibility
Service Layer
Primary service modules
actions/services/accountDirectService.jsgetPortalLogin(emailAddress)createAccount(formValues)getEmailAccountCheck(emailAddress)
Service responsibilities in this journey
getPortalLogin(...)- confirms whether a signed-in email already maps to a CRM contact
getEmailAccountCheck(...)- duplicate email/account existence check before create
createAccount(...)- sends the final CRM contact create request
API Layer
Principal routes
pages/api/endpoint/getportallogin_api.jspages/api/endpoint/getemailaccountcheck_api.jspages/api/endpoint/createaccount_api.js- adjacent auth route that routes new users toward registration:
pages/api/auth/[...nextauth].js
Route-family classification
-
getportallogin_api.js- CRM relay lookup
- signed route
- strict login/bootstrap lookup by email address
-
getemailaccountcheck_api.js- CRM relay lookup
- duplicate account/email existence check
-
createaccount_api.js- CRM relay create
- creates a CRM
contactsrecord from submitted registration payload
-
pages/api/auth/[...nextauth].js- auth/session platform-level
- responsible for sign-in flow and
newUserrouting to/account/register
Integration Boundaries
-
NextAuth
- root of authenticated identity
- decides whether a user is signed in at all
- sends new users toward
/account/register
-
CRM via Azure Relay
- contact existence lookup
- duplicate email check
- account/contact creation
-
GOV.UK Notify
- touched indirectly through auth sign-in/verification email route family, not the registration form itself
-
Azure Storage
- not a primary part of registration itself
- session container ID may already be set in homepage bootstrap before portal access completes
-
Local-only processing
- registration step UI state
- check-details transition
- account-created state presentation
Ownership Model
NextAuth session
→ session.user.email
→ getPortalLogin(email)
→ no CRM contact found
→ registration flow
→ create CRM contact
→ homepage bootstrap re-check
→ portal access
This journey is therefore the clearest explicit bridge between:
- authenticated identity
- CRM contact identity
- portal eligibility
Architectural Flow
User
→ sign in successfully
→ pages/index.js bootstrap
→ getPortalLogin(session.user.email)
→ no CRM contact found
→ /account/register
→ registration form / check / complete
→ getEmailAccountCheck(...)
→ createAccount(...)
→ CRM contacts create
→ later homepage bootstrap re-check
→ pinsUser cookie + /myportal
Change Entry Set
First files to inspect
pages/index.jspages/account/register.jscomponents/account/registerform.jscomponents/account/registerCheck.jscomponents/account/registerComplete.jsactions/services/accountDirectService.jspages/api/endpoint/getportallogin_api.jspages/api/endpoint/getemailaccountcheck_api.jspages/api/endpoint/createaccount_api.jspages/api/auth/[...nextauth].js
Likely adjacent files
pages/api/auth/resolve-locale.jsstore/accountDetails/reducer.jsstore/accountDetails/action.js
Highest-risk areas
- signed-in/no-CRM-contact detection at homepage bootstrap
- duplicate email/contact check assumptions
- registration completion state versus true CRM contact availability
newUserrouting assumptions in NextAuth
Risk Classification
High
Reasoning:
- foundational identity bootstrap journey
- ties auth/session state to portal business identity
- regressions could block portal entry for legitimate users
Personal Details / Account Management Journey Map
Purpose
Business purpose:
- allows an authenticated portal user with an existing CRM contact to view and update their personal/account details.
Maintainer purpose:
- this journey shows how ongoing account management depends on the already-established CRM contact identity and how that contact identity is then reused for account update routes.
Primary Entry Points
pages/account/personaldetails.jscomponents/account/personaldetails.jscomponents/account/personaldetailsCheck.jscomponents/account/personaldetailsComplete.js- adjacent account UI:
components/myportal/youraccount.js
- legacy/adjacent password flow:
pages/account/changepassword.jspages/api/endpoint/updatepassword_api.js
Loaders / Initialisation
Page entry
pages/account/personaldetails.js- no active SSR account-hydration loader in the reviewed code
- page is client-side session guarded via
useSession():loading->NoSessionWarningunauthenticated-> redirect to/auth/signin
- also writes
pinsUsercookie fromprops.accountDetails.loggedinUserId
State hydration assumption
- this page assumes account identity/details have already been hydrated into Redux by earlier portal bootstrap flows, especially through:
NextAuth session
→ getPortalLogin(session.user.email)
→ CRM contactid
→ getPersonalAccount(contactid)
→ store/accountDetails
Personal details form bootstrap
components/account/personaldetails.js- uses Redux Form
- reads initial account values from Redux-backed props rather than loading them fresh inside the page route
- disabled email field confirms that account management is not treating email as a freely editable identity source in the reviewed UI flow
State Ownership
Primary slices
-
store/accountDetails/reducer.js- primary owner for:
accountDetailsloggedinUserIdloggedinUserEmailcontainerID
- primary owner for:
-
store/currentView/reducer.js- only adjacent here for navigation/breadcrumb context, not the main owner of account state
accountDetails usage
loggedinUserId- acts as the CRM contact identifier for account update actions
accountDetails- supplies the current visible account field values
loggedinUserEmail- supports identity continuity, although the read/update journey itself mostly operates on contact ID plus form payload
Service Layer
Primary service modules
actions/services/accountDirectService.jsgetPersonalAccount(contactid)updateAccount(contactId, updateBody, ssr)getPreferredLanguage(email)updatePassword(contactId, newpassword)
API Layer
Principal routes
pages/api/endpoint/getpersonalaccount_api.jspages/api/endpoint/updateaccount_api.js- adjacent account-support routes:
pages/api/endpoint/getpreferredlanguage_api.jspages/api/endpoint/updatepassword_api.js
Route-family classification
-
getpersonalaccount_api.js- CRM relay lookup
- retrieves account/contact fields by CRM contact ID
-
updateaccount_api.js- CRM relay update
- patches the CRM contact record by contact ID
-
getpreferredlanguage_api.js- CRM relay lookup / support route
- resolves preferred language by email address
-
updatepassword_api.js- CRM relay update / historical-adjacent
- updates a contact record by contact ID
- relevant mainly as a legacy or alternate account-update path because the live account flow appears to use
updateaccount_api.jsfor password-like updates elsewhere
Integration Boundaries
-
NextAuth
- required for session/auth gate at page entry
- not the direct account record store
-
CRM via Azure Relay
- primary account read/update boundary
-
GOV.UK Notify
- not a main part of personal-details/account management itself
-
Azure Storage
- not a main part of account management itself
-
Local-only processing
- check-details transition
- completion-state routing
- preferred-language cookie update (
pedw_locale)
Ownership Model
NextAuth session
→ prior portal bootstrap
→ CRM contactid in Redux
→ getPersonalAccount(contactid)
→ account details form
→ updateAccount(contactId, payload)
→ CRM contact update
This journey therefore depends on the identity bridge having already succeeded:
- registration creates the CRM contact if missing
- dashboard/bootstrap hydrates it
- personal details reuses it as the account management key
Architectural Flow
User
→ /account/personaldetails
→ useSession() gate
→ Redux accountDetails already present from prior bootstrap
→ personal details form / check state
→ updateAccount(loggedinUserId, formValues)
→ pages/api/endpoint/updateaccount_api.js
→ CRM contact patch
→ completion view + locale cookie update
Change Entry Set
First files to inspect
pages/account/personaldetails.jscomponents/account/personaldetails.jscomponents/account/personaldetailsCheck.jscomponents/account/personaldetailsComplete.jsactions/services/accountDirectService.jspages/api/endpoint/getpersonalaccount_api.jspages/api/endpoint/updateaccount_api.jsstore/accountDetails/reducer.js
Likely adjacent files
pages/api/endpoint/getpreferredlanguage_api.jspages/api/endpoint/updatepassword_api.jscomponents/myportal/youraccount.jspages/api/auth/[...nextauth].js
Highest-risk areas
- dependence on pre-hydrated Redux account state rather than active SSR loading here
- contact ID trust between client-held Redux state and final API route
- preferred-language and locale-cookie side effects
- legacy/parallel password update path ambiguity
Risk Classification
High
Reasoning:
- user-critical account data
- depends on the session-to-CRM-contact bridge remaining coherent
- mixes present-day account update flow with legacy-adjacent account/password endpoints
Ownership Model
Identity bootstrap model reinforced by account journeys
NextAuth session
→ session.user.email
→ getPortalLogin(email)
→ CRM Contact
→ portal access / dashboard access
How registration fits
Session exists
→ no CRM Contact found
→ registration flow
→ CRM contact creation
→ later bootstrap succeeds
How account management fits
Session exists
→ CRM Contact already known
→ Redux accountDetails hydrated
→ account read/update by contactId
These journeys therefore make the identity model explicit in two phases:
- registration creates the missing CRM side of the bridge
- account management depends on and reuses the completed bridge
Future API Grouping Assessment
This section is a future grouping assessment only.
It is not an implementation recommendation.
| Current route | Journey owner | Integration touched | Future grouping candidate | Migration caution |
|---|---|---|---|---|
pages/api/endpoint/getportallogin_api.js |
account registration / auth bootstrap | CRM Relay + signed request integrity | candidate for api/account or api/auth |
used both for registration gating and broader auth/bootstrap; boundary ownership spans account and auth |
pages/api/endpoint/getpersonalaccount_api.js |
personal details / account management | CRM Relay | candidate for api/account |
strongly journey-owned by account/profile reads, but also reused by broader portal bootstrap |
pages/api/endpoint/createaccount_api.js |
account registration | CRM Relay | candidate for api/account |
clear registration ownership, but current route naming/usage is historical and tied to direct CRM create semantics |
pages/api/endpoint/updateaccount_api.js |
personal details / account management | CRM Relay | candidate for api/account |
central account mutation route; caution because other journeys or legacy flows may also reuse it |
pages/api/endpoint/getemailaccountcheck_api.js |
account registration | CRM Relay | candidate for api/account |
strong registration fit, but still part of broader bootstrap/identity support checks |
pages/api/endpoint/getpreferredlanguage_api.js |
auth/session and account support | CRM Relay | candidate for api/shared or api/auth |
supports locale resolution more broadly than account pages alone |
pages/api/endpoint/updatepassword_api.js |
legacy-adjacent account management | CRM Relay | unclear / historical | relevant to account domain, but reviewed flow suggests legacy or alternate-path status |
pages/api/auth/[...nextauth].js |
auth/session platform | NextAuth + Notify + CRM lookup support | candidate for api/auth |
should remain a platform/auth boundary because it owns session and verification flows, not just account registration |
pages/api/auth/resolve-locale.js |
auth/session locale support | CRM Relay + session/cookie locale context | candidate for api/auth or api/shared |
mixed support behavior; not purely account-owned despite using account identity lookup |
Classification notes
- candidate for
api/account- routes whose clearest journey owner is registration or personal-details/account management
- candidate for
api/auth- routes whose clearest owner is session/bootstrap/auth flow, even if they consult CRM account identity
- candidate for
api/shared- routes that support multiple journey families and are not cleanly owned by one journey alone
- should remain integration/platform-level
- routes whose current boundary is more platform/auth than journey-specific
- unclear / historical
- routes where the visible live journey ownership is mixed, legacy-shaped, or ambiguous in the reviewed code
Architectural Flows
Account Registration
User
→ NextAuth session exists
→ Homepage bootstrap
→ getPortalLogin(email)
→ no CRM contact
→ Registration page
→ duplicate email check
→ create CRM contact
→ later homepage bootstrap succeeds
→ portal access
Personal Details / Account Management
User
→ Session gate
→ pre-hydrated CRM contact/accountDetails in Redux
→ personal details form
→ updateAccount(contactId, payload)
→ CRM contact update
→ completion state
Change Entry Sets
Account Registration
- start with:
pages/index.jspages/account/register.jscomponents/account/registerform.jscomponents/account/registerCheck.jscomponents/account/registerComplete.jsactions/services/accountDirectService.jspages/api/endpoint/{getportallogin_api,getemailaccountcheck_api,createaccount_api}.jspages/api/auth/[...nextauth].js
Personal Details / Account Management
- start with:
pages/account/personaldetails.jscomponents/account/personaldetails.jscomponents/account/personaldetailsCheck.jscomponents/account/personaldetailsComplete.jsactions/services/accountDirectService.jspages/api/endpoint/{getpersonalaccount_api,updateaccount_api,getpreferredlanguage_api,updatepassword_api}.jsstore/accountDetails/reducer.js
Risk Classification
- Account Registration: High
- Personal Details / Account Management: High
Investigation Method
Files reviewed for Slice 4
Required context re-read:
context/journey-architecture-map.mdcontext/api-route-map.mdcontext/portal-api-platform-assessment.mdcontext/architecture.mdcontext/integration-map.mdmemory-bank/change-log.md
Journey pages / components:
pages/index.jspages/account/register.jspages/account/personaldetails.jscomponents/account/registerform.jscomponents/account/registerCheck.jscomponents/account/registerComplete.jscomponents/account/personaldetails.jscomponents/account/personaldetailsCheck.jscomponents/account/personaldetailsComplete.js
Services / state / API files:
actions/services/accountDirectService.jsstore/accountDetails/reducer.jspages/api/endpoint/getportallogin_api.jspages/api/endpoint/getpersonalaccount_api.jspages/api/endpoint/createaccount_api.jspages/api/endpoint/updateaccount_api.jspages/api/endpoint/getemailaccountcheck_api.jspages/api/endpoint/getpreferredlanguage_api.jspages/api/endpoint/updatepassword_api.jspages/api/auth/[...nextauth].jspages/api/auth/resolve-locale.js
Searches performed for Slice 4
pages:register|personaldetails|changepassword|youraccount|getServerSideProps|getSession\(|getPortalLogincomponents/account:register|personaldetails|changepassword|emailaddress1|updateAccount|createAccount|getPersonalAccountactions/services:createAccount|getPortalLogin|getPersonalAccount|updateAccount|getPreferredLanguage|updatePassword|getEmailAccountCheckpages/api:getportallogin_api|getpersonalaccount_api|createaccount_api|updateaccount_api|getemailaccountcheck_api|getpreferredlanguage_api|updatepassword_api|resolve-locale|nextauthstore:accountDetails|loggedinUserId|loggedinUserEmail|setAccountDetails|setLoggedInUserId|setLoggedInUserEmail|currentView
Limitations for Slice 4
- This slice was intentionally limited to registration and account-management journeys only.
- It did not reopen dashboard, appeal, or representation journeys except where identity/bootstrap continuity required it.
- The future grouping section is a classification exercise only.
- No runtime execution or auth-flow verification was performed.
- No implementation changes, tests, or scripts were run because this remained documentation-only discovery.
Risks / Cautions
- Registration completion state and actual portal eligibility are not identical; the visible portal-access transition still depends on a later homepage bootstrap re-check.
- Personal details page entry relies on client session gating and pre-hydrated Redux account state more than on active SSR account loading in the reviewed route.
- Several account-support APIs participate in both account and auth/session concerns, so future grouping ownership is architectural classification only, not a change proposal.
updatepassword_api.jsappears relevant as a legacy or alternate account-update path and should be treated cautiously in grouping assessments.
Validation Performed
- Re-read all required Slice 4 context files before investigation.
- Performed non-destructive code reading and targeted searches only.
- Traced the account identity model from:
- NextAuth session
- email-based portal login lookup
- CRM contact creation/read/update
- post-registration portal bootstrap
- Classified future grouping candidates using journey ownership, integration touched, and migration caution only.
- No runtime code changed.
- No lint/tests run because this was documentation-only work.
Recommendation
Next journey slice only:
- Authentication / Sign-In and Notifications / Email
This would extend the journey map into the cross-cutting auth/communication layer that supports registration, portal entry, and user-facing lifecycle communications without widening into implementation work.
Slice 5 — Authentication / Sign-In and Notifications / Email
Files Modified
context/journey-architecture-map.mdmemory-bank/change-log.md
Findings
- Authentication / Sign-In is a cross-cutting support journey centred on
pages/api/auth/[...nextauth].js, but its practical architecture also includes:- locale pre-resolution in
pages/auth/signin.js - CRM preferred-language / contact lookup support
- homepage bootstrap in
pages/index.js - registration redirect when session exists but CRM contact does not
- sign-out cleanup via
lib/auth/sessionClient.js
- locale pre-resolution in
- Notifications / Email is not one single journey shape.
It visibly contains two maintainability forms:
- a thin direct Notify send path via
pages/api/email/notify.js - a broader aggregation/orchestration path via
pages/api/email/getall.jsand supporting email data routes
- a thin direct Notify send path via
- GOV.UK Notify is used in two distinct ways:
- as an auth-support integration for passwordless sign-in emails
- as a business-notification integration for completion emails and watchlist/batch updates
- The strongest visible identity bridge across both journeys remains:
NextAuth session.user.email -> getPortalLogin(email) -> CRM contact / preferred language
- This slice does not reopen the completed Authorization Architecture Assessment. The focus here is how auth and email fit into journey architecture, ownership, and maintainer change-entry.
Authentication / Sign-In Journey Map
Purpose
Business purpose:
- allows a user to start a passwordless sign-in flow, receive a verification email, complete callback handling, establish a session, and then continue into portal bootstrap or registration.
Maintainer purpose:
- this journey is the clearest cross-cutting entry into authenticated portal behaviour because it joins:
- sign-in UI
- locale handling
- verification-email delivery
- session establishment
- CRM contact/bootstrap lookup
- registration redirect for first-time users
Primary Entry Points
pages/auth/signin.jspages/auth/verify-request.jspages/auth/error.jspages/api/auth/[...nextauth].jspages/api/auth/resolve-locale.js- homepage bootstrap after callback:
pages/index.js
- sign-out / reset touchpoints directly relevant to auth continuity:
components/header.jscomponents/myportal/servicebanner.jslib/auth/sessionClient.jspages/logout.js
Loaders / Initialisation
Sign-in page entry
pages/auth/signin.js- gated by
SHOWLOGIN - obtains
csrfTokenthroughgetCsrfToken(context) - builds callback URL from host/protocol and incoming
callbackUrl - appends
localeto the callback URL before rendering
- gated by
Locale resolution before email sign-in submit
-
pages/auth/signin.js- intercepts form submit in
handleSubmit(...) - POSTs to
pages/api/auth/resolve-locale.jswith:- entered email
- current UI locale
- writes
pedw_localecookie - rewrites the hidden callback URL to include resolved locale before posting to
/api/auth/signin/email
- intercepts form submit in
-
pages/api/auth/resolve-locale.js- resolves fallback locale from request/body/cookie
- if email is present, calls
getPortalLogin(email) - derives locale from CRM
pinswg_preferredlanguagewhen available - otherwise falls back to request locale
Verify-request page
pages/auth/verify-request.js- lightweight page with
getCsrfToken(context)only - presents the check-email state after verification email request
- keeps locale continuity through normal Next.js locale routing rather than extra bootstrap
- lightweight page with
NextAuth session and callback bootstrap
pages/api/auth/[...nextauth].js- defines the NextAuth boundary via
NextAuth(req, res, authOptions(req, res)) - derives request locale using:
- direct query/body locale
pedw_localecookie- callback URL locale parsing
- callback cookie fallback
- resolves effective locale by attempting CRM preferred-language lookup first and request locale second
- customises:
signInverifyRequesterrornewUserpage paths by locale
- uses Prisma adapter and database session strategy
- rewrites external redirects through the locale-aware redirect callback
- defines the NextAuth boundary via
Verification email generation
pages/api/auth/[...nextauth].jsEmailProvider.sendVerificationRequest(...)- builds localized verification URL
- selects EN/CY Notify template
- sends sign-in email through GOV.UK Notify
Post-auth portal bootstrap relationship
pages/index.js- calls
getSession(ctx)during SSR - if session exists:
- stores
session.user.idintocontainerID - calls
getPortalLogin(session.user.email)
- stores
- if CRM contact exists:
- writes
pinsUsercookie from CRMcontactid - redirects to
/myportal
- writes
- if CRM contact does not exist:
- redirects to
/account/register?id=<hashed email>
- redirects to
- calls
Logout / reset behaviour directly relevant to auth continuity
-
lib/auth/sessionClient.jsclearSessionArtifacts()clears:- localStorage
next-auth.csrf-token- callback URL cookies
pedw_localepinsUser
performPortalSignOut(...)triggers NextAuthsignOut(...)with locale-aware callback URL
-
components/header.js -
components/myportal/servicebanner.js- both call
performPortalSignOut(locale)
- both call
-
pages/logout.js- renders logged-out confirmation page
- resets Redux account state via
setLogout()on mount
State Ownership
Primary ownership layers
-
NextAuth session / Prisma-backed persistence
- owns whether the user is authenticated
- owns
session.user.id - owns
session.user.email
-
Cookie-level supporting state
pedw_locale- preserves locale continuity across sign-in and callback
pinsUser- stores CRM
contactidafter homepage bootstrap succeeds
- stores CRM
- NextAuth callback cookies
- preserve callback routing context during sign-in flow
-
store/accountDetails/reducer.js- not the owner of authentication itself
- becomes the owner of post-auth portal identity context after bootstrap:
loggedinUserIdloggedinUserEmailcontainerIDaccountDetails
-
store/currentView/reducer.js- participates through locale-related UI context rather than owning session state directly
Ownership note
- auth ownership is therefore split between:
- platform auth/session state in NextAuth/Prisma
- journey continuation state in cookies and homepage/bootstrap redirects
- portal business identity state after CRM bootstrap succeeds
Service Layer
Primary service modules
actions/services/accountDirectService.jsgetPortalLogin(emailAddress)getPreferredLanguage(email)
Supporting auth helpers
lib/auth/sessionClient.jsbuildSignedOutCallbackUrl(locale)clearSessionArtifacts()performPortalSignOut(...)
Journey role of services
getPortalLogin(...)- is the main bridge from session email to CRM contact/bootstrap state
getPreferredLanguage(...)- supports language-sensitive mail or locale decisions in adjacent flows
sessionClient- centralises sign-out/reset behaviour across public and portal surfaces
API Layer
Principal routes
pages/api/auth/[...nextauth].jspages/api/auth/resolve-locale.js- adjacent supporting routes directly relevant to auth bootstrap:
pages/api/endpoint/getportallogin_api.jspages/api/endpoint/getpreferredlanguage_api.js
Route-family classification
-
pages/api/auth/[...nextauth].js- auth/session platform route
- owns callback, verification-email, redirect, and session behaviour
-
pages/api/auth/resolve-locale.js- auth/session support route
- mixed local utility + CRM lookup for locale selection before sign-in submit
-
pages/api/endpoint/getportallogin_api.js- CRM relay lookup / auth bootstrap support
- resolves portal contact existence and preferred-language-bearing contact summary by email
-
pages/api/endpoint/getpreferredlanguage_api.js- CRM relay lookup / shared support
- resolves preferred language by email
Integration Boundaries
-
NextAuth + Prisma
- primary authentication/session boundary
- owns verification token and database session handling
-
GOV.UK Notify
- sends passwordless sign-in email
-
CRM via Azure Relay
- used for preferred-language lookup and portal-contact existence lookup
- touched because sign-in completion alone is not enough for portal bootstrap; CRM identity still determines portal continuity
-
Azure Storage
- not part of sign-in itself
- becomes relevant immediately after successful auth because homepage bootstrap sets container ownership from
session.user.id
-
Local-only processing
- callback URL rewriting
- locale cookie management
- sign-out artifact clearing
- post-auth redirect branching
Ownership / Identity Model
User email
→ sign-in request
→ NextAuth verification flow
→ session.user.email + session.user.id
→ getPortalLogin(email)
→ CRM contact found or not found
→ pinsUser cookie / registration redirect
→ portal bootstrap continuation
This journey makes visible three distinct but linked identity layers:
- authentication identity
session.user.emailsession.user.id
- business identity
- CRM
contactidfromgetPortalLogin(email)
- CRM
- portal continuity state
pinsUserpedw_locale- callback URL state
Architectural Flow
User
→ pages/auth/signin.js
→ pages/api/auth/resolve-locale.js
→ /api/auth/signin/email
→ pages/api/auth/[...nextauth].js
→ GOV.UK Notify verification email
→ /api/auth/callback/email
→ NextAuth session created
→ pages/index.js SSR bootstrap
→ getPortalLogin(session.user.email)
→ CRM contact exists?
→ yes: pinsUser + /myportal
→ no: /account/register
Change Entry Set
First files to inspect
pages/auth/signin.jspages/auth/verify-request.jspages/api/auth/[...nextauth].jspages/api/auth/resolve-locale.jspages/index.jslib/auth/sessionClient.jsactions/services/accountDirectService.jspages/api/endpoint/getportallogin_api.jspages/api/endpoint/getpreferredlanguage_api.js
Likely adjacent files
pages/auth/error.jscomponents/header.jscomponents/myportal/servicebanner.jspages/logout.jspages/account/register.jsstore/accountDetails/reducer.js
Highest-risk areas
- locale resolution before and during callback handling
- verification-email URL rewriting and EN/CY template selection
- homepage bootstrap distinction between:
- authenticated session exists
- CRM contact exists
- registration required
- callback URL / redirect continuity
- sign-out artifact cleanup across session, locale, and cached CRM-contact continuity
Risk Classification
Very High
Reasoning:
- foundational cross-cutting entry to authenticated portal behaviour
- combines NextAuth, Notify, CRM lookup, locale continuity, and registration branching
- regressions can block sign-in, misroute locale, or break portal bootstrap for all authenticated journeys
Notifications / Email Journey Map
Purpose
Business purpose:
- sends user-facing transactional and update emails including:
- auth sign-in emails
- appeal/representation completion emails
- watchlist and batch update emails
Maintainer purpose:
- this journey shows how PEDW email behaviour ranges from simple template sends to orchestration routes that collect CRM, document, and event data before building outgoing Notify payloads.
Primary Entry Points
- direct send entry:
pages/api/email/notify.js
- auth-support email send embedded in:
pages/api/auth/[...nextauth].js
- business completion callers:
components/newappeal/complete.jscomponents/case/representation/representationComplete.js
- watchlist/email-notification signup touchpoints:
components/search/searchresults.jscomponents/case/summary.js
- aggregation/batch routes:
pages/api/email/getall.jspages/api/email/getdocuments.jspages/api/email/getevents.jspages/api/email/getmailinglist.jspages/api/email/getcaseref.js
Loaders / Initialisation
Thin direct Notify send
-
actions/services/notifyDirectService.js- packages:
templateIdemailAddressreferencepersonalisation
- POSTs to
/api/email/notify
- packages:
-
pages/api/email/notify.js- validates
emailAddress - for
reference === "PEDW-NEW-CASEREF", optionally resolves CRM preferred language before final template selection - sends email via GOV.UK Notify
- validates
Completion email callers
-
components/newappeal/complete.js- derives EN/CY completion template ID from locale
- sends completion email from client-side completion effect path
- uses logged-in user email and case reference personalisation
-
components/case/representation/representationComplete.js- derives EN/CY and SIPS/non-SIPS template IDs
- sends representation completion email
- does so alongside representation completion side effects:
- involvement
- completion message
- watched-case creation/update
Watchlist / email-notification sign-up relationship
components/search/searchresults.jscomponents/case/summary.jsselectEmailNotifications(...)creates/updates a watched-case record withpinswg_emailnotifications: true- sign-in is prompted when a user attempts the action without the required authenticated/contact context
- email notification state is therefore primarily represented first in watchlist CRM state, not in Notify state directly
Aggregation / batch notification bootstrap
-
pages/api/email/getall.js- fetches watchlist entries with expanded contact data
- gathers recent documents, SIP events, and representation consultation-period data per watched case
- groups by contact email
- builds EN/CY Notify payloads
- sends outbound case-update emails in batch
-
pages/api/email/getdocuments.js- loads recent published documents for an incident
- enriches results with secure document download links
-
pages/api/email/getevents.js- loads SIP record and related SIP events for an incident
-
pages/api/email/getmailinglist.js- returns flattened watchlist/contact email data for notification audiences
-
pages/api/email/getcaseref.js- returns watchlist entries including watched-case reference and appeal-type context
State Ownership
Primary ownership layers
-
Notify payload state is mostly ephemeral
- built at send time in route or caller logic
- not owned by a long-lived Redux slice
-
CRM watchlist state
- is the strongest durable owner for business-notification intent
- specifically owns whether
pinswg_emailnotificationsis enabled for a watched case
-
store/watchedCases/reducer.js- owns client-visible watchlist and email-notification status after read/refresh
- supports search/case/myportal UI refresh after watched-case changes
-
store/accountDetails/reducer.js- provides email address and CRM contact identity used by completion email callers and watchlist email-notification mutations
Ownership note
- email sending itself is not the source of truth.
The durable ownership model differs by sub-journey:
- auth sign-in email -> NextAuth-driven
- completion email -> completion/orchestration caller-driven
- watchlist updates -> CRM watchlist state-driven
Service Layer
Primary service modules
-
actions/services/notifyDirectService.jssendEmail(...)
-
actions/services/notifyService.js- re-exports
sendEmail(...)
- re-exports
-
adjacent service callers:
actions/services/accountDirectService.jsgetPreferredLanguage(...)
actions/services/portalDirectService.js- completion-message related orchestration routes adjacent to email lifecycle
Journey role of services
sendEmail(...)- is the main thin abstraction for direct Notify sends from UI-side completion flows
- account service helpers
- supply preferred-language or contact context used to shape mail behaviour
API Layer
Principal routes
pages/api/email/notify.jspages/api/email/getall.jspages/api/email/getdocuments.jspages/api/email/getevents.jspages/api/email/getmailinglist.jspages/api/email/getcaseref.js- auth-support email path also embedded in:
pages/api/auth/[...nextauth].js
Route-family classification
-
notify.js- direct Notify send route
- thin send-focused route with small preferred-language exception for new-case-reference mail
-
getall.js- notification orchestration / batch route
- aggregates CRM watchlist, document, event, and consultation-period data before sending
-
getdocuments.js- notification-support data route
- document lookup and link-building for mail payload assembly
-
getevents.js- notification-support data route
- SIP-event lookup for mail payload assembly
-
getmailinglist.js- notification-support audience route
- mailing list flattening over watchlist/contact data
-
getcaseref.js- notification-support audience/context route
- watched-case reference and appeal-type lookup for notification context
Integration Boundaries
-
GOV.UK Notify
- direct outbound email send boundary for all reviewed notification types
-
CRM via Azure Relay
- used for:
- preferred-language lookup
- watchlist audience retrieval
- watched-case reference lookup
- recent documents lookup
- SIP events lookup
- representation consultation-period lookup
- used for:
-
NextAuth
- not the main owner of business notifications
- does own the auth sign-in mail use case
-
Azure Storage
- not directly part of reviewed email routes
- adjacent completion journeys may touch storage/finalisation before or around completion email send, but email routes themselves remain Notify/CRM-oriented here
-
Local-only processing
- template selection
- payload formatting
- bilingual section-building
- secure link concatenation for document mail content
Ownership / Identity Model
Account/contact identity
→ email address + preferred language
→ business event or watchlist state
→ Notify payload build
→ GOV.UK Notify send
For watchlist-driven notifications specifically:
CRM watched case
→ pinswg_emailnotifications == true
→ contact email + preferred language
→ case-linked document/event/reps aggregation
→ batch Notify send
Architectural Flow
Direct completion-style send
User completes journey
→ completion component (newappeal or representation)
→ actions/services/notifyService.sendEmail(...)
→ pages/api/email/notify.js
→ GOV.UK Notify
Watchlist-driven batch updates
Scheduler / triggered route call
→ pages/api/email/getall.js
→ watchlist/contact fetch
→ per-case documents/events/reps aggregation
→ bilingual payload shaping
→ GOV.UK Notify batch send
Auth-support sign-in email
User enters email
→ pages/api/auth/[...nextauth].js
→ localized verification URL build
→ GOV.UK Notify sign-in email
Change Entry Set
First files to inspect
pages/api/email/notify.jspages/api/email/getall.jspages/api/email/getdocuments.jspages/api/email/getevents.jspages/api/email/getmailinglist.jspages/api/email/getcaseref.jsactions/services/notifyDirectService.jsactions/services/notifyService.jscomponents/newappeal/complete.jscomponents/case/representation/representationComplete.js
Likely adjacent files
pages/api/auth/[...nextauth].jsactions/services/accountDirectService.jscomponents/search/searchresults.jscomponents/case/summary.jsstore/watchedCases/reducer.jspages/api/documents/download/[id].js
Highest-risk areas
- template selection and EN/CY parity
- business-event timing versus email send timing
- watchlist/contact grouping assumptions in batch notification route
- document-link and case-link generation inside email payloads
- mixed responsibility in
getall.jsacross audience retrieval, content aggregation, and send behaviour
Risk Classification
High
Reasoning:
- user-facing communications with visible side effects
- includes both simple sends and orchestration-heavy aggregation
- depends on CRM audience/content correctness and bilingual template continuity
Ownership / Identity Model
Authentication / Sign-In
NextAuth session
→ session.user.email
→ getPortalLogin(email)
→ CRM contact or registration redirect
→ pinsUser cookie
→ portal entry continuity
Notifications / Email
CRM contact / account email
→ preferred language + journey event or watchlist state
→ Notify payload
→ GOV.UK Notify
Combined interpretation
- Authentication owns the transition from:
- anonymous or pre-session identity
- into session identity
- and then into CRM-backed portal continuity
- Notifications own the transition from:
- CRM/account/contact context or journey completion context
- into outbound user communication
- The overlap is strongest where sign-in email and preferred-language resolution use the same CRM contact/email model that later business notifications also reuse.
Future API Grouping Assessment
This section is a future grouping assessment only.
It is not an implementation recommendation.
| Current route | Journey owner | Integration touched | Future grouping candidate | Migration caution |
|---|---|---|---|---|
pages/api/auth/[...nextauth].js |
authentication / sign-in platform | NextAuth + Notify + CRM lookup support | should remain platform-level | core auth/session boundary with callback, redirect, verify-request, and sign-in email behaviour; journey ownership is auth but boundary is platform-critical |
pages/api/auth/resolve-locale.js |
authentication / sign-in support | CRM Relay + cookie/request locale | candidate for api/auth or api/shared |
locale helper is auth-adjacent but also behaves like a small shared support route; migration caution because it joins locale and CRM lookup concerns |
pages/api/email/notify.js |
notifications / email direct send | GOV.UK Notify + optional CRM language | candidate for api/notifications |
mostly send-focused, but contains preferred-language exception for PEDW-NEW-CASEREF; migration caution because it is not purely transport-only |
pages/api/email/getall.js |
notifications / email batch orchestration | GOV.UK Notify + CRM Relay | candidate for api/notifications |
broad orchestration route with audience lookup, aggregation, payload building, and send side effects; migration caution due to mixed responsibilities |
pages/api/email/getdocuments.js |
notifications / email support | CRM Relay + secure document-link logic | candidate for api/notifications or api/shared |
supports email aggregation but overlaps with wider document-retrieval concepts; migration caution because route has support-role rather than standalone user journey |
pages/api/email/getevents.js |
notifications / email support | CRM Relay | candidate for api/notifications or api/shared |
support route for email payload assembly; migration caution because event data may also be meaningful outside notification use |
pages/api/email/getmailinglist.js |
notifications / email audience support | CRM Relay | candidate for api/notifications |
strong notification-audience fit, but still reflects watchlist CRM ownership rather than a standalone notification-owned record source |
pages/api/email/getcaseref.js |
notifications / email audience/context support | CRM Relay | candidate for api/notifications or api/shared |
context-support route for watched-case notification assembly; migration caution because ownership overlaps with watched-case domain context |
pages/api/endpoint/getpreferredlanguage_api.js |
auth/email shared support | CRM Relay | candidate for api/shared |
supports both sign-in locale resolution and mail-template decisions; journey ownership is shared, so migration caution is primarily boundary ambiguity |
pages/api/endpoint/getportallogin_api.js |
authentication / portal bootstrap | CRM Relay + signed request integrity | candidate for api/auth or api/account |
bootstrap identity lookup is used by sign-in continuity and registration branching; migration caution because journey ownership spans auth and account edges |
Classification notes
- candidate for
api/auth- routes whose clearest journey ownership is sign-in, callback, locale resolution, or portal-auth bootstrap
- candidate for
api/notifications- routes whose clearest journey ownership is outbound mail send, audience assembly, or mail payload orchestration
- candidate for
api/account- routes whose visible ownership is closer to account/bootstrap identity than to session mechanics alone
- candidate for
api/shared- support routes reused across auth and notification concerns
- should remain platform-level
- routes whose boundary is fundamentally platform/auth infrastructure rather than a narrow journey slice
- unclear / historical
- not the dominant classification for the sampled auth/email routes, but still relevant when route ownership is mixed or legacy-shaped
Auth vs Notification Comparison
Where they are independent
- Authentication / Sign-In owns:
- sign-in form entry
- verify-request page
- callback handling
- session creation
- redirect logic
- logout/reset continuity
- Notifications / Email owns:
- direct Notify sends
- completion emails
- watchlist update emails
- aggregation of documents/events/reps into outbound email content
Where they overlap
- both use email address as a key continuity field
- both use CRM preferred-language/contact lookup support
- both rely on GOV.UK Notify for actual outbound mail delivery in relevant sub-flows
- both have EN/CY template or locale-sensitive behaviour
Where Notify is used as an auth-support integration
- passwordless verification email in
pages/api/auth/[...nextauth].js - localized sign-in-link delivery based on CRM preferred language or request locale fallback
Where Notify is used as a business-notification integration
- appeal completion emails from
components/newappeal/complete.js - representation completion emails from
components/case/representation/representationComplete.js - watchlist / case-update batch sends in
pages/api/email/getall.js
Practical maintainer distinction
- auth email is identity-entry support
- notification email is business-event communication
Architectural Flows
Authentication / Sign-In
User
→ /auth/signin
→ resolve-locale(email, locale)
→ /api/auth/signin/email
→ NextAuth verification flow
→ GOV.UK Notify sign-in email
→ callback/email verification
→ session created
→ homepage bootstrap
→ getPortalLogin(email)
→ /myportal or /account/register
Notifications / Email
Business event or watchlist state
→ direct send route or aggregation route
→ optional CRM/document/event enrichment
→ EN/CY template selection
→ GOV.UK Notify send
→ user receives portal communication
Change Entry Sets
Authentication / Sign-In
- start with:
pages/auth/signin.jspages/auth/verify-request.jspages/api/auth/[...nextauth].jspages/api/auth/resolve-locale.jspages/index.jslib/auth/sessionClient.jspages/api/endpoint/{getportallogin_api,getpreferredlanguage_api}.jsactions/services/accountDirectService.js
Notifications / Email
- start with:
pages/api/email/{notify,getall,getdocuments,getevents,getmailinglist,getcaseref}.jsactions/services/{notifyDirectService,notifyService}.jscomponents/newappeal/complete.jscomponents/case/representation/representationComplete.jscomponents/search/searchresults.jscomponents/case/summary.jspages/api/auth/[...nextauth].jsfor auth-support mail continuity
Risk Classification
- Authentication / Sign-In: Very High
- Notifications / Email: High
Investigation Method
Files reviewed for Slice 5
Required context re-read:
context/journey-architecture-map.mdcontext/api-route-map.mdcontext/portal-api-platform-assessment.mdcontext/architecture.mdcontext/integration-map.mdmemory-bank/change-log.md
Guardrails/context discipline:
.clinerules/refactor-branch-rules.mdGUARDRAILS.md
Journey pages / components / helpers:
pages/auth/signin.jspages/auth/verify-request.jspages/index.jspages/logout.jspages/account/register.jscomponents/header.jscomponents/myportal/servicebanner.jscomponents/newappeal/complete.jscomponents/case/representation/representationComplete.jscomponents/search/searchresults.jscomponents/case/summary.jslib/auth/sessionClient.jsactions/services/accountDirectService.jsactions/services/notifyDirectService.jsactions/services/notifyService.js
API files:
pages/api/auth/[...nextauth].jspages/api/auth/resolve-locale.jspages/api/email/notify.jspages/api/email/getall.jspages/api/email/getdocuments.jspages/api/email/getevents.jspages/api/email/getmailinglist.jspages/api/email/getcaseref.jspages/api/endpoint/getpreferredlanguage_api.jspages/api/endpoint/getportallogin_api.js
Searches performed for Slice 5
pages:getServerSideProps|getSession\(|signIn\(|signOut\(|getCsrfToken\(|verify-request|nextauth|resolve-locale|logoutactions/services:notify|getPortalLogin|getPreferredLanguage|send.*email|create.*messagepages/api:NotifyClient|sendEmail|NextAuth|EmailProvider|verification|callback|session|getall|getdocuments|getevents|getmailinglist|getcasereflib:auth|session|locale|preferredLanguage|getPortalLogincomponents:logout|sign in|verify|email|notify|preferred language
Limitations for Slice 5
- This slice was intentionally limited to auth/sign-in and notifications/email only.
- It did not reopen the completed Authorization Architecture Assessment.
- It did not reassess exploitability, security posture, or ownership risk.
- It traced submission, representation, registration, watchlist, and portal pages only where needed to explain auth/email touchpoints.
- The future grouping section is a classification exercise only and not an implementation recommendation.
- No runtime execution, mail send, or sign-in flow testing was performed.
Risks / Cautions
- Authentication / Sign-In and Notifications / Email are both cross-cutting, so their practical ownership spans page, API, integration, and bootstrap boundaries rather than one narrow folder.
pages/api/auth/[...nextauth].jsincludes both platform-auth behaviour and Notify-backed email behaviour; its future grouping candidate should therefore be read as architectural classification only.pages/api/email/getall.jsis materially more orchestration-heavy thanpages/api/email/notify.js, so “notifications/email” is not one uniform route shape.getpreferredlanguage_api.jsandgetportallogin_api.jssupport both auth and email journeys; their future grouping candidates are shared/auth/account classifications only, not an implementation recommendation.- Watchlist email behaviour is partly owned by CRM watchlist state (
pinswg_emailnotifications) rather than by the Notify send layer alone.
Validation Performed
- Confirmed the required Slice 5 context files were read.
- Performed non-destructive code reading and targeted searches only.
- Verified the next journey-map section followed the same maintainability format as prior slices.
- Traced auth flow from sign-in page through locale resolution, NextAuth callback/session handling, homepage bootstrap, and registration redirect continuity.
- Traced email flow across direct Notify sends, auth sign-in email, completion emails, and watchlist/batch aggregation routes.
- No runtime code changed.
- No lint/tests run because this was documentation-only work.
Recommendation
Next journey slice only:
- Watchlist / Subscriptions and Unsubscribe Flows
This would extend the journey map into the cross-cutting subscription lifecycle that connects case pages, search results, my portal state, CRM watchlist records, email-notification intent, and unsubscribe routes without widening into implementation work.
Slice 6 — Watchlist / Subscriptions and Unsubscribe / Watchlist Removal
Files Modified
context/journey-architecture-map.mdmemory-bank/change-log.md
Findings
- The watchlist/subscription architecture is a cross-cutting portal support journey built around a visible CRM relationship model:
CRM Contact
↔ Watched Case
- The same watched-case relationship supports several visible behaviours at once:
- case watching
- dashboard visibility
- search-results visibility
- case-summary visibility
- email-notification participation through
pinswg_emailnotifications
- Watchlist creation and removal are not isolated to one page.
They are triggered from:
- search results
- case summary
- my portal top-three cards
- my portal view-all lists
- dedicated unsubscribe pages for email-only removal
- Dashboard watchlist viewing is not an independent data model. It is a dashboard projection over watched-case CRM retrieval plus additional detail enrichment.
- Notification participation is visibly a property of the watched-case relationship rather than a separate subscription entity in the reviewed frontend code.
- This slice does not reopen the authorization assessment. It documents only visible watchlist/subscription architecture and ownership behaviour.
Watchlist Creation Journey Map
Purpose
Business purpose:
- allows a signed-in portal user to mark a case as watched so that it appears in their portal context and can later participate in email-notification flows.
Maintainer purpose:
- this journey is the clearest entry into the visible watched-case relationship architecture because it shows how PEDW creates or updates a CRM relationship between:
- portal contact
- watched case
- optional email-notification participation
Primary Entry Points
components/search/searchresults.jscomponents/search/addresssearchresults.jscomponents/search/dnssearchresults.jscomponents/case/summary.js- adjacent authenticated search routes that preload watched-case state for the above components:
pages/myportal/searchresults.jspages/myportal/addresssearchresults.jspages/myportal/advancedsearchresults.js
Loaders / Initialisation
Signed-in watched-case availability in portal search flows
-
pages/myportal/searchresults.js- resolves session and CRM contact identity
- loads watched cases via
getWatchedCases(loggedInUser) - derives
watchedCasesDetailsviagetDetails(...) - dispatches:
setWatchedCases(...)setWatchedCasesDetails(...)setLoggedInUserId(...)setAccountDetails(...)
-
pages/myportal/addresssearchresults.js -
pages/myportal/advancedsearchresults.js- perform equivalent portal bootstrap for watched-case state before rendering search-style results pages in myportal context
Watch action branch in results and case summary
components/search/searchresults.jscomponents/case/summary.js- expose
selectWatchedCase(loggedInUser, incidentID, appealType) - construct watched-case relationship payload using:
pinswg_WatchedCase@odata.bindpinswg_Contact@odata.bindpinswg_appealcasetype
- call
createWatchedCases(updateBody) - refresh watched-case state after mutation using:
getWatchedCasesProxy(...)getDetailsProxy(..., "myWatchedCases")
- expose
Notification-enabled creation branch
components/search/searchresults.jscomponents/case/summary.js- expose
selectEmailNotifications(...) - create the same watched-case relationship with one additional visible field:
pinswg_emailnotifications: true
- this means initial subscription signup is visibly implemented as watched-case upsert, not a separate notification-only create route
- expose
State Ownership
Primary slices
-
store/watchedCases/reducer.js- owns:
watchedCaseswatchedCasesDetails
- owns:
-
store/accountDetails/reducer.js- provides:
loggedinUserIdaccountDetails.contactidaccountDetails.emailaddress1
- these values are used to create the watched-case relationship and optional email-notification participation
- provides:
-
store/currentView/reducer.js- participates in preserving origin/view context when navigating into watched cases or back into myportal list views
Ownership note
- creation-state ownership is therefore split between:
- contact identity in
accountDetails - watched-case list/read model in
watchedCases - view context in
currentView
- contact identity in
Service Layer
Primary service modules
actions/services/portalDirectService.jscreateWatchedCases(formValues)getWatchedCases(loggedInUserId)getWatchedCasesProxy(loggedInUserId)
Journey role of services
createWatchedCases(...)- is the main visible watched-case upsert entry
getWatchedCases(...)andgetWatchedCasesProxy(...)- are used immediately after mutation to refresh portal-visible state
API Layer
Principal routes
pages/api/endpoint/createwatchedcases_api.js- adjacent read routes used immediately after create:
pages/api/endpoint/getwatchedcases_api.jspages/api/endpoint/getwatchedcasesproxy_api.js
Route-family classification
-
createwatchedcases_api.js- CRM relationship upsert / orchestration route
- derives watched case id and contact id from odata bind payload
- checks for an existing relationship first
- patches an existing watchlist record or creates a new one
-
getwatchedcases_api.js- CRM relationship read
- retrieves watched cases by contact ownership
-
getwatchedcasesproxy_api.js- CRM relationship read / proxy variant
- returns a closely related watched-case read model for refresh and portal display support
Integration Boundaries
-
CRM via Azure Relay
- primary watched-case relationship store
- handles relationship create/read/update behavior
-
NextAuth
- not the route-local owner of watch creation itself
- but is the upstream identity root used to establish the CRM contact before watched-case actions become available
-
GOV.UK Notify
- not directly touched during watch creation itself
- but
pinswg_emailnotificationsvisibly links the created relationship into later notification participation
-
Local-only processing
- JSONPath watched/unwatched state checks in components
- post-mutation refresh of Redux state
- conditional watch/watch-email button rendering
Ownership Model
CRM Contact
→ watched-case payload bindings
→ createWatchedCases
→ CRM watchlist record exists or is created
→ watchedCases Redux refresh
Visible fields used to represent the relationship include:
pinswg_WatchedCase@odata.bindpinswg_Contact@odata.bindpinswg_appealcasetype- optionally
pinswg_emailnotifications
Architectural Flow
User
→ search results or case summary watch action
→ selectWatchedCase(...) or selectEmailNotifications(...)
→ actions/services/portalDirectService.createWatchedCases(...)
→ pages/api/endpoint/createwatchedcases_api.js
→ CRM pinswg_watchlists create/patch
→ getWatchedCasesProxy(...) refresh
→ Redux watchedCases + watchedCasesDetails
Change Entry Set
First files to inspect
components/search/searchresults.jscomponents/search/addresssearchresults.jscomponents/search/dnssearchresults.jscomponents/case/summary.jsactions/services/portalDirectService.jspages/api/endpoint/createwatchedcases_api.jspages/api/endpoint/getwatchedcases_api.jspages/api/endpoint/getwatchedcasesproxy_api.jsstore/watchedCases/reducer.js
Likely adjacent files
pages/myportal/searchresults.jspages/myportal/addresssearchresults.jspages/myportal/advancedsearchresults.jsstore/accountDetails/reducer.jsstore/currentView/reducer.js
Highest-risk areas
- relationship upsert behaviour in
createwatchedcases_api.js - immediate post-create refresh assumptions
- component-level JSONPath watched/not-watched checks
- dual use of the same create route for both watch and email-notification signup
Risk Classification
High
Reasoning:
- cross-cutting relationship creation used from multiple entry points
- state refresh must stay aligned across search, case, and portal contexts
- the same relationship underpins later dashboard and notification behaviour
Watchlist Viewing Journey Map
Purpose
Business purpose:
- allows a portal user to see watched cases in their dashboard and related portal list views.
Maintainer purpose:
- this journey shows how watched-case CRM records are read, filtered, classified, enriched, and then displayed across myportal and adjacent signed-in search/case contexts.
Primary Entry Points
pages/myportal/index.jscomponents/myportal/watchedcases.jscomponents/myportal/topthree.jscomponents/myportal/viewall.js- adjacent portal search pages that preload watched cases for watch/unwatch controls:
pages/myportal/searchresults.jspages/myportal/addresssearchresults.jspages/myportal/advancedsearchresults.js
Loaders / Initialisation
Dashboard bootstrap
pages/myportal/index.js- resolves session and CRM contact identity
- loads watched cases via
getWatchedCases(loggedInUser) - classifies results with
splitWatchedCasesBySubmissionState(watchedCases.value)into:watchedCasessubmittedRepresentations
- enriches watched cases with
getDetails(..., "myWatchedCases") - dispatches:
setWatchedCases(filteredWatchedCases)setWatchedCasesDetails(watchedCasesDetails)
Watched-cases dashboard card
components/myportal/watchedcases.js- renders the watched-cases card
- delegates list/card rendering to
TopThree - sends users to
/myportal/viewall?key=watchedCaseswithsetCurrentView({ viewName: "Watched Cases", viewKey: "watchedCases" })
Portal view-all bootstrap
components/myportal/viewall.js- treats
currentViewKey === "watchedCases"as one of the main list modes - uses:
props.watchedCases.watchedCasesprops.watchedCases.watchedCasesDetails
- sets watched cases into search/detail state when navigating deeper into a case from this list
- treats
Portal search viewing support
pages/myportal/searchresults.jspages/myportal/addresssearchresults.jspages/myportal/advancedsearchresults.js- preload watched-case state to support portal-context watch/unwatch controls inside results pages
State Ownership
Primary slices
-
store/watchedCases/reducer.js- durable view-state owner for:
- watched-case read model
- watched-case details enrichment model
- durable view-state owner for:
-
store/currentView/reducer.js- records whether the active dashboard/list context is:
watchedCases
- preserves navigation back into view-all and case contexts
- records whether the active dashboard/list context is:
-
store/searchOutput/reducer.js- is temporarily reused by
viewall.jswhen a watched-case item is opened via case-detail navigation
- is temporarily reused by
Ownership note
- watchlist viewing is not a separate standalone state store.
It is a combination of:
- watched-case list state
- watched-case detail enrichment
- dashboard/view context
Service Layer
Primary service modules
-
actions/services/portalDirectService.jsgetWatchedCases(...)getWatchedCasesProxy(...)
-
actions/services/caseDirectService.jsgetPortalModuleDetails(...)- used indirectly for watched-case detail enrichment
Supporting domain helper
lib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.js- separates plain watched cases from submitted representation-related records
API Layer
Principal routes
pages/api/endpoint/getwatchedcases_api.jspages/api/endpoint/getwatchedcasesproxy_api.js
Route-family classification
-
getwatchedcases_api.js- CRM relationship read
- filters
pinswg_watchlistsby_pinswg_contact_value eq loggedInUserId - selects visible watchlist fields including:
pinswg_emailnotificationspinswg_watchlistid_pinswg_watchedcase_value- submission/representation-related fields
-
getwatchedcasesproxy_api.js- CRM relationship read / proxy variant
- similar relationship retrieval with a slightly different selected field set
Integration Boundaries
-
CRM via Azure Relay
- primary watched-case retrieval boundary
-
NextAuth
- upstream identity root used to determine which CRM contact’s watched cases are loaded
-
Local-only processing
- classification of watched cases vs submitted representations
- sorting, detail enrichment, and view-all routing
Ownership Model
CRM Contact
→ getWatchedCases(contactId)
→ CRM watchlist rows
→ splitWatchedCasesBySubmissionState
→ detail enrichment
→ dashboard / view-all projection
Architectural Flow
User
→ pages/myportal/index.js
→ getWatchedCases(loggedInUser)
→ pages/api/endpoint/getwatchedcases_api.js
→ CRM watchlist rows
→ splitWatchedCasesBySubmissionState(...)
→ getPortalModuleDetails(...) enrichment
→ Redux watchedCases + watchedCasesDetails
→ dashboard card / top-three / view-all
Change Entry Set
First files to inspect
pages/myportal/index.jscomponents/myportal/watchedcases.jscomponents/myportal/topthree.jscomponents/myportal/viewall.jsactions/services/portalDirectService.jspages/api/endpoint/getwatchedcases_api.jspages/api/endpoint/getwatchedcasesproxy_api.jsstore/watchedCases/reducer.js
Likely adjacent files
lib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.jspages/myportal/searchresults.jspages/myportal/addresssearchresults.jspages/myportal/advancedsearchresults.jsstore/currentView/reducer.js
Highest-risk areas
- watched-case classification versus submitted-representation classification
- enrichment fan-out through portal module details
- reuse of watched-case state in view-all and case-detail navigation contexts
Risk Classification
High
Reasoning:
- dashboard-critical signed-in journey
- watched-case state is reused in several components and contexts
- visible relationship between dashboard and watchlist ownership is strong and cross-cutting
Watchlist Removal Journey Map
Purpose
Business purpose:
- allows a user to stop watching a case, removing it from portal watchlist views and related watch-state controls.
Maintainer purpose:
- this journey shows how removal uses the same watched-case CRM relationship record as creation/viewing, and how portal state is refreshed after deletion.
Primary Entry Points
components/search/searchresults.jscomponents/search/addresssearchresults.jscomponents/search/dnssearchresults.jscomponents/case/summary.jscomponents/myportal/topthree.jscomponents/myportal/viewall.js
Loaders / Initialisation
Removal in search and case contexts
components/search/searchresults.jscomponents/case/summary.js- use
deleteItem(caseID, "watchedCases") - call
deleteWatchedCases(caseID) - refresh watched cases via
getWatchedCasesProxy(...) - rehydrate
watchedCasesandwatchedCasesDetails
- use
Removal in dashboard card/view-all contexts
components/myportal/topthree.jscomponents/myportal/viewall.js- also use
deleteWatchedCases(...) - refresh and reclassify watched cases through:
getWatchedCasesProxy(...)splitWatchedCasesBySubmissionState(...)getDetailsProxy(..., "myWatchedCases")
- also use
State Ownership
Primary slices
-
store/watchedCases/reducer.js- is rewritten after every successful remove flow
-
store/currentView/reducer.js- retains watched-cases list context during myportal list refreshes
Service Layer
Primary service modules
actions/services/portalDirectService.jsdeleteWatchedCases(watchedCaseID)getWatchedCasesProxy(loggedInUserId)
API Layer
Principal routes
pages/api/endpoint/deletewatchedcases_api.jspages/api/endpoint/deletewatchedcasesproxy_api.js
Route-family classification
-
deletewatchedcases_api.js- CRM relationship delete
- deletes a
pinswg_watchlists(<watchedCaseID>)record
-
deletewatchedcasesproxy_api.js- proxy delete wrapper
- forwards watched-case deletion through local endpoint routing
Integration Boundaries
-
CRM via Azure Relay
- primary delete boundary for watched-case relationship removal
-
NextAuth / cached CRM identity context
- upstream source of the watched-case ids exposed to portal UI flows
-
Local-only processing
- list refresh
- classification refresh
- removal confirmation prompts
Ownership Model
Watched-case record id
→ deleteWatchedCases(watchedCaseID)
→ CRM watchlist record delete
→ watchedCases Redux refresh
Architectural Flow
User
→ unwatch action in search/case/dashboard/view-all
→ actions/services/portalDirectService.deleteWatchedCases(...)
→ pages/api/endpoint/deletewatchedcases_api.js
→ CRM watchlist record delete
→ getWatchedCasesProxy(...)
→ refreshed Redux watched-case state
Change Entry Set
First files to inspect
components/search/searchresults.jscomponents/case/summary.jscomponents/myportal/topthree.jscomponents/myportal/viewall.jsactions/services/portalDirectService.jspages/api/endpoint/deletewatchedcases_api.jspages/api/endpoint/deletewatchedcasesproxy_api.jsstore/watchedCases/reducer.js
Likely adjacent files
pages/api/endpoint/getwatchedcasesproxy_api.jslib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.jsstore/currentView/reducer.js
Highest-risk areas
- refresh behaviour after delete across multiple UI surfaces
- reuse of watched-case ids between UI and delete route
- view-all state continuity after record removal
Risk Classification
High
Reasoning:
- removal is available from multiple user-facing surfaces
- stale state or refresh drift can break dashboard/search/case consistency
- same relationship powers watch visibility and notification participation
CRM Relationship Ownership Model
Visible relationship model
CRM Contact
↔ Watched Case
Relationship entities used
- visible relationship entity:
pinswg_watchlists
- visible linked fields include:
pinswg_watchlistid_pinswg_contact_value_pinswg_watchedcase_valuepinswg_emailnotificationspinswg_appealcasetypepinswg_representationsubmittedpinswg_representationtype
Retrieval pattern
- read by CRM contact ownership:
pinswg_watchlists
→ filter _pinswg_contact_value eq loggedInUserId
→ expand pinswg_WatchedCase
→ flatten watched-case details for portal use
Creation pattern
- create/upsert path in
createwatchedcases_api.js:
payload contains pinswg_WatchedCase@odata.bind + pinswg_Contact@odata.bind
→ extract incidentId/contactId
→ lookup existing relationship in pinswg_watchlists
→ patch existing record or post new record
Deletion pattern
- delete by relationship record id:
watchedCaseID
→ pinswg_watchlists(watchedCaseID)
→ CRM delete
Ownership interpretation
- the visible durable owner is not a separate portal subscription table in frontend state.
- instead the CRM watchlist relationship record is the main persistent ownership unit joining:
- contact
- case
- notification participation flag
Future API Grouping Assessment
This section is a future grouping assessment only.
It is not an implementation recommendation.
| Current route | Journey owner | Integration touched | Future grouping candidate | Migration caution |
|---|---|---|---|---|
pages/api/endpoint/getwatchedcases_api.js |
watchlist viewing | CRM Relay | candidate for api/watchlist |
core read model for dashboard and portal watch visibility; migration caution because current payload is reused across several UI contexts |
pages/api/endpoint/getwatchedcasesproxy_api.js |
watchlist viewing / refresh support | CRM Relay | candidate for api/watchlist or api/shared |
proxy variant is tightly coupled to refresh behaviour and historical route usage; migration caution because caller expectations may differ |
pages/api/endpoint/createwatchedcases_api.js |
watchlist creation / subscription upsert | CRM Relay | candidate for api/watchlist |
handles both create and patch behaviour plus notification-participation flag updates; migration caution because it is not create-only |
pages/api/endpoint/deletewatchedcases_api.js |
watchlist removal | CRM Relay | candidate for api/watchlist |
central relationship delete route used from multiple surfaces; migration caution because many flows assume current watchedCaseID semantics |
pages/api/endpoint/deletewatchedcasesproxy_api.js |
watchlist removal proxy support | Local proxy + CRM Relay | candidate for api/watchlist or api/shared |
wrapper route reflects historical forwarding boundary; migration caution because path and caller behaviour are support-shaped rather than journey-pure |
Classification notes
- candidate for
api/watchlist- routes whose clearest visible owner is watched-case relationship creation, retrieval, or deletion
- candidate for
api/shared- proxy/support variants whose behaviour is coupled to refresh or forwarding patterns rather than one pure journey step
Watchlist vs Dashboard Comparison
Shared dependencies
- both depend on:
getPortalLogin(session.user.email)bootstrap upstream- watched-case CRM retrieval
getPortalModuleDetails(...)detail enrichmentsplitWatchedCasesBySubmissionState(...)when dashboard classification is involved
Shared state
- both use:
store/watchedCases.reducer.jsstore/currentView.reducer.jsstore/accountDetails.reducer.js
Shared APIs
- both directly or indirectly depend on:
getwatchedcases_api.jsgetwatchedcasesproxy_api.jscreatewatchedcases_api.jsdeletewatchedcases_api.js
Ownership relationship
- dashboard is a projection/consumer of watched-case ownership, not a separate watchlist owner
- watchlist journey owns the CRM relationship lifecycle
- dashboard journey owns the signed-in card/list presentation of that relationship
Architectural Flows
Watchlist Creation
User
→ search results / case summary watch action
→ createWatchedCases(payload)
→ createwatchedcases_api
→ CRM watchlist create/patch
→ getWatchedCasesProxy
→ watchedCases Redux refresh
Watchlist Viewing
User
→ myportal bootstrap
→ getWatchedCases(contactId)
→ CRM watchlist retrieval
→ splitWatchedCasesBySubmissionState
→ detail enrichment
→ dashboard card / top-three / view-all
Watchlist Removal
User
→ unwatch action or unsubscribe path
→ deleteWatchedCases(watchedCaseID)
→ CRM watchlist delete
→ watchedCases refresh or unsubscribe confirmation page
Change Entry Sets
Watchlist Creation
- start with:
components/search/searchresults.jscomponents/search/addresssearchresults.jscomponents/search/dnssearchresults.jscomponents/case/summary.jsactions/services/portalDirectService.jspages/api/endpoint/{createwatchedcases_api,getwatchedcases_api,getwatchedcasesproxy_api}.jsstore/watchedCases/reducer.js
Watchlist Viewing
- start with:
pages/myportal/index.jscomponents/myportal/watchedcases.jscomponents/myportal/topthree.jscomponents/myportal/viewall.jspages/api/endpoint/{getwatchedcases_api,getwatchedcasesproxy_api}.jslib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.jsstore/watchedCases/reducer.js
Watchlist Removal
- start with:
components/search/searchresults.jscomponents/case/summary.jscomponents/myportal/topthree.jscomponents/myportal/viewall.jsactions/services/portalDirectService.jspages/api/endpoint/{deletewatchedcases_api,deletewatchedcasesproxy_api}.jspages/unsubscribe/[watchlistid].jspages/unsubscribeall/[watchlistid].js
Risk Classification
- Watchlist Creation: High
- Watchlist Viewing: High
- Watchlist Removal: High
Investigation Method
Files reviewed for Slice 6
Required context re-read:
context/journey-architecture-map.mdcontext/api-route-map.mdcontext/portal-api-security-boundary-assessment.mdcontext/architecture.mdcontext/integration-map.mdmemory-bank/change-log.md
Journey pages / components / services / state:
pages/myportal/index.jspages/myportal/searchresults.jspages/myportal/addresssearchresults.jspages/myportal/advancedsearchresults.jspages/unsubscribe/[watchlistid].jspages/unsubscribeall/[watchlistid].jscomponents/search/searchresults.jscomponents/search/addresssearchresults.jscomponents/search/dnssearchresults.jscomponents/case/summary.jscomponents/myportal/watchedcases.jscomponents/myportal/topthree.jscomponents/myportal/viewall.jsactions/services/portalDirectService.jsstore/watchedCases/reducer.jsstore/watchedCases/action.js
API files:
pages/api/endpoint/getwatchedcases_api.jspages/api/endpoint/getwatchedcasesproxy_api.jspages/api/endpoint/createwatchedcases_api.jspages/api/endpoint/deletewatchedcases_api.jspages/api/endpoint/deletewatchedcasesproxy_api.jspages/api/email/getall.jspages/api/email/getmailinglist.jspages/api/email/getcaseref.js
Searches performed for Slice 6
pages:unsubscribe|watchlist|watchedcases|getWatchedCases|createWatchedCases|deleteWatchedCasescomponents:selectWatchedCase|selectEmailNotifications|deleteItem|watchedCases|unsubscribe|send-email-notifications|stop-sending-email-notificationsactions/services:getWatchedCases|createWatchedCases|deleteWatchedCases|getWatchedCasesProxy|watchlistpages/api:getwatchedcases|createwatchedcases|deletewatchedcases|unsubscribe|watchlist|pinswg_emailnotificationsstore:watchedCases|watchedCasesDetails|setWatchedCases|setWatchedCasesDetails|setCurrentView
Limitations for Slice 6
- This slice was intentionally limited to watched-case/subscription and unsubscribe/removal architecture only.
- It did not reopen the security assessment beyond reusing already-established ownership context.
- It did not speculate beyond visible fields, flows, and routes in the codebase.
- It did not propose route redesign, state redesign, or ownership redesign.
- No runtime execution or unsubscribe-flow testing was performed.
Risks / Cautions
- The watched-case relationship is used for both watch visibility and notification participation, so changes in one part of the journey can affect multiple user-visible surfaces.
createwatchedcases_api.jsbehaves as an upsert route rather than a simple create route, which is important for maintainers tracing watch vs email-subscription behaviour.- Dashboard watchlist displays are projections over watched-case CRM data and portal detail enrichment rather than a separate owned dashboard record set.
- Dedicated unsubscribe pages implement removal through direct CRM watchlist queries and deletes rather than the same portal service helpers used in signed-in UI flows.
- Proxy and non-proxy watched-case routes coexist, so journey ownership is clearer than folder ownership.
Validation Performed
- Confirmed the required Slice 6 context files were read.
- Performed non-destructive code reading and targeted searches only.
- Reused the established watched-cases route family context from the API route map and ownership assessment without reopening exploitability analysis.
- Traced creation, viewing, removal, unsubscribe, and notification-participation touchpoints across pages, components, services, store, and API handlers.
- No runtime code changed.
- No lint/tests run because this was documentation-only work.
Recommendation
Next journey slice only:
- Documents / Published Document Retrieval and Download
This would extend the journey map into the public-and-portal document access lifecycle that connects case/search document visibility, document metadata retrieval, download routing, and hash-link usage without widening into implementation work.
Slice 7 — Published Document Discovery and Published Document Retrieval / Download
Files Modified
context/journey-architecture-map.mdmemory-bank/change-log.md
Findings
- The visible published-document architecture is split into two linked but distinct maintainability shapes:
- Published Document Discovery is primarily a case-detail presentation journey backed by document metadata reads from the search/document endpoint family.
- Published Document Retrieval / Download is a dedicated download-proxy journey backed by a separate document-delivery route under
pages/api/documents/download/[id].js.
- The strongest visible entry path is:
- public search result
- case detail navigation
- case documents panel
- document metadata retrieval
- per-document hash-link download
- The clearest visible metadata source is CRM document data queried through the relay-backed
pinswg_documentsfamily. - The clearest visible delivery mechanism is:
- metadata route generates
pinswg_hashlink - browser fetches
/api/documents/download/[id]?hash=... - download proxy streams the relay-backed binary response to the browser
- metadata route generates
- Document history routes are present and classified in the API layer, but they were not surfaced by the reviewed case-detail UI path in this slice.
Published Document Discovery Journey Map
Purpose
Business purpose:
- allows a user to discover published case documents, review document names, document type labels, and publish dates, and navigate from a case view into downloadable published records.
Maintainer purpose:
- this journey shows how PEDW presents published-document metadata on case detail pages, including filter, sort, pagination, and document-type grouping behaviour without directly exposing CRM or relay details in the UI layer.
Primary Entry Points
components/search/searchresults.jspages/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/documents.js
Loaders / Initialisation
Search-to-case transition
components/search/searchresults.js- sets
currentReferencebefore navigating to the case route - establishes the visible search-to-document navigation handoff through the case journey rather than a dedicated document page
- sets
Case page loader
pages/case/[ticketnumber].js- bootstraps the case route via
getBasicSearch(developmentQuery) - expands case details via
getSearchDetails(searchResultsObj) - passes document-related runtime flags to the case page:
docsOfflineshowFilteredDocs
- does not SSR-hydrate document metadata itself
- bootstraps the case route via
Document metadata bootstrap in case UI
components/case/documents.js- is the main visible document-discovery loader for the reviewed journey
- on mount / dependency changes, calls:
getSearchDocumentTypes(incidentid)getSearchDocumentDetails(incidentid)getSearchDocumentDetailsPaged(...)
- populates Redux document state through
setDocumentDetails(...) - derives document-availability UI from
docsOfflineviagetDocLink(docsOffline)
State Ownership
Primary slices
-
store/searchOutput/reducer.js- owns
documentDetailsObj - this is the primary visible read model for case-document presentation
- owns
-
store/currentView/reducer.js- owns
currentPage - participates in document pagination state continuity
- owns
Document-discovery state in component layer
components/case/documents.js- owns local UI state for:
selectedOptiondocumentTypesselectedDocumentTypecheckedItemsselectAllorderByStatefieldSortState- loading and download-status overlays
- owns local UI state for:
Search-to-document continuity
currentView.caseReference- is set before case navigation in search results
- provides continuity from search discovery into case document discovery context
Service Layer
Primary service modules
-
actions/services/searchDirectService.jsgetSearchDocumentDetails(incidentID)getSearchDocumentTypes(incidentID)getSearchDocumentDetailsPaged(...)
-
actions/services/searchService.js- thin re-export layer used by case document UI
Supporting UI helpers
-
components/utils/downloads.js- consumes generated document hash links for user-triggered downloads
-
components/utils/downloadmanager.js- provides queued download orchestration in the browser
API Layer
Principal routes
pages/api/endpoint/getsearchdocumentdetails_api.jspages/api/endpoint/getsearchdocumentdetailspaged_api.jspages/api/endpoint/getsearchdocumentTypes_api.js- adjacent but not visibly surfaced in the reviewed UI path:
pages/api/endpoint/getsearchdocumenthistory_api.jspages/api/endpoint/getsearchdocumenthistorypaged_api.js
Route-family characteristics
-
getsearchdocumentdetails_api.js- CRM relay read with metadata shaping
- filters for published-to-web documents tied to the case incident id
- normalises
pinswg_documentpublisheddate - adds
pinswg_hashlinkfor downstream download use
-
getsearchdocumentdetailspaged_api.js- CRM relay read with pagination, filter, sort, and hash-link shaping
- supports:
- page number
- sort field
- sort direction
- record-count preference
- document-type filtering
-
getsearchdocumentTypes_api.js- CRM relay read with grouping transform
- returns grouped document-type buckets and counts for the case documents filter UI
-
getsearchdocumenthistory*_api.js- CRM relay read for document history metadata
- present in the route family, but not visibly consumed in the reviewed case-detail path
Integration Boundaries
-
CRM via Azure Relay
- primary source of published-document metadata
- touched through the search/document endpoint family
-
Local-only processing
- document-type grouping presentation
- filter state
- pagination state
- download queue state in the browser
- locale-based label translation in the UI
-
NextAuth
- not required for the public case document discovery path reviewed here
-
Azure Storage
- not part of this published-document discovery flow
Ownership Model
Case incident id
→ getSearchDocumentDetails / getSearchDocumentDetailsPaged
→ CRM published document metadata
→ documentDetailsObj Redux state
→ case documents presentation
Architectural Flow
User
→ components/search/searchresults.js case selection
→ pages/case/[ticketnumber].js
→ components/case.js / components/case/summary.js
→ components/case/documents.js
→ searchService.getSearchDocumentTypes(...) + getSearchDocumentDetails(...) / getSearchDocumentDetailsPaged(...)
→ pages/api/endpoint/getsearchdocumentTypes_api.js / getsearchdocumentdetails*_api.js
→ relayGet(...)
→ Azure Relay
→ Dynamics 365 CRM
Change Entry Set
First files to inspect
pages/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/documents.jsactions/services/searchDirectService.jsstore/searchOutput/action.jsstore/searchOutput/reducer.js
Likely adjacent files
components/search/searchresults.jspages/api/endpoint/getsearchdocumentdetails_api.jspages/api/endpoint/getsearchdocumentdetailspaged_api.jspages/api/endpoint/getsearchdocumentTypes_api.jspages/api/endpoint/getsearchdocumenthistory_api.jspages/api/endpoint/getsearchdocumenthistorypaged_api.jscomponents/utils/downloads.jscomponents/utils/downloadmanager.js
Highest-risk areas
- document metadata shape expected by
components/case/documents.js - generated
pinswg_hashlinkcontinuity between metadata and download - filter and pagination assumptions tied to
@odata.countand@odata.nextLink docsOfflineflag behaviour because it changes whether download links are surfaced
Risk Classification
High
Reasoning:
- public-facing document discovery behaviour
- metadata retrieval, UI filtering, and download-link generation are tightly coupled
- document presentation depends on multiple route variants rather than a single narrow loader
Published Document Retrieval / Download Journey Map
Purpose
Business purpose:
- allows a user to retrieve a published document file once a visible document link is selected.
Maintainer purpose:
- this journey shows the dedicated binary-delivery path, where the frontend does not download directly from CRM metadata routes but instead uses a separate download proxy route fed by the metadata-generated hash link.
Primary Entry Points
components/case/documents.jscomponents/utils/downloads.jspages/api/documents/download/[id].js
Loaders / Initialisation
Download link enablement
components/case/documents.js- uses
ShowDocLinks = getDocLink(docsOffline)to determine whether link/button download behaviour is available - passes document records into
DocumentLink
- uses
Browser-side download start
components/utils/downloads.js- receives
detailsObj.pinswg_hashlink - on click, fetches the hash-link URL
- reads stream data in the browser
- derives filename from
content-dispositionwhen available - creates a blob URL and triggers an
<a>download - emits a
DownloadedFileanalytics event
- receives
Download queuing
components/utils/downloadmanager.js- manages queued download tasks
- limits concurrent downloads
- tracks per-document statuses:
idlequeueddownloadingdonefailed
State Ownership
Primary ownership
- there is no dedicated Redux download slice in the reviewed path
- download state is owned locally in the component/helper layer:
useDownloadQueue(...)status map- local progress state in
DocumentLink
Metadata dependency
- download initiation depends on metadata-generated
pinswg_hashlinkstored in document rows withindocumentDetailsObj
Service Layer
Visible service/helper modules
-
components/utils/downloads.js- acts as the main browser-side download helper in the reviewed published-document path
-
components/utils/downloadmanager.js- acts as the visible queue/orchestration helper
Important boundary note
- this journey does not use a separate frontend
actions/services/*download helper for published documents in the reviewed case-document path - instead, the browser fetches the generated proxy URL directly
API Layer
Principal route
pages/api/documents/download/[id].js
Route-family characteristics
documents/download/[id].js- document download proxy
- requires
idpath param andhashquery param - obtains access token via
getToken() - forwards request to relay-backed
documents/download/{id}?hash=... - streams response to the browser with download headers
- redirects to
/filenotavailableon invalid input or downstream failure - includes retry behaviour before giving up
Integration Boundaries
-
CRM document delivery via Azure Relay
- visible downstream source of the streamed document response
-
Local proxy processing
- request validation for
idandhash - retry handling
- response header setting
- browser-stream handoff
- request validation for
-
Analytics
- browser-side
DownloadedFileevent emitted after successful client download flow
- browser-side
Ownership Model
Document metadata row
→ pinswg_hashlink
→ /api/documents/download/[id]
→ relay-backed document stream
→ browser file download
Architectural Flow
User
→ click document link/button in components/case/documents.js
→ components/utils/downloads.js
→ fetch detailsObj.pinswg_hashlink
→ pages/api/documents/download/[id].js
→ getToken()
→ relay-backed documents/download/{id}?hash=...
→ streamed response returned to browser
→ blob URL download trigger
Change Entry Set
First files to inspect
components/case/documents.jscomponents/utils/downloads.jscomponents/utils/downloadmanager.jspages/api/documents/download/[id].js
Likely adjacent files
pages/api/endpoint/getsearchdocumentdetails_api.jspages/api/endpoint/getsearchdocumentdetailspaged_api.jsactions/core/token.jsactions/core/logger.js
Highest-risk areas
- continuity between generated hash links and proxy-route expectations
- filename extraction from response headers
- retry and failure redirect behaviour
- divergence between discovery metadata and actual downloadable document reference
Risk Classification
High
Reasoning:
- direct user-visible download behaviour
- download success depends on cross-boundary continuity between metadata shaping and proxy delivery
- failure path redirects to a dedicated not-available route rather than returning document metadata errors in-place
Document Delivery Architecture
Visible architecture
User
→ Page
→ Service
→ API
→ CRM Metadata
→ Download Proxy
→ Document Delivery
Visible delivery flow
User
→ case documents UI
→ search document metadata route
→ CRM published document metadata
→ metadata row includes pinswg_hashlink
→ browser fetches /api/documents/download/[id]?hash=...
→ download proxy forwards to relay-backed documents/download/{id}
→ streamed file delivered to browser
Document metadata source
- visible source:
pinswg_documentsmetadata queried via relay-backed endpoint routes - key visible metadata fields include:
pinswg_isharedocumentreferencepinswg_namepinswg_latestpublisheddatepinswg_documentpublisheddatepinswg_isharedocumentlocations
Download mechanism
- metadata routes generate
pinswg_hashlink - browser fetches the hash link
- proxy streams the binary response
- browser creates a blob-backed local download
Proxy behaviour
- validates presence of
idandhash - retrieves access token
- retries the downstream fetch up to the visible configured attempt count
- sets
Content-Disposition - streams binary data to the browser
- redirects to
/filenotavailablewhen download cannot be served
Future API Grouping Assessment
This section is a future grouping assessment only.
It is not an implementation recommendation.
| Current route | Journey owner | Integration touched | Future grouping candidate | Migration caution |
|---|---|---|---|---|
pages/api/documents/download/[id].js |
published document retrieval / download | CRM Relay | candidate for api/documents/download |
dedicated streaming proxy with redirect-on-failure behaviour; migration caution because callers depend on binary delivery rather than JSON shape |
pages/api/endpoint/getsearchdocumentdetails_api.js |
published document discovery | CRM Relay | candidate for api/documents/discovery |
generates pinswg_hashlink consumed by download flow; migration caution because metadata and download continuity are tightly coupled |
pages/api/endpoint/getsearchdocumentdetailspaged_api.js |
published document discovery | CRM Relay | candidate for api/documents/discovery |
carries paging, sorting, and document-type filtering semantics; migration caution because UI list behaviour depends on current contract |
pages/api/endpoint/getsearchdocumentTypes_api.js |
published document discovery | CRM Relay | candidate for api/documents/discovery |
grouped bucket/count output is UI-shaped rather than raw CRM output; migration caution because filter UI depends on this grouped format |
pages/api/endpoint/getsearchdocumenthistory_api.js |
published document discovery support | CRM Relay | candidate for api/documents/history |
route is visible in the family but not surfaced in the reviewed UI path; migration caution because unseen callers may still depend on contract |
pages/api/endpoint/getsearchdocumenthistorypaged_api.js |
published document discovery support | CRM Relay | candidate for api/documents/history |
paged/history variant appears parallel to non-paged history route; migration caution because contract usage was not fully surfaced in this slice |
Classification notes
- candidate for
api/documents/discovery- routes whose clearest visible responsibility is published-document metadata retrieval, shaping, grouping, paging, or filtering
- candidate for
api/documents/history- routes whose clearest visible responsibility is history metadata rather than current document-list presentation
- candidate for
api/documents/download- routes whose clearest visible responsibility is binary document delivery
Discovery vs Download Comparison
Shared APIs
- both journeys depend on:
- metadata-generated
pinswg_hashlink - document reference continuity across search/document route family and download proxy route
- metadata-generated
Shared integrations
- both touch:
- CRM via Azure Relay
Shared ownership assumptions
- both assume the document journey is keyed by:
- case incident id for discovery
- document shared reference/id for download
- both assume published-web filtering occurs before a document becomes user-downloadable in the visible UI path
Where the journeys diverge
- discovery is metadata/list oriented:
- grouping
- filtering
- sorting
- pagination
- bilingual label presentation
- download is binary-delivery oriented:
- proxy forwarding
- stream handling
- filename extraction
- failure redirect
Architectural Flows
Published Document Discovery
User
→ search results
→ case detail route
→ case documents component
→ getSearchDocumentTypes / getSearchDocumentDetails / getSearchDocumentDetailsPaged
→ CRM published-document metadata
→ documentDetailsObj
→ visible document list
Published Document Retrieval / Download
User
→ click published document link
→ metadata row pinswg_hashlink
→ /api/documents/download/[id]
→ relay-backed document stream
→ browser blob download
Change Entry Sets
Published Document Discovery
- start with:
pages/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/documents.jsactions/services/searchDirectService.jspages/api/endpoint/{getsearchdocumentdetails_api,getsearchdocumentdetailspaged_api,getsearchdocumentTypes_api}.jsstore/searchOutput/{action,reducer}.js
Published Document Retrieval / Download
- start with:
components/case/documents.jscomponents/utils/downloads.jscomponents/utils/downloadmanager.jspages/api/documents/download/[id].js- adjacent metadata generators in
pages/api/endpoint/getsearchdocumentdetails*_api.js
Risk Classification
- Published Document Discovery: High
- Published Document Retrieval / Download: High
Investigation Method
Files reviewed for Slice 7
Required context re-read:
context/journey-architecture-map.mdcontext/api-route-map.mdcontext/portal-api-platform-assessment.mdcontext/architecture.mdcontext/integration-map.mdmemory-bank/change-log.md
Journey pages / components / services / state:
components/search/searchresults.jspages/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/documents.jscomponents/utils/downloads.jscomponents/utils/downloadmanager.jsactions/services/searchService.jsactions/services/searchDirectService.jsstore/searchOutput/action.jsstore/searchOutput/reducer.js
API files:
pages/api/endpoint/getsearchdocumentdetails_api.jspages/api/endpoint/getsearchdocumentdetailspaged_api.jspages/api/endpoint/getsearchdocumentTypes_api.jspages/api/endpoint/getsearchdocumenthistory_api.jspages/api/endpoint/getsearchdocumenthistorypaged_api.jspages/api/documents/download/[id].js
Searches performed for Slice 7
pages:documents/download|getsearchdocumentdetails|getsearchdocumenthistory|getsearchdocumentTypes|filenotavailablecomponents:document|download|filenotavailable|DocumentDetails|docsOffline|showFilteredDocsactions/services:getSearchDocumentDetails|getSearchDocumentTypes|getSearchDocumentDetailsPaged|downloadstore:documentDetailsObj|setDocumentDetails|setDocumentHistorypages/api:getsearchdocumentdetails|getsearchdocumenthistory|getsearchdocumentTypes|download/[id]|hashAPIPath|relayGet(
Limitations for Slice 7
- This slice was intentionally limited to visible published-document discovery and download architecture.
- It did not speculate about security posture, authorization posture, or relay-side implementation beyond the visible frontend code.
- It did not widen into draft/blob document upload flows, appeal-PDF download flows, or admin/latest-document reporting flows.
- It did not execute runtime downloads.
- It did not infer active use of document history beyond visible route presence, because the reviewed UI path did not surface it.
Risks / Cautions
- Discovery and download are separate route families but are tightly coupled by generated
pinswg_hashlinkvalues. docsOfflinevisibly suppresses live document-link behaviour, so maintainers should treat document availability messaging as part of the journey architecture.- The reviewed user-facing discovery path is case-detail-centric rather than a standalone document page, so changes can affect search-to-case continuity.
- Document history routes exist in the API family, but their visible user-facing ownership is weaker than current document discovery in the reviewed slice.
- Download behaviour is split between server-side streaming in
/api/documents/download/[id]and client-side blob handling incomponents/utils/downloads.js.
Validation Performed
- Confirmed the required Slice 7 context files were read.
- Performed non-destructive code reading and targeted searches only.
- Traced the visible discovery path from search-to-case navigation into the case documents component.
- Traced the visible delivery path from metadata-generated hash link into the download proxy and browser download helper.
- Confirmed that document history routes are present but not visibly surfaced by the reviewed case-document UI path.
- No runtime code changed.
- No lint/tests run because this was documentation-only work.
Recommendation
Next journey slice only:
- Case Messages / Notices and Related Published Case Communications
This would extend the journey map into another public case-detail-adjacent read flow, staying within discovery-only architecture work and preserving continuity with the already-mapped case, document, notification, and watchlist slices.
Slice 8 — Case Messages / Notices and Related Published Case Communications
Files Modified
context/journey-architecture-map.mdmemory-bank/change-log.md
Findings
- The visible case-communication architecture is centered on case-detail presentation, not outbound delivery.
- The strongest visible message path is:
- case route bootstrap
getCaseMessage(incidentid)messagesObjCaseNoticeBanner- user-visible notice content on the case details tab
- The reviewed case-message route retrieves CRM
tasksrecords filtered to subjects containingBanner, which is the clearest visible source of public case-level notices in this slice. - Published case communications are therefore distinct from:
- Published Documents, which are metadata + download driven
- Notifications / Email, which are outbound communication driven
- SIPS events and SIPS media are adjacent published case communications in the case journey, but they are visibly surfaced as separate tabs and a live-event banner rather than being part of the same
messagesObjnotice payload.
Case Messages / Notices Journey Map
Purpose
Business purpose:
- allows a user to see time-bounded public case notices or banner-style communications associated with a case.
Maintainer purpose:
- this journey shows how PEDW loads and renders public case notices directly in the case-detail experience, using a dedicated message route and a case-page presentation component rather than a document or notification delivery mechanism.
Primary Entry Points
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/caseNoticeBanner.js
Loaders / Initialisation
Public case loader
pages/case/[ticketnumber].js- bootstraps case detail through search-family reads
- separately retrieves messages with:
getCaseMessage(searchResultsObj.value[0].incidentid)
- passes
messagesObjinto the case page props
DNS case loader
pages/dns/[developmentName].js- bootstraps DNS case detail through DNS search-family reads
- separately retrieves messages with:
getCaseMessage(searchResultsObj.value[0].incidentid)
- passes
messagesObjinto the case page props
My Portal case loader
pages/myportal/case/[ticketnumber].js- bootstraps portal case detail with authenticated portal context plus case search/detail data
- separately retrieves messages with:
getCaseMessage(searchResultsObj.value[0].incidentid)
- passes
messagesObjinto the case page props
Case page presentation bootstrap
-
components/case.js- passes
messagesObjintocomponents/case/summary.js
- passes
-
components/case/summary.js- renders
CaseNoticeBannerinside thecase-detailstab when:props.messagesObj["@odata.count"] > 0
- renders
State Ownership
Primary ownership
messagesObj- is page-prop owned in the reviewed path
- is not stored in a dedicated Redux slice in the reviewed case-message flow
Adjacent state
-
store/searchOutput/reducer.js- owns adjacent case-detail communication state for:
eventDetailsObjmediaDetailsObj
- does not own
messagesObj
- owns adjacent case-detail communication state for:
-
store/currentView/reducer.js- participates only indirectly through case route/context continuity
UI ownership
components/case/caseNoticeBanner.js- owns the visible interpretation and rendering of message rows
- applies date-window checks and bilingual content splitting in the UI layer
Service Layer
Primary service modules
-
actions/services/caseDirectService.jsgetCaseMessage(searchString)- adjacent communication-related helpers:
getSIPSEvents(caseid)getSIPSMedia(caseid)
-
actions/services/caseService.js- thin re-export layer for the above helpers
API Layer
Principal routes
pages/api/endpoint/getcasemessage_api.js- adjacent directly relevant communication routes:
pages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.js
Route-family characteristics
-
getcasemessage_api.js- CRM relay read for case banner messages
- requires
id - queries CRM
tasks - filters by:
_regardingobjectid_value eq caseIdcontains(subject, 'Banner')statuscode ne 5
- orders by
createdon desc
-
getsipsevents_api.js- CRM relay read for published event records
- separate case-communication route family for event-tab content
-
getsipsmedia_api.js- CRM relay read for published media/event recordings
- separate case-communication route family for media-tab content
Integration Boundaries
-
CRM via Azure Relay
- primary source of visible case notices/messages
- also the source of adjacent SIPS communication content
-
Local-only processing
- date-window filtering in
CaseNoticeBanner - bilingual subject/description splitting in the UI layer
- case-tab placement and notice rendering
- date-window filtering in
-
NextAuth
- not required for the public case-message path
- used only in the authenticated myportal case variant upstream of the same message retrieval call
Ownership Model
Case incident id
→ getCaseMessage(incidentid)
→ CRM tasks filtered to Banner subjects
→ messagesObj page prop
→ CaseNoticeBanner
→ visible case notice
Architectural Flow
User
→ case route (pages/case/[ticketnumber].js or DNS/portal variant)
→ case bootstrap via search family
→ caseService.getCaseMessage(incidentid)
→ pages/api/endpoint/getcasemessage_api.js
→ relayGet(...)
→ Azure Relay
→ Dynamics 365 CRM tasks
→ messagesObj
→ components/case/summary.js
→ CaseNoticeBanner
Change Entry Set
First files to inspect
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/caseNoticeBanner.jsactions/services/caseDirectService.jspages/api/endpoint/getcasemessage_api.js
Likely adjacent files
pages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.jsstore/searchOutput/action.jsstore/searchOutput/reducer.js
Highest-risk areas
- message filtering assumptions based on
subjectcontainingBanner - bilingual content splitting conventions in
subjectanddescription - date-window visibility logic in
CaseNoticeBanner - page-prop ownership of
messagesObj, because it is not normalized into Redux in the reviewed path
Risk Classification
High
Reasoning:
- public case-page communication is user-visible and contract-sensitive
- message meaning is shaped partly in the UI layer rather than only in the API layer
- the same case journey mixes messages, documents, events, media, and status presentation
Related Published Case Communications Journey Map
Purpose
Business purpose:
- allows a user to see related published case communications around the case beyond banner notices, where those communications are directly surfaced in the case-detail journey.
Maintainer purpose:
- this journey shows how PEDW presents adjacent published communication surfaces, especially SIPS live-event, events, and media content, and how those differ from notice banners while still participating in the same case-page communication experience.
Primary Entry Points
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].jscomponents/case/summary.js- adjacent case communication components:
components/case/events.jscomponents/case/media.js
Loaders / Initialisation
SIPS communication bootstrap
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].js- conditionally load SIPS event records when appeal case type is
846040002 - load:
getSIPSEvents(searchDetailsObj[0].value[0].pinswg_sipsid)getSIPSMedia(searchResultsObj.value[0].incidentid)
- dispatch results into Redux:
setEventDetails(eventsObj)setMediaDetails(mediaObj)
- conditionally load SIPS event records when appeal case type is
Case-summary communication presentation
components/case/summary.js- derives:
hasEventsTabDatahasMediaTabDatalivePublishedEvent
- renders a GOV.UK notification banner for a live published event when available
- renders separate
EventsandMediatabs when corresponding data exists
- derives:
State Ownership
Primary slices
store/searchOutput/reducer.js- owns:
eventDetailsObjmediaDetailsObj
- this is the primary visible state owner for adjacent published case communications in the reviewed path
- owns:
UI ownership
components/case/summary.js- owns the live-event banner selection logic through derived view state
Service Layer
Primary service modules
actions/services/caseDirectService.jsgetSIPSEvents(caseid)getSIPSMedia(caseid)
API Layer
Principal routes
pages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.js
Route-family characteristics
-
getsipsevents_api.js- CRM relay read for event records
- requires
caseid - reads
pinswg_sipsevents
-
getsipsmedia_api.js- CRM relay read for published event recordings/media
- requires
caseid - filters to
pinswg_publishtoweb eq true - returns published recording metadata and URLs
Integration Boundaries
-
CRM via Azure Relay
- primary source of event/media communication records
-
Local-only processing
- live-event derivation and banner placement
- case-tab presentation
Ownership Model
Case / SIPS context
→ getSIPSEvents / getSIPSMedia
→ Redux eventDetailsObj / mediaDetailsObj
→ case summary tabs and live-event banner
Architectural Flow
User
→ case route
→ SIPS-specific conditional bootstrap
→ getSIPSEvents(...) / getSIPSMedia(...)
→ getsipsevents_api.js / getsipsmedia_api.js
→ CRM via relay
→ Redux event/media state
→ case summary live-event banner and tabs
Change Entry Set
First files to inspect
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].jscomponents/case/summary.jsactions/services/caseDirectService.jspages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.jsstore/searchOutput/action.jsstore/searchOutput/reducer.js
Likely adjacent files
components/case/events.jscomponents/case/media.jslib/domain/case-lifecycle/*
Highest-risk areas
- direct coupling between case type checks and SIPS communication loading
- live-event banner derivation logic in the case-summary layer
- adjacency between event/media communications and message/document/status tabs in one page shell
Risk Classification
Medium-High
Reasoning:
- user-visible communication content on public case pages
- conditional SIPS-specific branching adds hidden coupling
- adjacent but distinct from the main case-message banner route
Case Communication Architecture
Visible architecture
Case
→ message/notice source
→ case-detail presentation
→ user-visible communication
Visible communication flow
Case incident id
→ getcasemessage_api / getsipsevents_api / getsipsmedia_api
→ case-detail props or Redux state
→ case summary / notice banner / event-media tabs
→ user-visible communication on case page
Data source
- banner notices/messages:
- CRM
tasksrecords filtered bycontains(subject, 'Banner')
- CRM
- related SIPS communications:
- CRM
pinswg_sipsevents - CRM
pinswg_eventrecordings
- CRM
Route family
- primary notice route:
pages/api/endpoint/getcasemessage_api.js
- adjacent communication routes:
pages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.js
State ownership
messagesObj- page-prop owned
eventDetailsObj/mediaDetailsObj- Redux owned via
store/searchOutput
- Redux owned via
UI ownership
components/case/summary.js- main case-page owner of communication placement
components/case/caseNoticeBanner.js- owner of banner-message rendering
Where this differs from documents and notifications
- Messages / Notices
- case-page presentation of case-linked communications
- Published Documents
- document metadata retrieval + binary delivery path
- Notifications / Email
- outbound communication to a recipient rather than on-page case presentation
Future API Grouping Assessment
This section is a future grouping assessment only.
It is not an implementation recommendation.
| Current route | Journey owner | Integration touched | Future grouping candidate | Migration caution |
|---|---|---|---|---|
pages/api/endpoint/getcasemessage_api.js |
case messages / notices | CRM Relay | candidate for api/cases/messages |
route is the clearest visible public notice/banner source; migration caution because case pages currently depend on its specific tasks/banner shape |
pages/api/endpoint/getsipsevents_api.js |
related published case communications | CRM Relay | candidate for api/cases/events |
SIPS-specific route is conditionally loaded from case pages; migration caution because ownership is case-type dependent rather than universally case-owned |
pages/api/endpoint/getsipsmedia_api.js |
related published case communications | CRM Relay | candidate for api/cases/events or api/shared |
media is adjacent communication content but also its own tab family; migration caution because it overlaps event/media presentation concerns |
pages/api/notices/index.js |
shared site notice support | Local-only | candidate for api/shared or unclear / historical |
static notice route is not part of case-message retrieval; migration caution because it appears to be site-level notice support rather than case-owned |
Classification notes
- candidate for
api/cases/messages- routes whose clearest visible owner is case-page notice/message presentation
- candidate for
api/cases/events- routes whose clearest visible owner is case-page event/media communication content
- candidate for
api/shared- routes with broader site-level or cross-journey notice/support behaviour
- unclear / historical
- routes whose visible ownership is weaker or not clearly part of the active case-message journey
Messages vs Documents vs Notifications Comparison
Where they overlap
- all three are user-visible communication surfaces in the wider platform
- all can be associated with case context
- messages and documents are both directly surfaced on case pages
- notifications and messages can both convey case-related information, but in different delivery models
Where they differ
-
Case Messages / Notices
- case-page presentation
- time-bounded or banner-style communication
- no download proxy
- no outbound send path in the reviewed journey
-
Published Documents
- metadata list + binary delivery
- document-type grouping, sorting, filtering, and download behaviour
-
Notifications / Email
- outbound communication
- Notify/template driven
- recipient-address oriented rather than case-tab oriented
Which one is case-page presentation
- Case Messages / Notices
Which one is document delivery
- Published Documents
Which one is outbound communication
- Notifications / Email
Architectural Flows
Case Messages / Notices
User
→ case route bootstrap
→ getCaseMessage(incidentid)
→ getcasemessage_api
→ CRM Banner task records
→ messagesObj
→ CaseNoticeBanner
Related Published Case Communications
User
→ case route bootstrap
→ conditional SIPS event/media fetches
→ getsipsevents_api / getsipsmedia_api
→ Redux event/media state
→ live-event banner / event-media tabs
Change Entry Sets
Case Messages / Notices
- start with:
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/caseNoticeBanner.jsactions/services/caseDirectService.jspages/api/endpoint/getcasemessage_api.js
Related Published Case Communications
- start with:
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].jscomponents/case/summary.jsactions/services/caseDirectService.jspages/api/endpoint/{getsipsevents_api,getsipsmedia_api}.jsstore/searchOutput/{action,reducer}.js
Risk Classification
- Case Messages / Notices: High
- Related Published Case Communications: Medium-High
Investigation Method
Files reviewed for Slice 8
Required context re-read:
context/journey-architecture-map.mdcontext/api-route-map.mdcontext/portal-api-platform-assessment.mdcontext/architecture.mdcontext/integration-map.mdmemory-bank/change-log.md
Journey pages / components / services / state:
pages/case/[ticketnumber].jspages/dns/[developmentName].jspages/myportal/case/[ticketnumber].jscomponents/case.jscomponents/case/summary.jscomponents/case/caseNoticeBanner.jsactions/services/caseService.jsactions/services/caseDirectService.jsstore/searchOutput/action.jsstore/searchOutput/reducer.js
API files:
pages/api/endpoint/getcasemessage_api.jspages/api/endpoint/getsipsevents_api.jspages/api/endpoint/getsipsmedia_api.jspages/api/notices/index.js
Searches performed for Slice 8
components/case:messagesObj|getCaseMessage|notice|banner|messagepages:getCaseMessage|messagesObj|getcasemessage_api|notice|messageactions/services:getCaseMessage|getSIPSEvents|getSIPSMedia|message|noticepages/api/endpoint:getcasemessage_api|getsipsevents_api|getsipsmedia_api|message|noticestore:messagesObj|message|notice|eventDetailsObj|mediaDetailsObj
Limitations for Slice 8
- This slice was intentionally limited to visible case-message / notice / communication flows.
- It did not widen into outbound notification delivery or document download behaviour beyond comparison.
- It did not assess communication policy correctness or content governance.
- It did not infer a dedicated Redux ownership model for
messagesObjwhere none was visibly present. - It did not execute runtime case-page flows.
Risks / Cautions
- The main notice source is identified through a CRM
tasksquery filtered bycontains(subject, 'Banner'), so behaviour depends on content conventions as well as route logic. CaseNoticeBannerperforms visible bilingual splitting and date-window checks in the UI layer, which makes presentation logic part of the architectural behaviour.messagesObjis page-prop owned while adjacent event/media communications are Redux owned, so communication state is split across ownership models.- SIPS live-event/media communications overlap with case communications, but they are a separate visible route/state family from banner notices.
- The static
pages/api/notices/index.jsroute exists as a notice-like support surface but is not part of the reviewed case-message loading path.
Validation Performed
- Confirmed the required Slice 8 context files were read.
- Performed non-destructive code reading and targeted searches only.
- Traced the visible case-message flow from case loader to
CaseNoticeBannerpresentation. - Traced directly relevant SIPS event/media communication overlap only where it is visibly part of the case page.
- Compared message/notice presentation against the already documented document and notification slices without reopening those journeys.
- No runtime code changed.
- No lint/tests run because this was documentation-only work.
Recommendation
Next journey slice only:
- Case Status / Lifecycle Presentation and Related Published Timeline Signals
This would extend the journey map into another case-detail-adjacent presentation slice that naturally follows messages, notices, events, media, and documents while remaining discovery-only.