From 27fceffbc9ccedc20a3d6af215d37f445468a504 Mon Sep 17 00:00:00 2001 From: Robert Bond Date: Mon, 22 Jun 2026 05:36:30 +0000 Subject: [PATCH] Merged PR 2413: updated docs updated docs Related work items: #23754 --- .env.local | 159 +- context/api-route-map.md | 994 +++ context/architecture.md | 284 +- context/journey-architecture-map.md | 6065 +++++++++++++++++ context/portal-api-platform-assessment.md | 2834 ++++++++ ...portal-api-security-boundary-assessment.md | 4088 +++++++++++ memory-bank/change-log.md | 1559 +++++ 7 files changed, 15851 insertions(+), 132 deletions(-) create mode 100644 context/api-route-map.md create mode 100644 context/journey-architecture-map.md create mode 100644 context/portal-api-platform-assessment.md create mode 100644 context/portal-api-security-boundary-assessment.md diff --git a/.env.local b/.env.local index 5beabc0f..fe40be4d 100644 --- a/.env.local +++ b/.env.local @@ -33,18 +33,13 @@ CRMURL = "ts-pedw.crm11.dynamics.com" CRMURL_VERSION = "v9.2" # //New PreProd -# CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a -# CLIENT_SECRET = VWY8Q~9EtRzGXpd8Xs~5OS1fej2IyVJ8JgTMncw3 -# RELAY_ROOT = https://ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-ppe-uks-01" -# # RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-ukw-01/ -# # RELAYURI = "ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net" -# # RELAYPATH = "ar-hc-lz-pedw-ppe-ukw-01" -# # CRMURL = "ppcrm2016.llcdt.gov.wales/PPPINSWales" -# # CRMURL_VERSION = "v8.2" -# CRMURL = "wg-pp-pedw.crm11.dynamics.com" -# CRMURL_VERSION = "v9.2" +CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a +CLIENT_SECRET = VWY8Q~9EtRzGXpd8Xs~5OS1fej2IyVJ8JgTMncw3 +RELAY_ROOT = https://ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-uks-01/ +RELAYURI = "ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net" +RELAYPATH = "ar-hc-lz-pedw-ppe-uks-01" +CRMURL = "wg-pp-pedw.crm11.dynamics.com" +CRMURL_VERSION = "v9.2" # //Prod Oauth WGO @@ -61,61 +56,11 @@ CRMURL_VERSION = "v9.2" # RELAY_ROOT = https://ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net/ar-hc-lz-pedw-prod-uks-01/ # RELAYURI = "ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net" # RELAYPATH = "ar-hc-lz-pedw-prod-uks-01" -# # RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-prod-ukw-01/ -# # RELAYURI = "ar-ns-lz-pedw-prod-ukw-01.servicebus.windows.net" -# # RELAYPATH = "ar-hc-lz-pedw-prod-ukw-01" # CRMURL = "crm2016.llc.gov.wales/PEDW" # CRMURL_VERSION = "v8.2" -# D365 cnr WGO -# CLIENT_ID = 3125ac03-91ac-46a7-a166-0086f0ed90b3 -# CLIENT_SECRET = bFq8Q~9VX5Ra3lKnGGIXsB.G6-ZGMx7LqyqXjdr4 -# RELAY_ROOT = https://ar-ns-lz-pedw-d365cnr-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365cnr-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-d365cnr-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-d365cnr-uks-01" -# CRMURL = "dv-pedw.crm11.dynamics.com" -# CRMURL_VERSION = "v9.2" - -# D365 Dev WGO -# CLIENT_ID = 9b834097-03d7-409b-b038-ecfec31a394a -# CLIENT_SECRET = .Jf8Q~tKMObCJjhh_hy3J5AIctFrBalU4FS~tdxk -# RELAY_ROOT = https://ar-ns-lz-pedw-d365dev-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365dev-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-d365dev-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-d365dev-uks-01" -# CRMURL = "dv-pedw.crm11.dynamics.com" -# CRMURL_VERSION = "v9.2" - -# D365 Test WGO -# CLIENT_ID = d40b9373-96c2-4b04-8bd8-c2a2fe6444c1 -# CLIENT_SECRET = 6hT8Q~glL~jloWpGmvXw3UY37QFwjUE0r._jobEM -# RELAY_ROOT = https://ar-ns-lz-pedw-d365test-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365test-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-d365test-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-d365test-uks-01" -# CRMURL = "ts-pedw.crm11.dynamics.com" -# CRMURL_VERSION = "v9.2" - -# D365 PP WGO -# CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a -# CLIENT_SECRET = V288Q~P1lFMvvX9Xnkx2pLixK~GOREH983Owqc1E -# RELAY_ROOT = https://ar-ns-lz-pedw-d365ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365ppe-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-d365ppe-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-d365ppe-uks-01" -# CRMURL = "wg-pp-pedw.crm11.dynamics.com" -# CRMURL_VERSION = "v9.2" - -# D365 Prod WGO -# CLIENT_ID = 3e4141a3-1fbd-454e-98f0-7f3a4f14a3cc -# CLIENT_SECRET = ULC8Q~fZiXm0.teVLlZ_7LjxBOj2vIsWSApu2bAq -# RELAY_ROOT = https://ar-ns-lz-pedw-d365prod-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365prod-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-d365prod-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-d365prod-uks-01" -# CRMURL = "wg-prod-pedw.crm11.dynamics.com" -# CRMURL_VERSION = "v9.2" - - -//GOOGLE_TAG_MANAGER = GTM-T78CBC3 GOOGLE_TAG_MANAGER = G-GTWW3JT03Z SHOWLOGIN = true @@ -135,38 +80,24 @@ NEXTAUTH_URL = $API_ROOT NEXTAUTH_URL_INTERNAL = $API_ROOT NEXTAUTH_SECRET = SuperSecret -// CNR sql server +# CNR sql server # DATABASE_URL = sqlserver://sql-srv-plt-pedw-cnr-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword -// dev test sql server +# dev test sql server DATABASE_URL = sqlserver://sql-srv-plt-pedw-dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword -// test sql server +# test sql server DATABASE_URL = sqlserver://sql-srv-plt-pedw-test-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword -// Pre prod sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustservercertificate=true;hostnameincertificate=*.database.windows.net;logintimeout=30;authentication=SqlPassword +# Pre prod sql server +DATABASE_URL = sqlserver://sql-srv-plt-pedw-ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustservercertificate=true;hostnameincertificate=*.database.windows.net;logintimeout=30;authentication=SqlPassword -// Prod sql server +# Prod sql server # DATABASE_URL = sqlserver://pp-pedw-sql.database.windows.net:1433;database=PEDWPreProd;user=RobBondSQL;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword -// Prod sql server +# Prod sql server # DATABASE_URL = sqlserver://sql-srv-plt-pedw-prod-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword -// D365 CNR sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365cnr-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword - -// D365 Dev sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword - -// D365 Test sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365test-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword - -// D365 PPE sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword - -// D365 Prod sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365prod-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword @@ -174,31 +105,31 @@ SECRET = SuperSecret -//Azure Storage Account +# Azure Storage Account AZURE_CLIENT_ID = $CLIENT_ID AZURE_TENANT_ID = $TENANT AZURE_CLIENT_SECRET = $CLIENT_SECRET -// CNR key +# CNR key AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwcnruks" AZURE_STORAGE_ACCOUNT_KEY = hRffHq4IwctpaTKdS/U33Xq6nyTDF1t++WJgIwsw7gzstktZin/rRwbTIAL9KKqesVsK7EHrTTG6+ASt4+7skw== AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwcnruks.blob.core.windows.net -// new dev key +# new dev key AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwdevuks" AZURE_STORAGE_ACCOUNT_KEY = MmrsrCjBfop9pI2oImsP/+lJUWU2DrXsAB8nq5bOVROTmPovSGxjCjKw9+TAFr00k8WDUK9r6mVx+AStcMLyAA== AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwdevuks.blob.core.windows.net -// New test key +# New test key AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwtestuks" # AZURE_STORAGE_ACCOUNT_KEY = 5NdDO9GGlbTFLHFOXZRw8H2C0Ow5bFTvHwUHEbStyzg2mbd1uwHSeBSvM/dkN+HfdwrFzqAkwL5P+AStdO6cig== AZURE_STORAGE_ACCOUNT_KEY = bqJAz5pYQVGee8dnVhlsqW+xHwinNTjgY2AmojNyB+d4uAotMR20WcO4Op13yB+9dFaXAkkjtIA4+AStHT/IYg== AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwtestuks.blob.core.windows.net -// New preprod key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwppeuks" -# AZURE_STORAGE_ACCOUNT_KEY = B4GRKr+cA9b9mNM/GHFivzhzflJIZltG8bMmooXlYvGYySPAspTTHistAT30sHipnR+hh6dqUc9q+ASttAjPRQ== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwppeuks.blob.core.windows.net +# New preprod key +AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwppeuks" +AZURE_STORAGE_ACCOUNT_KEY = B4GRKr+cA9b9mNM/GHFivzhzflJIZltG8bMmooXlYvGYySPAspTTHistAT30sHipnR+hh6dqUc9q+ASttAjPRQ== +AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwppeuks.blob.core.windows.net # PROD key @@ -211,58 +142,32 @@ AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwtestuks.blob.core.windows.net # AZURE_STORAGE_ACCOUNT_KEY = OfV9MFqXcLLPam9GhEdMWadIOwIzhc5hd5TUqzIuQoswFdtV4ybaemjatSv+Kht0Ny1qQKjLplwY+AStUTZJrg== # AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwproduks.blob.core.windows.net -# D365 CNR key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365cnruks" -# AZURE_STORAGE_ACCOUNT_KEY = nEwCp+4FuVf/9a4DOyKuQO2yRRV4Ypigg20ch1YaiL09YmEhWMEO5pNLpYtI3WnQCkIqeu1CUqwM+AStCLW01g== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365cnruks.blob.core.windows.net - -# D365 DEV key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365devuks" -# AZURE_STORAGE_ACCOUNT_KEY = XzwQxuZXSwYrVgemDUD2ymTpYTIfWqhypbmEALw1ZEEP0M4D3b3Gh28clSymxwFu7Gu9mhwkW4/V+AStZ6RF8Q== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365devuks.blob.core.windows.net - -# D365 TEST key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365testuks" -# AZURE_STORAGE_ACCOUNT_KEY = 5NdDO9GGlbTFLHFOXZRw8H2C0Ow5bFTvHwUHEbStyzg2mbd1uwHSeBSvM/dkN+HfdwrFzqAkwL5P+AStdO6cig== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365testuks.blob.core.windows.net - -# D365 PP key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365ppeuks" -# AZURE_STORAGE_ACCOUNT_KEY = xQz4FWDnQDE09uPOCj1t7axRiquvZG9vbxL88ywzVsJnTsAgewrHnuAGkxGPBCp2xGE53ffYOme6+AStSr8OoQ== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365ppeuks.blob.core.windows.net - -# D365 Prod key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365produks" -# AZURE_STORAGE_ACCOUNT_KEY = DvKcWhDpFKMFdVXFF2SqDG0sYQ3AkNCmJUBG0cvgzi5NBHS4Ahd2hPAlfA3hGi2EbohUag3vCucK+ASt1IdBhA== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365produks.blob.core.windows.net - AZURE_PEDW_CONTAINER = "pedwapplications" AZURE_PEDW_QUEUE_ENDPOINT = https://$AZURE_STORAGE_ACCOUNT_NAME.queue.core.windows.net -//gov.notify -//NOTIFY_API_KEY = pedwnextauthkey-90f93710-9dae-423e-aea1-11eccd0dc132-0c9469d9-d671-444d-9e9d-3e5a62001043 -NOTIFY_API_KEY = pedwnextauthlivekey-90f93710-9dae-423e-aea1-11eccd0dc132-5c0ed187-754a-4b04-8c99-978e5710c2c1 +# gov.notify +# NOTIFY_API_KEY = pedwnextauthkey-90f93710-9dae-423e-aea1-11eccd0dc132-0c9469d9-d671-444d-9e9d-3e5a62001043 +# NOTIFY_API_KEY = pedwnextauthlivekey-90f93710-9dae-423e-aea1-11eccd0dc132-5c0ed187-754a-4b04-8c99-978e5710c2c1 +NOTIFY_API_KEY = pedwnextauthlive01key-90f93710-9dae-423e-aea1-11eccd0dc132-72bfb1e1-c0b7-47cb-9887-3d47a4b11ad4 SHOW_DEBUG = true -// DEV Connection string +# DEV Connection string NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=4b7568b3-70bd-4591-9dc2-267c0a6501d1;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=447d38b5-5d40-4238-8fb3-0ef4741b3db1 - APPLICATIONINSIGHTS_CONNECTIONSTRING = InstrumentationKey=7a06e24a-793c-47bc-8987-ce80f180e035;IngestionEndpoint=https://uksouth-1.in.applicationinsights.azure.com/;LiveEndpoint=https://uksouth.livediagnostics.monitor.azure.com/;ApplicationId=9906b6d0-b791-4add-8d53-c827a534e1d4 - -// Test Connection string +# Test Connection string NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=cc60e8e7-5b30-4c2b-a176-8b96b21b12df;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=2a4094be-6fd1-46ab-a67d-5f241692b129 -// Preprod Connection string +# Preprod Connection string # NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=7370f0f9-834d-4c7a-b3c4-9951fcad5ad2;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=04362cb7-6dfc-469e-8274-12243e8be851 # NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=a41b91bf-f7dc-4f44-bb66-a67df4eee435;IngestionEndpoint=https://uksouth-1.in.applicationinsights.azure.com/;LiveEndpoint=https://uksouth.livediagnostics.monitor.azure.com/;ApplicationId=473d0632-16f1-4065-8984-d6bac62402c0 -// Prod Connection string +# Prod Connection string # NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=61f358c2-8075-4164-b897-17bc6dd31fee;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=0c297349-396b-4dc6-8b56-85f5c680adc6 @@ -273,4 +178,6 @@ SHOWSIPS = true ALLOWED_IPS=::1,203.0.113.42,198.51.100.17 UPLOAD_BATCH_COUNT = 5 -NRWDOMAIN = rdbmedia.co.uk \ No newline at end of file +NRWDOMAIN = rdbmedia.co.uk + +MAP_TARGET = pedw-dev \ No newline at end of file diff --git a/context/api-route-map.md b/context/api-route-map.md new file mode 100644 index 00000000..805a9878 --- /dev/null +++ b/context/api-route-map.md @@ -0,0 +1,994 @@ +# API Route Map & Maintainer Guide + +## Status + +First-generation maintainer guide. + +This document follows the completed: + +- Domain Architecture Programme +- Authorization Architecture Assessment +- Portal Integration Contract & API Platform Assessment + +It is maintainability-focused. + +It is **not** a full route inventory. + +## Purpose + +This route map is intended to help a maintainer answer four practical questions before changing API behaviour: + +```text +Where should I start? +Which APIs are involved? +Which helpers are involved? +What integrations and risks am I touching? +``` + +The key working assumption from the completed assessment is: + +> Routes are primarily owned by journeys/features, not folders. + +--- + +## Journey Catalogue + +### 1. Public Search + +#### Purpose + +Supports public case search, filtering, pagination, and result shaping across standard and advanced search journeys. + +#### Primary UI Entry Points + +- `pages/searchresults.js` +- `pages/advancedsearchresults.js` +- `components/search/searchresults.js` +- `components/search/addresssearchresults.js` +- `components/search/dnssearchresults.js` + +#### Service Layer + +- `actions/services/searchDirectService.js` + - `getBasicSearchPaged` + - `getAdvancedSearchPaged` + - related basic/advanced search helpers + +#### API Routes + +- `pages/api/endpoint/getbasicsearch_api.js` +- `pages/api/endpoint/getbasicsearchpaged_api.js` +- `pages/api/endpoint/getadvancedsearch_api.js` +- `pages/api/endpoint/getadvancedsearchpaged_api.js` +- adjacent variants: + - `getbasicsearch_by_address_api.js` + - `getbasicsearch_by_lparref_api.js` + - DNS/public search variants + +#### Integrations + +- CRM Relay +- Local-only + +#### Ownership Type + +- **feature-owned** + +#### Change Risk + +- **high** + +Reason: + +- public-facing +- contract-critical results and filters +- paged/unpaged variants +- transform-heavy output shaping + +#### First-Look Checklist + +Before changing public search: + +1. Inspect the relevant `getbasicsearch*` / `getadvancedsearch*` route family +2. Inspect `searchDirectService` callers +3. Confirm paging, sorting, and transform expectations +4. Check whether related document/detail routes are also affected + +--- + +### 2. Case Details + +#### Purpose + +Supports public and portal case detail retrieval, linked-case lookups, messages, and related case-view data. + +#### Primary UI Entry Points + +- `pages/case/[ticketnumber].js` +- `pages/case/id/[incident].js` +- `components/case/summary.js` +- `components/case.js` + +#### Service Layer + +- `actions/services/caseDirectService.js` + - `getCase` + - `getCaseByID` + - `getCaseMessage` + - `getIncidentbyID` + +#### API Routes + +- `pages/api/endpoint/getcase_api.js` +- `pages/api/endpoint/getcasebyid_api.js` +- `pages/api/endpoint/getincidentbyid_api.js` +- `pages/api/endpoint/getcasemessage_api.js` +- `pages/api/endpoint/getlinkedcases_api.js` + +#### Integrations + +- CRM Relay + +#### Ownership Type + +- **feature-owned** + +#### Change Risk + +- **high** + +Reason: + +- case detail contracts are widely consumed +- related routes often share assumptions about identifiers and transformed fields + +#### First-Look Checklist + +Before changing case details: + +1. Inspect the relevant case detail route family +2. Inspect `caseDirectService` callers +3. Check linked-case/message side routes +4. Confirm UI expectations in case summary/detail components + +--- + +### 3. Documents + +#### Purpose + +Supports published document metadata retrieval and published document download. + +#### Primary UI Entry Points + +- `components/search/searchresults.js` +- case/search document links surfaced in search and case journeys + +#### Service Layer + +- `actions/services/searchDirectService.js` +- `actions/services/caseDirectService.js` for adjacent case-document lookups + +#### API Routes + +- `pages/api/endpoint/getsearchdocumentdetails_api.js` +- `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` +- `pages/api/endpoint/getsearchdocumenthistory_api.js` +- `pages/api/endpoint/getsearchdocumenthistorypaged_api.js` +- `pages/api/endpoint/getsearchdocumentTypes_api.js` +- `pages/api/documents/download/[id].js` + +#### Integrations + +- CRM Relay +- Local-only + +#### Ownership Type + +- **feature-owned** with an **integration-owned** download proxy boundary + +#### Change Risk + +- **high** + +Reason: + +- direct user-facing download behaviour +- metadata, hash-link generation, and binary delivery split across multiple areas + +#### First-Look Checklist + +Before changing documents: + +1. Inspect metadata/detail/history routes +2. Inspect `documents/download/[id].js` +3. Confirm hash-link generation expectations +4. Check search/case UI consumers that surface document links + +--- + +### 4. My Portal Dashboard + +#### Purpose + +Supports authenticated portal lists and dashboard cards for cases, representations, and awaiting-submission work. + +#### Primary UI Entry Points + +- `pages/myportal/index.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` +- `components/myportal.js` + +#### Service Layer + +- `actions/services/portalDirectService.js` +- `actions/services/documentDirectService.js` for draft/blob-backed dashboard items + +#### API Routes + +- `pages/api/endpoint/getmycases_api.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getawaitingsubmission_api.js` +- adjacent blob/proxy routes for draft-backed data + +#### Integrations + +- CRM Relay +- Azure Storage + +#### Ownership Type + +- **feature-owned** + +#### Change Risk + +- **high** + +Reason: + +- authenticated portal-critical journey +- mixes CRM-owned and draft/blob-backed data + +#### First-Look Checklist + +Before changing my portal dashboard: + +1. Inspect primary dashboard list routes +2. Inspect `portalDirectService` and `documentDirectService` +3. Check dashboard components (`topthree`, `viewall`) +4. Check portal state modules and current-view assumptions + +--- + +### 5. Watched Cases + +#### Purpose + +Supports create/read/delete behaviour for watched cases across search, case, and myportal journeys. + +#### Primary UI Entry Points + +- `components/search/searchresults.js` +- `components/case/summary.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` + +#### Service Layer + +- `actions/services/portalDirectService.js` + - `getWatchedCases` + - `getWatchedCasesProxy` + - `createWatchedCases` + - `deleteWatchedCases` + +#### API Routes + +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getwatchedcasesproxy_api.js` +- `pages/api/endpoint/createwatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcasesproxy_api.js` + +#### Integrations + +- CRM Relay +- Local-only + +#### Ownership Type + +- **feature-owned** with orchestration on create/upsert + +#### Change Risk + +- **high** + +Reason: + +- multiple entry points +- state refresh after mutation +- mixed proxy/non-proxy and upsert behaviour + +#### First-Look Checklist + +Before changing watched cases: + +1. Inspect watched case route family +2. Inspect `portalDirectService` +3. Inspect `store/watchedCases/*` and `store/currentView/*` +4. Inspect CRM relationship and duplicate-check assumptions + +--- + +### 6. Representations + +#### Purpose + +Supports representation retrieval, editing, and related case/portal representation views. + +#### Primary UI Entry Points + +- `pages/myportal/representation.js` +- `components/representation.js` +- `components/case/representation/*` + +#### Service Layer + +- `actions/services/portalDirectService.js` +- `actions/services/documentDirectService.js` + +#### API Routes + +- `pages/api/endpoint/getrepresentations_api.js` +- `pages/api/endpoint/getrepresentationsproxy_api.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getmyrepresentationsproxy_api.js` +- adjacent representation draft/blob routes in `pages/api/file` + +#### Integrations + +- CRM Relay +- Azure Storage + +#### Ownership Type + +- **feature-owned** + +#### Change Risk + +- **high** + +Reason: + +- representation journeys span CRM records and blob-backed draft/edit data + +#### First-Look Checklist + +Before changing representations: + +1. Inspect representation read routes +2. Inspect blob-backed draft/edit support routes +3. Inspect portal/document service callers +4. Inspect currentView and related representation state + +--- + +### 7. Draft Appeals + +#### Purpose + +Supports draft appeal progress, draft files, and storage-backed resume state before submission. + +#### Primary UI Entry Points + +- `pages/newappeal/[appealtypes].js` +- `pages/myportal/[appealtypes].js` +- `lib/newappeal/loadNewAppealPage.js` +- `lib/myportal/loadMyPortalAppealPage.js` + +#### Service Layer + +- `actions/services/documentDirectService.js` +- `actions/azurestorage.js` + +#### API Routes + +- `pages/api/file/getprogressobjblob.js` +- `pages/api/file/getbloblist.js` +- `pages/api/file/upload.js` +- `pages/api/file/uploadsinglefile.js` +- `pages/api/file/deleteblobcase.js` +- `pages/api/file/setupcontainer.js` + +#### Integrations + +- Azure Storage +- Local-only + +#### Ownership Type + +- **integration-owned** supporting a feature journey + +#### Change Risk + +- **high** + +Reason: + +- storage-backed draft integrity +- upload/delete/progress flows are user-critical before submission + +#### First-Look Checklist + +Before changing draft appeals: + +1. Inspect progress/blob list/upload/delete routes +2. Inspect `documentDirectService` +3. Inspect `actions/azurestorage.js` +4. Inspect new appeal loaders and draft state assumptions + +--- + +### 8. Draft Representations + +#### Purpose + +Supports representation draft JSON/files before final representation submission. + +#### Primary UI Entry Points + +- `pages/myportal/representation.js` +- `components/case/representation/*` +- `lib/representation/pageLoaders.js` + +#### Service Layer + +- `actions/services/documentDirectService.js` +- `actions/azurestorage.js` + +#### API Routes + +- `pages/api/file/getrepsblob.js` +- `pages/api/file/getrepsblobproxy.js` +- `pages/api/file/editRepJson.js` +- `pages/api/file/upload.js` +- `pages/api/file/deleteblobrep.js` + +#### Integrations + +- Azure Storage + +#### Ownership Type + +- **integration-owned** supporting a feature journey + +#### Change Risk + +- **high** + +Reason: + +- draft representation data and uploads are part of a sensitive user submission path + +#### First-Look Checklist + +Before changing draft representations: + +1. Inspect rep blob routes and edit JSON route +2. Inspect `documentDirectService` +3. Inspect storage helper behaviour in `actions/azurestorage.js` +4. Check representation page loader and currentView dependencies + +--- + +### 9. Appeal Submission / Finalisation + +#### Purpose + +Transitions draft appeal state into submitted/finalised processing. + +#### Primary UI Entry Points + +- `pages/myportal/[appealtypes].js` +- `lib/myportal/loadMyPortalAppealPage.js` +- new appeal completion and check-answer flows + +#### Service Layer + +- `actions/services/documentDirectService.js` +- `actions/services/accountDirectService.js` +- `actions/services/caseDirectService.js` +- `actions/azurestorage.js` + +#### API Routes + +- `pages/api/file/createappealcompletemessage_api.js` +- `pages/api/file/createappealcompletemessageproxy_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/endpoint/patchcase_api.js` +- `pages/api/endpoint/updatecase_api.js` + +#### Integrations + +- CRM Relay +- Azure Storage +- Azure Queue +- Local-only + +#### Ownership Type + +- **orchestration-owned** + +#### Change Risk + +- **very high** + +Reason: + +- crosses storage, queue/finalisation, and CRM write boundaries +- contract-critical workflow transition + +#### First-Look Checklist + +Before changing appeal submission/finalisation: + +1. Inspect completion/finalisation routes +2. Inspect `createcase_api`, `patchcase_api`, `updatecase_api` +3. Inspect `documentDirectService` and `azurestorage` helpers +4. Inspect draft loaders and state handoff assumptions + +--- + +### 10. Representation Submission / Finalisation + +#### Purpose + +Transitions drafted or newly entered representation content into submitted representation processing. + +#### Primary UI Entry Points + +- `pages/myportal/representation.js` +- `components/case/representation/representationComplete.js` + +#### Service Layer + +- `actions/services/portalDirectService.js` +- `actions/services/documentDirectService.js` +- `actions/services/notifyDirectService.js` +- `actions/azurestorage.js` + +#### API Routes + +- `pages/api/file/createrepcompletemessage_api.js` +- `pages/api/file/createrepinvolvement_api.js` +- `pages/api/endpoint/deletemyrepresentations_api.js` +- adjacent representation read/write support routes in `endpoint` and `file` + +#### Integrations + +- CRM Relay +- Azure Storage +- Azure Queue +- GOV.UK Notify + +#### Ownership Type + +- **orchestration-owned** + +#### Change Risk + +- **very high** + +Reason: + +- multi-integration workflow +- user submission and notification side effects + +#### First-Look Checklist + +Before changing representation submission/finalisation: + +1. Inspect completion and involvement routes +2. Inspect representation completion component and callers +3. Inspect portal/document/notify service helpers +4. Confirm storage, CRM, and notification sequencing assumptions + +--- + +### 11. Account Registration + +#### Purpose + +Creates CRM-backed portal account/contact records for authenticated users who do not yet have portal account state. + +#### Primary UI Entry Points + +- `pages/account/register.js` +- `components/account/registerform.js` +- `components/account/registerCheck.js` +- `components/account/registerComplete.js` + +#### Service Layer + +- `actions/services/accountDirectService.js` + - `createAccount` + - `getPortalLogin` + +#### API Routes + +- `pages/api/endpoint/createaccount_api.js` +- `pages/api/endpoint/getemailaccountcheck_api.js` +- `pages/api/endpoint/getportallogin_api.js` + +#### Integrations + +- CRM Relay +- NextAuth + +#### Ownership Type + +- **feature-owned** + +#### Change Risk + +- **high** + +Reason: + +- identity bootstrap and portal account creation are foundational + +#### First-Look Checklist + +Before changing account registration: + +1. Inspect create-account and login/account-check routes +2. Inspect `accountDirectService` +3. Inspect registration pages/components +4. Confirm session-to-contact bootstrap assumptions + +--- + +### 12. Personal Details / Account Management + +#### Purpose + +Supports personal details retrieval and update for authenticated portal users. + +#### Primary UI Entry Points + +- `pages/account/personaldetails.js` +- `components/account/personaldetails.js` +- `components/myportal/youraccount.js` + +#### Service Layer + +- `actions/services/accountDirectService.js` + - `getPersonalAccount` + - `updateAccount` + +#### API Routes + +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/updateaccount_api.js` +- adjacent support routes: + - `getpreferredlanguage_api.js` + - `updatepassword_api.js` + +#### Integrations + +- CRM Relay +- NextAuth +- Local-only + +#### Ownership Type + +- **feature-owned** + +#### Change Risk + +- **high** + +Reason: + +- user-critical account data +- identity/bootstrap coupling + +#### First-Look Checklist + +Before changing personal details/account management: + +1. Inspect account read/update routes +2. Inspect `accountDirectService` +3. Inspect account pages/components +4. Inspect accountDetails state and session/bootstrap dependencies + +--- + +### 13. Authentication / Sign-In + +#### Purpose + +Handles sign-in, verify-request, callback, redirect, and locale-aware auth/session behaviour. + +#### Primary UI Entry Points + +- `pages/index.js` +- auth pages and callback entry paths + +#### Service Layer + +- `lib/auth/*` +- `actions/services/accountDirectService.js` for portal login resolution + +#### API Routes + +- `pages/api/auth/[...nextauth].js` +- `pages/api/auth/resolve-locale.js` +- adjacent support routes: + - `pages/api/endpoint/getportallogin_api.js` + - `pages/api/endpoint/getpreferredlanguage_api.js` + +#### Integrations + +- NextAuth +- GOV.UK Notify +- CRM Relay + +#### Ownership Type + +- **support-owned** with platform-critical behavior + +#### Change Risk + +- **very high** + +Reason: + +- session and redirect behaviour are highly sensitive +- cross-cutting impact across the platform + +#### First-Look Checklist + +Before changing authentication/sign-in: + +1. Inspect `[...nextauth].js` +2. Inspect locale resolution behavior +3. Inspect portal login/preferred-language supporting routes +4. Confirm callback, redirect, and EN/CY assumptions + +--- + +### 14. Notifications / Email + +#### Purpose + +Handles direct Notify sends and broader notification workflows that gather CRM/document/event data before sending. + +#### Primary UI Entry Points + +- journey completion flows +- auth verify-request/sign-in flows +- background or triggered notification flows + +#### Service Layer + +- `actions/services/notifyDirectService.js` +- `actions/services/portalDirectService.js` + +#### API Routes + +- `pages/api/email/notify.js` +- `pages/api/email/getall.js` +- `pages/api/email/getdocuments.js` +- `pages/api/email/getevents.js` +- `pages/api/email/getmailinglist.js` +- `pages/api/email/getcaseref.js` + +#### Integrations + +- GOV.UK Notify +- CRM Relay +- Local-only + +#### Ownership Type + +- **orchestration-owned** + +#### Change Risk + +- **high** + +Reason: + +- user communications +- template and timing side effects +- some routes aggregate data before sending + +#### First-Look Checklist + +Before changing notifications/email: + +1. Inspect whether the route is a thin send route or an aggregation route +2. Inspect Notify helper usage +3. Inspect document/event/list side data dependencies +4. Confirm EN/CY template assumptions + +--- + +### 15. Admin / Reporting + +#### Purpose + +Provides internal/admin reporting and grouped status/document views. + +#### Primary UI Entry Points + +- admin pages/components +- internal reporting views + +#### Service Layer + +- `actions/services/adminDirectService.js` + +#### API Routes + +- `pages/api/admin/getnewappeals_api.js` +- `pages/api/admin/getlatestdocuments_api.js` +- `pages/api/admin/getStatusCountsByAppealAndLPA_api.js` +- adjacent status/reporting routes in `admin` + +#### Integrations + +- CRM Relay + +#### Ownership Type + +- **support-owned** + +#### Change Risk + +- **medium** + +Reason: + +- smaller, more coherent area +- still CRM-transform heavy and potentially used operationally + +#### First-Look Checklist + +Before changing admin/reporting: + +1. Inspect the relevant admin route family +2. Inspect `adminDirectService` +3. Confirm reporting/grouping transform assumptions +4. Check whether public or portal-facing contracts are indirectly reused + +--- + +## Shared Platform Section + +### Shared API Building Blocks + +#### `relayGet(...)` + +- **Responsibility:** standardized CRM relay GET forwarding +- **Commonly used in:** `pages/api/endpoint/**`, especially read families +- **Preferred for future work?** yes, for new CRM read routes where the shared relay-read model fits + +#### `relayGetData(...)` + +- **Responsibility:** supplementary relay-backed data fetches inside transforms/orchestration +- **Commonly used in:** advanced search enrichment, lookup hybrids, upsert pre-checks +- **Preferred for future work?** yes, where a route needs sub-queries without directly writing to `res` + +#### `respondSuccess(...)` + +- **Responsibility:** shared success JSON response envelope +- **Commonly used in:** endpoint, file, email, admin, and middleware-aware routes +- **Preferred for future work?** yes + +#### `respondError(...)` + +- **Responsibility:** shared error JSON response envelope +- **Commonly used in:** endpoint, file, email, admin, and middleware-aware routes +- **Preferred for future work?** yes + +#### Relay policy helpers / presets + +- **Responsibility:** shared timeout/retry profiles for relay reads +- **Commonly used in:** modern helper-oriented relay routes +- **Preferred for future work?** yes, where an existing policy profile is appropriate + +#### Hash helpers + +- **Responsibility:** path signing and request-integrity validation +- **Commonly used in:** relay-bound routes and storage/blob routes +- **Preferred for future work?** yes, where the existing signed-route model must be preserved + +#### Signed request helpers + +- **Responsibility:** shared signed GET/POST/DELETE request execution +- **Commonly used in:** service/client layer helpers for signed route access +- **Preferred for future work?** yes, where signed request composition already exists + +#### Azure storage helpers + +- **Responsibility:** blob/container/queue operations and related metadata handling +- **Commonly used in:** `pages/api/file/**`, draft/finalisation helpers, storage-backed journeys +- **Preferred for future work?** yes for storage-facing behavior + +#### Notify helpers + +- **Responsibility:** GOV.UK Notify send behavior and related helper flows +- **Commonly used in:** `pages/api/email/**`, auth email flow, service layer +- **Preferred for future work?** yes, but keep send routes thin unless orchestration is required + +#### Auth/session helpers + +- **Responsibility:** session establishment, locale resolution, auth-related supporting context +- **Commonly used in:** `pages/api/auth/**`, SSR loaders, auth support flows +- **Preferred for future work?** yes within the established NextAuth/session boundary + +--- + +## Maintainer Guidance + +### When Adding a New API + +Recommended decision sequence: + +```text +1. Which journey owns this? +2. Which integration does it touch? +3. Does an existing route family already exist? +4. Can existing helpers be reused? +5. Is the route contract-critical? +``` + +Guidance notes: + +- start from journey ownership before folder ownership +- prefer existing families and helpers where they already fit +- do not copy older direct-wrapper patterns by default when newer shared patterns exist +- do not refactor stable legacy routes without explicit approval and characterization + +--- + +## Risks / Cautions + +1. This is a **first-generation maintainer guide**, not a full inventory. +2. It is intentionally journey-first and route-family-first, not exhaustive route-by-route documentation. +3. Folder names still do not reliably indicate current ownership. +4. High-risk changes still need direct file inspection before editing, especially in `endpoint`, `file`, `auth`, and finalisation flows. + +--- + +## Validation performed + +Manual consolidation only. + +Performed: + +- re-read required assessment and architecture context +- reused the stable findings from the completed API platform assessment +- converted folder-oriented conclusions into a journey-owned maintainer route map + +Not performed: + +- no new runtime analysis +- no scripts +- no automated inventory generation +- no code changes + +--- + +## Recommendation + +This route map is sufficient as a **first-generation maintainer guide**. + +An additional documentation slice is justified only if the team wants one of the following future planning outputs: + +- a more detailed **API Route Map / Maintainer Guide v2** with deeper per-journey edge cases +- an **API Rationalisation Planning** document focused on future consolidation candidates + +No implementation work is recommended from this guide alone. diff --git a/context/architecture.md b/context/architecture.md index 1871804d..90e374c5 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -18,15 +18,287 @@ Adoption Planning ### Next Recommended Architecture Stream -Portal API Security & Access Boundary Assessment +Authorization architecture stream complete. + +Next recommended architecture stream: + +Portal authorization hardening / consistency planning (documentation-first, implementation only by explicit approval) ### Objectives -- endpoint inventory -- authenticated/public classification -- ownership validation review -- access-control consistency review -- security boundary assessment +- record completed assessment conclusions +- preserve stable authorization architecture model +- use the model as a baseline for future hardening/change review + +## Portal API Platform Assessment Status (2026-06-20) + +### Stream status + +**Portal Integration Contract & API Platform Assessment: COMPLETE** + +### Consolidated architectural conclusion + +The PEDW API platform is large in route count but materially smaller in underlying structure than the file count first suggests. + +At an architecture level it is best understood as: + +```text +Large route surface + ↓ +small route-family vocabulary + ↓ +small contract-shape vocabulary + ↓ +small implementation-style vocabulary +``` + +The main architectural and maintenance issue is therefore not discovery of a fundamentally different API architecture. + +It is primarily: + +- findability +- ownership clarity +- consistency and reuse discipline + +### Stable route-family model + +The completed assessment supports the following stable API platform families: + +- CRM relay routes +- storage/blob routes +- finalisation/orchestration routes +- email/notification routes +- document download routes +- auth/session routes +- admin/internal routes +- middleware/helper routes +- local utility/meta routes + +### Stable contract-shape model + +The completed assessment supports the following repeated contract shapes: + +- Public CRM read +- User-owned CRM read +- CRM create +- CRM update/patch +- CRM delete +- Proxy/pass-through +- Lookup/config/support +- Hybrid upsert/orchestration +- Storage read/write/delete +- Queue/finalisation +- Notify send / notification orchestration + +### Stable implementation-style model + +Three main implementation styles explain most of the API surface: + +1. **Newer helper-oriented** + - `relayGet(...)` + - `relayGetData(...)` + - `respondSuccess(...)` + - `respondError(...)` + - relay policy presets +2. **Older direct-wrapper** + - `getToken()` + - direct `axios(config)` + - manual `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)` +3. **Orchestration-heavy** + - finalisation routes + - email aggregation routes + - storage + queue + CRM side-effect routes + +These older patterns are not inherently incorrect; they reflect prior delivery constraints. The key future discipline is whether they should be copied forward when shared helper patterns already exist. + +### Folder drift and maintenance hotspots + +Stable drift model: + +- low drift: `documents`, `admin`, `middleware`, top-level utility/meta +- low/moderate drift: `auth` +- moderate drift: `email` +- high drift: `endpoint`, `file` + +Highest maintenance hotspots: + +- **high:** `pages/api/endpoint`, `pages/api/file` +- **medium-high:** `pages/api/auth`, `pages/api/middleware` +- **medium:** `pages/api/email` +- **lower:** `pages/api/documents`, `pages/api/admin` + +### Proven / not proven status + +#### Proven + +- the API platform assessment is representative at the pattern level +- route count overstates true structural diversity +- most routes are explained by a small number of repeated route families, contract shapes, and implementation styles +- the main maintenance problem is findability and ownership clarity + +#### Not proven + +- no full route-by-route inventory was produced +- no route consolidation safety assessment has been performed +- no implementation readiness decision has been approved +- no route movement or removal is recommended at this stage + +### Programme guidance + +This stream should now be considered complete. + +If future work is approved, it should be framed as: + +- API Route Map / Maintainer Guide planning +- API rationalisation planning + +and not as implementation work by default. + +## Portal Authorization Architecture Status (2026-06-19) + +### Stream status + +**Portal API Security & Access Boundary Assessment: COMPLETE** + +### Consolidated architectural conclusion + +PEDW currently exhibits a **distributed authorization model**. + +The dominant observed pattern is: + +```text +Identity established + ↓ +Ownership scope established + ↓ +Ownership identifier propagated + ↓ +Integrity controls applied + ↓ +Operation executed +``` + +rather than a uniformly route-local model where identity and ownership are re-derived and re-proven inside each final handler. + +### Principal authorization roots + +#### Public anonymous + +```text +Anonymous +→ public search +→ public case viewing +``` + +#### CRM-owned data + +```text +NextAuth session +→ session.user.email +→ getPortalLogin(email) +→ CRM Contact +→ CRM relationships +→ CRM operations +``` + +#### Draft / storage-owned data + +```text +NextAuth session +→ session.user.id +→ user-specific storage container +→ draft JSON +→ uploaded files +``` + +### Integrity and execution controls + +#### Signed hash + +The signed hash most clearly provides: + +- route integrity +- query integrity +- parameter integrity +- identifier integrity + +It should be understood as: + +> an integrity control rather than an object-authorization control. + +#### Azure Storage execution + +```text +PEDW API +→ Azure SDK +→ storage account credentials +→ Azure Storage +``` + +Users do not directly access Azure Storage in the reviewed architecture. + +#### Azure Relay execution + +```text +PEDW API +→ signed hash +→ Azure Relay +→ CRM +``` + +Relay hash validation is a route/path integrity mechanism. + +Relay-to-CRM authentication remains out of scope for this architecture conclusion. + +### Proven / not proven status + +#### Proven + +- distributed authorization model exists +- ownership is generally established upstream +- identifiers are propagated downstream +- route-local referential verification is not consistently visible +- storage ownership is rooted in `session.user.id` +- CRM ownership is rooted in CRM Contact identity +- signed hash strengthens integrity controls +- storage execution is server-mediated rather than direct browser-to-storage + +#### Not proven + +- no confirmed exploitability +- no demonstrated User A → User B mutation +- no demonstrated authorization bypass +- no evidence that prior OWASP assessments, health checks, or penetration tests are invalid +- no evidence of direct browser-to-storage or direct browser-to-CRM access + +### Risk characterization + +The completed stream should be understood primarily as: + +- architectural integrity risk +- auditability risk +- maintainability risk +- future-change risk + +It should **not** currently be characterised as: + +- a confirmed vulnerability +- a demonstrated exploit +- broken authorization + +unless materially new evidence emerges. + +### Programme guidance + +No immediate remediation programme is recommended on current evidence alone. + +If future work is approved, it should be framed as: + +- authorization hardening +- consistency improvements +- maintainability improvements + +rather than emergency security remediation. ## Runtime Topology diff --git a/context/journey-architecture-map.md b/context/journey-architecture-map.md new file mode 100644 index 00000000..6e0efb80 --- /dev/null +++ b/context/journey-architecture-map.md @@ -0,0 +1,6065 @@ +## Journey Architecture Map + +This document is a maintainability-focused architecture map for two PEDW journeys: + +1. Public Search → Case Details +2. 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.js` +- `components/search/searchresults.js` +- `pages/case/[ticketnumber].js` +- `components/case.js` +- `components/case/summary.js` +- adjacent breadcrumb/state context: + - `components/breadcrumbs.js` + - `store/currentView/*` + +#### Loaders / Initialisation + +##### Search results page + +- `pages/searchresults.js` + - `getServerSideProps` does **light bootstrap only** + - captures request IP via `getIP(req)` + - derives linked-case mode from `query.lk` + - reads feature flags: + - `SHOWLOGIN` + - `SHOWREPRESENTATIONS` + - 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.js` via paged service calls. + +##### Search result data bootstrap + +- `components/search/searchresults.js` + - reads `searchResultsObj` and `searchDetailsObj` from 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(...)` + +##### Case details page + +- `pages/case/[ticketnumber].js` + - `getServerSideProps` is 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 relevant + - `setMediaDetails(mediaObj)` when relevant + - `setCurrentReference({...})` + - redirects to `/404` if 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 + +- `store/searchOutput/reducer.js` + - owns: + - `searchResultsObj` + - `searchDetailsObj` + - `documentDetailsObj` + - `representationsObj` + - `eventDetailsObj` + - `mediaDetailsObj` + - this is the main read model for both results and case-detail rendering + +- `store/currentView/reducer.js` + - owns: + - `caseReference` + - `currentPage` + - `showReps` + - `showLogin` + - `linkedCaseReferences` + - `locale` + - `caseReference` is the key bridge from result selection into detail context + +- `store/accountDetails/reducer.js` + - only participates when a user is signed in on the case detail route + - owns signed-in account context: + - `accountDetails` + - `loggedinUserId` + - `containerID` + +- `store/watchedCases/reducer.js` + - participates when signed-in users watch/unwatch or manage email notifications from results + - owns: + - `watchedCases` + - `watchedCasesDetails` + +##### `currentView` usage + +- `components/search/searchresults.js` + - sets `currentReference` on case link click + - updates `currentPage` during pagination/sort + +- `pages/case/[ticketnumber].js` + - sets canonical case reference context for the detail page + +- `components/breadcrumbs.js` + - uses `currentView.caseReference` to reconstruct breadcrumb state and origin context + +##### `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.js` + - `getBasicSearch(...)` + - `getBasicSearchPaged(...)` + - `getAdvancedSearchPaged(...)` + - `getBasicSearchDetails(...)` + - `getBasicSearchDetailsPaged(...)` + - `getSearchDocumentDetails(...)` + - `getLinkedCases(...)` + +- `actions/services/caseDirectService.js` + - `getCaseMessage(...)` + - `getCase(...)` + - `getCaseByID(...)` + - `getSIPSEvents(...)` + - `getSIPSMedia(...)` + - `getPortalModuleDetails(...)` for adjacent case-detail enrichment patterns + +- `actions/services/accountDirectService.js` + - `getPortalLogin(...)` + - `getPersonalAccount(...)` + - only used on the optional signed-in branch of case detail bootstrap + +##### Supporting maintainability helper + +- `components/utils/index.js` + - `getSearchDetails(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.js` + - `pages/api/endpoint/getbasicsearchpaged_api.js` + - `pages/api/endpoint/getadvancedsearch_api.js` + - `pages/api/endpoint/getadvancedsearchpaged_api.js` + +- Search detail expansion / supporting reads + - `pages/api/endpoint/getbasicsearchdetails_api.js` + - `pages/api/endpoint/getbasicsearchdetailspaged_api.js` + - `pages/api/endpoint/getlinkedcases_api.js` + +- Case-specific reads + - `pages/api/endpoint/getcase_api.js` + - `pages/api/endpoint/getcasebyid_api.js` + - `pages/api/endpoint/getcasemessage_api.js` + - `pages/api/endpoint/getsipsevents_api.js` + - `pages/api/endpoint/getsipsmedia_api.js` + +- Signed-in account bootstrap on the case page + - `pages/api/endpoint/getportallogin_api.js` + - `pages/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 + +#### 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 + +- **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.js` +- `components/search/searchresults.js` +- `pages/case/[ticketnumber].js` +- `components/case.js` +- `components/case/summary.js` +- `actions/services/searchDirectService.js` +- `actions/services/caseDirectService.js` +- `components/utils/index.js` +- `store/search/reducer.js` +- `store/searchOutput/reducer.js` +- `store/currentView/reducer.js` + +##### Likely adjacent files + +- `pages/api/endpoint/getbasicsearch_api.js` +- `pages/api/endpoint/getbasicsearchpaged_api.js` +- `pages/api/endpoint/getbasicsearchdetails_api.js` +- `pages/api/endpoint/getbasicsearchdetailspaged_api.js` +- `pages/api/endpoint/getcase_api.js` +- `pages/api/endpoint/getcasemessage_api.js` +- `pages/api/endpoint/getlinkedcases_api.js` +- `pages/api/endpoint/getsipsevents_api.js` +- `pages/api/endpoint/getsipsmedia_api.js` +- `store/accountDetails/reducer.js` +- `store/watchedCases/reducer.js` +- `components/breadcrumbs.js` + +##### Highest-risk areas + +- Search contract shape and pagination assumptions (`searchResultsObj`, `@odata.nextLink`) +- Result-to-detail expansion in `components/utils/index.js` +- `currentView.caseReference` as 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.js` +- `components/myportal.js` +- `components/myportal/mycases.js` +- `components/myportal/watchedcases.js` +- `components/myportal/myrepresentations.js` +- `components/myportal/mysubmittedrepresentations.js` +- `components/myportal/awaitingsubmissionfromblob.js` +- `components/myportal/topthree.js` +- `components/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/signin` when 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`, local `getDetails(...)` + - 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.viewKey` or `router.query.key` + - sets `currentView` and `currentReference` before navigating to case detail or representation edit flows + +#### State Ownership + +##### Primary state slices + +- `store/accountDetails/reducer.js` + - owns: + - `accountDetails` + - `loggedinUserId` + - `containerID` + - this slice is the main ownership root for: + - CRM contact identity + - storage container identity + - user display/account context + +- `store/currentView/reducer.js` + - owns: + - `currentView` + - `caseReference` + - `currentPage` + - `showReps` + - `showLogin` + - `locale` + - this slice drives which dashboard sub-view is active and what downstream case/representation context should be used + +- `store/myCases/reducer.js` + - owns: + - `myCases` + - `myCasesDetails` + +- `store/watchedCases/reducer.js` + - owns: + - `watchedCases` + - `watchedCasesDetails` + +- `store/awaitingSubmission/reducer.js` + - owns: + - `awaitingSubmission` + - `awaitingSubmissionDetails` + - `awaitingSubmissionFromBlob` + +- `store/myRepresentations/*` + - not re-read in full for this slice, but used by `pages/myportal/index.js` as a primary journey state owner for: + - `myRepresentations` + - `myRepresentationsDetails` + - `mySubmittedReps` + - `mySubmittedRepsDetails` + +##### `currentView` usage + +- `components/myportal/topthree.js` + - sets `currentReference` before opening case/resume routes + +- `components/myportal/viewall.js` + - uses `currentView.viewKey` to determine whether the page is showing: + - my cases + - watched cases + - awaiting submission + - my representations + - submitted reps + - updates `currentView` after list mutations to keep the dashboard sub-view stable + +- `components/breadcrumbs.js` + - depends on `currentView` and `caseReference` to reconstruct myportal-origin breadcrumbs + +##### `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.js` and `components/myportal/viewall.js` + - use `loggedinUserId` for watched-case mutations and refreshes + - use `containerID` for draft/blob deletion and resume pathways + +#### Service Layer + +##### Primary services + +- `actions/services/accountDirectService.js` + - `getPortalLogin(...)` + - `getPersonalAccount(...)` + +- `actions/services/portalDirectService.js` + - `getMyCases(...)` + - `getMyLPACases(...)` + - `getWatchedCases(...)` + - `getWatchedCasesProxy(...)` + - `getAwaitingSubmission(...)` + - `getAwaitingSubmissionProxy(...)` + - `createWatchedCases(...)` + - `deleteWatchedCases(...)` + +- `actions/services/documentDirectService.js` + - `createContainerProxy(...)` + - `getRepsFromBlob(...)` + - `getRepsFromBlobProxy(...)` + - `getAwaitingSubmissionFromBlob(...)` + - `getAwaitingSubmissionFromBlobProxy(...)` + - `deleteAwaitingSubmissionsFromBlob(...)` + - `deleteMyRepresentationsFromBlob(...)` + +- `actions/services/caseDirectService.js` + - `getPortalModuleDetails(...)` + - `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.js` +- `pages/api/endpoint/getmylpacases_api.js` +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getawaitingsubmission_api.js` +- `pages/api/endpoint/getportalmoduledetails_api.js` +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/getportallogin_api.js` + +##### Principal storage-backed routes + +- `pages/api/file/setupcontainer.js` +- `pages/api/file/getrepsblob.js` +- `pages/api/file/getawaitingsubmissionfromblob.js` +- `pages/api/file/getrepsblobproxy.js` +- `pages/api/file/getawaitingsubmissionfromblobproxy.js` +- `pages/api/file/deleteblobcase.js` +- `pages/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 `title` into `pinswg_title` for 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 `container` and `hash` + - 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.email` and `session.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 + +- **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 + +- **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.js` +- `components/myportal.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` +- `actions/services/accountDirectService.js` +- `actions/services/portalDirectService.js` +- `actions/services/documentDirectService.js` +- `actions/services/caseDirectService.js` +- `store/accountDetails/reducer.js` +- `store/currentView/reducer.js` +- `store/myCases/reducer.js` +- `store/watchedCases/reducer.js` +- `store/awaitingSubmission/reducer.js` + +##### Likely adjacent files + +- `pages/api/endpoint/getmycases_api.js` +- `pages/api/endpoint/getmylpacases_api.js` +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getportalmoduledetails_api.js` +- `pages/api/endpoint/getportallogin_api.js` +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/file/getawaitingsubmissionfromblob.js` +- `pages/api/file/getrepsblob.js` +- `pages/api/file/setupcontainer.js` +- `pages/api/file/deleteblobcase.js` +- `pages/api/file/deleteblobrep.js` +- `lib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.js` +- `components/myportal/mycases.js` +- `components/myportal/watchedcases.js` +- `components/myportal/myrepresentations.js` +- `components/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 in `viewall.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.md` +- `context/api-route-map.md` +- `context/integration-map.md` +- `context/portal-api-platform-assessment.md` +- `memory-bank/change-log.md` + +Guardrails/context discipline: + +- `.clinerules/refactor-branch-rules.md` +- `GUARDRAILS.md` + +Journey pages and major components: + +- `pages/searchresults.js` +- `pages/case/[ticketnumber].js` +- `pages/myportal/index.js` +- `components/search/searchresults.js` +- `components/case.js` +- `components/myportal.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` + +Supporting services/helpers: + +- `actions/services/searchDirectService.js` +- `actions/services/caseDirectService.js` +- `actions/services/portalDirectService.js` +- `actions/services/documentDirectService.js` +- `actions/services/accountDirectService.js` +- `components/utils/index.js` + +API handlers: + +- `pages/api/endpoint/getbasicsearchpaged_api.js` +- `pages/api/endpoint/getcase_api.js` +- `pages/api/endpoint/getmycases_api.js` +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/file/getawaitingsubmissionfromblob.js` + +Redux ownership files: + +- `store/accountDetails/reducer.js` +- `store/currentView/reducer.js` +- `store/search/reducer.js` +- `store/searchOutput/reducer.js` +- `store/watchedCases/reducer.js` +- `store/myCases/reducer.js` +- `store/awaitingSubmission/reducer.js` + +### Searches performed + +- `pages`: `getServerSideProps|getInitialProps` +- `store`: `currentView|accountDetails|search|myportal|watchedCases` +- `actions/services`: `getBasicSearchPaged|getAdvancedSearchPaged|getCase\(|getCaseByID|getMyCases|getMyRepresentations|getAwaitingSubmission|getWatchedCases` +- `lib`: `loadMyPortal|resolveMyPortalAuthContext|search|case` +- `components`: `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: + +1. Treat this map as the maintainer-first companion to `context/api-route-map.md`. +2. When changing either journey, start from the journey entry page and confirm the owning Redux slices before reading deeper API files. +3. 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. +4. Keep identity bootstrap and state ownership explicitly documented in future journey maps, because they are as important to maintainability as the page/component structure. +5. 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.md` +- `memory-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. +- The entry pages for both new and resumed appeals converge on the same page shell and flow components: + - `pages/newappeal/[appealtypes].js` + - `pages/myportal/[appealtypes].js` + - `components/newappeal/newAppealFlow.js` +- The strongest visible ownership model remains: + - `NextAuth session.user.id -> Azure Storage container` + - `pinsUser 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.id` as storage/container ownership before CRM submission occurs. + +#### Primary Entry Points + +- `pages/newappeal/index.js` +- `components/newappeal/createCase.js` +- `pages/newappeal/[appealtypes].js` +- `pages/myportal/[appealtypes].js` +- `components/newappeal/newAppealFlow.js` +- `components/newappeal/buildsection.js` + +#### Loaders / Initialisation + +##### Draft creation entry + +- `pages/newappeal/index.js` + - session-gated via `getSession(ctx)` + - uses `pinsUser` cookie 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)` + +##### Draft section loader + +- `lib/newappeal/loadNewAppealPage.js` + - validates required query params: + - `appealtypes` + - `apt` + - `id` + - requires `getSession(ctx)` + - requires `session.user.id` and `session.user.email` + - requires `pinsUser` cookie 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)` + +- `pages/newappeal/[appealtypes].js` + - delegates SSR loading to `loadNewAppealPage(ctx)` + - hydrates Redux using `hydrateNewAppealStore(...)` + +##### Draft resume loader + +- `lib/myportal/loadMyPortalAppealPage.js` + - validates required query params: + - `appealtypes` + - `apt` + - `casereference` + - requires `getSession(ctx)` + - requires `pinsUser` cookie + - 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)` when `query.key` is present + - reads form XML through `readFormXml(query.appealtypes)` + +- `pages/myportal/[appealtypes].js` + - delegates SSR loading to `loadMyPortalAppealPage(ctx)` + - hydrates Redux using `hydrateMyPortalAppealStore(...)` + +##### Draft bootstrap logic + +- `lib/newappeal/hydrateNewAppealStore.js` + - constructs `appealType.caseReference` as: + - `ticketnumber: query.id` + - `incidentid: query.id` + - `caseDetails: blobProgress` + - dispatches: + - `setLoggedInUserId(...)` + - `setLoggedInUserEmail(...)` + - `setAppealLPA(query.lpa)` + - `setAppealTypeID(query.apt)` + - `setCaseReference(...)` + - `setForm(xmlStr, mandatoryFieldsData, pickListData)` + - `setAppealType(appealTypeData)` + - `setContainerID(session.user.id)` + - `setAccountDetails(accountDetails)` + +- `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: + - `appealTypeOptions` + - `appealTypeID` + - `currentSection` + - `appealLPA` + - `caseReference` + - `formComplete` + - `documentList` + - `fileList` + - `fileCount` + - `progress` + +- `store/formData/reducer.js` + - owns: + - `formData` + - `mandatoryFieldsData` + - `pickListData` + +- `store/accountDetails/reducer.js` + - owns: + - `accountDetails` + - `loggedinUserId` + - `loggedinUserEmail` + - `containerID` + - `containerID` is the strongest visible draft-ownership identifier in the UI/store layer + +- `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(...)` sets `currentView` when the draft resume path originates from awaiting-submission UI +- component flows use current section progression through `appealType.currentSection` rather than `currentView` + +##### `accountDetails` usage + +- provides: + - CRM contact identity (`loggedinUserId`) + - email for partial-save/completion emails (`loggedinUserEmail`) + - container ownership (`containerID`) +- `components/newappeal/buildsection.js` and `buildchecksection.js` rely on `accountDetails.containerID` to persist and finalise draft material + +##### Draft ownership state + +- draft ownership is represented across: + - `accountDetails.containerID` + - `appealType.caseReference.ticketnumber` + - `appealType.caseReference.caseDetails` + - `appealType.fileList` + - blob-backed `progress` / file objects retrieved from storage + +#### Service Layer + +##### Primary service modules + +- `actions/services/documentDirectService.js` + - `createContainerProxy(...)` + - `getProgressFromBlob(...)` + - `getFilesFromBlob(...)` + - `uploadFiles(...)` + - `generateAppealPDF(...)` + - `deleteAwaitingSubmissionsFromBlob(...)` + +- `actions/services/accountDirectService.js` + - `getPersonalAccount(...)` + +- `actions/services/referenceDataService.js` + - `getAppealsTypesForNewAppeal(...)` + - `getMandatoryFields(...)` + - `getPickLists(...)` + - `getLPA(...)` + +- `lib/newappeal/journeyEffects.js` + - `uploadAppealFilesEffect(...)` + - `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` + +#### API Layer + +##### Principal routes + +- Azure Storage / draft persistence + - `pages/api/file/setupcontainer.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/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` + +- **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 + +```text +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.js` +- `pages/newappeal/[appealtypes].js` +- `pages/myportal/[appealtypes].js` +- `lib/newappeal/loadNewAppealPage.js` +- `lib/myportal/loadMyPortalAppealPage.js` +- `lib/newappeal/hydrateNewAppealStore.js` +- `lib/myportal/hydrateMyPortalAppealStore.js` +- `components/newappeal/buildsection.js` +- `actions/services/documentDirectService.js` +- `actions/azurestorage.js` +- `store/appealType/reducer.js` +- `store/formData/reducer.js` +- `store/accountDetails/reducer.js` + +##### Adjacent files + +- `pages/api/file/setupcontainer.js` +- `pages/api/file/getprogressobjblob.js` +- `pages/api/file/getbloblist.js` +- `pages/api/file/upload.js` +- `pages/api/file/uploadsinglefile.js` +- `pages/api/file/deleteblobcase.js` +- `components/newappeal/newAppealFlow.js` +- `lib/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.js` +- `components/newappeal/complete.js` +- `components/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 + +##### Check answers bootstrap + +- `components/newappeal/newAppealFlow.js` + - routes to `BuildCheckSection` when `currentSection === sectionCount + 1` + +- `components/newappeal/buildchecksection.js` + - assembles final review payload from: + - `legacyFormState.appealForm.values` + - `appealType.fileList` + - `accountDetails.containerID` + - `appealType.caseReference.ticketnumber` + - deduplicates file list before finalisation + - requires explicit user confirmation before submission button becomes active + +#### State Ownership + +##### Primary slices + +- `store/appealType/reducer.js` + - controls finalisation stage through: + - `currentSection` + - `caseReference` + - `fileList` + - `formComplete` + +- `store/accountDetails/reducer.js` + - provides: + - `containerID` + - `loggedinUserId` + - `loggedinUserEmail` + - `accountDetails.pinswg_typeofinvolvement` + +- `store/formData/reducer.js` + - provides mandatory fields/picklist/form shape used to render and validate final answers + +##### Completion-state transition + +- `BuildCheckSection.finaliseAppeal()` sets `setCurrentSection(9999)` after PDF generation and finalisation message trigger +- `components/newappeal/newAppealFlow.js` then switches to `CompleteAppeal` +- `SECTION_COMPLETE` therefore acts as the visible client-side completion-state boundary + +#### Service Layer + +##### Primary services + +- `actions/services/documentDirectService.js` + - `generateAppealPDF(...)` + +- `actions/services/portalDirectService.js` + - `sendCaseCompleteMessage(...)` + - constructs signed URL to `createappealcompletemessage_api` + +- `actions/services/caseDirectService.js` + - `createNewCase(...)` + - `updateCase(...)` + - `patchCase(...)` + - these are the visible CRM mutation helpers adjacent to finalisation ownership transition + +- `lib/newappeal/journeyEffects.js` + - `generateAppealPDFEffect(...)` + - `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.js` + - `pages/api/file/createappealcompletemessageproxy_api.js` + +- CRM mutation support + - `pages/api/endpoint/createcase_api.js` + - `pages/api/endpoint/updatecase_api.js` + - `pages/api/endpoint/patchcase_api.js` + +- draft storage dependencies used during finalisation + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/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` + +- **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`, and `filespath` + +- **GOV.UK Notify** + - used for completion email from `components/newappeal/complete.js` + +- **Local processing** + - deduplication, payload cleanup, confirmation-state UI, and client-side section completion transition + +#### Ownership Model + +```text +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: + +```text +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.js` +- `components/newappeal/complete.js` +- `lib/newappeal/journeyEffects.js` +- `actions/services/portalDirectService.js` +- `actions/services/documentDirectService.js` +- `actions/services/caseDirectService.js` +- `pages/api/file/createappealcompletemessage_api.js` +- `actions/azurestorage.js` + +##### Adjacent files + +- `pages/api/file/createappealcompletemessageproxy_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/endpoint/updatecase_api.js` +- `pages/api/endpoint/patchcase_api.js` +- `components/newappeal/newAppealFlow.js` +- `store/appealType/reducer.js` +- `store/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 + +```text +NextAuth session.user.id +→ setContainerID(session.user.id) +→ storage container identity +→ casefolderID / ticketnumber prefix +→ draft appeal JSON +→ case blob +→ uploaded files +``` + +#### Submitted ownership transition + +```text +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 + +```text +User +→ New Appeal page +→ SSR loader +→ Redux hydration +→ BuildSection +→ document services +→ file APIs +→ Azure Storage +``` + +#### Draft Resume + +```text +User +→ My Portal resume page +→ SSR loader +→ getProgressFromBlob + getFilesFromBlob +→ Redux hydration +→ NewAppealFlow +``` + +#### Appeal Submission / Finalisation + +```text +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.js` + - `pages/newappeal/[appealtypes].js` + - `lib/newappeal/loadNewAppealPage.js` + - `components/newappeal/buildsection.js` + - `actions/services/documentDirectService.js` + - `pages/api/file/{setupcontainer,getprogressobjblob,getbloblist,upload,deleteblobcase}.js` + +#### Appeal Submission / Finalisation + +- start with: + - `components/newappeal/buildchecksection.js` + - `components/newappeal/complete.js` + - `lib/newappeal/journeyEffects.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `actions/azurestorage.js` + - `pages/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.md` +- `context/api-route-map.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +Journey pages / loaders / hydration: + +- `pages/newappeal/index.js` +- `pages/newappeal/[appealtypes].js` +- `pages/myportal/[appealtypes].js` +- `lib/newappeal/loadNewAppealPage.js` +- `lib/myportal/loadMyPortalAppealPage.js` +- `lib/newappeal/hydrateNewAppealStore.js` +- `lib/myportal/hydrateMyPortalAppealStore.js` + +Journey components / effects: + +- `components/newappeal/newAppealFlow.js` +- `components/newappeal/buildsection.js` +- `components/newappeal/buildchecksection.js` +- `components/newappeal/complete.js` +- `lib/newappeal/journeyEffects.js` + +State / service files: + +- `store/appealType/reducer.js` +- `store/formData/reducer.js` +- `store/accountDetails/reducer.js` +- `store/currentView/reducer.js` +- `store/awaitingSubmission/reducer.js` +- `actions/services/documentDirectService.js` +- `actions/services/portalDirectService.js` +- `actions/services/caseDirectService.js` +- `actions/services/accountDirectService.js` +- `actions/azurestorage.js` + +API handlers: + +- `pages/api/file/setupcontainer.js` +- `pages/api/file/getprogressobjblob.js` +- `pages/api/file/getbloblist.js` +- `pages/api/file/upload.js` +- `pages/api/file/deleteblobcase.js` +- `pages/api/file/createappealcompletemessage_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/endpoint/updatecase_api.js` +- `pages/api/endpoint/patchcase_api.js` + +#### Searches performed for Slice 2 + +- `pages`: `newappeal|createappeal|checkanswers|appealtypes|casereference` +- `lib`: `loadNewAppealPage|loadMyPortalAppealPage|hydrateNewAppealStore|hydrateMyPortalAppealStore|journeyEffects|session.user.id|containerID` +- `actions/services`: `getProgressFromBlob|getFilesFromBlob|createContainerProxy|sendCaseCompleteMessage|createNewCase|updateCase|patchCase|uploadFiles|generateAppealPDF` +- `pages/api`: `createappealcompletemessage_api|setupcontainer|getprogressobjblob|getbloblist|uploadsinglefile|upload\.js|deleteblobcase|createcase_api|updatecase_api|patchcase_api` +- `store`: `appealType|containerID|caseReference|formData|currentView|accountDetails` +- `components/newappeal`: `check|complete|upload|save|submit|partial|generateAppealPDF|sendCaseCompleteMessage` +- `actions`: `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 + +1. 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. +2. 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. +3. Completion UI state (`currentSection = 9999`) should not be treated as equivalent to a fully independently verified downstream submitted-record outcome. +4. 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.md` +- `memory-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.id` container + - 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 `currentView` representation-specific state than the appeal flow, especially for: + - `representationCapacity` + - `representationSubmit` + - `representationSubmitConfirmation` + - `representationMessageSent` + - 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.js` +- `lib/representation/pageLoaders.js` +- `components/representation.js` +- `components/case/representation/*` +- adjacent dashboard/list entry points that navigate into it: + - `components/myportal/viewall.js` + - `components/myportal/topthree_reps.js` + +#### Loaders / Initialisation + +##### Primary page loader + +- `pages/myportal/representation.js` + - uses `wrapper.getServerSideProps` + - delegates SSR bootstrap to `loadRepresentationPage({ store, ctx })` + +##### Shared representation bootstrap + +- `lib/representation/pageLoaders.js` + - `loadRepresentationBootstrap({ 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(...)` and `getPortalModuleDetails(...)` + +##### 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)` + +##### Existing representation draft bootstrap + +- `loadExistingRepresentation({ store, ctx, bootstrap })` + - requires: + - `query.case` + - `query.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)` + +#### State Ownership + +##### Primary slices + +- `store/currentView/reducer.js` + - main representation journey owner for: + - `caseReference` + - `representationCapacity` + - `representationSubmit` + - `representationSubmitConfirmation` + - `representationMessageSent` + - `fileList` + - `locale` + +- `store/myRepresentations/reducer.js` + - owns: + - `myRepresentations` + - `myRepresentationsDetails` + - `mySubmittedReps` + - `mySubmittedRepsDetails` + +- `store/accountDetails/reducer.js` + - owns: + - `accountDetails` + - `loggedinUserId` + - `containerID` + - `containerID` is the strongest visible draft-representation storage owner + +- `store/searchOutput/reducer.js` + - provides case context to the representation journey through: + - `searchResultsObj` + - `searchDetailsObj` + +##### `currentView` usage + +- representation draft lifecycle uses `currentView` more directly than the appeal lifecycle for journey state: + - `caseReference.repDetails` + - `representationCapacity` + - `representationSubmit` + - `representationSubmitConfirmation` + - `representationMessageSent` + - 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.js` + - `getRepsFromBlob(...)` + - `getRepsFromBlobProxy(...)` + - `deleteMyRepresentationsFromBlob(...)` + - `generateRepPDF(...)` + +- `actions/services/portalDirectService.js` + - `getMyRepresentations(...)` + - `getMyRepresentationsProxy(...)` + - `getRepresentations(...)` + - `getRepresentationsProxy(...)` + - `sendRepCompleteMessage(...)` + - `setRepInvolvment(...)` + +- `actions/services/accountDirectService.js` + - `getPortalLogin(...)` + - `getPersonalAccount(...)` + +- `actions/services/caseDirectService.js` + - `getCase(...)` + - `getPortalModuleDetails(...)` + +- `actions/services/searchDirectService.js` + - `getBasicSearch(...)` + +#### API Layer + +##### Principal routes + +- storage-backed draft routes + - `pages/api/file/getrepsblob.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/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.js` + - `pages/api/endpoint/getmyrepresentationsproxy_api.js` + - `pages/api/endpoint/getrepresentations_api.js` + - `pages/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 + +```text +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_name` draft 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.js` +- `lib/representation/pageLoaders.js` +- `components/representation.js` +- `actions/services/documentDirectService.js` +- `actions/services/portalDirectService.js` +- `store/currentView/reducer.js` +- `store/myRepresentations/reducer.js` + +##### Adjacent files + +- `pages/api/file/getrepsblob.js` +- `pages/api/file/getrepsblobproxy.js` +- `pages/api/file/deleteblobrep.js` +- `pages/api/file/editRepJson.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getrepresentations_api.js` +- `components/case/representation/*` + +##### Highest-risk areas + +- draft identity through `repfile_name` +- combined use of case search context and storage-backed representation context +- `currentView` representation-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.js` +- `pages/myportal/representation.js` +- `components/representation.js` + +#### Loaders / Initialisation + +- uses the same `loadRepresentationPage()` bootstrap as draft creation +- finalisation-specific client state is carried through `currentView` rather than a separate SSR loader +- representation completion depends on hydrated: + - `currentView.caseReference` + - `currentView.representationCapacity` + - `currentView.representationMessageSent` + - `accountDetails.containerID` + - `repFormData.repfile_name` + +#### State Ownership + +##### Primary slices + +- `store/currentView/reducer.js` + - main submission-state owner for: + - `representationSubmit` + - `representationSubmitConfirmation` + - `representationMessageSent` + - `representationCapacity` + - `caseReference.repDetails` + +- `store/accountDetails/reducer.js` + - provides: + - CRM contact identity + - email address + - container identity + +- `store/myRepresentations/reducer.js` + - stores list-level representation state before and after submission refreshes + +#### Service Layer + +##### Primary services + +- `actions/services/portalDirectService.js` + - `sendRepCompleteMessage(...)` + - `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.js` + - `generateRepPDF(...)` where applicable in representation flows + +- `actions/azurestorage.js` + - `createRepCompleteMessage(...)` + +#### API Layer + +##### Principal routes + +- finalisation/orchestration + - `pages/api/file/createrepcompletemessage_api.js` + - `pages/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(...)` + +- **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 + +```text +Session identity +→ container identity +→ representation draft ownership +→ completion message route +→ queue handoff +→ CRM involvement / representation side effects +``` + +Visible submitted transition: + +```text +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.js` +- `actions/services/portalDirectService.js` +- `pages/api/file/createrepcompletemessage_api.js` +- `pages/api/file/createrepinvolvement_api.js` +- `store/currentView/reducer.js` + +##### Adjacent files + +- `pages/api/endpoint/deletemyrepresentations_api.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getrepresentations_api.js` +- `actions/azurestorage.js` +- `components/case/representation/*` + +##### Highest-risk areas + +- representation involvement sequencing +- queue handoff for representation completion +- side effects combined in one completion component +- `representationMessageSent` guarding 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 + +- **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 + +```text +NextAuth session.user.id +→ accountDetails.containerID +→ Azure Storage container +→ representation draft blob set +→ representation file subtree +``` + +#### Representation submitted transition + +```text +Representation draft ownership +→ completion message route +→ Azure Queue +→ CRM involvement / representation boundary +``` + +### Architectural Flows + +#### Draft Representation Creation + +```text +User +→ Session +→ Representation loader +→ Redux currentView/accountDetails/myRepresentations +→ Storage draft retrieval / save +→ Azure Storage +``` + +#### Representation Submission / Finalisation + +```text +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.js` + - `lib/representation/pageLoaders.js` + - `actions/services/documentDirectService.js` + - `pages/api/file/{getrepsblob,getrepsblobproxy,deleteblobrep}.js` + - `store/currentView/reducer.js` + - `store/myRepresentations/reducer.js` + +#### Representation Submission / Finalisation + +- start with: + - `components/case/representation/representationComplete.js` + - `actions/services/portalDirectService.js` + - `pages/api/file/{createrepcompletemessage_api,createrepinvolvement_api}.js` + - `pages/api/endpoint/deletemyrepresentations_api.js` + - `actions/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.md` +- `context/api-route-map.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +Journey pages / loaders / components: + +- `pages/myportal/representation.js` +- `lib/representation/pageLoaders.js` +- `components/representation.js` +- `components/case/representation/representationComplete.js` + +State / service / API files: + +- `store/currentView/reducer.js` +- `store/myRepresentations/reducer.js` +- `actions/services/documentDirectService.js` +- `actions/services/portalDirectService.js` +- `pages/api/file/getrepsblob.js` +- `pages/api/file/deleteblobrep.js` +- `pages/api/file/createrepcompletemessage_api.js` +- `pages/api/file/createrepinvolvement_api.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getrepresentations_api.js` +- `pages/api/endpoint/deletemyrepresentations_api.js` + +#### Searches performed for Slice 3 + +- `pages`: `representation|repsblob|repcompletemessage|created=|case=|state=edit` +- `lib`: `loadRepresentationPage|loadExistingRepresentation|loadNewRepresentation|representation|getRepsFromBlob|createRepCompleteMessage|questionnaire|showQuestionnaireSection` +- `actions/services`: `getRepsFromBlob|getRepsFromBlobProxy|sendRepCompleteMessage|setRepInvolvment|getMyRepresentations|getRepresentations|deleteMyRepresentationsFromBlob|generateRepPDF` +- `pages/api`: `getrepsblob|editRepJson|deleteblobrep|createrepcompletemessage_api|createrepinvolvement_api|deletemyrepresentations_api|getmyrepresentations_api|getrepresentations_api` +- `store`: `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 + +1. Representation draft state is more distributed across `currentView` flags than the appeal draft lifecycle. +2. Representation completion currently concentrates several side effects in one completion component, which raises maintenance sensitivity even when behaviour is stable. +3. Queue creation is visible, but downstream representation processing is not visible in this repository slice. +4. 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.md` +- `memory-bank/change-log.md` + +### Findings + +- These two journeys are the clearest maintainer-facing view of the PEDW identity bootstrap model: + +```text +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 Contact` transition 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.js` +- `pages/account/register.js` +- `components/account/registerform.js` +- `components/account/registerCheck.js` +- `components/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)` + - client-side branch then decides: + - if `loggedInUserId.value` is empty -> redirect to `/account/register?id=` + - if CRM contact exists -> set `pinsUser` cookie and redirect to `/myportal` + +##### Registration page loader + +- `pages/account/register.js` + - requires `getSession(ctx)` + - redirects to `/auth/signin` if missing + - passes `loggedInUserEmail: session.user.email` into the page props + +##### Registration form bootstrap + +- `components/account/registerform.js` + - uses Redux Form with `enableReinitialize` + - seeds `initialValues.emailaddress1` from `loggedInUserEmail` + - keeps email field disabled, so the current signed-in email remains the registration identity source in the reviewed flow + +##### Post-registration bootstrap + +- registration completion does not itself grant portal access directly +- visible post-registration bootstrap remains: + +```text +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: + - `accountDetails` + - `loggedinUserId` + - `loggedinUserEmail` + - `accCr` + - `containerID` + +##### Registration-specific state + +- `accCr` + - used as the registration completion state marker: + - `false` + - `created` + - `exists` +- `components/account/register.js` also uses local component state for form/check/complete progression: + - `registerFormComplete` + - `accountCreatedComplete` + +##### `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.js` + - `getPortalLogin(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.js` +- `pages/api/endpoint/getemailaccountcheck_api.js` +- `pages/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 `contacts` record from submitted registration payload + +- `pages/api/auth/[...nextauth].js` + - **auth/session platform-level** + - responsible for sign-in flow and `newUser` routing 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 + +```text +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.js` +- `pages/account/register.js` +- `components/account/registerform.js` +- `components/account/registerCheck.js` +- `components/account/registerComplete.js` +- `actions/services/accountDirectService.js` +- `pages/api/endpoint/getportallogin_api.js` +- `pages/api/endpoint/getemailaccountcheck_api.js` +- `pages/api/endpoint/createaccount_api.js` +- `pages/api/auth/[...nextauth].js` + +##### Likely adjacent files + +- `pages/api/auth/resolve-locale.js` +- `store/accountDetails/reducer.js` +- `store/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 +- `newUser` routing 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.js` +- `components/account/personaldetails.js` +- `components/account/personaldetailsCheck.js` +- `components/account/personaldetailsComplete.js` +- adjacent account UI: + - `components/myportal/youraccount.js` +- legacy/adjacent password flow: + - `pages/account/changepassword.js` + - `pages/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` -> `NoSessionWarning` + - `unauthenticated` -> redirect to `/auth/signin` + - also writes `pinsUser` cookie from `props.accountDetails.loggedinUserId` + +##### State hydration assumption + +- this page assumes account identity/details have already been hydrated into Redux by earlier portal bootstrap flows, especially through: + +```text +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: + - `accountDetails` + - `loggedinUserId` + - `loggedinUserEmail` + - `containerID` + +- `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.js` + - `getPersonalAccount(contactid)` + - `updateAccount(contactId, updateBody, ssr)` + - `getPreferredLanguage(email)` + - `updatePassword(contactId, newpassword)` + +#### API Layer + +##### Principal routes + +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/updateaccount_api.js` +- adjacent account-support routes: + - `pages/api/endpoint/getpreferredlanguage_api.js` + - `pages/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.js` for 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 + +```text +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.js` +- `components/account/personaldetails.js` +- `components/account/personaldetailsCheck.js` +- `components/account/personaldetailsComplete.js` +- `actions/services/accountDirectService.js` +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/updateaccount_api.js` +- `store/accountDetails/reducer.js` + +##### Likely adjacent files + +- `pages/api/endpoint/getpreferredlanguage_api.js` +- `pages/api/endpoint/updatepassword_api.js` +- `components/myportal/youraccount.js` +- `pages/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 + +```text +NextAuth session +→ session.user.email +→ getPortalLogin(email) +→ CRM Contact +→ portal access / dashboard access +``` + +#### How registration fits + +```text +Session exists +→ no CRM Contact found +→ registration flow +→ CRM contact creation +→ later bootstrap succeeds +``` + +#### How account management fits + +```text +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 + +```text +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 + +```text +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.js` + - `pages/account/register.js` + - `components/account/registerform.js` + - `components/account/registerCheck.js` + - `components/account/registerComplete.js` + - `actions/services/accountDirectService.js` + - `pages/api/endpoint/{getportallogin_api,getemailaccountcheck_api,createaccount_api}.js` + - `pages/api/auth/[...nextauth].js` + +#### Personal Details / Account Management + +- start with: + - `pages/account/personaldetails.js` + - `components/account/personaldetails.js` + - `components/account/personaldetailsCheck.js` + - `components/account/personaldetailsComplete.js` + - `actions/services/accountDirectService.js` + - `pages/api/endpoint/{getpersonalaccount_api,updateaccount_api,getpreferredlanguage_api,updatepassword_api}.js` + - `store/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.md` +- `context/api-route-map.md` +- `context/portal-api-platform-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +Journey pages / components: + +- `pages/index.js` +- `pages/account/register.js` +- `pages/account/personaldetails.js` +- `components/account/registerform.js` +- `components/account/registerCheck.js` +- `components/account/registerComplete.js` +- `components/account/personaldetails.js` +- `components/account/personaldetailsCheck.js` +- `components/account/personaldetailsComplete.js` + +Services / state / API files: + +- `actions/services/accountDirectService.js` +- `store/accountDetails/reducer.js` +- `pages/api/endpoint/getportallogin_api.js` +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/createaccount_api.js` +- `pages/api/endpoint/updateaccount_api.js` +- `pages/api/endpoint/getemailaccountcheck_api.js` +- `pages/api/endpoint/getpreferredlanguage_api.js` +- `pages/api/endpoint/updatepassword_api.js` +- `pages/api/auth/[...nextauth].js` +- `pages/api/auth/resolve-locale.js` + +#### Searches performed for Slice 4 + +- `pages`: `register|personaldetails|changepassword|youraccount|getServerSideProps|getSession\(|getPortalLogin` +- `components/account`: `register|personaldetails|changepassword|emailaddress1|updateAccount|createAccount|getPersonalAccount` +- `actions/services`: `createAccount|getPortalLogin|getPersonalAccount|updateAccount|getPreferredLanguage|updatePassword|getEmailAccountCheck` +- `pages/api`: `getportallogin_api|getpersonalaccount_api|createaccount_api|updateaccount_api|getemailaccountcheck_api|getpreferredlanguage_api|updatepassword_api|resolve-locale|nextauth` +- `store`: `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 + +1. Registration completion state and actual portal eligibility are not identical; the visible portal-access transition still depends on a later homepage bootstrap re-check. +2. 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. +3. Several account-support APIs participate in both account and auth/session concerns, so future grouping ownership is architectural classification only, not a change proposal. +4. `updatepassword_api.js` appears 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.md` +- `memory-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` +- 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.js` and supporting email data routes +- 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.js` +- `pages/auth/verify-request.js` +- `pages/auth/error.js` +- `pages/api/auth/[...nextauth].js` +- `pages/api/auth/resolve-locale.js` +- homepage bootstrap after callback: + - `pages/index.js` +- sign-out / reset touchpoints directly relevant to auth continuity: + - `components/header.js` + - `components/myportal/servicebanner.js` + - `lib/auth/sessionClient.js` + - `pages/logout.js` + +#### Loaders / Initialisation + +##### Sign-in page entry + +- `pages/auth/signin.js` + - gated by `SHOWLOGIN` + - obtains `csrfToken` through `getCsrfToken(context)` + - builds callback URL from host/protocol and incoming `callbackUrl` + - appends `locale` to the callback URL before rendering + +##### Locale resolution before email sign-in submit + +- `pages/auth/signin.js` + - intercepts form submit in `handleSubmit(...)` + - POSTs to `pages/api/auth/resolve-locale.js` with: + - entered email + - current UI locale + - writes `pedw_locale` cookie + - rewrites the hidden callback URL to include resolved locale before posting to `/api/auth/signin/email` + +- `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_preferredlanguage` when 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 + +##### 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_locale` cookie + - callback URL locale parsing + - callback cookie fallback + - resolves effective locale by attempting CRM preferred-language lookup first and request locale second + - customises: + - `signIn` + - `verifyRequest` + - `error` + - `newUser` + page paths by locale + - uses Prisma adapter and database session strategy + - rewrites external redirects through the locale-aware redirect callback + +##### Verification email generation + +- `pages/api/auth/[...nextauth].js` + - `EmailProvider.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.id` into `containerID` + - calls `getPortalLogin(session.user.email)` + - if CRM contact exists: + - writes `pinsUser` cookie from CRM `contactid` + - redirects to `/myportal` + - if CRM contact does not exist: + - redirects to `/account/register?id=` + +##### Logout / reset behaviour directly relevant to auth continuity + +- `lib/auth/sessionClient.js` + - `clearSessionArtifacts()` clears: + - localStorage + - `next-auth.csrf-token` + - callback URL cookies + - `pedw_locale` + - `pinsUser` + - `performPortalSignOut(...)` triggers NextAuth `signOut(...)` with locale-aware callback URL + +- `components/header.js` +- `components/myportal/servicebanner.js` + - both call `performPortalSignOut(locale)` + +- `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 `contactid` after homepage bootstrap succeeds + - 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: + - `loggedinUserId` + - `loggedinUserEmail` + - `containerID` + - `accountDetails` + +- `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.js` + - `getPortalLogin(emailAddress)` + - `getPreferredLanguage(email)` + +##### Supporting auth helpers + +- `lib/auth/sessionClient.js` + - `buildSignedOutCallbackUrl(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].js` +- `pages/api/auth/resolve-locale.js` +- adjacent supporting routes directly relevant to auth bootstrap: + - `pages/api/endpoint/getportallogin_api.js` + - `pages/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 + +```text +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.email` + - `session.user.id` +- **business identity** + - CRM `contactid` from `getPortalLogin(email)` +- **portal continuity state** + - `pinsUser` + - `pedw_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.js` +- `pages/auth/verify-request.js` +- `pages/api/auth/[...nextauth].js` +- `pages/api/auth/resolve-locale.js` +- `pages/index.js` +- `lib/auth/sessionClient.js` +- `actions/services/accountDirectService.js` +- `pages/api/endpoint/getportallogin_api.js` +- `pages/api/endpoint/getpreferredlanguage_api.js` + +##### Likely adjacent files + +- `pages/auth/error.js` +- `components/header.js` +- `components/myportal/servicebanner.js` +- `pages/logout.js` +- `pages/account/register.js` +- `store/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.js` + - `components/case/representation/representationComplete.js` +- watchlist/email-notification signup touchpoints: + - `components/search/searchresults.js` + - `components/case/summary.js` +- aggregation/batch routes: + - `pages/api/email/getall.js` + - `pages/api/email/getdocuments.js` + - `pages/api/email/getevents.js` + - `pages/api/email/getmailinglist.js` + - `pages/api/email/getcaseref.js` + +#### Loaders / Initialisation + +##### Thin direct Notify send + +- `actions/services/notifyDirectService.js` + - packages: + - `templateId` + - `emailAddress` + - `reference` + - `personalisation` + - POSTs to `/api/email/notify` + +- `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 + +##### 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.js` +- `components/case/summary.js` + - `selectEmailNotifications(...)` creates/updates a watched-case record with `pinswg_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_emailnotifications` is 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.js` + - `sendEmail(...)` + +- `actions/services/notifyService.js` + - re-exports `sendEmail(...)` + +- adjacent service callers: + - `actions/services/accountDirectService.js` + - `getPreferredLanguage(...)` + - `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.js` +- `pages/api/email/getall.js` +- `pages/api/email/getdocuments.js` +- `pages/api/email/getevents.js` +- `pages/api/email/getmailinglist.js` +- `pages/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 + +- **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 + +```text +Account/contact identity +→ email address + preferred language +→ business event or watchlist state +→ Notify payload build +→ GOV.UK Notify send +``` + +For watchlist-driven notifications specifically: + +```text +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.js` +- `pages/api/email/getall.js` +- `pages/api/email/getdocuments.js` +- `pages/api/email/getevents.js` +- `pages/api/email/getmailinglist.js` +- `pages/api/email/getcaseref.js` +- `actions/services/notifyDirectService.js` +- `actions/services/notifyService.js` +- `components/newappeal/complete.js` +- `components/case/representation/representationComplete.js` + +##### Likely adjacent files + +- `pages/api/auth/[...nextauth].js` +- `actions/services/accountDirectService.js` +- `components/search/searchresults.js` +- `components/case/summary.js` +- `store/watchedCases/reducer.js` +- `pages/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.js` across 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 + +```text +NextAuth session +→ session.user.email +→ getPortalLogin(email) +→ CRM contact or registration redirect +→ pinsUser cookie +→ portal entry continuity +``` + +#### Notifications / Email + +```text +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 + +```text +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 + +```text +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.js` + - `pages/auth/verify-request.js` + - `pages/api/auth/[...nextauth].js` + - `pages/api/auth/resolve-locale.js` + - `pages/index.js` + - `lib/auth/sessionClient.js` + - `pages/api/endpoint/{getportallogin_api,getpreferredlanguage_api}.js` + - `actions/services/accountDirectService.js` + +#### Notifications / Email + +- start with: + - `pages/api/email/{notify,getall,getdocuments,getevents,getmailinglist,getcaseref}.js` + - `actions/services/{notifyDirectService,notifyService}.js` + - `components/newappeal/complete.js` + - `components/case/representation/representationComplete.js` + - `components/search/searchresults.js` + - `components/case/summary.js` + - `pages/api/auth/[...nextauth].js` for 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.md` +- `context/api-route-map.md` +- `context/portal-api-platform-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +Guardrails/context discipline: + +- `.clinerules/refactor-branch-rules.md` +- `GUARDRAILS.md` + +Journey pages / components / helpers: + +- `pages/auth/signin.js` +- `pages/auth/verify-request.js` +- `pages/index.js` +- `pages/logout.js` +- `pages/account/register.js` +- `components/header.js` +- `components/myportal/servicebanner.js` +- `components/newappeal/complete.js` +- `components/case/representation/representationComplete.js` +- `components/search/searchresults.js` +- `components/case/summary.js` +- `lib/auth/sessionClient.js` +- `actions/services/accountDirectService.js` +- `actions/services/notifyDirectService.js` +- `actions/services/notifyService.js` + +API files: + +- `pages/api/auth/[...nextauth].js` +- `pages/api/auth/resolve-locale.js` +- `pages/api/email/notify.js` +- `pages/api/email/getall.js` +- `pages/api/email/getdocuments.js` +- `pages/api/email/getevents.js` +- `pages/api/email/getmailinglist.js` +- `pages/api/email/getcaseref.js` +- `pages/api/endpoint/getpreferredlanguage_api.js` +- `pages/api/endpoint/getportallogin_api.js` + +#### Searches performed for Slice 5 + +- `pages`: `getServerSideProps|getSession\(|signIn\(|signOut\(|getCsrfToken\(|verify-request|nextauth|resolve-locale|logout` +- `actions/services`: `notify|getPortalLogin|getPreferredLanguage|send.*email|create.*message` +- `pages/api`: `NotifyClient|sendEmail|NextAuth|EmailProvider|verification|callback|session|getall|getdocuments|getevents|getmailinglist|getcaseref` +- `lib`: `auth|session|locale|preferredLanguage|getPortalLogin` +- `components`: `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 + +1. 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. +2. `pages/api/auth/[...nextauth].js` includes both platform-auth behaviour and Notify-backed email behaviour; its future grouping candidate should therefore be read as architectural classification only. +3. `pages/api/email/getall.js` is materially more orchestration-heavy than `pages/api/email/notify.js`, so “notifications/email” is not one uniform route shape. +4. `getpreferredlanguage_api.js` and `getportallogin_api.js` support both auth and email journeys; their future grouping candidates are shared/auth/account classifications only, not an implementation recommendation. +5. 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.md` +- `memory-bank/change-log.md` + +### Findings + +- The watchlist/subscription architecture is a cross-cutting portal support journey built around a visible CRM relationship model: + +```text +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.js` +- `components/search/addresssearchresults.js` +- `components/search/dnssearchresults.js` +- `components/case/summary.js` +- adjacent authenticated search routes that preload watched-case state for the above components: + - `pages/myportal/searchresults.js` + - `pages/myportal/addresssearchresults.js` + - `pages/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 `watchedCasesDetails` via `getDetails(...)` + - 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.js` +- `components/case/summary.js` + - expose `selectWatchedCase(loggedInUser, incidentID, appealType)` + - construct watched-case relationship payload using: + - `pinswg_WatchedCase@odata.bind` + - `pinswg_Contact@odata.bind` + - `pinswg_appealcasetype` + - call `createWatchedCases(updateBody)` + - refresh watched-case state after mutation using: + - `getWatchedCasesProxy(...)` + - `getDetailsProxy(..., "myWatchedCases")` + +##### Notification-enabled creation branch + +- `components/search/searchresults.js` +- `components/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 + +#### State Ownership + +##### Primary slices + +- `store/watchedCases/reducer.js` + - owns: + - `watchedCases` + - `watchedCasesDetails` + +- `store/accountDetails/reducer.js` + - provides: + - `loggedinUserId` + - `accountDetails.contactid` + - `accountDetails.emailaddress1` + - these values are used to create the watched-case relationship and optional email-notification participation + +- `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` + +#### Service Layer + +##### Primary service modules + +- `actions/services/portalDirectService.js` + - `createWatchedCases(formValues)` + - `getWatchedCases(loggedInUserId)` + - `getWatchedCasesProxy(loggedInUserId)` + +##### Journey role of services + +- `createWatchedCases(...)` + - is the main visible watched-case upsert entry +- `getWatchedCases(...)` and `getWatchedCasesProxy(...)` + - 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.js` + - `pages/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_emailnotifications` visibly 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 + +```text +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.bind` +- `pinswg_Contact@odata.bind` +- `pinswg_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.js` +- `components/search/addresssearchresults.js` +- `components/search/dnssearchresults.js` +- `components/case/summary.js` +- `actions/services/portalDirectService.js` +- `pages/api/endpoint/createwatchedcases_api.js` +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getwatchedcasesproxy_api.js` +- `store/watchedCases/reducer.js` + +##### Likely adjacent files + +- `pages/myportal/searchresults.js` +- `pages/myportal/addresssearchresults.js` +- `pages/myportal/advancedsearchresults.js` +- `store/accountDetails/reducer.js` +- `store/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.js` +- `components/myportal/watchedcases.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` +- adjacent portal search pages that preload watched cases for watch/unwatch controls: + - `pages/myportal/searchresults.js` + - `pages/myportal/addresssearchresults.js` + - `pages/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: + - `watchedCases` + - `submittedRepresentations` + - 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=watchedCases` with `setCurrentView({ 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.watchedCases` + - `props.watchedCases.watchedCasesDetails` + - sets watched cases into search/detail state when navigating deeper into a case from this list + +##### Portal search viewing support + +- `pages/myportal/searchresults.js` +- `pages/myportal/addresssearchresults.js` +- `pages/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 + +- `store/currentView/reducer.js` + - records whether the active dashboard/list context is: + - `watchedCases` + - preserves navigation back into view-all and case contexts + +- `store/searchOutput/reducer.js` + - is temporarily reused by `viewall.js` when a watched-case item is opened via case-detail navigation + +##### 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.js` + - `getWatchedCases(...)` + - `getWatchedCasesProxy(...)` + +- `actions/services/caseDirectService.js` + - `getPortalModuleDetails(...)` + - 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.js` +- `pages/api/endpoint/getwatchedcasesproxy_api.js` + +##### Route-family classification + +- `getwatchedcases_api.js` + - **CRM relationship read** + - filters `pinswg_watchlists` by `_pinswg_contact_value eq loggedInUserId` + - selects visible watchlist fields including: + - `pinswg_emailnotifications` + - `pinswg_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 + +```text +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.js` +- `components/myportal/watchedcases.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` +- `actions/services/portalDirectService.js` +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getwatchedcasesproxy_api.js` +- `store/watchedCases/reducer.js` + +##### Likely adjacent files + +- `lib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.js` +- `pages/myportal/searchresults.js` +- `pages/myportal/addresssearchresults.js` +- `pages/myportal/advancedsearchresults.js` +- `store/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.js` +- `components/search/addresssearchresults.js` +- `components/search/dnssearchresults.js` +- `components/case/summary.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` + +#### Loaders / Initialisation + +##### Removal in search and case contexts + +- `components/search/searchresults.js` +- `components/case/summary.js` + - use `deleteItem(caseID, "watchedCases")` + - call `deleteWatchedCases(caseID)` + - refresh watched cases via `getWatchedCasesProxy(...)` + - rehydrate `watchedCases` and `watchedCasesDetails` + +##### Removal in dashboard card/view-all contexts + +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` + - also use `deleteWatchedCases(...)` + - refresh and reclassify watched cases through: + - `getWatchedCasesProxy(...)` + - `splitWatchedCasesBySubmissionState(...)` + - `getDetailsProxy(..., "myWatchedCases")` + +#### 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.js` + - `deleteWatchedCases(watchedCaseID)` + - `getWatchedCasesProxy(loggedInUserId)` + +#### API Layer + +##### Principal routes + +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcasesproxy_api.js` + +##### Route-family classification + +- `deletewatchedcases_api.js` + - **CRM relationship delete** + - deletes a `pinswg_watchlists()` 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 + +```text +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.js` +- `components/case/summary.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` +- `actions/services/portalDirectService.js` +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcasesproxy_api.js` +- `store/watchedCases/reducer.js` + +##### Likely adjacent files + +- `pages/api/endpoint/getwatchedcasesproxy_api.js` +- `lib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.js` +- `store/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 + +```text +CRM Contact +↔ Watched Case +``` + +#### Relationship entities used + +- visible relationship entity: + - `pinswg_watchlists` +- visible linked fields include: + - `pinswg_watchlistid` + - `_pinswg_contact_value` + - `_pinswg_watchedcase_value` + - `pinswg_emailnotifications` + - `pinswg_appealcasetype` + - `pinswg_representationsubmitted` + - `pinswg_representationtype` + +#### Retrieval pattern + +- read by CRM contact ownership: + +```text +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`: + +```text +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: + +```text +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 enrichment + - `splitWatchedCasesBySubmissionState(...)` when dashboard classification is involved + +#### Shared state + +- both use: + - `store/watchedCases.reducer.js` + - `store/currentView.reducer.js` + - `store/accountDetails.reducer.js` + +#### Shared APIs + +- both directly or indirectly depend on: + - `getwatchedcases_api.js` + - `getwatchedcasesproxy_api.js` + - `createwatchedcases_api.js` + - `deletewatchedcases_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 + +```text +User +→ search results / case summary watch action +→ createWatchedCases(payload) +→ createwatchedcases_api +→ CRM watchlist create/patch +→ getWatchedCasesProxy +→ watchedCases Redux refresh +``` + +#### Watchlist Viewing + +```text +User +→ myportal bootstrap +→ getWatchedCases(contactId) +→ CRM watchlist retrieval +→ splitWatchedCasesBySubmissionState +→ detail enrichment +→ dashboard card / top-three / view-all +``` + +#### Watchlist Removal + +```text +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.js` + - `components/search/addresssearchresults.js` + - `components/search/dnssearchresults.js` + - `components/case/summary.js` + - `actions/services/portalDirectService.js` + - `pages/api/endpoint/{createwatchedcases_api,getwatchedcases_api,getwatchedcasesproxy_api}.js` + - `store/watchedCases/reducer.js` + +#### Watchlist Viewing + +- start with: + - `pages/myportal/index.js` + - `components/myportal/watchedcases.js` + - `components/myportal/topthree.js` + - `components/myportal/viewall.js` + - `pages/api/endpoint/{getwatchedcases_api,getwatchedcasesproxy_api}.js` + - `lib/domain/dashboard-policy/splitWatchedCasesBySubmissionState.js` + - `store/watchedCases/reducer.js` + +#### Watchlist Removal + +- start with: + - `components/search/searchresults.js` + - `components/case/summary.js` + - `components/myportal/topthree.js` + - `components/myportal/viewall.js` + - `actions/services/portalDirectService.js` + - `pages/api/endpoint/{deletewatchedcases_api,deletewatchedcasesproxy_api}.js` + - `pages/unsubscribe/[watchlistid].js` + - `pages/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.md` +- `context/api-route-map.md` +- `context/portal-api-security-boundary-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +Journey pages / components / services / state: + +- `pages/myportal/index.js` +- `pages/myportal/searchresults.js` +- `pages/myportal/addresssearchresults.js` +- `pages/myportal/advancedsearchresults.js` +- `pages/unsubscribe/[watchlistid].js` +- `pages/unsubscribeall/[watchlistid].js` +- `components/search/searchresults.js` +- `components/search/addresssearchresults.js` +- `components/search/dnssearchresults.js` +- `components/case/summary.js` +- `components/myportal/watchedcases.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` +- `actions/services/portalDirectService.js` +- `store/watchedCases/reducer.js` +- `store/watchedCases/action.js` + +API files: + +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getwatchedcasesproxy_api.js` +- `pages/api/endpoint/createwatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcasesproxy_api.js` +- `pages/api/email/getall.js` +- `pages/api/email/getmailinglist.js` +- `pages/api/email/getcaseref.js` + +#### Searches performed for Slice 6 + +- `pages`: `unsubscribe|watchlist|watchedcases|getWatchedCases|createWatchedCases|deleteWatchedCases` +- `components`: `selectWatchedCase|selectEmailNotifications|deleteItem|watchedCases|unsubscribe|send-email-notifications|stop-sending-email-notifications` +- `actions/services`: `getWatchedCases|createWatchedCases|deleteWatchedCases|getWatchedCasesProxy|watchlist` +- `pages/api`: `getwatchedcases|createwatchedcases|deletewatchedcases|unsubscribe|watchlist|pinswg_emailnotifications` +- `store`: `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 + +1. 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. +2. `createwatchedcases_api.js` behaves as an upsert route rather than a simple create route, which is important for maintainers tracing watch vs email-subscription behaviour. +3. Dashboard watchlist displays are projections over watched-case CRM data and portal detail enrichment rather than a separate owned dashboard record set. +4. 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. +5. 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.md` +- `memory-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_documents` family. +- 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 +- 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.js` +- `pages/case/[ticketnumber].js` +- `components/case.js` +- `components/case/summary.js` +- `components/case/documents.js` + +#### Loaders / Initialisation + +##### Search-to-case transition + +- `components/search/searchresults.js` + - sets `currentReference` before navigating to the case route + - establishes the visible search-to-document navigation handoff through the case journey rather than a dedicated document page + +##### 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: + - `docsOffline` + - `showFilteredDocs` + - does not SSR-hydrate document metadata itself + +##### 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 `docsOffline` via `getDocLink(docsOffline)` + +#### State Ownership + +##### Primary slices + +- `store/searchOutput/reducer.js` + - owns `documentDetailsObj` + - this is the primary visible read model for case-document presentation + +- `store/currentView/reducer.js` + - owns `currentPage` + - participates in document pagination state continuity + +##### Document-discovery state in component layer + +- `components/case/documents.js` + - owns local UI state for: + - `selectedOption` + - `documentTypes` + - `selectedDocumentType` + - `checkedItems` + - `selectAll` + - `orderByState` + - `fieldSortState` + - loading and download-status overlays + +##### 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.js` + - `getSearchDocumentDetails(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.js` +- `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` +- `pages/api/endpoint/getsearchdocumentTypes_api.js` +- adjacent but not visibly surfaced in the reviewed UI path: + - `pages/api/endpoint/getsearchdocumenthistory_api.js` + - `pages/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_hashlink` for 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 + +```text +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].js` +- `components/case.js` +- `components/case/summary.js` +- `components/case/documents.js` +- `actions/services/searchDirectService.js` +- `store/searchOutput/action.js` +- `store/searchOutput/reducer.js` + +##### Likely adjacent files + +- `components/search/searchresults.js` +- `pages/api/endpoint/getsearchdocumentdetails_api.js` +- `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` +- `pages/api/endpoint/getsearchdocumentTypes_api.js` +- `pages/api/endpoint/getsearchdocumenthistory_api.js` +- `pages/api/endpoint/getsearchdocumenthistorypaged_api.js` +- `components/utils/downloads.js` +- `components/utils/downloadmanager.js` + +##### Highest-risk areas + +- document metadata shape expected by `components/case/documents.js` +- generated `pinswg_hashlink` continuity between metadata and download +- filter and pagination assumptions tied to `@odata.count` and `@odata.nextLink` +- `docsOffline` flag 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.js` +- `components/utils/downloads.js` +- `pages/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` + +##### 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-disposition` when available + - creates a blob URL and triggers an `` download + - emits a `DownloadedFile` analytics event + +##### Download queuing + +- `components/utils/downloadmanager.js` + - manages queued download tasks + - limits concurrent downloads + - tracks per-document statuses: + - `idle` + - `queued` + - `downloading` + - `done` + - `failed` + +#### 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_hashlink` stored in document rows within `documentDetailsObj` + +#### 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 `id` path param and `hash` query 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 `/filenotavailable` on 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 `id` and `hash` + - retry handling + - response header setting + - browser-stream handoff + +- **Analytics** + - browser-side `DownloadedFile` event emitted after successful client download flow + +#### Ownership Model + +```text +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.js` +- `components/utils/downloads.js` +- `components/utils/downloadmanager.js` +- `pages/api/documents/download/[id].js` + +##### Likely adjacent files + +- `pages/api/endpoint/getsearchdocumentdetails_api.js` +- `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` +- `actions/core/token.js` +- `actions/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 + +```text +User +→ Page +→ Service +→ API +→ CRM Metadata +→ Download Proxy +→ Document Delivery +``` + +#### Visible delivery flow + +```text +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_documents` metadata queried via relay-backed endpoint routes +- key visible metadata fields include: + - `pinswg_isharedocumentreference` + - `pinswg_name` + - `pinswg_latestpublisheddate` + - `pinswg_documentpublisheddate` + - `pinswg_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 `id` and `hash` +- 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 `/filenotavailable` when 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 + +#### 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 + +```text +User +→ search results +→ case detail route +→ case documents component +→ getSearchDocumentTypes / getSearchDocumentDetails / getSearchDocumentDetailsPaged +→ CRM published-document metadata +→ documentDetailsObj +→ visible document list +``` + +#### Published Document Retrieval / Download + +```text +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].js` + - `components/case.js` + - `components/case/summary.js` + - `components/case/documents.js` + - `actions/services/searchDirectService.js` + - `pages/api/endpoint/{getsearchdocumentdetails_api,getsearchdocumentdetailspaged_api,getsearchdocumentTypes_api}.js` + - `store/searchOutput/{action,reducer}.js` + +#### Published Document Retrieval / Download + +- start with: + - `components/case/documents.js` + - `components/utils/downloads.js` + - `components/utils/downloadmanager.js` + - `pages/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.md` +- `context/api-route-map.md` +- `context/portal-api-platform-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +Journey pages / components / services / state: + +- `components/search/searchresults.js` +- `pages/case/[ticketnumber].js` +- `components/case.js` +- `components/case/summary.js` +- `components/case/documents.js` +- `components/utils/downloads.js` +- `components/utils/downloadmanager.js` +- `actions/services/searchService.js` +- `actions/services/searchDirectService.js` +- `store/searchOutput/action.js` +- `store/searchOutput/reducer.js` + +API files: + +- `pages/api/endpoint/getsearchdocumentdetails_api.js` +- `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` +- `pages/api/endpoint/getsearchdocumentTypes_api.js` +- `pages/api/endpoint/getsearchdocumenthistory_api.js` +- `pages/api/endpoint/getsearchdocumenthistorypaged_api.js` +- `pages/api/documents/download/[id].js` + +#### Searches performed for Slice 7 + +- `pages`: `documents/download|getsearchdocumentdetails|getsearchdocumenthistory|getsearchdocumentTypes|filenotavailable` +- `components`: `document|download|filenotavailable|DocumentDetails|docsOffline|showFilteredDocs` +- `actions/services`: `getSearchDocumentDetails|getSearchDocumentTypes|getSearchDocumentDetailsPaged|download` +- `store`: `documentDetailsObj|setDocumentDetails|setDocumentHistory` +- `pages/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 + +1. Discovery and download are separate route families but are tightly coupled by generated `pinswg_hashlink` values. +2. `docsOffline` visibly suppresses live document-link behaviour, so maintainers should treat document availability messaging as part of the journey architecture. +3. The reviewed user-facing discovery path is case-detail-centric rather than a standalone document page, so changes can affect search-to-case continuity. +4. Document history routes exist in the API family, but their visible user-facing ownership is weaker than current document discovery in the reviewed slice. +5. Download behaviour is split between server-side streaming in `/api/documents/download/[id]` and client-side blob handling in `components/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.md` +- `memory-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)` + - `messagesObj` + - `CaseNoticeBanner` + - user-visible notice content on the case details tab +- The reviewed case-message route retrieves CRM `tasks` records filtered to subjects containing `Banner`, 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 `messagesObj` notice 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].js` +- `pages/dns/[developmentName].js` +- `pages/myportal/case/[ticketnumber].js` +- `components/case.js` +- `components/case/summary.js` +- `components/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 `messagesObj` into 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 `messagesObj` into 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 `messagesObj` into the case page props + +##### Case page presentation bootstrap + +- `components/case.js` + - passes `messagesObj` into `components/case/summary.js` + +- `components/case/summary.js` + - renders `CaseNoticeBanner` inside the `case-details` tab when: + - `props.messagesObj["@odata.count"] > 0` + +#### 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: + - `eventDetailsObj` + - `mediaDetailsObj` + - does not own `messagesObj` + +- `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.js` + - `getCaseMessage(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.js` + - `pages/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 caseId` + - `contains(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 + +- **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 + +```text +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].js` +- `pages/dns/[developmentName].js` +- `pages/myportal/case/[ticketnumber].js` +- `components/case.js` +- `components/case/summary.js` +- `components/case/caseNoticeBanner.js` +- `actions/services/caseDirectService.js` +- `pages/api/endpoint/getcasemessage_api.js` + +##### Likely adjacent files + +- `pages/api/endpoint/getsipsevents_api.js` +- `pages/api/endpoint/getsipsmedia_api.js` +- `store/searchOutput/action.js` +- `store/searchOutput/reducer.js` + +##### Highest-risk areas + +- message filtering assumptions based on `subject` containing `Banner` +- bilingual content splitting conventions in `subject` and `description` +- 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].js` +- `pages/dns/[developmentName].js` +- `pages/myportal/case/[ticketnumber].js` +- `components/case/summary.js` +- adjacent case communication components: + - `components/case/events.js` + - `components/case/media.js` + +#### Loaders / Initialisation + +##### SIPS communication bootstrap + +- `pages/case/[ticketnumber].js` +- `pages/dns/[developmentName].js` +- `pages/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)` + +##### Case-summary communication presentation + +- `components/case/summary.js` + - derives: + - `hasEventsTabData` + - `hasMediaTabData` + - `livePublishedEvent` + - renders a GOV.UK notification banner for a live published event when available + - renders separate `Events` and `Media` tabs when corresponding data exists + +#### State Ownership + +##### Primary slices + +- `store/searchOutput/reducer.js` + - owns: + - `eventDetailsObj` + - `mediaDetailsObj` + - this is the primary visible state owner for adjacent published case communications in the reviewed path + +##### 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.js` + - `getSIPSEvents(caseid)` + - `getSIPSMedia(caseid)` + +#### API Layer + +##### Principal routes + +- `pages/api/endpoint/getsipsevents_api.js` +- `pages/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 + +```text +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].js` +- `pages/dns/[developmentName].js` +- `pages/myportal/case/[ticketnumber].js` +- `components/case/summary.js` +- `actions/services/caseDirectService.js` +- `pages/api/endpoint/getsipsevents_api.js` +- `pages/api/endpoint/getsipsmedia_api.js` +- `store/searchOutput/action.js` +- `store/searchOutput/reducer.js` + +##### Likely adjacent files + +- `components/case/events.js` +- `components/case/media.js` +- `lib/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 + +```text +Case +→ message/notice source +→ case-detail presentation +→ user-visible communication +``` + +#### Visible communication flow + +```text +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 `tasks` records filtered by `contains(subject, 'Banner')` +- related SIPS communications: + - CRM `pinswg_sipsevents` + - CRM `pinswg_eventrecordings` + +#### Route family + +- primary notice route: + - `pages/api/endpoint/getcasemessage_api.js` +- adjacent communication routes: + - `pages/api/endpoint/getsipsevents_api.js` + - `pages/api/endpoint/getsipsmedia_api.js` + +#### State ownership + +- `messagesObj` + - page-prop owned +- `eventDetailsObj` / `mediaDetailsObj` + - Redux owned via `store/searchOutput` + +#### 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 + +```text +User +→ case route bootstrap +→ getCaseMessage(incidentid) +→ getcasemessage_api +→ CRM Banner task records +→ messagesObj +→ CaseNoticeBanner +``` + +#### Related Published Case Communications + +```text +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].js` + - `pages/dns/[developmentName].js` + - `pages/myportal/case/[ticketnumber].js` + - `components/case.js` + - `components/case/summary.js` + - `components/case/caseNoticeBanner.js` + - `actions/services/caseDirectService.js` + - `pages/api/endpoint/getcasemessage_api.js` + +#### Related Published Case Communications + +- start with: + - `pages/case/[ticketnumber].js` + - `pages/dns/[developmentName].js` + - `pages/myportal/case/[ticketnumber].js` + - `components/case/summary.js` + - `actions/services/caseDirectService.js` + - `pages/api/endpoint/{getsipsevents_api,getsipsmedia_api}.js` + - `store/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.md` +- `context/api-route-map.md` +- `context/portal-api-platform-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +Journey pages / components / services / state: + +- `pages/case/[ticketnumber].js` +- `pages/dns/[developmentName].js` +- `pages/myportal/case/[ticketnumber].js` +- `components/case.js` +- `components/case/summary.js` +- `components/case/caseNoticeBanner.js` +- `actions/services/caseService.js` +- `actions/services/caseDirectService.js` +- `store/searchOutput/action.js` +- `store/searchOutput/reducer.js` + +API files: + +- `pages/api/endpoint/getcasemessage_api.js` +- `pages/api/endpoint/getsipsevents_api.js` +- `pages/api/endpoint/getsipsmedia_api.js` +- `pages/api/notices/index.js` + +#### Searches performed for Slice 8 + +- `components/case`: `messagesObj|getCaseMessage|notice|banner|message` +- `pages`: `getCaseMessage|messagesObj|getcasemessage_api|notice|message` +- `actions/services`: `getCaseMessage|getSIPSEvents|getSIPSMedia|message|notice` +- `pages/api/endpoint`: `getcasemessage_api|getsipsevents_api|getsipsmedia_api|message|notice` +- `store`: `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 `messagesObj` where none was visibly present. +- It did not execute runtime case-page flows. + +### Risks / Cautions + +1. The main notice source is identified through a CRM `tasks` query filtered by `contains(subject, 'Banner')`, so behaviour depends on content conventions as well as route logic. +2. `CaseNoticeBanner` performs visible bilingual splitting and date-window checks in the UI layer, which makes presentation logic part of the architectural behaviour. +3. `messagesObj` is page-prop owned while adjacent event/media communications are Redux owned, so communication state is split across ownership models. +4. SIPS live-event/media communications overlap with case communications, but they are a separate visible route/state family from banner notices. +5. The static `pages/api/notices/index.js` route 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 `CaseNoticeBanner` presentation. +- 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. diff --git a/context/portal-api-platform-assessment.md b/context/portal-api-platform-assessment.md new file mode 100644 index 00000000..ba420d9c --- /dev/null +++ b/context/portal-api-platform-assessment.md @@ -0,0 +1,2834 @@ +# Portal API Platform Assessment + +## Status + +Assessment-only. + +First bounded slice complete: + +- **Slice A — API Family Classification Sample** + +No refactor, implementation, or runtime behaviour change was performed. + +## Required context read + +The following files were read before this assessment slice: + +- `context/architecture.md` +- `context/portal-api-security-boundary-assessment.md` +- `context/integration-map.md` +- `memory-bank/debt-list.md` +- `memory-bank/change-log.md` + +## Sample scope + +This slice intentionally inspected only the following API areas: + +- `pages/api/endpoint` +- `pages/api/file` +- `pages/api/email` +- `pages/api/documents` +- `pages/api/auth` + +This is **not** a full route catalogue. + +Representative sample files reviewed directly: + +- `pages/api/endpoint/getbasicsearch_api.js` +- `pages/api/endpoint/getmycases_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/file/getbloblist.js` +- `pages/api/file/upload.js` +- `pages/api/file/createappealcompletemessage_api.js` +- `pages/api/email/notify.js` +- `pages/api/email/getall.js` +- `pages/api/documents/download/[id].js` +- `pages/api/auth/[...nextauth].js` +- `pages/api/auth/resolve-locale.js` + +Supporting bounded inspection also covered: + +- top-level file listings for each sampled folder +- `pages/api/middleware/*` +- targeted pattern searches inside the sampled folders only + +## Findings + +The sampled API surface already shows a clear split between: + +1. **newer helper-oriented relay handlers** + - typically using `relayGet(...)` + - standard `respondError(...)` + - explicit required-query validation +2. **older direct integration/orchestration handlers** + - typically using `axios(...)` directly + - constructing CRM relay URLs manually + - invoking `hashAPIPath(...)` inline + - mixing multiple responsibilities in a single route + +The sample suggests the platform is not one uniform API layer, but a mixed platform of: + +- CRM relay facades +- Azure Storage facades +- queue/finalisation orchestration routes +- GOV.UK Notify routes +- NextAuth/session routes +- a small number of utility/meta helpers + +## API family classification + +### 1. CRM relay read + +Observed sample indicators: + +- `pages/api/endpoint/getbasicsearch_api.js` +- `pages/api/endpoint/getmycases_api.js` + +Characteristics: + +- reads CRM data through relay-backed query URLs +- often uses `relayGet(...)` +- usually validates required query fields first +- may lightly transform CRM results before returning + +Assessment classification: + +- `endpoint/` is primarily **CRM relay read** plus some write variants + +### 2. CRM relay write + +Observed sample indicators: + +- `pages/api/endpoint/createcase_api.js` + +Characteristics: + +- uses direct `axios(...)` with bearer token +- constructs CRM URL as `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)` +- forwards create/update/patch/delete actions into CRM + +Assessment classification: + +- part of `endpoint/` is **CRM relay write** + +### 3. storage/blob read + +Observed sample indicators: + +- `pages/api/file/getbloblist.js` + +Characteristics: + +- validates `container`, `casefolderID`, `hash` +- verifies request integrity with candidate-path hash comparison +- calls Azure storage helpers such as `getBlobs(...)` + +Assessment classification: + +- part of `file/` is **storage/blob read** + +### 4. storage/blob write + +Observed sample indicators: + +- `pages/api/file/upload.js` + +Characteristics: + +- validates upload/body fields and hash +- uses multipart middleware +- writes through Azure storage helpers such as `createBlob(...)` / `createRepBlob(...)` + +Assessment classification: + +- part of `file/` is **storage/blob write** + +### 5. queue/finalisation + +Observed sample indicators: + +- `pages/api/file/createappealcompletemessage_api.js` + +Characteristics: + +- reads and rewrites draft/progress blob state +- updates completion markers +- writes case blobs +- triggers completion/finalisation helper `createCaseCompleteMessage(...)` +- may also trigger secondary account side effects + +Assessment classification: + +- part of `file/` is **queue/finalisation** and **mixed orchestration** + +### 6. email/notification + +Observed sample indicators: + +- `pages/api/email/notify.js` +- `pages/api/email/getall.js` + +Characteristics: + +- GOV.UK Notify send operations +- some routes also fetch CRM/watchlist/document/event data before building email payloads +- bilingual template selection appears embedded in route logic + +Assessment classification: + +- `email/` is primarily **email/notification** +- some routes are simple send helpers, others are broader orchestration/batch workflows + +### 7. document download + +Observed sample indicators: + +- `pages/api/documents/download/[id].js` + +Characteristics: + +- streams a relay-backed document response +- passes through `id` + `hash` +- sets download headers +- redirects to `/filenotavailable` on failure + +Assessment classification: + +- `documents/` is **document download** + +### 8. auth/session + +Observed sample indicators: + +- `pages/api/auth/[...nextauth].js` +- `pages/api/auth/resolve-locale.js` + +Characteristics: + +- NextAuth session establishment and callback handling +- verification email send +- locale-aware redirect handling +- CRM-backed preferred-language resolution + +Assessment classification: + +- `auth/` is primarily **auth/session** + +### 9. local utility/meta + +Observed sample indicators: + +- `pages/api/auth/resolve-locale.js` + +Characteristics: + +- small helper-style route +- returns locale metadata rather than handling a full business operation + +Assessment classification: + +- some sampled routes behave as **local utility/meta**, even when they consult CRM + +### 10. middleware/helper + +Observed sampled middleware area: + +- `pages/api/middleware/apiResponse.js` +- `pages/api/middleware/middleware.js` +- `pages/api/middleware/relayForwarding.js` +- `pages/api/middleware/relayPolicyPresets.js` + +Characteristics: + +- response envelope helper +- multipart parsing helper +- shared relay forwarding helper +- relay policy presets + +Assessment classification: + +- `pages/api/middleware/*` is clearly **middleware/helper** + +## Integration responsibility map + +| Sampled family | Primary responsibility | Backing integration classification | +| ------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------- | +| `pages/api/endpoint` read sample (`getbasicsearch_api`, `getmycases_api`) | CRM query relay | **CRM relay-backed** | +| `pages/api/endpoint` write sample (`createcase_api`) | CRM create/write | **CRM relay-backed** | +| `pages/api/file` read sample (`getbloblist`) | blob listing/read | **Azure Storage SDK-backed** | +| `pages/api/file` write sample (`upload`) | blob upload/write | **Azure Storage SDK-backed** | +| `pages/api/file` finalisation sample (`createappealcompletemessage_api`) | draft/blob + completion + account side effects | **mixed/orchestration** | +| `pages/api/email/notify` | outbound transactional email | **GOV.UK Notify-backed** | +| `pages/api/email/getall` | CRM/watchlist/document/event aggregation + notify send | **mixed/orchestration** | +| `pages/api/documents/download/[id]` | streamed document retrieval via relay | **CRM relay-backed** | +| `pages/api/auth/[...nextauth]` | session/auth + verification email | **NextAuth-backed** with **GOV.UK Notify-backed** email send and CRM locale lookup | +| `pages/api/auth/resolve-locale` | locale helper | **mixed/orchestration** leaning **local-only utility with CRM lookup** | +| `pages/api/middleware/*` | shared route support | **local-only** | + +### Responsibility notes from the sample + +- `endpoint/` is mostly a **CRM relay platform family**. +- `file/` is mostly an **Azure Storage platform family**, but includes finalisation/orchestration routes that cross into CRM/account workflows. +- `email/` is not just a thin Notify wrapper; at least one sampled route is an orchestration layer over CRM + Notify. +- `auth/` is centered on NextAuth, but locale and sign-in email behavior pull in CRM and Notify responsibilities. + +## Repeated patterns observed + +Observed only from the bounded sample and targeted searches within sampled folders. + +### hash validation + +Clearly repeated in sampled `file/` routes and visible more broadly in sampled-folder searches. + +Observed forms: + +- candidate path arrays +- raw/encoded path variants +- compare `hashAPIPath(candidatePath)` against `&hash=` or `?hash=` + provided hash + +Examples: + +- `pages/api/file/getbloblist.js` +- `pages/api/file/upload.js` +- `pages/api/file/createappealcompletemessage_api.js` + +### signed request helper usage + +Observed in the newer relay family through shared helpers rather than inline hash construction. + +Examples: + +- `relayGet(...)` in `getbasicsearch_api.js` +- `relayGet(...)` in `getmycases_api.js` + +Assessment note: + +- helper usage is present, but not yet universal across sampled families + +### relay forwarding + +Observed strongly in the newer `endpoint/` read family. + +Characteristics: + +- `relayGet(...)` +- relay policy preset import +- optional transform step +- structured error envelope + +### required query validation + +Strongly repeated across all sampled families. + +Examples: + +- `searchString` required +- `loggedInUserId` required +- `container`, `casefolderID`, `hash` required +- `id`/`hash` required in download flow + +### response envelope style + +Repeated modern pattern: + +- `respondSuccess(...)` +- `respondError(...)` + +Observed across sampled endpoint/file/email routes. + +Assessment note: + +- response helpers are widespread in the sample even when integration patterns differ underneath + +### raw CRM URL building + +Repeated in older direct-write/orchestration routes. + +Examples: + +- `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)` +- direct axios config objects with bearer headers + +Observed in: + +- `pages/api/endpoint/createcase_api.js` +- `pages/api/email/getall.js` +- visible across targeted searches in multiple sampled folders + +### Azure blob path construction + +Repeated in sampled `file/` routes. + +Examples: + +- `container` + `casefolderID` +- `tempCaseRef + "/" + tempCaseRef + "_case.json"` +- split path handling for representation files using `/` + +### logging style + +Mixed styles observed: + +- structured-ish redacted `console.info(...)` in `email/notify.js` and `auth/[...nextauth].js` +- plain `console.log(...)` in `createcase_api.js` and `documents/download/[id].js` +- `consoleLogger(...)` used for error logging in many routes + +Assessment note: + +- logging style is visibly inconsistent within the sample + +## Obvious duplication candidates + +These are sample-based family-level observations only. No consolidation is recommended yet. + +### paged / unpaged endpoint variants + +Observed from folder listing and targeted search patterns: + +- `getbasicsearch_api` / `getbasicsearchpaged_api` +- `getbasicsearchdetails_api` / `getbasicsearchdetailspaged_api` +- `getadvancedsearch_api` / `getadvancedsearchpaged_api` +- document detail/history paged variants + +Classification: + +- **likely intentional** + +Reason: + +- naming and existing contract-hardening history suggest explicit contract variants rather than accidental duplicates + +### search / detail / history variants + +Observed from `endpoint/` sample family: + +- search results +- detail retrieval +- history retrieval +- document-type retrieval + +Classification: + +- **likely intentional** + +### blob list / download / delete variants + +Observed from `file/` sample family and listings: + +- `getbloblist` +- `downloadblob` +- `deleteblob` +- `deleteblobcase` +- `deleteblobrep` +- proxy variants + +Classification: + +- **likely historical** + +Reason: + +- the split between case/rep/proxy variants and repeated hash-validation shapes suggests growth by accretion + +### create / update / patch CRM wrappers + +Observed from `endpoint/` and `file/` folder listings plus sample: + +- `createcase_api` +- `updatecase_api` +- `patchcase_api` +- `updateaccount_api` +- `createaccount_api` + +Classification: + +- **likely historical** + +Reason: + +- similar direct axios + token + hash + relay URL patterns recur across wrappers + +### email batch / send helpers + +Observed from `email/` sample: + +- `notify.js` as a focused send route +- `getall.js` as a broad gather-and-send batch route +- related support routes `getdocuments.js`, `getevents.js`, `getmailinglist.js`, `getcaseref.js` + +Classification: + +- **unclear** + +Reason: + +- some separation may reflect legitimate batch assembly boundaries, but the family also shows orchestration overlap + +## Contract-critical families + +From the sample, the following families appear contract-critical. + +### public search + +Yes. + +Evidence: + +- `getbasicsearch_api.js` +- search/detail/history family naming in `endpoint/` + +Why critical: + +- public-facing query/filter/result contracts likely feed core search UI behavior + +### myportal / dashboard + +Yes. + +Evidence: + +- `getmycases_api.js` + +Why critical: + +- dashboard and authenticated case-list flows depend on these payloads and identifiers + +### submission / finalisation + +Yes. + +Evidence: + +- `createappealcompletemessage_api.js` + +Why critical: + +- this looks like a transition point from draft/blob state into completion/finalisation processing + +### file upload / download + +Yes. + +Evidence: + +- `upload.js` +- `getbloblist.js` +- broader file folder sample + +Why critical: + +- these routes underpin draft persistence and user document handling + +### document download + +Yes. + +Evidence: + +- `documents/download/[id].js` + +Why critical: + +- direct binary/document delivery contract with failure redirect behavior + +### auth / session + +Yes. + +Evidence: + +- `auth/[...nextauth].js` + +Why critical: + +- session establishment, callback routing, verification email, and locale-aware redirects are central platform contracts + +### email / notification + +Yes. + +Evidence: + +- `email/notify.js` +- `email/getall.js` + +Why critical: + +- user communications, watchlist notifications, and sign-in email flows depend on these contracts + +## Risks / cautions + +This slice is intentionally high-level and sample-based. + +Key cautions: + +1. **Do not over-generalise from one sample per family** + - especially in `endpoint/`, where newer helper-based routes and older direct axios routes coexist +2. **Integration responsibility is not always single-system** + - some routes are clearly orchestration routes rather than thin adapters +3. **Naming can conceal contract differences** + - proxy, paged, history, detail, and case/rep variants may preserve subtle caller expectations +4. **The sample confirms structural inconsistency, not implementation priority** + - this assessment does not yet decide what should be consolidated +5. **Auth/email boundaries are cross-cutting** + - `auth/[...nextauth].js` is not just auth/session; it also includes Notify and CRM locale lookups + +## Validation performed + +Manual inspection and small targeted searches only. + +Performed: + +- direct reading of required context files +- small directory listings for: + - `pages/api/endpoint` + - `pages/api/file` + - `pages/api/email` + - `pages/api/documents` + - `pages/api/auth` + - `pages/api/middleware` +- direct bounded file inspection of representative samples only +- targeted pattern searches limited to sampled folders for: + - `relayGet` + - `respondError` / `respondSuccess` + - `hashAPIPath` + - `axios` + - `NotifyClient` + - `getPortalLogin` + - locale/redirect handling + +Not performed: + +- no repo-wide automated analysis +- no bulk inventory generation +- no Python +- no runtime code changes +- no lint/tests, because this slice is documentation-only + +## Recommendation + +Next bounded slice only: + +### Slice B — Endpoint CRM Relay Shape Sample + +Inspect a small representative subset within `pages/api/endpoint` only, split into: + +- public search/read routes +- authenticated myportal read routes +- CRM mutation/write routes +- proxy variants + +Goal of Slice B: + +- distinguish newer `relayGet(...)` handler families from older direct `axios + hashAPIPath` relay wrappers +- identify the main contract shapes and the most common wrapper patterns inside `endpoint/` +- remain classification-only, with no consolidation proposal yet + +--- + +## Slice B — Endpoint CRM Relay Contract Shape Sample + +### Required context read + +The following files were re-read before Slice B: + +- `context/portal-api-platform-assessment.md` +- `context/portal-api-security-boundary-assessment.md` +- `context/architecture.md` +- `memory-bank/change-log.md` + +### Sample scope + +Slice B intentionally focused only on: + +- `pages/api/endpoint` + +This remained a representative subset only, not a folder-wide route inventory. + +Representative routes reviewed directly: + +#### 1. Public read/search + +- `getbasicsearch_api.js` +- `getbasicsearchpaged_api.js` +- `getadvancedsearch_api.js` +- `getsearchdocumentdetails_api.js` +- `getsearchdocumenthistory_api.js` + +#### 2. Authenticated/myportal read + +- `getmycases_api.js` +- `getmyrepresentations_api.js` +- `getwatchedcases_api.js` +- `getawaitingsubmission_api.js` +- `getpersonalaccount_api.js` + +#### 3. CRM create/write + +- `createcase_api.js` +- `createaccount_api.js` +- `createwatchedcases_api.js` + +#### 4. CRM update/patch/delete + +- `updateaccount_api.js` +- `patchcase_api.js` +- `deletewatchedcases_api.js` +- `deletemyrepresentations_api.js` + +#### 5. Proxy/pass-through or legacy variants + +- `getwatchedcasesproxy_api.js` + +Supporting bounded inspection also included targeted endpoint-only searches for: + +- `relayGet(...)` +- `relayGetData(...)` +- `axios(...)` +- `hashAPIPath(...)` +- `respondError(...)` / `respondSuccess(...)` +- `transformData` +- `@odata.nextLink` +- common explicit status codes + +### Findings + +The sampled `pages/api/endpoint` family appears to collapse into a modest number of repeated contract shapes even though the implementation layer is mixed. + +The clearest split is: + +1. **newer helper-oriented relay read contracts** + - structured request guards + - `relayGet(...)` + - shared response helper usage + - optional `transformData` + - optional relay policy presets +2. **older direct-wrapper mutation contracts** + - `getToken()` + - manual `queryUrl` + - direct `axios(config)` + - `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)` + - explicit method-specific config for create/update/delete + +The sample suggests the endpoint layer is not dominated by many bespoke business contracts. + +Instead, it looks like a relatively small set of repeated relay contract shapes with route-by-route variations in: + +- validation breadth +- transforms +- pagination options +- identifier type +- whether the route is read vs write vs delete + +### Endpoint contract shapes + +#### A. Public CRM read + +Observed sample routes: + +- `getbasicsearch_api.js` +- `getbasicsearchpaged_api.js` +- `getadvancedsearch_api.js` +- `getsearchdocumentdetails_api.js` +- `getsearchdocumenthistory_api.js` + +Observed shape: + +```text +query params +→ CRM relay read +→ optional transform / pagination normalization +→ response +``` + +Characteristics: + +- public-style filter or lookup params +- no route-local session enforcement visible +- usually `relayGet(...)` +- often optional transform stage +- often `@odata.nextLink` normalization for paged families + +#### B. User-owned CRM read + +Observed sample routes: + +- `getmycases_api.js` +- `getmyrepresentations_api.js` +- `getwatchedcases_api.js` +- `getawaitingsubmission_api.js` +- `getpersonalaccount_api.js` + +Observed shape: + +```text +contact/user identifier +→ CRM relay read +→ optional transform +→ response +``` + +Characteristics: + +- caller supplies `loggedInUserId` or `contactid` +- route validates identifier presence +- route forwards CRM query via `relayGet(...)` +- some routes enrich/flatten CRM response before return + +#### C. CRM create + +Observed sample routes: + +- `createcase_api.js` +- `createaccount_api.js` + +Observed shape: + +```text +payload + identifiers +→ CRM create +→ optional side effects +→ response +``` + +Characteristics: + +- `req.body` plus required identifiers +- manual relay URL construction +- direct bearer-token axios config +- response typically passes upstream CRM response through `respondSuccess(...)` +- some routes add secondary side effects (`createcase_api.js` also writes a case blob) + +#### D. CRM update/patch + +Observed sample routes: + +- `updateaccount_api.js` +- `patchcase_api.js` + +Observed shape: + +```text +record identifier + payload or patch intent +→ CRM patch/update +→ response +``` + +Characteristics: + +- single record identifier in query +- patch body either supplied directly or built in-route +- direct `axios(config)` with hashed relay URL + +#### E. CRM delete + +Observed sample routes: + +- `deletewatchedcases_api.js` +- `deletemyrepresentations_api.js` + +Observed shape: + +```text +record identifier +→ CRM delete +→ response +``` + +Characteristics: + +- single record identifier required +- direct `axios(config)` delete +- same manual hashed relay URL pattern as create/update wrappers + +#### F. Proxy/pass-through + +Observed sample routes: + +- `getwatchedcasesproxy_api.js` + +Observed shape: + +```text +caller request +→ relay-backed wrapper +→ optional transform +→ response +``` + +Characteristics: + +- structurally very close to corresponding non-proxy read route +- often preserves nearly identical query and transform logic +- main distinction is naming/contract boundary rather than drastically different internal shape + +#### G. Lookup/config/support + +Observed indirectly in sample and endpoint-only search evidence: + +- account/login/config/lookup families such as `getaccounts_api.js`, `getpreferredlanguage_api.js`, `getappealtypes_api.js`, `getformdata_api.js` + +Observed shape: + +```text +lookup key or config selector +→ CRM option/config/query +→ response +``` + +Characteristics: + +- frequently helper-oriented `relayGet(...)` +- lighter transforms than search/myportal routes +- often shared response helper/error contracts + +#### H. Upsert/orchestration hybrid + +Observed sample route: + +- `createwatchedcases_api.js` + +Observed shape: + +```text +payload bindings +→ pre-check / existence lookup +→ create or patch decision +→ CRM write +→ response +``` + +Characteristics: + +- not a pure create route +- combines a read-style pre-check (`relayGetData(...)`) with a write-style direct axios relay call +- looks like a distinct hybrid contract shape inside `endpoint/` + +### Implementation style map + +#### Newer helper-oriented routes + +Strongly represented by sampled public reads, myportal reads, and many proxy/read families. + +Typical characteristics: + +- `relayGet(...)` +- sometimes `relayGetData(...)` for supplementary lookups +- `respondError(...)` used for input guards +- shared `errorResponse` object passed to relay helper +- optional `transformData` +- optional relay policy preset import +- optional header-builder injection for paged/custom reads + +Sample examples: + +- `getbasicsearch_api.js` +- `getbasicsearchpaged_api.js` +- `getadvancedsearch_api.js` +- `getsearchdocumentdetails_api.js` +- `getsearchdocumenthistory_api.js` +- `getmycases_api.js` +- `getmyrepresentations_api.js` +- `getwatchedcases_api.js` +- `getawaitingsubmission_api.js` +- `getpersonalaccount_api.js` +- `getwatchedcasesproxy_api.js` + +#### Older direct-wrapper routes + +Strongly represented by sampled create/update/delete families. + +Typical characteristics: + +- direct `axios(config)` +- explicit `getToken()` call +- manual `queryUrl` +- manual `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)` construction +- method-specific config (`post`, `patch`, `delete`) +- direct upstream response passthrough via `respondSuccess(...)` + +Sample examples: + +- `createcase_api.js` +- `createaccount_api.js` +- `updateaccount_api.js` +- `patchcase_api.js` +- `deletewatchedcases_api.js` +- `deletemyrepresentations_api.js` + +#### Hybrid routes + +Observed route: + +- `createwatchedcases_api.js` + +Typical characteristics: + +- combines helper-oriented pre-check (`relayGetData`) with direct-wrapper mutation (`axios + hashAPIPath`) +- performs route-local branching between `post` and `patch` + +### Contract consistency findings + +#### Request parameter style + +Observed pattern: + +- generally explicit and route-local +- mostly query-string driven for reads and record-targeted mutations +- body + query combination common for create/update routes + +Assessment: + +- **partially consistent** +- parameter naming still varies by route family (`searchString`, `searchstring`, `loggedInUserId`, `contactid`, `contactId`, `incidentid`, `watchedCaseID`, `myRepresentationsID`) + +#### Response envelope style + +Observed pattern: + +- `respondError(...)` and `respondSuccess(...)` are broadly used across both newer and older implementations + +Assessment: + +- **more consistent than implementation style** +- shared response helpers appear to be a stronger common contract layer than the underlying integration code + +#### Error handling + +Observed pattern: + +- required-input guards often return `400` +- route-level catch blocks usually convert failures to route-specific coded `400` +- some non-sampled evidence in the folder shows occasional `404` and `405` + +Assessment: + +- **generally consistent at a high level** +- but status-code precision is not fully uniform across the folder + +#### Status codes + +Observed sample emphasis: + +- `400` dominates for missing input and upstream failure handling +- endpoint-only searches show some explicit `404` and `405` usage in other route types + +Assessment: + +- **mostly normalized but not fully uniform** +- the sampled create/update/delete routes all converge heavily on `400` + +#### Logging + +Observed pattern: + +- helper-oriented read routes often avoid explicit in-route logging except for special enrichment failures +- older direct-wrapper routes commonly call `consoleLogger(error)` in catch blocks +- some routes retain more bespoke logging around supplementary sub-queries + +Assessment: + +- **mixed** +- logging is more consistent in direct-wrapper mutations than in complex read/orchestration routes + +#### Transforms + +Observed pattern: + +- common in read families +- used for: + - title projection + - flattening/expanding related CRM objects + - hash-link enrichment + - document-date fallback + - `@odata.nextLink` normalization + - post-filter enrichment for advanced search + +Assessment: + +- **transform-heavy read contracts are a real recurring shape** +- not every read route is a thin pass-through + +#### Pagination handling + +Observed pattern: + +- paged search/document families require extra params such as `orderby`, `fieldSort`, `showNumberOfRecords` +- `@odata.nextLink` normalization is repeatedly applied +- paged headers are injected through custom request option builders + +Assessment: + +- **paged read is a distinct repeated contract variant**, not just a small option on base reads + +#### Hash/signing assumptions + +Observed pattern: + +- helper-oriented reads generally hide relay signing behind shared helper layers +- older mutations perform route-local `hashAPIPath(queryUrl)` construction directly +- some support/lookup routes such as `getportallogin_api.js` visibly validate caller-provided hash input before relay read + +Assessment: + +- **signing assumptions are structurally inconsistent across shapes** +- read families usually abstract signing away; direct write/delete families usually expose it explicitly in route code + +### Rationalisation insight + +Evidence from the sample suggests: + +- **few repeated contract shapes** + +More specifically: + +- the folder appears to contain a large number of routes +- but many of those routes seem to fit into a comparatively small number of recurring patterns: + - public CRM read + - user-owned CRM read + - paged read variant + - config/lookup read + - CRM create + - CRM patch/update + - CRM delete + - proxy/pass-through + - hybrid upsert/orchestration + +So the route count likely overstates the number of distinct underlying contract shapes. + +### Contract-critical cautions + +The following sampled contract shapes look high-risk to change. + +#### Public search/read + +High caution. + +Reason: + +- public-facing search and document contracts likely drive multiple search and case-view experiences +- paged/unpaged variants may have frontend expectations around filtering, `@odata.nextLink`, ordering, and transformed fields + +#### Myportal/dashboard reads + +High caution. + +Reason: + +- these contracts appear central to dashboard, watched-case, representation, and awaiting-submission views +- transforms such as `pinswg_title`, watched-case flattening, and title projection may be relied upon directly by callers + +#### Account/profile reads + +High caution. + +Reason: + +- `getpersonalaccount_api.js` is a small route shape but contract-critical because it likely underpins profile bootstrap and account editing journeys + +#### Case creation/update + +High caution. + +Reason: + +- create/update routes use older direct wrappers and sometimes carry side effects beyond the CRM write itself +- `createcase_api.js` is especially sensitive because it combines CRM write with blob-side persistence work + +#### Deletion routes + +High caution. + +Reason: + +- delete contracts are simple in visible shape but operationally sensitive +- they rely on record identifiers and direct CRM deletion wrappers + +#### Proxy variants + +Medium-high to high caution. + +Reason: + +- proxy variants often look almost identical to primary routes, which suggests they may exist for compatibility or caller-specific contract reasons rather than redundancy alone + +### Risks / cautions + +1. **Sample bias remains real** + - this slice still used a bounded subset, not all endpoint handlers +2. **Read routes are not all equally simple** + - some helper-based reads are effectively mini-orchestration routes because transforms and supplementary lookups are embedded in them +3. **Route count may exaggerate uniqueness** + - many files appear to be repeated contract variants rather than genuinely new platform shapes +4. **Implementation age and contract shape are related but not identical** + - newer helper-based reads and older direct-wrapper writes are dominant patterns, but hybrid routes like `createwatchedcases_api.js` show overlap +5. **Security-boundary concerns should not be conflated with this slice** + - Slice B is about platform contract shapes and maintainability patterns, even though the sampled user-owned routes still expose the previously documented identifier-trust model + +### Validation performed + +Manual inspection and bounded searches only. + +Performed: + +- re-read required Slice B context files +- direct inspection of a representative endpoint-only subset across: + - public read/search + - authenticated/myportal read + - CRM create + - CRM update/patch/delete + - proxy/pass-through +- targeted endpoint-only searches for: + - `relayGet(...)` + - `relayGetData(...)` + - `axios(...)` + - `hashAPIPath(...)` + - `respondSuccess(...)` / `respondError(...)` + - `transformData` + - `@odata.nextLink` + - explicit status code patterns + +Not performed: + +- no full endpoint inventory +- no repo-wide automated analysis +- no Python +- no runtime code changes +- no lint/tests, because this remains documentation-only assessment work + +### Recommendation + +Next bounded slice only: + +#### Slice C — Endpoint Transform and Pagination Pattern Sample + +Restrict scope to a representative subset of `pages/api/endpoint` routes that use: + +- `transformData` +- `@odata.nextLink` normalization +- paged/unpaged route pairs +- supplementary `relayGetData(...)` enrichment + +Goal: + +- determine whether transform/pagination behavior itself collapses into a small number of reusable contract sub-patterns +- stay assessment-only +- do not propose implementation yet + +--- + +## Slice C — API Maintenance Map & Reuse Baseline + +### Required context read + +The following files were read before Slice C: + +- `context/portal-api-platform-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/debt-list.md` +- `memory-bank/change-log.md` + +### Findings + +The current API surface appears maintainable only with prior local knowledge because folder responsibility and naming have drifted over time. + +The strongest current maintenance pattern is: + +- **`pages/api/endpoint` is still the main historical CRM relay catch-all** +- later folders (`file`, `email`, `documents`, `auth`, `admin`) were added for newer needs +- but those later folders are not purely technical or domain-clean boundaries + +The result is a maintenance problem of **findability first, consistency second**: + +1. a developer must often know the history of the route family to know where to look +2. once found, they must still determine whether the family uses newer shared helpers or older direct wrappers + +The codebase does already contain reusable API building blocks, but they are not yet expressed in one obvious maintenance navigation model. + +### Folder Responsibility Map + +#### `pages/api/endpoint` + +- **Intended responsibility if visible:** general portal business-data APIs via CRM relay +- **Actual responsibility based on sampled evidence:** broad catch-all for CRM reads, account/profile support, portal user reads, public search, document metadata, lookup/config, and many create/update/delete mutation wrappers +- **Does folder naming still match responsibility?** partially +- **Assessment:** **mixed / historical** + +Maintenance note: + +- this is still the first place to look for most CRM-backed portal behavior +- but it is too broad to communicate ownership clearly by folder name alone + +#### `pages/api/file` + +- **Intended responsibility if visible:** file/blob/document upload/download operations +- **Actual responsibility based on sampled evidence:** Azure Storage blob operations, uploads, draft reads, delete/download/list flows, generated PDFs, finalisation routes, some queue/completion work, and some CRM-adjacent orchestration/involvement routes +- **Does folder naming still match responsibility?** only partially +- **Assessment:** **mixed** + +Maintenance note: + +- a developer working on drafts/submission may need this folder even when the main change is not “just files” + +#### `pages/api/email` + +- **Intended responsibility if visible:** email and notification handling +- **Actual responsibility based on sampled evidence:** direct Notify send, mailing list/event/document gathering, watchlist notification batch assembly, and CRM-backed orchestration before send +- **Does folder naming still match responsibility?** partially +- **Assessment:** **mixed** + +Maintenance note: + +- some email routes are thin notification endpoints, others are multi-step orchestration routes + +#### `pages/api/documents` + +- **Intended responsibility if visible:** document download +- **Actual responsibility based on sampled evidence:** essentially a narrow direct document download proxy family +- **Does folder naming still match responsibility?** yes +- **Assessment:** **coherent** + +Maintenance note: + +- the folder is clear, but broader document-related behavior is still split across `documents`, `endpoint`, and `file` + +#### `pages/api/auth` + +- **Intended responsibility if visible:** authentication/session routes +- **Actual responsibility based on sampled evidence:** NextAuth boundary plus locale-resolution/auth-support logic with CRM preferred-language lookup and Notify email behavior +- **Does folder naming still match responsibility?** yes, mostly +- **Assessment:** **acceptable** + +Maintenance note: + +- coherent enough for findability, but auth behavior still crosses into CRM and Notify concerns + +#### `pages/api/admin` + +- **Intended responsibility if visible:** admin/internal reporting routes +- **Actual responsibility based on sampled evidence:** administrative CRM reporting/read APIs, including status counts, latest documents, and new-appeal reporting-style queries +- **Does folder naming still match responsibility?** yes +- **Assessment:** **coherent** + +Maintenance note: + +- this folder looks like one of the clearest top-level API ownership areas + +#### `pages/api/middleware` + +- **Intended responsibility if visible:** shared API support utilities +- **Actual responsibility based on sampled evidence:** response envelopes, multipart parsing, shared relay forwarding, relay retry/timeouts/policy presets +- **Does folder naming still match responsibility?** yes +- **Assessment:** **coherent** + +Maintenance note: + +- this folder already contains some of the strongest reuse primitives for future API work + +#### Top-level utility API files + +Reviewed/observed: + +- `pages/api/health.js` +- `pages/api/doc.ts` +- `pages/api/notices/index.js` + +Assessment: + +- **Intended responsibility:** utility/meta/support endpoints +- **Actual responsibility:** health/status, swagger/openapi docs, static notice feed +- **Does naming still match?** mostly yes +- **Assessment:** **coherent** for utility/meta + +Maintenance note: + +- these files are easy to find, but they sit alongside folders in a way that reinforces the overall mixed top-level structure + +### Feature-to-API Maintenance Map + +| Feature area | Likely API folder(s) | Likely route family names | Integration touched | Contract-critical? | Folder fit | +| -------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | ------------------ | ------------------ | +| public search | `endpoint` | `getbasicsearch*`, `getadvancedsearch*`, `getbasicdnssearch*` | CRM relay | yes | acceptable | +| advanced/address search | `endpoint` | `getadvancedsearch*`, `getbasicsearch_by_address_api`, `getbasicsearch_by_lparref_api` | CRM relay | yes | acceptable | +| case details | `endpoint` | `getcase*`, `getincidentbyid_api`, `getcasemessage_api`, `getlinkedcases_api` | CRM relay | yes | acceptable | +| document download | `documents`, `endpoint`, `file` | `documents/download/[id]`, `getsearchdocumentdetails*`, `getsearchdocumenthistory*`, blob download routes | CRM relay, Azure Storage | yes | confusing | +| my portal dashboard | `endpoint`, `file` | `getmycases_api`, `getmyrepresentations_api`, `getwatchedcases_api`, `getawaitingsubmission_api`, draft blob reads | CRM relay, Azure Storage | yes | acceptable | +| watched cases | `endpoint` | `getwatchedcases*`, `createwatchedcases_api`, `deletewatchedcases*` | CRM relay | yes | good | +| representations | `endpoint`, `file` | `getrepresentations*`, `getmyrepresentations*`, representation involvement/completion routes | CRM relay, Azure Storage | yes | acceptable | +| new appeal drafts | `file` | `getprogressobjblob`, `getbloblist`, `upload*`, `deleteblobcase`, `setupcontainer` | Azure Storage | yes | acceptable | +| representation drafts | `file` | `getrepsblob*`, `upload*`, `deleteblobrep`, `editRepJson` | Azure Storage | yes | acceptable | +| appeal submission/finalisation | `file`, `endpoint` | `createappealcompletemessage*`, `createcase_api`, `patchcase_api`, `updatecase_api` | Azure Storage, Azure Queue, CRM relay | yes | historical/unclear | +| representation submission/finalisation | `file`, `endpoint` | `createrepcompletemessage_api`, involvement routes, representation delete/update families | Azure Storage, Azure Queue, CRM relay | yes | historical/unclear | +| account registration | `endpoint`, `auth` | `createaccount_api`, `getemailaccountcheck_api`, `getportallogin_api` | CRM relay, NextAuth | yes | acceptable | +| personal details/account update | `endpoint` | `getpersonalaccount_api`, `updateaccount_api`, `updatepassword_api` | CRM relay | yes | acceptable | +| authentication/sign-in | `auth`, `endpoint` | `[...nextauth]`, `resolve-locale`, `getportallogin_api`, `getpreferredlanguage_api` | NextAuth, GOV.UK Notify, CRM relay | yes | confusing | +| email/notifications | `email`, `auth` | `notify`, `getall`, `getdocuments`, `getevents`, next-auth verification email flow | GOV.UK Notify, CRM relay | yes | acceptable | +| admin/internal reporting | `admin`, `endpoint` | `getnewappeals_api`, `getlatestdocuments_api`, status-count routes | CRM relay | no/mostly internal | good | + +### Folder Drift / Naming Drift + +Observed maintenance drift areas: + +#### CRM routes living under `file` + +Examples from existing sampled evidence: + +- involvement creation routes +- completion/finalisation routes +- case-related orchestration in `file` + +Why it affects maintenance: + +- a developer may not think to search `file/` when the change is really about case submission, involvement, or CRM completion side effects + +#### Orchestration routes living under `email` + +Example: + +- `pages/api/email/getall.js` + +Why it affects maintenance: + +- the route is not just “send an email”; it assembles CRM/watchlist/document/event data before notification send + +#### Document behavior split across `endpoint`, `documents`, and `file` + +Examples: + +- `documents/download/[id].js` +- search document metadata under `endpoint` +- blob document/file flows under `file` + +Why it affects maintenance: + +- developers must know whether they are dealing with published CRM-backed document retrieval, draft/blob files, or a direct download proxy before they know where to look + +#### Finalisation routes under `file` + +Examples: + +- `createappealcompletemessage_api.js` +- `createrepcompletemessage_api.js` + +Why it affects maintenance: + +- these are not merely file operations; they sit near a business transition boundary from draft to submitted state + +#### Account/auth-support routes under `endpoint` + +Examples: + +- `getportallogin_api.js` +- `getpreferredlanguage_api.js` +- `getemailaccountcheck_api.js` +- `getpersonalaccount_api.js` + +Why it affects maintenance: + +- account and auth-adjacent responsibilities are split between `auth/` and `endpoint/`, which weakens findability + +#### Historical naming conventions + +Examples: + +- `_api` suffix routes +- `proxy` variants +- `paged` variants +- `details` / `history` / `Types` variants + +Why it affects maintenance: + +- naming often reflects historical delivery slices more than current ownership boundaries +- similar names can hide materially different caller expectations or integration behavior + +### Common Building Blocks + +#### `relayGet(...)` + +- **What it standardizes:** CRM relay GET forwarding, token/header/hash handling, success response path, error response path, optional transforms +- **Where it is already used:** widely across `pages/api/endpoint/**`, especially read families +- **Preferred for future work?** yes, for new CRM read APIs where the route fits the shared relay-read model +- **Do legacy alternatives still exist?** yes, many older direct axios wrappers remain + +#### `relayGetData(...)` + +- **What it standardizes:** relay-backed data fetches for sub-queries or enrichments without directly writing to `res` +- **Where it is already used:** complex endpoint reads and hybrids such as advanced search enrichment, address/LPA lookups, `createwatchedcases_api.js` +- **Preferred for future work?** yes, where a route needs supplementary relay reads inside orchestration logic +- **Do legacy alternatives still exist?** yes, direct `axios.get(WEBAPI_URL + queryUrl + hashAPIPath(queryUrl))` + +#### `respondSuccess(...)` / `respondError(...)` + +- **What they standardize:** top-level JSON response conventions and coded error envelopes +- **Where they are already used:** broadly across endpoint, file, email, admin, and middleware-aware routes +- **Preferred for future work?** yes +- **Do legacy alternatives still exist?** yes, some raw `res.status(...).json(...)` patterns still exist in the wider API surface and utility endpoints + +#### Relay policy presets + +- **What they standardize:** retry/timeout profiles for read families (`STRICT_LOGIN`, `LOOKUP`, `BOUNDED_READ`, `SEARCH_PAGED`) +- **Where they are already used:** many `endpoint` read routes +- **Preferred for future work?** yes, where relay-backed reads need an existing policy profile +- **Do legacy alternatives still exist?** yes, older routes without policy presets and direct axios wrappers + +#### `signedRequestClient` helpers + +- **What they standardize:** signed GET/POST/DELETE execution using existing hashed URL generation +- **Where they are already used:** service/client layer rather than route layer (`actions/clients/signedRequestClient.js`) +- **Preferred for future work?** likely yes for new signed client/service flows +- **Do legacy alternatives still exist?** yes, many inline `buildHashedQueryUrl` and direct request patterns remain outside or beneath this layer + +#### Hash/path validation helpers + +- **What they standardize:** HMAC path signing and validation expectations (`hashAPIPath(...)`, signed URL generation) +- **Where they are already used:** widespread across relay and file/blob flows +- **Preferred for future work?** yes, where existing signed route models must be preserved +- **Do legacy alternatives still exist?** yes, many route-local manual candidate-path checks and inline hash construction blocks + +#### Azure storage helpers + +- **What they standardize:** blob/container/queue operations and related metadata assembly +- **Where they are already used:** `pages/api/file/**`, some endpoint/file orchestration flows, service layer helpers +- **Preferred for future work?** yes for storage-facing work +- **Do legacy alternatives still exist?** some routes still mix storage helpers with route-local orchestration logic rather than remaining thin wrappers + +#### Notify helpers / Notify integration patterns + +- **What they standardize:** currently more implicit than explicit; direct Notify client usage exists, but some shared behavior lives in service modules and auth flow +- **Where they are already used:** `pages/api/email/**`, `pages/api/auth/[...nextauth].js`, service layer +- **Preferred for future work?** partially; direct Notify usage is established, but orchestration should be chosen deliberately +- **Do legacy alternatives still exist?** yes, thin send routes and broad orchestration routes coexist + +#### Auth/session helpers + +- **What they standardize:** session establishment primarily via NextAuth route; locale support via `resolve-locale`; broader auth-context helpers live outside `pages/api` +- **Where they are already used:** `pages/api/auth/**`, SSR/service layers elsewhere in repo +- **Preferred for future work?** yes for auth-specific work inside existing NextAuth boundary +- **Do legacy alternatives still exist?** account/auth-support behavior still partly lives under `endpoint/` + +#### Logging helpers + +- **What they standardize:** `consoleLogger(...)`, redaction helper usage, some structured relay logging +- **Where they are already used:** many API routes, especially catch blocks and relay middleware +- **Preferred for future work?** yes +- **Do legacy alternatives still exist?** yes, direct `console.log(...)` / `console.info(...)` still exist in sampled folders + +### Future Consistency Baseline + +This is guidance only for future maintainability work, not an implementation mandate. + +Suggested baseline: + +- new CRM read APIs should prefer shared relay helpers such as `relayGet(...)` where the route fits that contract shape +- new relay-backed sub-query/enrichment logic should prefer existing helper-style relay fetches rather than bespoke inline wrappers where suitable +- new JSON responses should prefer `respondSuccess(...)` / `respondError(...)` +- new storage-facing APIs should reuse existing Azure storage helpers and established hash-validation patterns +- new Notify-facing APIs should keep thin send routes thin, and only mix broader orchestration where there is a clear need +- new APIs should make it easier to identify: + - owning feature area + - integration touched + - whether the route is contract-critical +- existing direct `axios + hashAPIPath` wrappers may remain where behaviour is stable, but they should not be copied blindly into new work when shared helper patterns already exist +- future API additions should aim to improve findability as well as technical consistency, so developers can more quickly identify the right folder and family before editing + +### Maintenance Pain Points + +The main maintenance pain points visible in this slice are: + +#### Route findability + +- similar behavior is split across multiple top-level folders +- some folders are historical rather than cleanly responsibility-based + +#### Route name similarity + +- many near-identical names (`proxy`, `paged`, `details`, `history`, `_api`) increase scan cost + +#### Folder responsibility overlap + +- CRM behavior exists in `endpoint`, `file`, `email`, `auth`, and `admin` +- document behavior exists in `endpoint`, `documents`, and `file` + +#### Mixed integration responsibilities + +- some folders contain thin wrappers, others contain orchestration routes that cross CRM/storage/Notify boundaries + +#### Older/newer implementation styles + +- helper-oriented relay contracts and older direct wrappers coexist, so developers must determine style before making even a small change + +#### Missing route ownership documentation + +- a future contributor often has to infer “owning feature” from names and code shape rather than from explicit maintenance guidance + +#### Repeated boilerplate and historical growth + +- even where shared helpers now exist, route families still show signs of incremental historical accretion + +### Risks / Cautions + +1. **This is still a bounded maintenance assessment** + - it is not a full route inventory or migration plan +2. **Folder drift does not automatically mean incorrect architecture** + - some historical placements may still reflect practical delivery constraints or compatibility concerns +3. **Reuse guidance should not erase contract-specific nuance** + - some route families genuinely need transforms, enrichments, or special caller behavior +4. **Findability and consistency are related but different problems** + - a route can be technically well-structured yet still be hard to find in the current folder layout +5. **Legacy patterns should be treated carefully** + - older direct wrappers are not automatically wrong; the main risk is copying them forward uncritically for new work + +### Validation performed + +Manual inspection and bounded searches only. + +Performed: + +- re-read required Slice C context files +- small top-level API listings for: + - `pages/api` + - `pages/api/admin` +- direct representative inspection of: + - admin routes + - top-level utility/meta routes + - shared middleware/support files + - existing shared client/helper files + - previously reviewed assessment evidence from Slices A and B +- targeted searches for existing building blocks and helper usage across `pages/api` + +Not performed: + +- no repo-wide automated analysis +- no Python +- no bulk route inventory generation +- no runtime code changes +- no lint/tests, because this remains documentation-only assessment work + +### Recommendation + +Next bounded assessment step only: + +#### Slice D — API Route Ownership & Change-Entry Sample + +Focus on a bounded set of high-value feature journeys and trace: + +- first likely API entry points a maintainer would touch +- adjacent supporting routes/helpers they would also need to inspect +- the minimum “change entry set” for safe edits + +Suggested bounded feature sample: + +- watched cases +- account/personal details +- public document retrieval +- appeal submission/finalisation + +Goal: + +- make future change entry points more explicit without proposing refactor yet + +--- + +## Slice D — API Route Ownership & Change-Entry Sample + +### Required context read + +The following files were re-read before Slice D: + +- `context/portal-api-platform-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +### Findings + +For the sampled journeys, the main maintainer problem is not usually “what does this one route do?” but rather: + +```text +which UI entry point started this journey, +which service helper actually owns the API call, +which route performs the final contract, +and which adjacent state/helpers must also be checked before changing behaviour? +``` + +The four sampled journeys differ in complexity: + +- watched cases = feature-led CRM relay journey with several UI entry points +- account/personal details = account/profile journey split across SSR/page state and CRM update APIs +- public document retrieval = public search-driven document metadata + download proxy journey +- appeal submission/finalisation = orchestration-heavy draft-to-submission journey crossing storage, queue/finalisation, and CRM mutation boundaries + +### Journey Change-Entry Sets + +#### 1. Watched cases + +##### Primary UI/component entry points + +- `components/search/searchresults.js` +- `components/search/addresssearchresults.js` +- `components/search/dnssearchresults.js` +- `components/case/summary.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` + +##### Likely service helpers + +- `actions/services/portalDirectService.js` + - `getWatchedCases` + - `getWatchedCasesProxy` + - `createWatchedCases` + - `deleteWatchedCases` + +##### API routes + +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getwatchedcasesproxy_api.js` +- `pages/api/endpoint/createwatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcasesproxy_api.js` as adjacent compatibility variant + +##### Shared clients/helpers + +- `relayGet(...)` +- `relayGetData(...)` +- `respondError(...)` / `respondSuccess(...)` +- relay policy presets +- `hashAPIPath(...)` +- route/query builders in portal services +- `splitWatchedCasesBySubmissionState(...)` +- `getDetailsProxy(...)` + +##### State/store modules + +- `store/watchedCases/{action,reducer}.js` +- `store/currentView/action.js` +- `store/accountDetails/action.js` + +##### External integration touched + +- CRM relay +- local-only state/helpers + +#### 2. Account / personal details + +##### Primary UI/component entry points + +- `pages/account/personaldetails.js` +- `components/account/personaldetails.js` +- adjacent completion/check components referenced there: + - `components/account/personaldetailsCheck.js` + - `components/account/personaldetailsComplete.js` + +##### Likely service helpers + +- `actions/services/accountDirectService.js` + - `getPersonalAccount` + - `updateAccount` + - `getPortalLogin` as identity/bootstrap adjacent helper + +##### API routes + +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/updateaccount_api.js` +- adjacent account/auth-support routes often relevant during investigation: + - `getportallogin_api.js` + - `getemailaccountcheck_api.js` + - `updatepassword_api.js` + +##### Shared clients/helpers + +- `relayGet(...)` +- `respondError(...)` / `respondSuccess(...)` +- `getToken()` +- `hashAPIPath(...)` +- signed request / relay client helpers in the service layer + +##### State/store modules + +- `store/accountDetails/action.js` + - `setAccountDetails` + - `setLoggedInUserId` + +##### External integration touched + +- CRM relay +- NextAuth (session presence at page level) +- local-only state/helpers + +#### 3. Public document retrieval + +##### Primary UI/component entry points + +- `components/search/searchresults.js` and related search flows as user discovery entry points +- document metadata/search document routes act as upstream API entry before download + +##### Likely service helpers + +- `actions/services/searchDirectService.js` + - `getSearchDocumentDetails` + - related search document retrieval helpers + +##### API routes + +- `pages/api/endpoint/getsearchdocumentdetails_api.js` +- `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` +- `pages/api/endpoint/getsearchdocumenthistory_api.js` +- `pages/api/endpoint/getsearchdocumenthistorypaged_api.js` +- `pages/api/endpoint/getsearchdocumentTypes_api.js` +- `pages/api/documents/download/[id].js` + +##### Shared clients/helpers + +- `relayGet(...)` +- `respondError(...)` +- document hash-link generation logic using HMAC/hash helpers +- `getToken()` inside direct download proxy +- `consoleLogger(...)` + +##### State/store modules + +- usually lighter direct store involvement than account/watched cases +- search result/detail state may still be relevant through search result flows + +##### External integration touched + +- CRM relay +- local-only helper logic + +#### 4. Appeal submission / finalisation + +##### Primary UI/component entry points + +- `pages/myportal/[appealtypes].js` as journey page entry +- `lib/myportal/loadMyPortalAppealPage.js` as key journey loader +- adjacent finalisation/completion UI evidence from completion components and journey state transitions + +##### Likely service helpers + +- `actions/services/documentDirectService.js` + - `getFilesFromBlob` + - `getProgressFromBlob` + - `getAwaitingSubmissionFromBlob` +- `actions/services/accountDirectService.js` + - `getPersonalAccount` +- route-adjacent helpers in storage layer: + - `actions/azurestorage.js` + +##### API routes + +- `pages/api/file/createappealcompletemessage_api.js` +- `pages/api/file/createappealcompletemessageproxy_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/endpoint/patchcase_api.js` +- `pages/api/endpoint/updatecase_api.js` +- adjacent draft/blob routes usually relevant: + - `pages/api/file/getbloblist.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/upload.js` + - `pages/api/file/deleteblobcase.js` + +##### Shared clients/helpers + +- Azure storage helpers (`getCaseBlob`, progress/blob helpers, completion-message helpers) +- `respondError(...)` / `respondSuccess(...)` +- `hashAPIPath(...)` +- `relayGet(...)` and direct-wrapper mutation patterns +- route-builder/signed-request helpers in services +- loader guards in `lib/myportal/loadMyPortalAppealPage.js` + +##### State/store modules + +- `store/accountDetails/*` +- `store/appealType/*` +- `store/currentView/*` +- `store/awaitingSubmission/*` +- `store/formData/*` as adjacent journey support + +##### External integration touched + +- Azure Storage +- Azure Queue / completion handoff +- CRM relay +- local-only state/helpers + +### Route Ownership Classification + +#### Watched cases journey + +- `getwatchedcases_api` + - **feature-owned** by watched cases + - integration: CRM relay +- `getwatchedcasesproxy_api` + - **ambiguous/historical** + - integration: CRM relay +- `createwatchedcases_api` + - **orchestration-owned** by watched cases + - integrations: CRM relay, helper/read pre-check +- `deletewatchedcases_api` + - **feature-owned** by watched cases + - integration: CRM relay +- `deletewatchedcasesproxy_api` + - **ambiguous/historical** + - integration: CRM relay + +#### Account / personal details journey + +- `getpersonalaccount_api` + - **feature-owned** by account/profile + - integration: CRM relay +- `updateaccount_api` + - **feature-owned** by account/profile + - integration: CRM relay +- `getportallogin_api` + - **helper/support** for account/auth bootstrap + - integration: CRM relay +- `getemailaccountcheck_api` + - **helper/support** for registration/account support + - integration: CRM relay +- `updatepassword_api` + - **ambiguous/historical** in current live account journey terms + - integration: CRM relay + +#### Public document retrieval journey + +- `getsearchdocumentdetails_api` + - **feature-owned** by public document retrieval/search detail + - integration: CRM relay +- `getsearchdocumentdetailspaged_api` + - **feature-owned** by public document retrieval/search detail + - integration: CRM relay +- `getsearchdocumenthistory_api` + - **feature-owned** by public document retrieval/history + - integration: CRM relay +- `getsearchdocumenthistorypaged_api` + - **feature-owned** by public document retrieval/history + - integration: CRM relay +- `getsearchdocumentTypes_api` + - **helper/support** for document retrieval/search filtering + - integration: CRM relay +- `documents/download/[id].js` + - **integration-owned** download proxy used by public document retrieval + - integration: CRM relay + +#### Appeal submission / finalisation journey + +- `createappealcompletemessage_api` + - **orchestration-owned** by appeal finalisation + - integrations: Azure Storage, Azure Queue/finalisation, CRM/account side effect +- `createappealcompletemessageproxy_api` + - **helper/support** or **ambiguous/historical** compatibility wrapper + - integrations: local proxy + file/finalisation path +- `createcase_api` + - **orchestration-owned** by appeal submission + - integrations: CRM relay, Azure Storage side effect +- `patchcase_api` + - **feature-owned** by appeal submission/final transition + - integration: CRM relay +- `updatecase_api` + - **feature-owned** by appeal mutation/submission support + - integration: CRM relay +- draft/blob support routes (`getbloblist`, `getprogressobjblob`, `upload`, `deleteblobcase`) + - **integration-owned** storage support for the appeal draft/finalisation journey + - integration: Azure Storage + +### Change Risk Classification + +#### Watched cases + +- **Risk:** high + +Reason: + +- multiple UI entry points +- CRM read + create/delete paths +- feature state refreshed after mutation +- proxy and non-proxy variants add maintenance ambiguity + +#### Account / personal details + +- **Risk:** high + +Reason: + +- account/profile contracts are user-critical +- session/bootstrap and CRM profile identity are tightly coupled +- mutation route is simple in code shape but high impact to users + +#### Public document retrieval + +- **Risk:** medium-high + +Reason: + +- public users are affected directly +- metadata, hash-link generation, and download proxy are split across different areas +- fewer stateful side effects than account/finalisation + +#### Appeal submission / finalisation + +- **Risk:** very high + +Reason: + +- multiple integrations touched +- orchestration-heavy +- draft/blob + submission transition + finalisation behavior +- contract-critical journey +- state passed across loaders, services, blob data, and CRM write paths + +### First-Look Checklists + +#### Watched cases + +Before changing this journey, inspect: + +1. `components/search/searchresults.js` and `components/case/summary.js` +2. `components/myportal/viewall.js` and `components/myportal/topthree.js` +3. `actions/services/portalDirectService.js` watched-case helpers +4. `pages/api/endpoint/{getwatchedcases_api,getwatchedcasesproxy_api,createwatchedcases_api,deletewatchedcases_api}.js` +5. `store/watchedCases/*` and `store/currentView/*` +6. Slice B / security-boundary notes touching watched-case identifiers in `context/portal-api-platform-assessment.md` + +#### Account / personal details + +Before changing this journey, inspect: + +1. `pages/account/personaldetails.js` +2. `components/account/personaldetails.js` and adjacent check/complete components +3. `actions/services/accountDirectService.js` +4. `pages/api/endpoint/{getpersonalaccount_api,updateaccount_api}.js` +5. adjacent bootstrap/support routes: `getportallogin_api.js`, `getemailaccountcheck_api.js` +6. `store/accountDetails/*` + +#### Public document retrieval + +Before changing this journey, inspect: + +1. search UI entry points that surface document links +2. `actions/services/searchDirectService.js` search document helpers +3. `pages/api/endpoint/{getsearchdocumentdetails_api,getsearchdocumenthistory_api,getsearchdocumentTypes_api}.js` +4. `pages/api/documents/download/[id].js` +5. any route-local document hash-link generation logic in endpoint/admin/email families +6. Slice B findings on public read/document contract shapes + +#### Appeal submission / finalisation + +Before changing this journey, inspect: + +1. `lib/myportal/loadMyPortalAppealPage.js` +2. the page entry and current appeal/draft UI entry points +3. `actions/services/documentDirectService.js` draft blob helpers +4. `pages/api/file/createappealcompletemessage_api.js` +5. `pages/api/endpoint/{createcase_api,patchcase_api,updatecase_api}.js` +6. `actions/azurestorage.js` completion/blob helpers +7. `store/{appealType,currentView,awaitingSubmission,accountDetails}/*` +8. Slice C maintenance-map notes on finalisation folder drift + +### Reuse Opportunities + +#### Watched cases + +- `relayGet(...)` for read routes +- `relayGetData(...)` for upsert pre-check style behavior +- `respondSuccess(...)` / `respondError(...)` +- relay policy presets +- logging helpers +- route-builder/query helper patterns in portal services + +#### Account / personal details + +- `relayGet(...)` for profile reads +- `respondSuccess(...)` / `respondError(...)` +- token/hash helpers in update flows +- signed/relay client helpers in account service layer +- logging helpers + +#### Public document retrieval + +- `relayGet(...)` for document metadata/history reads +- `respondError(...)`-style guard handling +- document hash-link generation logic already present in multiple route families +- logging helpers + +#### Appeal submission / finalisation + +- Azure storage helpers +- `respondSuccess(...)` / `respondError(...)` +- hash/path validation helpers +- direct-wrapper mutation pattern already established in CRM write routes +- signed request / route builder helpers in service layer +- loader guard patterns in `lib/myportal/loadMyPortalAppealPage.js` + +### Risks / Cautions + +1. **These are first-look entry sets, not exhaustive dependency maps** + - each journey has adjacent supporting files beyond what is listed here +2. **UI entry points are distributed** + - watched cases especially can begin from search results, case pages, and myportal views +3. **Route ownership is sometimes mixed or historical** + - proxy and finalisation routes especially need careful interpretation +4. **Submission/finalisation should be treated as the most sensitive sampled journey** + - storage, queue/finalisation, and CRM boundaries all meet there +5. **Account and watched-case journeys rely on earlier identity/bootstrap context** + - a maintainer may need to inspect SSR/session/bootstrap files even when changing a single API route + +### Validation performed + +Manual inspection and bounded searches only. + +Performed: + +- re-read required Slice D context files +- targeted bounded searches across `components`, `actions/services`, `pages`, `lib`, and `store` for the four sampled journeys +- direct file inspection of representative first-look journey files including: + - `components/myportal/viewall.js` + - `pages/account/personaldetails.js` + - `components/account/personaldetails.js` + - `components/search/searchresults.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `components/case/representation/representationComplete.js` +- reuse of already reviewed route/service evidence from Slices A–C for linked API ownership and building-block classification + +Not performed: + +- no full dependency inventory +- no repo-wide automated analysis +- no Python +- no runtime code changes +- no lint/tests, because this remains documentation-only assessment work + +### Recommendation + +Next bounded assessment step only: + +#### Slice E — API Maintainer Decision Guide Sample + +Focus on a small sample of common maintenance intents, such as: + +- adding a new CRM read route +- extending a watched-case style mutation +- adding a document-related route +- extending a finalisation-side orchestration path + +Goal: + +- document which current patterns a maintainer would most likely copy or reuse +- identify which existing patterns appear preferred vs legacy-for-compatibility +- remain assessment-only + +--- + +## Slice E — Wider API Surface Pattern Validation + +### Required context read + +The following files were re-read before Slice E: + +- `context/portal-api-platform-assessment.md` +- `context/architecture.md` +- `context/integration-map.md` +- `memory-bank/change-log.md` + +### Findings + +The wider validation pass supports the earlier assessment model. + +The API surface is large in file count, but the underlying vocabulary still appears relatively small: + +- **large route surface** +- **small contract vocabulary** +- **small implementation-style vocabulary** +- **main maintenance issue remains findability and ownership** + +The folder scan and outlier checks did **not** reveal a major additional platform family that would overturn Slices A–D. + +Instead, the wider surface mostly reinforces that: + +1. `pages/api/endpoint` contains the majority of CRM-facing route variants +2. `pages/api/file` is the main secondary mixed folder where storage plus orchestration concerns accumulate +3. most routes still fit the previously identified contract shapes and implementation styles +4. the main uncertainty is not “unknown route technology” but “where a maintainer should look first” + +### Folder-Level Validation + +Approximate file counts from the bounded listing/count pass: + +- `pages/api/endpoint` -> **74** files +- `pages/api/file` -> **26** files +- `pages/api/email` -> **6** files +- `pages/api/documents` -> **1** file +- `pages/api/auth` -> **2** files +- `pages/api/admin` -> **5** files +- `pages/api/middleware` -> **4** files +- top-level `pages/api` files -> **4** + +#### `pages/api/endpoint` + +- **Approximate size:** very large +- **Apparent responsibility:** CRM relay-facing portal/business-data API catch-all +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no major new family found + +Validation note: + +- route names continue to cluster around search/read, myportal reads, document metadata, account/profile, create/update/delete, lookup/config, and proxy variants + +#### `pages/api/file` + +- **Approximate size:** medium-large +- **Apparent responsibility:** storage/blob routes plus draft/finalisation and some CRM-adjacent orchestration +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no major new family, but PDF-generation and involvement routes reinforce the mixed/orchestration character + +Validation note: + +- outlier review (`createcaseinvolvement_api.js`, `generateappealpdf.js`) still fits the same mixed file/storage/orchestration pattern already documented + +#### `pages/api/email` + +- **Approximate size:** small +- **Apparent responsibility:** Notify send plus document/event/list aggregation for email workflows +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no + +Validation note: + +- `getdocuments.js` reinforces that email routes often combine CRM read + document-link generation + downstream notification support rather than introducing a new route family + +#### `pages/api/documents` + +- **Approximate size:** very small +- **Apparent responsibility:** direct document download proxy +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no + +#### `pages/api/auth` + +- **Approximate size:** very small +- **Apparent responsibility:** auth/session and locale support +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no + +#### `pages/api/admin` + +- **Approximate size:** small +- **Apparent responsibility:** internal/admin reporting and grouped CRM read views +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no + +Validation note: + +- `getStatusCountsByAppealAndLPA_api.js` strengthens the view that admin routes are mostly grouped reporting/read transforms over CRM relay data + +#### `pages/api/middleware` + +- **Approximate size:** very small +- **Apparent responsibility:** shared API support and relay/runtime helper logic +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no + +#### Top-level API routes + +- **Approximate size:** very small +- **Apparent responsibility:** health/meta/static support +- **Do Slice A–D findings still appear representative?** yes +- **Unexpected route families?** no + +### Route Family Validation + +Previously identified route families still appear to cover the wider surface: + +- search/read +- myportal reads +- account/profile +- create/update/delete +- document retrieval +- storage/blob +- queue/finalisation +- notifications +- admin/reporting +- auth/session +- lookup/config/support + +Additional observations from wider route-name validation: + +- static/local support data routes such as `getmandatoryfields_api`, `getpicklists_api`, and `getappealtypesfornewappeal_api` are better treated as part of the already-documented **lookup/config/support** family rather than a genuinely new family +- CRM task/contact routes such as `createcrmtask_api.js` fit the existing **CRM create/write** family +- involvement routes under `file/` fit the existing **orchestration-owned CRM-related mutation** pattern already noted in earlier slices +- PDF-generation routes under `file/` fit the existing **storage/document generation orchestration** pattern rather than requiring a wholly separate top-level family + +Assessment: + +- **no genuinely new major route family identified** + +### Contract Shape Validation + +The previously identified contract shapes still appear sufficient for most of the wider surface: + +- Public CRM read +- User-owned CRM read +- CRM create +- CRM update/patch +- CRM delete +- Proxy/pass-through +- Lookup/config/support +- Hybrid upsert/orchestration + +Additional validation insight: + +- local/static JSON/data support routes (`getmandatoryfields_api`, `getpicklists_api`) fit comfortably within **lookup/config/support** +- admin grouped-reporting routes fit as **CRM read + transform** rather than a separate contract family +- file-side involvement and PDF routes fit as **orchestration** or **support** variants rather than forcing a new contract class + +Assessment: + +- **these contract shapes appear to cover most routes** +- no additional mandatory contract shape is currently required for the architecture model + +### Implementation Style Validation + +The implementation-style model also still holds: + +#### 1. Helper-oriented relay routes + +Still strongly present, especially in `endpoint/` and modern admin/reporting reads. + +Indicators: + +- `relayGet(...)` +- `relayGetData(...)` +- `respondError(...)` / `respondSuccess(...)` +- relay policy presets + +#### 2. Direct-wrapper routes + +Still strongly present, especially for CRM create/update/delete and some file-side CRM mutations. + +Indicators: + +- `getToken()` +- route-local `queryUrl` +- `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)` +- direct `axios(config)` + +#### 3. Orchestration routes + +Still clearly present in: + +- `file/` completion/finalisation +- PDF generation +- involvement creation +- email-side aggregation routes + +Additional major implementation style found? + +- **No** + +Closest extra pattern: + +- local/static support routes using in-repo JSON/data reads + +But this is better treated as a small **support variant**, not a fourth major implementation style. + +### Folder Drift Validation + +#### `pages/api/documents` + +- **Drift:** low +- **Reason:** narrow folder name still matches actual responsibility closely + +#### `pages/api/auth` + +- **Drift:** low to moderate +- **Reason:** still mostly coherent, though CRM locale and Notify behavior cross the folder boundary conceptually + +#### `pages/api/admin` + +- **Drift:** low +- **Reason:** sampled routes still align with internal/admin reporting responsibility + +#### `pages/api/middleware` + +- **Drift:** low +- **Reason:** folder remains clearly reuse/support oriented + +#### Top-level API routes + +- **Drift:** low +- **Reason:** health/meta/static support usage remains straightforward + +#### `pages/api/email` + +- **Drift:** moderate +- **Reason:** naming suggests email-only behavior, but actual routes can perform CRM/document/event aggregation before notification send + +#### `pages/api/file` + +- **Drift:** high +- **Reason:** storage/blob naming no longer fully describes draft orchestration, finalisation, involvement creation, PDF generation, and CRM-adjacent journey behavior housed there + +#### `pages/api/endpoint` + +- **Drift:** high +- **Reason:** folder behaves as the historical general-purpose CRM relay catch-all rather than a clearly bounded current responsibility area + +### Maintenance Hotspots + +#### `pages/api/endpoint` + +- **Hotspot level:** high + +Reason: + +- highest route count +- mixed business responsibilities +- helper-oriented and direct-wrapper styles coexist +- many contract-critical routes +- ownership ambiguity highest here + +#### `pages/api/file` + +- **Hotspot level:** high + +Reason: + +- second-largest route area +- storage + orchestration + finalisation mixed together +- touches high-risk flows and multiple integrations + +#### `pages/api/email` + +- **Hotspot level:** medium + +Reason: + +- smaller route count, but orchestration overlap can surprise maintainers + +#### `pages/api/admin` + +- **Hotspot level:** medium-low + +Reason: + +- coherent but still CRM-transform heavy + +#### `pages/api/auth` + +- **Hotspot level:** medium-high + +Reason: + +- low file count, but highly sensitive behavior and cross-cutting auth/session implications + +#### `pages/api/documents` + +- **Hotspot level:** low + +Reason: + +- very small, narrow responsibility + +#### `pages/api/middleware` + +- **Hotspot level:** medium-high + +Reason: + +- tiny surface, but shared helper impact is wide and mistakes would propagate broadly + +### Confidence Assessment + +#### Representativeness of Slices A–D + +- **Confidence:** high confidence + +Why: + +1. folder-size validation shows the biggest unknown risk area was `endpoint/`, and that area has already been sampled repeatedly across earlier slices +2. `file/` outlier checks still fit the previously identified mixed storage/orchestration model +3. smaller folders (`email`, `documents`, `auth`, `admin`, `middleware`) did not reveal materially new route families or implementation styles +4. the outlier review found support variants and special cases, but not a new dominant architectural pattern + +Residual caution: + +- confidence is high at the **platform pattern** level, not at the full-route behavioral nuance level + +### Risks / Cautions + +1. **This remains a pattern-validation pass, not a full inventory** + - there may still be route-local nuances not captured here +2. **High confidence does not mean all individual routes are equivalent** + - some outlier handlers still have special transforms, signing rules, or side effects +3. **Folder-level conclusions should not be mistaken for migration recommendations** + - this slice validates representativeness, not reorganisation scope +4. **The biggest remaining maintenance risk is still ownership ambiguity** + - especially in `endpoint/` and `file/` + +### Validation performed + +Manual inspection and bounded searches only. + +Performed: + +- re-read required Slice E context files +- bounded folder-size count pass across top-level API folders +- targeted route-name searches inside `endpoint`, `file`, and `pages/api` to validate naming families and implementation indicators +- direct representative inspection of selected outlier-style routes, including: + - `pages/api/endpoint/getmandatoryfields_api.js` + - `pages/api/endpoint/createcrmtask_api.js` + - `pages/api/file/createcaseinvolvement_api.js` + - `pages/api/file/generateappealpdf.js` + - `pages/api/email/getdocuments.js` + - `pages/api/admin/getStatusCountsByAppealAndLPA_api.js` +- reuse of conclusions and evidence from Slices A–D for comparison + +Not performed: + +- no full route inventory +- no repo-wide automated analysis +- no Python +- no scripts +- no runtime code changes +- no lint/tests, because this remains documentation-only assessment work + +### Architectural Conclusion + +The existing API platform model is now considered **representative of the wider API surface at the pattern level**. + +Current architectural conclusion: + +- the wider API surface does **not** appear to require a materially different platform model from Slices A–D +- the earlier findings generalise well: + - `endpoint/` is the dominant historical CRM relay catch-all + - `file/` is the main secondary mixed storage/orchestration area + - route families largely collapse into a limited contract vocabulary + - implementation styles largely collapse into helper-oriented, direct-wrapper, and orchestration styles + - the primary maintainability issue remains **findability and ownership**, not discovery of a fundamentally different API architecture + +### Recommendation + +Next bounded assessment step only: + +#### Slice F — API Platform Final Synthesis + +Goal: + +- consolidate Slices A–E into one concise architecture-level API platform view +- state the stable route family model, contract model, implementation-style model, drift model, and maintainer guidance baseline +- remain assessment-only + +--- + +## Slice F — API Platform Final Synthesis + +### Programme status + +```text +Portal Integration Contract & API Platform Assessment +Status: COMPLETE +``` + +Closure is supported by the existing evidence because: + +- Slices A–E established a stable route-family model +- the wider validation pass reached high confidence at the platform pattern level +- no materially different API platform family or implementation model emerged during bounded wider-surface validation + +### Executive Summary + +The PEDW API platform is large in route count but smaller in underlying structure than the file count first suggests. + +At a platform level it can be understood as: + +```text +~120 route files + ↓ +small route-family vocabulary + ↓ +small contract-shape vocabulary + ↓ +small implementation-style vocabulary +``` + +The API platform therefore feels large primarily because: + +- route files are numerous +- route naming has grown historically +- folder responsibility has drifted unevenly +- feature ownership is not always obvious from folder or file name alone +- older and newer implementation styles coexist + +The main maintenance problem is not discovery of a fundamentally different API architecture. + +The main maintenance problem is: + +- **findability** +- **ownership clarity** +- **consistency and reuse discipline** + +### Final API Platform Model + +Stable architecture-level model: + +- the dominant platform family is still **CRM relay-backed portal APIs** +- the main secondary platform family is **storage/blob plus draft/finalisation orchestration** +- notification, auth, documents, admin, and middleware form smaller supporting families +- route count overstates structural diversity because many routes are variants of repeated contract shapes +- the platform is mixed but explainable once grouped by route family, contract shape, and implementation style + +### Route-Family Model + +The stable route-family model is: + +#### 1. CRM relay routes + +- mainly under `pages/api/endpoint` +- includes search, myportal reads, account/profile reads, lookups, and CRM mutations + +#### 2. Storage/blob routes + +- mainly under `pages/api/file` +- includes blob listing, upload, delete, progress, and draft file handling + +#### 3. Finalisation/orchestration routes + +- concentrated in `pages/api/file`, with supporting CRM mutation routes in `endpoint` +- includes draft-to-submission transitions, completion-message flows, PDF generation, and related side effects + +#### 4. Email/notification routes + +- mainly under `pages/api/email` +- includes thin Notify send routes and broader aggregation/orchestration routes + +#### 5. Document download routes + +- mainly under `pages/api/documents` +- narrow published-document download proxy behavior + +#### 6. Auth/session routes + +- mainly under `pages/api/auth` +- includes NextAuth/session behavior and locale-aware auth support + +#### 7. Admin/internal routes + +- mainly under `pages/api/admin` +- grouped CRM reporting/read transforms for internal/admin use + +#### 8. Middleware/helper routes + +- under `pages/api/middleware` +- response helpers, relay forwarding, multipart support, retry/policy helpers + +#### 9. Local utility/meta routes + +- top-level API utility files and a few support-style routes +- health, docs/static/meta support, and local/config data reads + +### Contract-Shape Model + +The stable contract-shape model is: + +#### CRM-facing shapes + +- Public CRM read +- User-owned CRM read +- CRM create +- CRM update/patch +- CRM delete +- Proxy/pass-through +- Lookup/config/support +- Hybrid upsert/orchestration + +#### Storage/finalisation/notification shapes + +- Storage read/write/delete +- Queue/finalisation +- Notify send / notification orchestration + +Assessment note: + +- these shapes are sufficient to explain most of the observed API surface +- many apparent route differences are variants of one of these shapes rather than separate architectural categories + +### Implementation-Style Model + +The stable implementation-style model consists of three main styles. + +#### 1. Newer helper-oriented + +Typical indicators: + +- `relayGet(...)` +- `relayGetData(...)` +- `respondSuccess(...)` +- `respondError(...)` +- relay policy presets + +Typical fit: + +- CRM read routes +- proxy/read routes +- grouped read/transform routes + +#### 2. Older direct-wrapper + +Typical indicators: + +- `getToken()` +- direct `axios(config)` +- manual `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)` + +Typical fit: + +- CRM create/update/delete wrappers +- some file-side CRM mutation helpers + +Assessment note: + +- these patterns are not “wrong”; they reflect prior delivery needs and stable historical route construction +- the main maintainability question is whether they should be copied forward into new work when shared helpers now exist + +#### 3. Orchestration-heavy + +Typical fit: + +- finalisation routes +- email aggregation routes +- storage + queue + CRM side-effect routes +- PDF generation and file-adjacent journey transitions + +Assessment note: + +- these routes are structurally important because they cross integration boundaries and often sit at contract-critical workflow transitions + +### Folder Drift Model + +Folder drift affects maintainability because folder name and actual responsibility do not always align. + +#### Low drift + +- `documents` +- `admin` +- `middleware` +- top-level utility/meta + +These are relatively easy to find and reason about. + +#### Low / moderate drift + +- `auth` + +The folder is still coherent, but auth behavior crosses into CRM and Notify concerns. + +#### Moderate drift + +- `email` + +The folder name suggests email-only work, but some routes perform CRM/document/event aggregation before notification send. + +#### High drift + +- `endpoint` +- `file` + +Why this matters: + +- `endpoint` behaves as a historical general-purpose CRM relay catch-all +- `file` now contains blob/storage behavior, draft support, finalisation, involvement creation, PDF generation, and CRM-adjacent orchestration +- this drift increases scan cost and weakens ownership clarity + +### Maintenance Hotspots + +#### High + +- `pages/api/endpoint` +- `pages/api/file` + +Why: + +- highest route concentration +- mixed responsibilities +- mixed implementation styles +- many contract-critical routes +- ownership ambiguity is strongest here + +#### Medium-high + +- `pages/api/auth` +- `pages/api/middleware` + +Why: + +- small surfaces but high leverage and high sensitivity +- mistakes there can have broad downstream effects + +#### Medium + +- `pages/api/email` + +Why: + +- route count is small, but orchestration overlap makes behavior less obvious than the folder name suggests + +#### Lower + +- `pages/api/documents` +- `pages/api/admin` + +Why: + +- narrower and more coherent responsibilities +- still important, but less structurally confusing than `endpoint` and `file` + +### Maintainer Guidance Baseline + +For future API work, the stable baseline should be: + +1. **Identify the owning feature or journey first** + - do not start from route name alone if the flow is contract-critical +2. **Identify the integration touched** + - CRM relay, Azure Storage, Azure Queue/finalisation, Notify, NextAuth, or local-only support +3. **Identify contract-criticality before editing** + - public search, myportal, file handling, document delivery, auth/session, and finalisation need higher caution +4. **Prefer existing shared helpers where suitable** + - especially for CRM read routes and standardized response handling +5. **Prefer `respondSuccess(...)` / `respondError(...)` for new JSON responses** +6. **Prefer helper-oriented relay reads for new CRM reads where appropriate** +7. **Reuse storage helpers and established hash validation patterns for storage APIs** +8. **Keep Notify routes thin unless orchestration is genuinely required** +9. **Do not blindly copy older direct-wrapper patterns into new work** +10. **Do not refactor stable legacy routes without explicit approval and characterization** + +### What Is Proven / Not Proven + +#### Proven + +- Slices A–E are representative at the API platform pattern level +- route count overstates true structural diversity +- the main maintenance issue is findability and ownership +- a small number of repeated contract and implementation patterns explain most routes + +#### Not Proven + +- no full route-by-route inventory was produced +- no consolidation safety assessment has been performed +- no implementation readiness has been approved +- no route movement or removal is recommended yet + +### Recommendation + +The Portal Integration Contract & API Platform Assessment should now be considered **complete**. + +Recommended future planning stream only: + +- **API Route Map / Maintainer Guide** + +or + +- **API Rationalisation Planning** + +But only as a future planning stream. + +No implementation work is recommended from this assessment alone. diff --git a/context/portal-api-security-boundary-assessment.md b/context/portal-api-security-boundary-assessment.md new file mode 100644 index 00000000..4e2b6e1b --- /dev/null +++ b/context/portal-api-security-boundary-assessment.md @@ -0,0 +1,4088 @@ +# Portal API Security & Access Boundary Assessment + +## Executive summary + +This first pass reviews `pages/api/**` with a priority focus on `pages/api/file/**`, `pages/api/email/**`, `pages/api/auth/**`, document retrieval, and account / my-portal CRM endpoints. + +Primary conclusion: + +- The API surface is large (`121` JS/TS files under `pages/api`). +- Authentication is **not consistently enforced at route level** across sensitive handlers. +- The shared `pages/api/middleware/middleware.js` middleware is **multipart parsing only** and does not perform session or ownership checks. +- The only explicit session gate found in the reviewed API surface is `pages/api/endpoint/gethash_api.js`, which uses `getSession({ req })` before issuing hashes for a narrow allowlist of sensitive downstream routes. +- Many sensitive file/blob and CRM write/read routes appear to rely on: + - a signed path hash, + - caller-supplied identifiers such as `contactId`, `loggedInUserId`, `incidentId`, `watchedCaseID`, `container`, `casefolderID`, `blobname`, + - and indirect client trust, + rather than proving in-handler that the authenticated portal user owns the referenced object. + +This creates a notable architectural distinction: + +- **Session boundary exists** in NextAuth and in `gethash_api` +- **Access boundary enforcement is fragmented** and often not visible in the endpoint itself +- **Ownership enforcement is not consistently self-evident** in user-owned CRM and blob/file routes + +## Scope reviewed + +Reviewed context documents: + +- `context/architecture.md` +- `context/integration-map.md` +- `GUARDRAILS.md` (repository root; no `context/GUARDRAILS.md` file exists in this workspace) +- `context/runbook.md` +- `memory-bank/debt-list.md` +- `memory-bank/open-questions.md` +- `memory-bank/change-log.md` + +Reviewed API surface: + +- `pages/api/**` +- route inventory via directory listing and command-line count +- targeted code review of high-risk handlers in: + - `pages/api/auth/**` + - `pages/api/file/**` + - `pages/api/email/**` + - `pages/api/documents/**` + - `pages/api/endpoint/*_api.js` + - `pages/api/middleware/**` + +High-signal files reviewed directly: + +- `pages/api/auth/[...nextauth].js` +- `pages/api/endpoint/gethash_api.js` +- `pages/api/middleware/middleware.js` +- `pages/api/file/downloadblob.js` +- `pages/api/file/getbloblist.js` +- `pages/api/file/upload.js` +- `pages/api/file/uploadsinglefile.js` +- `pages/api/file/generateappealpdf.js` +- `pages/api/file/createcaseinvolvement_api.js` +- `pages/api/file/createrepinvolvement_api.js` +- `pages/api/email/notify.js` +- `pages/api/email/getall.js` +- `pages/api/documents/download/[id].js` +- `pages/api/endpoint/getmycases_api.js` +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/getportallogin_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/endpoint/updateaccount_api.js` +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/deletemyrepresentations_api.js` + +## Endpoint inventory + +### Surface size by top-level API area + +| Area | Approx. file count | Primary role | +| ------------------------------------- | -----------------: | ----------------------------------- | +| `endpoint/` | 74 | CRM relay/read-write endpoints | +| `file/` | 26 | blob/file/document/PDF/upload flows | +| `email/` | 6 | notification and mailing workflows | +| `admin/` | 5 | administrative/internal reporting | +| `middleware/` | 4 | relay and multipart helpers | +| `auth/` | 2 | auth/session and locale resolution | +| `documents/` | 1 | document download proxy | +| other (`health`, `notices`, `doc.ts`) | 3 | utility/meta | + +### Endpoint family classification table + +| Endpoint family | Examples | Classification | Auth visible in route? | Ownership visible in route? | Risk | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------- | +| Auth/session | `auth/[...nextauth].js`, `auth/resolve-locale.js` | auth/session | Yes in NextAuth route; no obvious session gate in locale resolver | N/A | High | +| Hash minting | `endpoint/gethash_api.js` | auth/session, relay support | **Yes** (`getSession`) | No object ownership check; only path allowlist | High | +| Public/read search & case discovery | `endpoint/getbasicsearch*_api.js`, `getadvancedsearch*_api.js`, `getdns*`, `getlinkedcases_api.js`, `getappealtypes_api.js` | public read | Usually no | Usually N/A / relies on publish filters | Low-Medium | +| Public/document metadata | `getsearchdocumentdetails*_api.js`, `getsearchdocumenthistory*_api.js`, `getsearchdocumentTypes_api.js`, `getappealpdfdocuments_api.js` | public read / document access | Usually no | Limited visibility; mostly incident/document keyed | Medium-High | +| Direct document retrieval | `documents/download/[id].js` | document access | No visible session gate | No visible ownership check; trusts `id` + `hash` | High | +| Portal login/account lookup by email | `getportallogin_api.js`, `getemailaccountcheck_api.js`, `getpreferredlanguage_api.js`, `getaccounts_api.js` | authenticated/read-adjacent or auth-support lookup | `getportallogin_api` uses hash, others vary; session not visible | No | Medium-High | +| User-owned account/profile read | `getpersonalaccount_api.js` | authenticated user-owned read | No visible session gate | No visible ownership verification; trusts `contactid` | High | +| User-owned account/profile write | `updateaccount_api.js`, `updatepassword_api.js`, `createaccount_api.js` | authenticated write | No visible session gate | No visible ownership verification; trusts `contactId` | High-Very High | +| User-owned case/representation reads | `getmycases_api.js`, `getawaitingsubmission_api.js`, `getmyrepresentations_api.js`, `getwatchedcases_api.js` | authenticated user-owned read | No visible session gate | Query filters use caller-supplied `loggedInUserId`; no proof of session-to-contact binding in route | High | +| User-owned delete/write actions | `deletewatchedcases_api.js`, `deletemyrepresentations_api.js`, `createwatchedcases_api.js`, `createcase_api.js`, `patchcase_api.js`, `updatecase_api.js` | authenticated write | No visible session gate in sampled handlers | No visible ownership verification; trusts IDs from query/body | Very High | +| Blob/file read/list/download | `file/getbloblist.js`, `getprogressobjblob.js`, `downloadblob.js`, `getawaitingsubmissionfromblob.js`, `getrepsblob.js` | file/blob access | No visible session gate | No visible ownership check; relies on signed hash + container/path identifiers | Very High | +| Blob/file delete/upload | `file/upload.js`, `uploadsinglefile.js`, `deleteblob*.js`, `setupcontainer.js` | authenticated write / file/blob access | No visible session gate | No visible ownership verification; relies on signed hash + identifiers | Very High | +| Generated PDFs / completion files | `file/generatepdf.js`, `generateappealpdf.js`, `createappealcompletemessage_api.js`, `createrepcompletemessage_api.js` | file/blob access, write side effects | No visible session gate | No visible ownership verification; signed hash only | Very High | +| Involvement creation | `file/createcaseinvolvement_api.js`, `file/createrepinvolvement_api.js` | authenticated write | No visible session gate | No visible ownership verification; trusts `contactid` + `incidentid` | Very High | +| Email/notification send and batch | `email/notify.js`, `email/getall.js`, `email/getevents.js`, `email/getdocuments.js` | email/notification | No visible session gate in sampled routes | No per-user ownership check; some are batch/operational | High-Very High | +| Administrative/internal | `admin/get*` | administrative/internal | Not assessed deeply in pass 1 | Unknown | High (pending) | +| Health/meta | `health.js`, `doc.ts`, `notices/index.js` | public/internal utility | Likely none | N/A | Low | + +## Risk classification table + +| Risk level | Endpoint families | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Low | `health.js`, likely `doc.ts`, static/meta utilities | +| Medium | public search/read families where only published/public data is expected | +| High | auth/session, public document metadata, account lookup by email, user-owned reads, direct document download, notification APIs | +| Very High | blob/file access, generated PDF/file routes, user-owned writes/deletes, involvement creation, account mutation routes | + +## Access-control observations + +### 1. Route-level session enforcement is sparse + +Evidence reviewed: + +- `pages/api/auth/[...nextauth].js` is the NextAuth boundary. +- `pages/api/endpoint/gethash_api.js` calls `getSession({ req })` and returns `401` when unauthenticated. +- Search across `pages/api/**/*.js` found no broad pattern of `getSession`, `getServerSession`, or server-side NextAuth enforcement outside that narrow area. + +Assessment: + +- Authentication appears to be enforced primarily at the application/session layer and selectively through hash minting, **not consistently at each sensitive endpoint**. + +### 2. Shared API middleware is not a security boundary + +`pages/api/middleware/middleware.js`: + +- parses multipart form data with `multiparty` +- sets `req.body` and `req.files` +- performs **no authentication, authorization, ownership, CSRF, or session resolution** + +Assessment: + +- Any route using this middleware gains parsing convenience, **not** access-control protection. + +### 3. Sensitive routes often trust caller-supplied identifiers + +Observed trusted identifiers include: + +- `loggedInUserId` +- `contactid` / `contactId` +- `watchedCaseID` +- `myRepresentationsID` +- `incidentid` / `incidentId` +- `emailAddress` +- `container` / `containerID` +- `casefolderID` +- `blobname` +- `tempcaseref` + +In sampled handlers, these values are usually: + +- validated for presence / basic shape +- interpolated into CRM OData queries, CRM write URLs, or Azure blob operations +- **not cross-checked against the authenticated session in the route itself** + +### 4. File/blob routes rely heavily on signed path hashes + +Evidence: + +- `file/downloadblob.js` +- `file/getbloblist.js` +- `file/upload.js` +- `file/uploadsinglefile.js` +- `file/generateappealpdf.js` +- `documents/download/[id].js` (document hash passed through to relay) + +Assessment: + +- Signed hashes are an important trust control. +- However, a valid hash is **not equivalent to ownership proof**. +- Current visible model suggests: + - authenticated user -> `gethash_api` -> signed path -> sensitive route + - but downstream route often does not re-check session or resolve the portal user to the referenced CRM/blob object + +Architecturally, that is a **capability-token style boundary**, not a clearly enforced per-route authorization boundary. + +### 5. Session-to-contact resolution is not consistently visible in the endpoint layer + +What was found: + +- `gethash_api` checks for a session, but does not resolve ownership of the target object. +- Many portal-user routes use `loggedInUserId` or `contactId` directly from client input. +- No common visible pattern in sampled routes for: + - reading `session.user.email` + - resolving CRM contact server-side from session + - comparing resolved contact to query/body identifiers + +Assessment: + +- This is the central access-boundary visibility gap for the current architecture stream. + +## Ownership enforcement concerns + +### Caller-supplied portal identity values + +#### `loggedInUserId` + +Observed in: + +- `getmycases_api.js` +- `getawaitingsubmission_api.js` +- `getmyrepresentations_api.js` +- `getwatchedcases_api.js` +- proxy variants + +Concern: + +- Routes filter CRM data using the supplied contact ID. +- No in-route proof was found that `loggedInUserId` belongs to the current authenticated session. + +Risk: + +- Potential insecure direct object reference if caller can obtain or guess another valid contact ID and reach the route. + +#### `contactid` / `contactId` + +Observed in: + +- `createcase_api.js` +- `updateaccount_api.js` +- `updatepassword_api.js` +- `getpersonalaccount_api.js` +- `createcaseinvolvement_api.js` +- `createrepinvolvement_api.js` + +Concern: + +- Account read/update and case/involvement creation use direct client-supplied contact identifiers. +- Sampled routes do not visibly prove session ownership of that contact. + +Risk: + +- Cross-account read/update or unauthorized involvement linkage if route is callable with substituted IDs. + +### Caller-supplied record IDs + +#### `watchedCaseID`, `myRepresentationsID` + +Observed in: + +- `deletewatchedcases_api.js` +- `deletemyrepresentations_api.js` + +Concern: + +- Delete routes operate directly on a provided CRM record ID. +- Sampled routes do not first fetch-and-verify ownership against current portal user. + +Risk: + +- Unauthorized deletion risk if identifiers are exposed or enumerable enough through user flows. + +#### `incidentid`, `caseId`, related case/document keys + +Observed in many `endpoint/*_api.js` and document metadata routes. + +Concern: + +- Public/public-adjacent read routes often key by incident ID. +- In user-owned or side-effect routes, ownership and publication posture are not consistently visible in-handler. + +### Blob/storage identifiers + +#### `container`, `containerID`, `casefolderID`, `blobname`, `tempcaseref` + +Observed in many `pages/api/file/**` routes. + +Concern: + +- Storage actions are scoped by caller-supplied path components. +- Signed hash protects path integrity but does not itself prove that the current portal user owns that container/blob/case folder. + +Risk: + +- High-value document/blob exposure or mutation risk if hash issuance or reuse is broader than intended. + +### Email addresses + +Observed in: + +- `getportallogin_api.js` +- `getpreferredlanguage_api.js` +- `getemailaccountcheck_api.js` +- `email/notify.js` + +Concern: + +- Some routes reveal or act on account existence / preference state using email address input. +- Authentication and anti-enumeration posture are not consistently visible in sampled handlers. + +## Highest-risk endpoint categories + +### 1. `pages/api/file/**` + +Why highest risk: + +- document/blob retrieval and mutation +- upload/write side effects +- generated PDFs and completion artifacts +- reliance on hash + path parameters rather than visible route-level ownership proof + +Examples reviewed: + +- `downloadblob.js` +- `getbloblist.js` +- `upload.js` +- `uploadsinglefile.js` +- `generateappealpdf.js` +- `createcaseinvolvement_api.js` +- `createrepinvolvement_api.js` + +### 2. `pages/api/email/**` + +Why high risk: + +- sends user-facing external notifications +- touches contact data and preference data +- includes batch mail generation and unsubscribe links +- no visible route-level auth in sampled handlers + +Examples reviewed: + +- `notify.js` +- `getall.js` + +### 3. `pages/api/auth/**` + +Why high risk: + +- session/authentication boundary +- locale-sensitive redirect behavior +- identity and email sign-in flows + +Observation: + +- NextAuth route is clearly security-sensitive and better structured than most non-auth handlers, but it is not itself an ownership enforcement layer for downstream CRM/blob operations. + +### 4. User-owned CRM routes in `pages/api/endpoint/*_api.js` + +Why high risk: + +- often operate on contact-owned data +- commonly trust `loggedInUserId` / `contactId` from the request +- write/delete routes show no sampled ownership verification step + +Examples reviewed: + +- `getmycases_api.js` +- `getwatchedcases_api.js` +- `getpersonalaccount_api.js` +- `createcase_api.js` +- `updateaccount_api.js` +- `deletewatchedcases_api.js` +- `deletemyrepresentations_api.js` + +### 5. Document retrieval endpoints + +Why high risk: + +- direct access to published or semi-sensitive documents +- document IDs and hashes act as capability inputs +- logging includes document identifiers and filenames in places + +Examples reviewed: + +- `documents/download/[id].js` +- search document detail/history families + +## Unknowns / evidence gaps + +This pass is intentionally endpoint-focused. The following remain unresolved and should be tested or traced in the next pass: + +1. **How hashes are actually minted in the client flows** + - `gethash_api` is authenticated, but we have not yet mapped every client consumer and whether hashes can be replayed, shared, or over-broadened. +2. **Whether some ownership checks are enforced outside the route** + - for example in page-level flow logic, hidden upstream middleware, relay-side controls, or CRM-side permissions. +3. **Administrative route exposure model** + - `pages/api/admin/**` needs a dedicated pass. +4. **Whether document download hashes are scoped narrowly enough to prevent cross-user use** + - especially `documents/download/[id].js` and blob/file routes. +5. **How much route access is implicitly protected by UI/session flow only** + - which is weaker evidence than explicit server-side authorization. +6. **Logging/redaction consistency in sensitive handlers** + - some sampled routes still log identifiers or payload-adjacent context. + +## Ownership enforcement trace + +This second pass narrows scope from broad endpoint inventory to one question only: + +**Where is ownership enforcement actually performed?** + +Important scope clarification for this pass: + +- relay hash = request integrity / portal-to-relay trust +- relay hash != user authentication +- relay hash != object ownership authorization +- relay upstream authentication to CRM is treated as infrastructure and is out of scope + +### Session -> email -> portal user -> CRM contact flow + +The dominant portal-owned identity chain is: + +```text +NextAuth session +-> session.user.email +-> getPortalLogin(email) +-> CRM contactid +-> portal/account/my-cases/my-watched-cases CRM queries +``` + +Observed evidence: + +- `pages/myportal/index.js` + - gets NextAuth session with `getSession(ctx)` + - calls `getPortalLogin(thisSession.user.email)` + - extracts `contacts[0]?.contactid` as `loggedInUser` + - then calls `getPersonalAccount(loggedInUser)`, `getMyCases(loggedInUser)`, `getWatchedCases(loggedInUser)` +- `lib/representation/pageLoaders.js` + - `loadRepresentationBootstrap()` resolves `thisSession.user.email` + - calls `getPortalLogin(...)` + - extracts `loggedInUser = ...contactid` + - then calls `getPersonalAccount(loggedInUser)` +- `pages/myportal/case/[ticketnumber].js` + - resolves session -> `getPortalLogin(email)` -> CRM `contactid` + - stores `setLoggedInUserId(loggedInUser)` +- `lib/auth/resolveMyPortalAuthContext.js` + - confirms session presence and `session.user.id` + `session.user.email` + - also requires `pinsUser` cookie + - returns all three identity values together: + - `sessionUserId` + - `sessionUserEmail` + - `pinsUser` + +### Split identity model + +The current architecture does not use one single identity key consistently. + +Instead it uses a split model: + +#### A. CRM ownership identity + +Used for CRM-owned user data and many portal lists: + +- `session.user.email` +- resolved to CRM contact via `getPortalLogin(email)` +- resulting `contactid` passed into CRM-facing APIs as: + - `loggedInUserId` + - `contactid` + - `contactId` + +#### B. Blob/storage ownership identity + +Used for draft/blob/container-scoped data: + +- `session.user.id` +- used as container identifier in SSR/page loaders and store state +- examples: + - `setContainerID(thisSession.user.id)` in `pages/myportal/index.js` + - `getRepsFromBlob(thisSession.user.id)` + - `getAwaitingSubmissionFromBlob(thisSession.user.id)` + - `getProgressFromBlob(loggedInUserIdent, query.id)` in `lib/newappeal/loadNewAppealPage.js` + - `getFilesFromBlob(loggedInUserIdent, query.casereference)` in `lib/myportal/loadMyPortalAppealPage.js` + +#### C. Cookie-based CRM identity shortcut + +Some SSR loaders use the `pinsUser` cookie directly as CRM contact identity rather than resolving it fresh from session email. + +Observed in: + +- `lib/newappeal/loadNewAppealPage.js` + - `loggedInUser = cookies?.pinsUser` + - then `getPersonalAccount(loggedInUser)` +- `lib/myportal/loadMyPortalAppealPage.js` + - `loggedInUser = cookies?.pinsUser` + - then `getPersonalAccount(loggedInUser)` +- `lib/auth/resolveMyPortalAuthContext.js` + - treats missing `pinsUser` as auth-context failure + +### Contact resolution location map + +| File | Responsibility | Resolution mechanism | Server-side or client-side | +| ------------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------- | -------------------------- | +| `pages/myportal/index.js` | my-portal dashboard SSR bootstrap | `getSession(ctx)` -> `getPortalLogin(session.user.email)` -> CRM `contactid` | server-side | +| `lib/representation/pageLoaders.js` | representation SSR bootstrap | `getSession(ctx)` -> `getPortalLogin(session.user.email)` -> CRM `contactid` | server-side | +| `pages/myportal/case/[ticketnumber].js` | my-portal case detail SSR bootstrap | `getSession(ctx)` -> `getPortalLogin(session.user.email)` -> CRM `contactid` | server-side | +| `lib/newappeal/loadNewAppealPage.js` | new-appeal SSR bootstrap | `getSession(ctx)` + `pinsUser` cookie already treated as CRM contact | server-side | +| `lib/myportal/loadMyPortalAppealPage.js` | resume-draft appeal SSR bootstrap | `getSession(ctx)` + `pinsUser` cookie already treated as CRM contact | server-side | +| `actions/services/accountDirectService.js` | account service access | `getPortalLogin(email)` calls signed API lookup returning CRM contact | shared helper | + +Assessment: + +- There **is** a shared resolution pattern, but it is only partially centralized. +- The dominant server-side CRM-contact resolution mechanism is `getPortalLogin(session.user.email)`. +- Some flows instead rely on the pre-existing `pinsUser` cookie as the CRM contact source. + +## Ownership enforcement map + +### Location 1: SSR / page loaders + +This is the most visible ownership-establishment layer. + +Observed responsibilities: + +- ensure a NextAuth session exists +- derive either: + - CRM contact ID from `session.user.email`, or + - container ID from `session.user.id` +- hydrate Redux/store and page props using those identifiers + +Mechanism: + +- **explicit session gating** +- **explicit portal identity derivation** +- **ownership then passed downstream as identifiers to service helpers / APIs** + +Files with this behavior: + +- `pages/myportal/index.js` +- `pages/myportal/case/[ticketnumber].js` +- `lib/representation/pageLoaders.js` +- `lib/newappeal/loadNewAppealPage.js` +- `lib/myportal/loadMyPortalAppealPage.js` +- `lib/auth/resolveMyPortalAuthContext.js` + +Assessment: + +- ownership is often **established here**, before API requests are made +- but it is not always **re-validated later** in downstream API handlers + +### Location 2: Client/service helper query construction + +Files: + +- `actions/services/portalDirectService.js` +- `actions/services/accountDirectService.js` +- `actions/services/documentDirectService.js` + +Responsibility: + +- package caller-supplied identifiers into API requests + +Mechanism: + +- `loggedInUserId` is inserted into `/api/endpoint/getmycases_api`, `getmyrepresentations_api`, `getwatchedcases_api`, `getawaitingsubmission_api` +- `contactId` / `contactid` inserted into account or involvement requests +- `session.user.id`-derived container values inserted into blob routes + +Assessment: + +- these helpers do **not** enforce ownership +- they propagate identity/ownership assumptions established earlier +- therefore ownership at this layer is **advisory/pass-through**, not authoritative + +### Location 3: API route query construction + +Files: + +- `pages/api/endpoint/getmycases_api.js` +- `pages/api/endpoint/getmyrepresentations_api.js` +- `pages/api/endpoint/getwatchedcases_api.js` +- `pages/api/endpoint/getawaitingsubmission_api.js` +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/createwatchedcases_api.js` +- `pages/api/endpoint/updateaccount_api.js` + +Mechanism: + +- ownership is represented mainly as CRM filtering or CRM target record selection +- examples: + - `_customerid_value eq loggedInUserId` + - `_pinswg_contact_value eq loggedInUserId` + - `contacts(contactid)` + - `pinswg_watchlists(watchedCaseID)` + +Assessment: + +- CRM query construction is a **distributed enforcement location** in the sense that ownership is expressed in the query or record target +- but many handlers still trust the incoming identifier rather than resolving it from session themselves +- so enforcement is only as strong as the provenance of that identifier + +### Location 4: CRM-side filtering / record existence checks + +Observed in: + +- `pages/api/endpoint/createwatchedcases_api.js` + +Mechanism: + +- checks whether a watchlist already exists for a given `(incidentId, contactId)` pair using: + - `pinswg_WatchedCase/incidentid eq incidentId` + - `pinswg_Contact/contactid eq contactId` + +Assessment: + +- this is not an independent ownership proof +- it is a CRM uniqueness/association lookup based on caller-supplied values + +### Location 5: blob/container scoping + +Observed in: + +- `lib/newappeal/loadNewAppealPage.js` +- `lib/myportal/loadMyPortalAppealPage.js` +- `lib/representation/pageLoaders.js` +- `pages/myportal/index.js` +- `actions/services/documentDirectService.js` + +Mechanism: + +- draft/blob ownership is implicitly scoped to `session.user.id` +- that value is used as: + - container name + - container lookup key + - draft file/progress retrieval key + +Assessment: + +- for draft/blob flows, the actual ownership boundary appears to be: + - **knowledge/use of the correct session user container identity** +- this is a different mechanism from CRM contact ownership +- in reviewed code, this is mostly established in SSR and then passed through to services/routes + +### Location 6: nowhere visible in route + +In many sampled sensitive routes, there is no visible route-local check that: + +- resolves current session +- derives the CRM contact server-side +- compares resolved contact to incoming `loggedInUserId` / `contactId` +- verifies that record IDs belong to the resolved contact + +This is especially true in sampled handlers for: + +- `getmycases_api.js` +- `getmyrepresentations_api.js` +- `getwatchedcases_api.js` +- `getpersonalaccount_api.js` +- `updateaccount_api.js` +- `deletewatchedcases_api.js` +- `deletemyrepresentations_api.js` + +## High-risk flow traces + +### My Cases + +```text +User +-> NextAuth session +-> SSR loader resolves session.user.email +-> getPortalLogin(email) +-> CRM contactid +-> getMyCases(contactid) +-> /api/endpoint/getmycases_api?loggedInUserId=contactid +-> CRM query filters _customerid_value eq loggedInUserId +``` + +Where ownership is established: + +- SSR/page loader (`pages/myportal/index.js`, `pages/myportal/case/[ticketnumber].js`) + +Where ownership is enforced: + +- implicitly in CRM query filter + +Whether explicit or implicit: + +- **partially explicit** in SSR +- **implicit / trust-based** in API route + +### My Representations + +There are two parallel ownership models: + +#### CRM/contact model + +```text +User +-> session.user.email +-> getPortalLogin(email) +-> CRM contactid +-> getMyRepresentations(contactid) +-> /api/endpoint/getmyrepresentations_api?loggedInUserId=contactid +-> CRM filter _pinswg_contact_value eq loggedInUserId +``` + +#### Blob draft model + +```text +User +-> session.user.id +-> getRepsFromBlob(session.user.id) +-> blob container scoped by session.user.id +``` + +Where ownership is established: + +- SSR bootstrap in `lib/representation/pageLoaders.js` + +Where ownership is enforced: + +- CRM filter for submitted/contact-owned representation records +- blob container identity for draft/representation blob state + +Whether explicit or implicit: + +- **distributed split model** + +### Watched Cases + +```text +User +-> session.user.email +-> getPortalLogin(email) +-> CRM contactid +-> getWatchedCases(contactid) +-> /api/endpoint/getwatchedcases_api?loggedInUserId=contactid +-> CRM filter _pinswg_contact_value eq loggedInUserId +``` + +Create/update flow: + +```text +Client creates pinswg_WatchedCase@odata.bind + pinswg_Contact@odata.bind +-> /api/endpoint/createwatchedcases_api +-> route extracts incidentId/contactId from request body +-> checks recordExists(incidentId, contactId) +-> upserts watchlist +``` + +Where ownership is established: + +- SSR resolution of contact ID for reads +- client/request body for create/update + +Where ownership is enforced: + +- read path: CRM filter by contact ID +- create path: association uniqueness lookup only + +Whether explicit or implicit: + +- read path: **implicit via CRM filter** +- create path: **largely client-trusting** + +### Draft Appeals + +```text +User +-> NextAuth session +-> session.user.id +-> getProgressFromBlob(session.user.id, draftId) +-> getFilesFromBlob(session.user.id, caseReference) +``` + +Where ownership is established: + +- SSR loader (`lib/newappeal/loadNewAppealPage.js`, `lib/myportal/loadMyPortalAppealPage.js`) + +Where ownership is enforced: + +- by using `session.user.id` as blob container identity + +Whether explicit or implicit: + +- **explicit at SSR identity selection** +- **implicit at blob access layer** + +### Draft Representations + +```text +User +-> NextAuth session +-> session.user.id +-> getRepsFromBlob(session.user.id) +-> representation blobs listed from session-scoped container +-> optional rep file retrieval with container + rep paths +``` + +Where ownership is established: + +- SSR representation bootstrap + +Where ownership is enforced: + +- blob container scoping on `session.user.id` + +Whether explicit or implicit: + +- **implicit storage ownership model** + +### Document Retrieval + +Two distinct models exist: + +#### Published/search document retrieval + +```text +User +-> portal page/search result +-> document hash link / document reference +-> /api/documents/download/[id] +-> relay document fetch +``` + +Observed ownership behavior: + +- no user-specific ownership enforcement was visible in reviewed route +- appears to behave as document-reference + hash based access to a publishable document path + +#### User blob retrieval + +```text +User +-> session.user.id or caller-provided container/casefolder/blobname +-> /api/file/getbloblist or /api/file/downloadblob +-> blob route fetch +``` + +Observed ownership behavior: + +- ownership is inferred from correct container/path identity +- not visibly re-proven against current session in the sampled route itself + +## Client-supplied identifier review + +| Identifier | Main locations | Role in architecture | Classification | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `loggedInUserId` | `portalDirectService`, `getmycases_api`, `getmyrepresentations_api`, `getwatchedcases_api`, `getawaitingsubmission_api` | CRM contact selector for user-owned reads | **Advisory at request boundary; authoritative only if derived server-side beforehand; validated for presence, not session-bound in route** | +| `contactId` / `contactid` | account routes, involvement routes, create case, create watched cases | CRM contact target / association key | **Advisory to API route; presence-validated; not independently ownership-validated in sampled routes** | +| `userId` | mainly `session.user.id` in SSR/store/blob flows | blob/container ownership identity | **Authoritative when sourced from session server-side; becomes advisory once passed onward** | +| `incidentId` / `incidentid` / `caseId` | watched cases, involvements, search/case details | CRM case/incident target | **Validated for presence/format in places; not generally ownership-validated in sampled handlers** | +| `representationId` / `myRepresentationsID` | delete representation routes | target record ID | **Unvalidated ownership; presence-validated only** | +| `watchedCaseID` | delete watched case route | target record ID | **Unvalidated ownership; presence-validated only** | +| `blobname` | file download/delete routes | blob object selector | **Path-level validated / hash-protected in some routes; not visibly user-ownership validated** | +| `casefolderID` | draft/blob list/progress/download routes | draft case folder selector | **Advisory; often combined with container identity; not visibly session-compared in route** | +| `container` / `containerID` | blob routes, draft flows | storage container identity | **Authoritative when taken from `session.user.id` in SSR; otherwise advisory at route boundary** | + +## Evidence-backed architectural conclusion + +### 1. What mechanism prevents User A accessing User B's data? + +There is no single visible centralized mechanism. + +Instead, prevention appears to rely on a combination of: + +- SSR/session guards that require a valid NextAuth session +- server-side resolution of `session.user.email` to CRM `contactid` in some page loaders +- use of that CRM `contactid` in downstream CRM filters for user-owned lists +- use of `session.user.id` as the blob container identity for draft/blob flows +- pre-existing `pinsUser` cookie in some SSR flows as a CRM-contact shortcut + +For many routes, what prevents cross-user access is therefore: + +- **correct upstream derivation and propagation of the right identifier**, +- not an independently visible authorization check inside each API handler. + +### 2. Is ownership enforcement centralized, partially centralized, distributed, or unclear? + +**Partially centralized at SSR/auth-context bootstrap, but overall distributed.** + +More precisely: + +- session establishment is centralized in NextAuth +- session-to-contact resolution pattern exists and is repeated in several SSR loaders +- ownership enforcement itself is **distributed** across: + - SSR/page loaders + - cookies + - service helper parameter passing + - CRM query filters + - blob container naming conventions + +It is not fully centralized in: + +- one middleware +- one API guard +- one reusable authorization helper +- one API boundary layer + +### 3. What is the actual authorization boundary? + +The actual authorization boundary appears to be: + +#### For CRM-owned portal data + +```text +NextAuth session +-> session.user.email +-> CRM contact lookup +-> CRM query filtered by that contact +``` + +#### For draft/blob-owned portal data + +```text +NextAuth session +-> session.user.id +-> blob container identity +-> blob/file operations scoped to that container +``` + +Therefore the current practical authorization boundary is not simply `pages/api/**`. + +It is a **cross-layer boundary** spanning: + +- NextAuth session +- SSR/page-loader identity derivation +- cookie/session-carried identifiers +- downstream CRM query scoping +- downstream blob container scoping + +That means ownership enforcement is real in some flows, but it is often **implicit, propagated, and distributed**, rather than visibly re-proven at the API route boundary itself. + +## pinsUser lifecycle + +### What `pinsUser` contains + +Evidence indicates `pinsUser` stores a CRM contact identifier, not a session ID and not an email address. + +Observed writes: + +- `pages/index.js` + - writes `setCookie(null, "pinsUser", loggedInUserId.value[0].contactid, { path: "/" })` + - source value comes from `getPortalLogin(thisSession.user.email)` result +- `pages/account/personaldetails.js` + - writes `setCookie(null, "pinsUser", props.accountDetails.loggedinUserId, { path: "/" })` +- `components/search/searchresults.js` + - writes `setCookie(null, "pinsUser", props.accountDetails.loggedinUserId, { path: "/" })` +- `components/search/addresssearchresults.js` + - also writes `pinsUser` from `props.accountDetails.loggedinUserId` + +Assessment: + +- cookie content = **CRM contact id** +- source of truth at creation time = **CRM contact lookup result or account details already holding CRM contact id** + +### Creation path + +The clearest canonical creation path is: + +```text +NextAuth session +-> session.user.email +-> getPortalLogin(email) +-> CRM contactid +-> setCookie("pinsUser", contactid) +``` + +Primary evidence: + +- `pages/index.js` + - on successful signed-in home flow: + - `portalUserObj = await getPortalLogin(thisSession.user.email)` + - if `portalUserObj.value` is not empty: + - writes `pinsUser = portalUserObj.value[0].contactid` + - redirects to `/myportal` + - if no CRM contact exists: + - redirects to `/account/register` + +This means the homepage/login landing flow explicitly creates `pinsUser` only after CRM contact existence has been established. + +### Update / refresh path + +Observed refresh/update behavior is lightweight and mostly overwrite-based. + +Evidence: + +- `pages/account/personaldetails.js` + - rewrites `pinsUser` from `props.accountDetails.loggedinUserId` +- search result components also rewrite the cookie from Redux-held account details + +Assessment: + +- `pinsUser` is refreshed by simply overwriting the cookie with the CRM contact id already held in page state +- there is no distinct refresh protocol or expiry/verification flow visible in reviewed files + +### Deletion / clear path + +Observed in: + +- `lib/auth/sessionClient.js` + - `clearSessionArtifacts()` destroys `pinsUser` +- `pages/error.js` + - destroys `pinsUser` on go-home/sign-out action +- `pages/_error.js` + - destroys `pinsUser` on go-home/sign-out action + +Assessment: + +- `pinsUser` is treated as a session-adjacent artifact and cleared on sign-out/error-reset paths + +### Validation path + +What is visible: + +- `lib/auth/resolveMyPortalAuthContext.js` + - checks whether `ctx.req.cookies.pinsUser` exists + - treats absence as auth-context failure +- SSR loaders such as: + - `lib/newappeal/loadNewAppealPage.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - read `cookies.pinsUser` and proceed to account lookups + +What is not visible: + +- no independent server-side validation that cookie value matches `session.user.email` +- no signature or server-issued verification wrapper around `pinsUser` +- no explicit re-resolution of `pinsUser` from session in the same loader before use in those cookie-based flows + +Assessment: + +- `pinsUser` is usually **presence-checked**, but not strongly revalidated in the flows that consume it directly + +### Consumption path + +`pinsUser` is consumed in two main ways: + +#### A. SSR loaders + +- `lib/newappeal/loadNewAppealPage.js` + - uses `cookies.pinsUser` as `loggedInUser` + - calls `getPersonalAccount(loggedInUser)` +- `lib/myportal/loadMyPortalAppealPage.js` + - same pattern for resume-draft appeal flow +- `pages/myportal/advancedsearch.js` +- `pages/myportal/dnsapplications.js` +- `pages/newappeal/aboutyou.js` + - use cookie-derived contact identity in surrounding SSR flow + +#### B. Client/runtime flows + +- `components/search/searchresults.js` +- `components/search/dnssearchresults.js` +- `components/case/summary.js` +- `components/myportal/topthree.js` +- `components/myportal/topthree_reps.js` +- `components/myportal/viewall.js` + +These components read `parseCookies().pinsUser` and use it directly in watched-case / awaiting-submission / representation-related requests. + +## Identity comparison + +### Path A: session email -> CRM contact + +```text +session.user.email +-> getPortalLogin(email) +-> CRM contact +``` + +Properties: + +- server-side derivation from active session +- explicitly re-established in several SSR/dashboard flows +- aligned to the expected business boundary of authenticated portal user -> CRM contact + +### Path B: pinsUser + +```text +pinsUser +-> CRM contact id (cached cookie value) +``` + +Properties: + +- cookie-carried CRM contact identity +- created from Path A or from state already derived from Path A +- later reused directly in some flows without repeating Path A + +### Do they always resolve to the same CRM contact? + +They appear intended to resolve to the same contact, because `pinsUser` is initially set from CRM-contact results. + +However, based on visible code, divergence is possible in principle because: + +- some flows derive CRM contact fresh from `session.user.email` +- other flows trust existing `pinsUser` +- reviewed cookie-based flows do not always re-resolve cookie value from session email before use + +### Is divergence handled? + +What is handled: + +- missing `pinsUser` often causes redirect/auth-context failure +- missing CRM contact from `getPortalLogin(email)` often causes redirect or registration flow + +What is not clearly handled: + +- mismatch between: + - current session-derived CRM contact from `getPortalLogin(session.user.email)` + - existing `pinsUser` cookie value + +Assessment: + +- divergence handling is **not visibly explicit** in reviewed files +- the architecture appears to assume consistency rather than prove it everywhere + +## Flow dependency map + +### Flows depending primarily on CRM contact resolution + +| Flow | Dependency | +| ----------------------------- | ----------------------------------------------- | +| Homepage signed-in routing | `session.user.email -> getPortalLogin(email)` | +| Dashboard / my portal index | fresh CRM contact resolution from session email | +| My Cases | CRM contact resolution then CRM-filtered query | +| My Representations (CRM side) | CRM contact resolution then CRM-filtered query | +| Watched Cases (read side) | CRM contact resolution then CRM-filtered query | +| My portal case detail pages | CRM contact resolution in SSR loader | + +### Flows depending primarily on `pinsUser` + +| Flow | Dependency | +| -------------------------------------------------- | ------------------------------------------------------------ | +| New appeal SSR loader | `pinsUser` used as CRM contact for `getPersonalAccount(...)` | +| Resume appeal SSR loader | `pinsUser` used as CRM contact for account lookup | +| Some myportal SSR/search utility flows | `pinsUser` read directly from cookies | +| Watched-case client interactions in search/case UI | `parseCookies().pinsUser` used directly | +| Awaiting-submission / top-three client widgets | `pinsUser` used directly in some portal helper calls | + +### Flows depending on both paths + +Some flows are hybrid: + +- Dashboard/home establishes `pinsUser` from fresh CRM contact resolution +- later UI or SSR flows consume `pinsUser` directly + +This makes `pinsUser` operationally a bridge/cache between: + +```text +NextAuth session-derived CRM resolution +and +later cookie-based contact reuse +``` + +## Trust boundary review + +### Is `pinsUser` trusted directly? + +Yes, in several reviewed flows it is trusted directly once present. + +Evidence: + +- SSR loaders read `cookies.pinsUser` and use it as `loggedInUser` +- client components read `parseCookies().pinsUser` and pass it to portal-service helpers + +### Is `pinsUser` revalidated? + +Not consistently. + +Visible validation is mostly: + +- existence check +- redirect if missing + +Not visibly present in reviewed flows: + +- resolve session email -> CRM contact -> compare with cookie before use + +### Is `pinsUser` derived from session? + +Originally, yes. + +Primary creation flow in `pages/index.js` derives it from: + +- active session +- `getPortalLogin(session.user.email)` +- CRM contact result + +But later consumption is not always re-derived. + +### Is `pinsUser` treated as authoritative? + +Operationally, yes in some flows. + +Architecturally, it looks more like: + +- a cached CRM contact identity +- a convenience shortcut +- and likely a legacy compatibility mechanism for flows built around cookie-carried contact identity + +It does **not** look like the best evidence for canonical business identity, because fresh CRM-contact resolution from session email remains present and appears closer to the intended business boundary. + +## pinsUser architectural conclusion + +### 1. What is the canonical portal identity? + +The strongest canonical portal identity visible in the codebase is: + +```text +NextAuth session user +-> session.user.email +-> CRM contact lookup +``` + +This is the most direct path from authenticated user to business-owned portal identity. + +### 2. Is CRM Contact the true business authorization boundary? + +Yes, for CRM-owned dashboard/user data, CRM contact appears to be the true business authorization boundary. + +The visible business model is: + +```text +Authenticated NextAuth user +-> matching CRM contact +-> permitted dashboard / user-owned CRM views +``` + +### 3. What role does `pinsUser` play? + +`pinsUser` appears to be: + +- **A) a cached CRM Contact identity** +- **B) a convenience shortcut** +- **C) in some flows, a legacy compatibility mechanism** + +It does **not** appear to be the primary canonical ownership authority. + +### 4. Can `pinsUser` and CRM Contact diverge? + +Based on reviewed code, divergence appears possible because: + +- one path resolves CRM contact fresh from session email +- another path trusts cookie state already set earlier +- no consistent explicit cookie-vs-session reconciliation was found + +Whether divergence happens in practice is not proven here, but architectural protection against divergence is not strongly visible. + +### 5. Is ownership ultimately enforced through session -> CRM Contact, or through some alternative mechanism? + +For CRM-owned data, ownership is ultimately most convincingly enforced through: + +```text +Session +-> CRM Contact +-> CRM query scoping +``` + +For blob/draft-owned data, ownership is enforced through a second mechanism: + +```text +Session +-> session.user.id +-> blob container identity +``` + +`pinsUser` sits between those models as a reused CRM-contact cache, not as a separate business authorization system. + +## Registration and post-registration bootstrap trace + +This pass focuses on the transition from: + +```text +session exists +-> no CRM contact +-> registration required +-> CRM contact created +-> dashboard access becomes valid +``` + +## Registration entry map + +### Detection point: signed-in session but no CRM contact + +The clearest decision point is in `pages/index.js`. + +Observed flow: + +```text +getSession(ctx) +-> if session exists: + getPortalLogin(session.user.email) +-> if CRM contact exists: + set pinsUser and redirect to /myportal +-> if CRM contact does not exist: + redirect to /account/register +``` + +Evidence: + +- `pages/index.js` + - `thisSession = await getSession(ctx)` + - `portalUserObj = await getPortalLogin(thisSession.user.email)` + - client-side branch on returned `loggedInUserId.value` + - empty -> `router.replace({ pathname: "/account/register", query: { id: encodeURI(loggedInUserEmail) } })` + - non-empty -> set `pinsUser` and redirect to `/myportal` + +Assessment: + +- registration entry occurs after authentication succeeds but before dashboard access is granted +- the deciding condition is effectively: + +```text +session exists && getPortalLogin(session.user.email) returns no CRM contact +``` + +### Auth route alignment + +`pages/api/auth/[...nextauth].js` configures: + +- `newUser: /account/register` + +Assessment: + +- the auth layer is aware of registration as the next-step destination for new users +- however, the concrete dashboard-vs-register branching evidence reviewed here is strongest in `pages/index.js` + +## Registration flow map + +### Route/page entry + +File: + +- `pages/account/register.js` + +Behavior: + +- requires active NextAuth session in `getServerSideProps` +- if no session -> redirect to `/auth/signin` +- if session exists -> passes `loggedInUserEmail: thisSession.user.email` into page props + +Assessment: + +- registration page entry is gated by session +- session email is injected server-side into the page + +### Registration form state + +Files: + +- `components/account/registerform.js` +- `components/account/registerCheck.js` +- `components/account/registerComplete.js` + +Flow: + +```text +Register page +-> form entry (`registerform.js`) +-> confirmation screen (`registerCheck.js`) +-> completion/create effect (`registerComplete.js`) +``` + +### Form inputs and identity source + +Evidence from `components/account/registerform.js`: + +- `initialValues.emailaddress1 = props.loggedInUserEmail` +- email field is rendered as: + - `name="emailaddress1"` + - `disabled` + +Assessment: + +- registration uses the authenticated session email as the prefilled email +- the user does not appear able to edit it in the reviewed form implementation +- this strongly suggests registration email identity is intended to come from session, not free user input + +### Submitted data + +Data collected in form includes: + +- `firstname` +- `lastname` +- `emailaddress1` (session-derived, disabled in form) +- `telephone1` +- `company` +- address fields + +### API endpoint called + +Evidence from `components/account/registerComplete.js`: + +- loads body from Redux form state: + - `props.props.props.form.accountRegisterForm.values` +- assigns: + - `pinswg_typeofinvolvement: 846040061` +- calls: + - `getEmailAccountCheck(accountBody.emailaddress1)` + - if none exists -> `createAccount(accountBody)` + +API/service chain: + +```text +RegisterComplete +-> createAccount(accountBody) +-> actions/services/accountDirectService.createAccount(...) +-> /api/endpoint/createaccount_api +-> CRM contacts create +``` + +### CRM contact creation payload + +Evidence: + +- `components/account/registerComplete.js` + - mutates account body with `pinswg_typeofinvolvement = 846040061` + - strips `custom_password_check` +- `pages/api/endpoint/createaccount_api.js` + - takes `req.body` + - POSTs it directly to CRM `contacts` + +Assessment: + +- CRM contact creation payload is largely client-assembled form data +- the API route does not visibly enrich the payload with server-derived session identity +- instead it forwards the request body to CRM + +### Default involvement / role assignment + +Observed assignment: + +- `pinswg_typeofinvolvement = 846040061` + +Comment in code indicates this is not appellant and appears to be a default role path. + +## Post-registration bootstrap map + +### After submit + +`components/account/registerCheck.js`: + +- confirmation page sets: + - `setAccCr(false)` + - `setAccountCreatedComplete(true)` + +`components/account/registerComplete.js` then runs creation logic in `useEffect`. + +### Duplicate handling and contact creation + +`components/account/registerComplete.js`: + +- calls `getEmailAccountCheck(email)` +- if `@odata.count > 0` -> `setAccCr("exists")` +- else -> `createAccount(accountBody)` then `setAccCr("created")` + +Assessment: + +- duplicate detection is email-based +- handling is deterministic in the reviewed code path: + - existing email -> not created + - missing email -> create contact + +### How contact becomes active portal identity + +What is visible: + +- on successful creation, UI shows a created-state message and a link back to `/` +- there is **no immediate server-side bootstrap** in the reviewed registration components that: + - fetches the newly created contact ID + - sets `pinsUser` directly + - redirects immediately to `/myportal` + +Instead, the visible model is: + +```text +registration completes +-> user returns to / +-> homepage runs signed-in flow again +-> getPortalLogin(session.user.email) now expected to find CRM contact +-> pinsUser set +-> redirect to /myportal +``` + +Assessment: + +- dashboard access does not appear to be granted directly by the registration completion component itself +- it becomes valid after a subsequent homepage/bootstrap pass that re-runs CRM contact lookup + +### Is `getPortalLogin(session.user.email)` re-run? + +Indirectly, yes. + +Evidence: + +- `RegisterComplete` success state links back to `/` +- `pages/index.js` re-runs `getPortalLogin(thisSession.user.email)` on signed-in load + +### Is `pinsUser` set immediately? + +Not visibly in the reviewed registration flow. + +The visible setting point remains the homepage signed-in flow in `pages/index.js`. + +## Authorization boundary observations + +### Does registration bind created CRM contact to authenticated session identity? + +Partially, but mostly through UI flow and prefilled/disabled form state rather than route-local server-side enforcement. + +What supports binding: + +- registration route requires session +- page props inject `thisSession.user.email` +- form initial value for `emailaddress1` comes from session email +- email field is disabled in reviewed UI + +What weakens binding visibility: + +- `createaccount_api.js` accepts arbitrary request body and forwards it to CRM +- no visible API-route check that `req.body.emailaddress1 === session.user.email` +- no visible server-side session resolution inside `createaccount_api.js` + +Assessment: + +- authoritative identity during registration appears intended to be `session.user.email` +- but the final server-side binding is not strongly enforced in the sampled API route itself + +### Can submitted email differ from `session.user.email`? + +In the reviewed UI flow, it appears not meant to: + +- `emailaddress1` is prefilled from session +- field is disabled in the form + +However, at the API boundary, this is not visibly enforced server-side. + +### Is duplicate CRM contact handling safe and deterministic? + +Observed logic is deterministic but email-based: + +```text +getEmailAccountCheck(email) +-> if existing contact count > 0: exists +-> else create contact +``` + +This appears operationally deterministic in the reviewed flow, but is still dependent on caller/body email matching session-derived email. + +## Identity transition map + +```text +Anonymous +-> public portal browsing + +Authenticated session exists +-> pages/index.js signed-in path +-> getPortalLogin(session.user.email) + +If CRM contact missing +-> redirect to /account/register + +Registration page +-> session-gated entry +-> session email passed into props +-> disabled email field prefilled +-> form confirmation +-> createAccount API call +-> CRM contact created + +Post-registration +-> user returns to / +-> pages/index.js re-runs getPortalLogin(session.user.email) +-> CRM contact now found +-> pinsUser set +-> redirect to /myportal + +Dashboard access granted +``` + +## Registration boundary conclusion + +### 1. Where does the system transition from session-only to CRM-contact-backed portal user? + +The transition becomes operationally real in two stages: + +1. **Detection stage** at `pages/index.js` + - session exists + - CRM contact lookup is attempted + - absence triggers registration +2. **Activation stage** after registration, when homepage/bootstrap is revisited and `getPortalLogin(session.user.email)` can now resolve a CRM contact + +So the effective session-only -> CRM-contact-backed transition completes when homepage/bootstrap re-runs contact lookup successfully after registration. + +### 2. What identity is authoritative during registration? + +The intended authoritative identity is: + +```text +session.user.email +``` + +because: + +- session is required +- email is injected from session +- form email is prefilled and disabled + +But this authority is enforced more clearly in UI/bootstrap flow than in the create-account API boundary itself. + +### 3. When does dashboard access become valid? + +Dashboard access becomes valid after a CRM contact exists and a subsequent signed-in bootstrap resolves that contact successfully. + +In reviewed code, that practical access point is: + +```text +return to / +-> pages/index.js +-> getPortalLogin(session.user.email) +-> contact found +-> pinsUser set +-> redirect to /myportal +``` + +### 4. Does registration strengthen or weaken the Session -> CRM Contact boundary? + +It strengthens the business model conceptually by creating the missing CRM contact needed for dashboard authorization. + +But from an implementation-visibility perspective, the boundary remains only partially enforced server-side because: + +- registration UI is session-bound and email-prefilled from session +- yet `createaccount_api.js` does not visibly prove that created contact email is bound to current session email before forwarding to CRM + +## Recommended next assessment pass + +Next step only: + +**Trace account and profile mutation flows (`personaldetails`, password, account updates) to determine whether post-registration CRM-contact-backed users continue to rely on session-derived identity, `pinsUser`, or caller-supplied contact IDs when mutating their own account data.** + +## Account and profile mutation trace + +This pass focuses on CRM-contact-backed account/profile read and mutation flows. + +Question under review: + +```text +What actually protects account/profile read and mutation? + +session.user.email -> CRM contact? +pinsUser? +caller-supplied contactId/contactid/loggedInUserId? +``` + +Scope reviewed directly: + +- `pages/account/personaldetails.js` +- `pages/account/changepassword.js` +- `components/account/personaldetails.js` +- `components/account/personaldetailsCheck.js` +- `components/account/personaldetailsComplete.js` +- `components/account/changepassword.js` +- `components/myportal/youraccount.js` +- `pages/index.js` +- `pages/myportal/index.js` +- `actions/services/accountDirectService.js` +- `pages/api/endpoint/getpersonalaccount_api.js` +- `pages/api/endpoint/updateaccount_api.js` +- `pages/api/endpoint/updatepassword_api.js` +- `pages/api/endpoint/getemailaccountcheck_api.js` +- `pages/api/endpoint/getportallogin_api.js` +- `store/accountDetails/action.js` +- `store/accountDetails/reducer.js` + +## Account/profile entry map + +### User entry point + +The visible account entry point is the dashboard “Your details” card. + +Evidence: + +- `components/myportal/youraccount.js` + - links to `/account/personaldetails` +- the password card/link is commented out in the same component + +Assessment: + +- live user-facing account navigation visibly exposes personal-details update +- change-password route still exists, but its primary dashboard entry is commented out + +### Session requirement on account pages + +`pages/account/personaldetails.js`: + +- calls `useSession()` +- while loading -> renders `NoSessionWarning` +- when unauthenticated -> `router.push("/auth/signin")` + +`pages/account/changepassword.js`: + +- same `useSession()` pattern +- unauthenticated users are redirected to `/auth/signin` + +Assessment: + +- account/profile pages are session-gated at page/UI level +- these pages do not perform their own server-side `getServerSideProps` CRM-contact resolution + +### How account identity is established for page use + +The live account page does not freshly resolve CRM identity from session email. + +Instead: + +- `pages/myportal/index.js` + - server-side: `getSession(ctx)` + - resolves `getPortalLogin(thisSession.user.email)` + - extracts `contacts[0]?.contactid` as `loggedInUser` + - calls `getPersonalAccount(loggedInUser)` + - dispatches: + - `setAccountDetails(accountDetails)` + - `setLoggedInUserId(loggedInUser)` +- `pages/account/personaldetails.js` + - consumes `props.accountDetails.loggedinUserId` + - rewrites `pinsUser` cookie from that value + +Assessment: + +- dashboard SSR bootstrap is where session email is freshly resolved to CRM contact +- account page then relies on Redux-held `loggedinUserId` / `accountDetails` +- `pinsUser` is rewritten from Redux state, not freshly derived from the current session on this page + +### Missing CRM contact behavior + +Fresh missing-contact behavior is most visible in signed-in bootstrap routes, not the account page itself. + +Evidence: + +- `pages/index.js` + - signed-in flow calls `getPortalLogin(session.user.email)` + - if no CRM contact result -> redirect to `/account/register` +- `pages/myportal/index.js` + - if CRM contact lookup fails or yields no contact ID -> redirect to `/auth/signin` + +Assessment: + +- missing CRM contact is handled before normal account-page entry, during signed-in bootstrap +- `pages/account/personaldetails.js` itself does not freshly detect “no CRM contact, redirect to register”; it assumes account state already exists + +## Account read path + +### End-to-end read flow + +Observed flow: + +```text +Session +-> session.user.email +-> getPortalLogin(session.user.email) [dashboard/bootstrap SSR] +-> CRM contactid +-> setLoggedInUserId(loggedInUser) +-> getPersonalAccount(loggedInUser) +-> /api/endpoint/getpersonalaccount_api?contactid=... +-> CRM contacts(contactid) +-> Redux accountDetails +-> /account/personaldetails form initialValues +``` + +### Source of contact ID + +Primary source in the reviewed live flow: + +- `pages/myportal/index.js` + - `getPortalLogin(thisSession.user.email)` + - `contacts[0]?.contactid` + +Secondary carried state: + +- `store/accountDetails.reducers` + - `loggedinUserId` stored in Redux +- `pages/account/personaldetails.js` + - reads `props.accountDetails.loggedinUserId` +- `pinsUser` + - rewritten from `loggedinUserId` on account page load + +Assessment: + +- read path originates from `getPortalLogin(session.user.email)` in bootstrap +- once hydrated, account page itself relies on Redux/carried contact ID rather than resolving again from session + +### API boundary behavior for account read + +`actions/services/accountDirectService.js`: + +- `getPersonalAccount(contactid)` calls: + - `/api/endpoint/getpersonalaccount_api?contactid=${contactid}` + +`pages/api/endpoint/getpersonalaccount_api.js`: + +- requires only query `contactid` +- constructs CRM query: + +```text +contacts(contactid)?$select=... +``` + +- does not call `getSession` +- does not call `getPortalLogin` +- does not compare `contactid` to session-derived CRM contact + +Assessment: + +- account read route trusts caller-supplied `contactid` +- ownership is not re-checked server-side in the route + +## Account update path + +### UI submission flow + +Observed UI flow: + +```text +Profile form +-> redux-form personalDetailsForm values +-> confirmation screen +-> PersonalDetailsComplete useEffect +-> updateAccount(loggedinUserId, formValues) +-> /api/endpoint/updateaccount_api?contactId=... +-> CRM contacts(contactId) PATCH +``` + +Evidence: + +- `components/account/personaldetails.js` + - form fields are populated from `state.accountDetails.accountDetails` + - submit only sets `personalDetailsComplete=true` +- `components/account/personaldetailsCheck.js` + - confirmation step shows current form values + - continue sets `setAccountUpdatedComplete(true)` +- `components/account/personaldetailsComplete.js` + - reads `loggedinUserId` from Redux account state + - reads `formValues` from `personalDetailsForm` + - calls `updateAccount(loggedinUserId, formValues)` in `useEffect` + +### Submitted payload contents + +Visible editable fields include: + +- `pinswg_preferredlanguage` +- `firstname` +- `lastname` +- `telephone1` +- `pinswg_companyname` +- address fields + +Visible email behavior: + +- `emailaddress1` field is present in the form +- rendered with `disabled` + +Assessment: + +- live profile UI submits the full form object, but the email field is not editable in the reviewed page implementation +- account updates are targeted by Redux-held `loggedinUserId` + +### API boundary behavior for account update + +`actions/services/accountDirectService.js`: + +- `updateAccount(contactId, updateBody)` calls: + +```text +/api/endpoint/updateaccount_api?contactId=${contactId} +``` + +`pages/api/endpoint/updateaccount_api.js`: + +- requires query `contactId` +- requires non-empty body +- patches CRM target: + +```text +contacts(contactId) +``` + +- does not resolve current session +- does not resolve CRM contact from `session.user.email` +- does not compare submitted/target `contactId` against session-derived contact + +Assessment: + +- account update route trusts caller-supplied `contactId` +- there is no visible route-local session binding or ownership check + +## Password / auth-account update path + +### Live UI path + +Observed flow: + +- `pages/account/changepassword.js` exists and is session-gated via `useSession()` +- `components/account/changepassword.js` + - reads `props.props.accountDetails.loggedinUserId` + - reads `newpassword` from Redux form state + - calls `updatePassword(contactid, updateBody)` + +But in `actions/services/accountDirectService.js`: + +- `updatePassword(contactId, newpassword)` actually calls: + +```text +/api/endpoint/updateaccount_api?contactId=${contactId} +body = { pinswg_custom_password: newpassword } +``` + +Assessment: + +- live password UI updates the CRM contact through `updateaccount_api`, not `updatepassword_api` + +### Standalone `updatepassword_api` route + +`pages/api/endpoint/updatepassword_api.js`: + +- exists +- accepts query `contactId` +- PATCHes `contacts(contactId)` with supplied body +- does not resolve session or compare to session-derived CRM contact + +Search evidence: + +- no active reviewed UI/service path calls `updatepassword_api` +- the main dashboard password entry point is commented out in `components/myportal/youraccount.js` + +Assessment: + +- `updatepassword_api` appears exposed but not used by the reviewed live UI path +- it is likely legacy or inconsistent with the active passwordless NextAuth model + +### Relationship to NextAuth identity + +Observed auth model elsewhere in assessment: + +- active sign-in uses NextAuth passwordless email login + +Observed account password behavior here: + +- password change writes `pinswg_custom_password` on the CRM contact +- `pages/api/endpoint/getlogin_api.js` still queries `pinswg_custom_password` + +Assessment: + +- CRM password fields/routes appear legacy relative to the passwordless NextAuth identity model +- reviewed password mutation does not appear to update SQL/Prisma/NextAuth identity +- reviewed password mutation appears CRM-contact-only + +## Email identity observations + +### Is email editable in account/profile UI? + +In the reviewed live personal-details form, no. + +Evidence: + +- `components/account/personaldetails.js` + - `Field name="emailaddress1" ... disabled` + +### Does API permit email mutation? + +Visible route behavior suggests yes in principle. + +Reason: + +- `updateaccount_api.js` forwards the provided body to CRM without allowlisting fields +- there is no route-local block on `emailaddress1` + +Assessment: + +- reviewed live UI does not expose editable email change +- reviewed API boundary does not visibly prevent caller-supplied email mutation if `emailaddress1` were posted directly + +### Can CRM email diverge from NextAuth session email? + +Based on visible code, yes in principle. + +Reasoning: + +- canonical contact resolution uses `getPortalLogin(session.user.email)` +- CRM contact read/update targets use `contactId` directly +- `updateaccount_api` does not reconcile submitted body with `session.user.email` +- no reviewed route updates NextAuth/SQL email identity from CRM email changes + +Potential divergence model: + +```text +NextAuth session.user.email remains A +CRM contact emailaddress1 changed to B +future getPortalLogin(A) may no longer find the same CRM contact +``` + +Whether this occurs in the current live UI is limited by the disabled email field, but the API boundary does not visibly guard against it. + +### Is there visible reconciliation between SQL/NextAuth email and CRM email? + +No explicit reconciliation was found in the reviewed account mutation path. + +Assessment: + +- the strongest alignment mechanism is initial bootstrap via `getPortalLogin(session.user.email)` +- no visible post-mutation reconciliation path was found in the account/profile APIs reviewed here + +## Authorization boundary observations + +### 1. Is account/profile mutation bound to authenticated session? + +At page-entry level: **yes**. + +At API route boundary: **not visibly**. + +Reason: + +- account pages require a session in the UI +- but `updateaccount_api.js` and `getpersonalaccount_api.js` do not resolve or verify session server-side + +### 2. Is mutation bound to the CRM contact resolved from session email? + +Indirectly in the normal bootstrap flow: **yes**. + +At the mutation route itself: **not visibly**. + +Reason: + +- normal dashboard bootstrap derives `loggedinUserId` from `getPortalLogin(session.user.email)` +- mutation route then trusts the already-supplied `contactId` + +### 3. Is mutation bound to `pinsUser`? + +Not primarily in the reviewed live personal-details flow. + +Reason: + +- mutation target comes from Redux `loggedinUserId` +- `pinsUser` is rewritten from that state on page load +- reviewed profile update component does not source the target ID from `pinsUser` + +Assessment: + +- `pinsUser` is adjacent/cached state here, not the primary authoritative mutation input + +### 4. Is mutation bound to caller-supplied contact ID? + +At API route level: **yes**. + +Evidence: + +- `getpersonalaccount_api.js` trusts `req.query.contactid` +- `updateaccount_api.js` trusts `req.query.contactId` +- `updatepassword_api.js` trusts `req.query.contactId` + +### 5. Can User A attempt to mutate User B's CRM contact if they know or can supply another contact ID? + +Based on reviewed route code, **the route-local protection is not visibly preventing this**. + +Evidence-backed statement only: + +- the sampled account mutation/read routes do not compare caller-supplied `contactId/contactid` to a session-derived CRM contact +- therefore the server-side route boundary appears caller-ID-trusting + +Whether upstream controls elsewhere prevent exploitation was not proven in this pass. + +## Account mutation boundary conclusion + +### Account entry map + +```text +User reaches /myportal +-> pages/myportal/index.js requires NextAuth session +-> getPortalLogin(session.user.email) +-> CRM contactid +-> getPersonalAccount(contactid) +-> Redux accountDetails + loggedinUserId hydrated +-> dashboard Your details link +-> /account/personaldetails +-> page checks useSession() +-> page uses Redux-held loggedinUserId/accountDetails +``` + +### Account read map + +```text +Session +-> session.user.email +-> getPortalLogin(session.user.email) [SSR bootstrap] +-> contactid +-> getPersonalAccount(contactid) +-> /api/endpoint/getpersonalaccount_api?contactid=... +-> CRM contacts(contactid) +-> Redux accountDetails +-> personal details form initialValues +``` + +### Account update map + +```text +Profile form +-> personalDetailsForm values +-> confirmation screen +-> updateAccount(loggedinUserId, formValues) +-> /api/endpoint/updateaccount_api?contactId=... +-> CRM contacts(contactId) PATCH +``` + +### Architectural answers + +#### 1. What identity protects account/profile mutation? + +In normal live flow, the practical identity chain is: + +```text +Session +-> session.user.email +-> getPortalLogin(session.user.email) +-> CRM contactid +-> Redux loggedinUserId +-> caller-supplied contactId to updateaccount_api +``` + +So protection is strongest at bootstrap/session-derivation time, not at the final API mutation boundary. + +#### 2. Does the account mutation path strengthen or weaken the Session -> CRM Contact authorization boundary? + +It **weakens** that boundary at the final route layer. + +Reason: + +- UI/bootstrap starts from session-derived CRM contact +- but `getpersonalaccount_api` and `updateaccount_api` do not re-bind target contact to session-derived identity server-side + +#### 3. Is `pinsUser` authoritative in account mutation flows? + +No, not in the reviewed personal-details mutation path. + +It is: + +- rewritten from Redux-held contact identity +- session-adjacent/cached +- not the primary target-ID source in the reviewed profile mutation components + +#### 4. Are any account/password update routes legacy or inconsistent with passwordless NextAuth? + +Yes. + +Evidence suggests: + +- `updatepassword_api.js` exists but is not used by the reviewed live UI path +- live change-password UI writes `pinswg_custom_password` via `updateaccount_api` +- CRM password fields/routes are inconsistent with the primary passwordless NextAuth model + +## Recommended next assessment pass + +Next step only: + +**Trace whether other user-owned CRM mutation routes (for example watchlist create/delete, case involvement creation, representation mutation, and case creation/update) follow the same caller-supplied contact-ID pattern without route-local session-to-contact rebinding.** + +## User-owned CRM mutation route trace + +This pass extends the account/profile finding to the wider user-owned CRM mutation surface. + +Question under review: + +```text +Is caller-supplied CRM identity trust isolated to account/profile routes, +or systemic across watchlists, case mutation, involvements, representations, +and completion/finalisation side effects? +``` + +Scope reviewed directly: + +- `pages/api/endpoint/createwatchedcases_api.js` +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/endpoint/updatecase_api.js` +- `pages/api/endpoint/patchcase_api.js` +- `pages/api/file/createcaseinvolvement_api.js` +- `pages/api/file/createrepinvolvement_api.js` +- `pages/api/endpoint/deletemyrepresentations_api.js` +- `pages/api/file/createappealcompletemessage_api.js` +- `pages/api/file/createrepcompletemessage_api.js` +- `pages/api/file/createcase_api.js` +- `pages/api/file/updatecase_api.js` +- `actions/services/portalDirectService.js` +- `actions/services/caseDirectService.js` +- `components/case/summary.js` +- `components/myportal/viewall.js` +- `components/newappeal/createCase.js` +- `components/newappeal/buildsection.js` +- `components/case/representation/representationComplete.js` +- `lib/newappeal/journeyEffects.js` + +## Mutation route inventory + +| Route | Purpose | Caller / helper | Input identifiers | CRM entity affected | Reads session in route? | Resolves CRM contact from session email? | Trusts caller-supplied contact / record IDs? | Ownership check before mutation? | Classification | +| --------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ----------------------- | ---------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------- | -------------- | +| `pages/api/endpoint/createwatchedcases_api.js` | Create or update watchlist entry / email notifications | `portalDirectService.createWatchedCases`; `components/case/summary.js`; `components/myportal/viewall.js`; search result components; representation completion | `pinswg_WatchedCase@odata.bind`, `pinswg_Contact@odata.bind`, `pinswg_appealcasetype`, optional `pinswg_emailnotifications`, `pinswg_representationsubmitted`, `pinswg_representationtype` | `pinswg_watchlists` | No | No | Yes | Only duplicate/existing record check for supplied `(incidentId, contactId)` pair | C / E | +| `pages/api/endpoint/deletewatchedcases_api.js` | Delete watched case entry | `portalDirectService.deleteWatchedCases`; `components/case/summary.js`; `components/myportal/viewall.js`; top-three widgets | `watchedCaseID` | `pinswg_watchlists` | No | No | Yes (record ID) | No visible ownership check | D | +| `pages/api/endpoint/createcase_api.js` | Create CRM case / appeal record | `caseDirectService.createNewCase`; SSR/UI new appeal flow conceptually | `contactid`, `appealTypeId`, `containername`, `lpaID`, body | `incidents` | No | No | Yes (`contactid`, `lpaID`) | No visible ownership check on contact binding | C | +| `pages/api/endpoint/updatecase_api.js` | Update CRM appeal-type-specific record | `caseDirectService.updateCase` via new appeal progress save flow | `appealObj`, `updateFormCollection`, body | `updateFormCollection(appealObj)` | No | No | Yes (`appealObj`, collection) | No visible record ownership check | D | +| `pages/api/endpoint/patchcase_api.js` | Patch CRM incident `servicestage` | `caseDirectService.patchCase` | `incidentid` | `incidents(incidentid)` | No | No | Yes (`incidentid`) | No visible ownership check | D | +| `pages/api/file/createcaseinvolvement_api.js` | Create case-contact involvement relationship | direct caller not prominent in active UI; intended portal helper family | `contactid`, `incidentid` | case/contact relationship ref | No | No | Yes | No visible ownership check; only CRM 412 exists handling | C | +| `pages/api/file/createrepinvolvement_api.js` | Create representation/contact involvement relationship | `portalDirectService.setRepInvolvment`; `components/case/representation/representationComplete.js` | `contactid`, `incidentid`, `involvement` | `pinswg_contactinvolvements` | No | No | Yes | Only duplicate existing involvement check for supplied `(contactid, incidentid, type)` | C / E | +| `pages/api/endpoint/deletemyrepresentations_api.js` | Delete representation record | `portalDirectService.deleteMyRepresentations` | `myRepresentationsID` | `pinswg_representationses` | No | No | Yes (record ID) | No visible ownership check | D | +| `pages/api/file/createappealcompletemessage_api.js` | Appeal finalisation side effects; blob finalisation; contact role update | `portalDirectService.sendCaseCompleteMessage`; `lib/newappeal/journeyEffects.js` | `container`, `tempcaseref`, `inv`, `hash` | blob state + `contacts(contactId)` via `updateAccount` | No | No | Yes (container/case-derived contact) | No visible session/contact rebinding before `updateAccount(contactId, ...)` | B / C | +| `pages/api/file/createrepcompletemessage_api.js` | Representation finalisation side effect message creation | `portalDirectService.sendRepCompleteMessage`; `components/case/representation/representationComplete.js` | `container`, `tempcaseref`, `repid`, `hash` | blob/message side effect | No | No | Yes (container / rep file identifiers) | No visible user ownership proof beyond signed path and caller inputs | B / F | +| `pages/api/file/createcase_api.js` | Blob-only draft case creation mirror | `caseDirectService.createNewCaseBlob`; `components/newappeal/createCase.js` | `contactid`, `appealTypeId`, `containername`, `lpaID`, body | blob draft payload, not active CRM write | No | No | Yes | No route-local ownership proof; blob-side draft helper only | C | +| `pages/api/file/updatecase_api.js` | Blob-side draft appeal-type record update mirror | `caseDirectService.updateCaseBlob` | `appealObj`, `incident`, `updateFormCollection`, body | blob-side / file route mutation helper path | No | No | Yes | No visible ownership check | D / F | + +## Watchlist mutation boundary + +### Add / upsert path + +Observed caller flow: + +```text +User +-> UI builds pinswg_WatchedCase@odata.bind + pinswg_Contact@odata.bind +-> createWatchedCases(updateBody) +-> /api/endpoint/createwatchedcases_api +-> extract incidentId + contactId from caller-supplied binds +-> recordExists(incidentId, contactId) +-> POST or PATCH pinswg_watchlists +``` + +Evidence: + +- `components/case/summary.js` + - `selectWatchedCase(loggedInUser, incidentID, appealType)` builds both binds directly + - `loggedInUser` is passed in from UI state / props +- `components/myportal/viewall.js` + - `selectEmailNotifications(...)` builds the same contact/case bind payload from `props.accountDetails.loggedinUserId` +- `pages/api/endpoint/createwatchedcases_api.js` + - extracts `incidentId` and `contactId` from body binds + - duplicate check uses supplied values only + - no session resolution + - no `getPortalLogin(session.user.email)` rebinding + +Assessment: + +- add/update watchlist flow is **contact-ID-bound but caller-supplied** +- route-local check is only: + - are the binds present? + - does a watchlist already exist for this supplied `(incidentId, contactId)` pair? +- no visible proof that the supplied contact belongs to the authenticated user + +### Delete path + +Observed flow: + +```text +User +-> deleteWatchedCases(watchedCaseID) +-> /api/endpoint/deletewatchedcases_api?watchedCaseID=... +-> CRM delete pinswg_watchlists(watchedCaseID) +``` + +Evidence: + +- `components/case/summary.js` and `components/myportal/viewall.js` + - delete uses `pinswg_watchlistid` +- `pages/api/endpoint/deletewatchedcases_api.js` + - requires only `watchedCaseID` + - deletes `pinswg_watchlists(watchedCaseID)` + - does not fetch the watchlist first to verify `_pinswg_contact_value` + +Assessment: + +- watchlist delete is **record-ID-bound with no visible ownership check** + +## Case / appeal mutation boundary + +### CRM case creation path + +Observed flow: + +```text +New appeal UI +-> createNewCase(appealTypeId, lpaID, contactid, createBody, containerName) +-> /api/endpoint/createcase_api?contactid=...&appealTypeId=...&containername=...&lpaID=... +-> CRM incidents POST +-> customerid_contact@odata.bind = /contacts(contactid) +``` + +Evidence: + +- `components/newappeal/createCase.js` + - passes `props.loggedInUser` as `contactid` into `createNewCaseBlob` +- `actions/services/caseDirectService.js` + - `createNewCase(...)` and `createNewCaseBlob(...)` take `contactid` as direct parameter +- `pages/api/endpoint/createcase_api.js` + - trusts `req.query.contactid` + - binds `customerid_contact@odata.bind` using that value + - no session read or contact verification + +Assessment: + +- CRM case creation is **contact-ID-bound but caller-supplied** +- contact ownership is assumed from upstream UI/bootstrap flow, not re-proven in-route + +### Case / appeal update path + +Observed flow: + +```text +Appeal progress save +-> updateCaseProgress(...) +-> updateBody includes pinswg_Appellant@odata.bind = /contacts(loggedinUserId) +-> caseDirectService.updateCase(...) +-> getAppealID(caseReference, updateFormCollection, primaryAttribute) +-> /api/endpoint/updatecase_api?updateFormCollection=...&appealObj=... +-> CRM PATCH updateFormCollection(appealObj) +``` + +Evidence: + +- `components/newappeal/buildsection.js` + - builds `updateBody` with `pinswg_Appellant@odata.bind` from `legacyAccountDetails.loggedinUserId` + - uses `caseReference` / `incidentid` carried in page state +- `actions/services/caseDirectService.js` + - `updateCase(...)` resolves `appealObj` by `caseReference` + - submits `appealObj` and `updateFormCollection` +- `pages/api/endpoint/updatecase_api.js` + - trusts `appealObj` and `updateFormCollection` + - no session resolution + - no ownership verification against contact or incident + +Assessment: + +- case update is **session-bound upstream only**, but **record-ID-bound at the mutation route** +- mutation route trusts supplied record/collection target + +### Case patch path + +Observed flow: + +```text +patchCase(incidentid) +-> /api/endpoint/patchcase_api?incidentid=... +-> CRM PATCH incidents(incidentid) { servicestage: 0 } +``` + +Evidence: + +- `actions/services/caseDirectService.js` exposes `patchCase(incidentid)` +- `pages/api/endpoint/patchcase_api.js` + - accepts only `incidentid` + - directly patches `incidents(incidentid)` + - no ownership check + +Assessment: + +- case patch is **record-ID-bound with no visible ownership check** + +## Involvement creation boundary + +### Case involvement creation + +Observed flow: + +```text +contactid + incidentid +-> /api/file/createcaseinvolvement_api +-> CRM incidents(incidentid)/.../$ref +-> @odata.id points to contacts(contactid) +``` + +Evidence: + +- `pages/api/file/createcaseinvolvement_api.js` + - requires body `contactid` and `incidentid` + - creates relationship ref directly from supplied values + - only special handling is CRM `412 -> { record: "exists" }` + +Assessment: + +- case involvement creation is **contact-ID-bound but caller-supplied** +- route proves only parameter presence and duplicate/existing involvement behavior via CRM response +- no visible authorization proof that caller may create involvement for that contact/case pair + +### Representation involvement creation + +Observed flow: + +```text +Representation completion UI +-> setRepInvolvment(caseid, contactid, involvement) +-> /api/file/createrepinvolvement_api +-> existing involvement check for supplied (contactid, incidentid, type) +-> create pinswg_contactinvolvements record with supplied contact/case binds +``` + +Evidence: + +- `components/case/representation/representationComplete.js` + - passes `props...accountDetails.accountDetails.contactid` + - case ID comes from `currentView.caseReference.incidentid` +- `actions/services/portalDirectService.js` + - `setRepInvolvment(caseid, contactid, involvement)` forwards those values directly +- `pages/api/file/createrepinvolvement_api.js` + - trusts `contactid`, `incidentid`, `involvement` + - uses `getPersonalAccount(contactid)` only to populate email/name fields, not to verify ownership + - only checks for existing involvement on the supplied pair/type + +Assessment: + +- representation involvement creation is also **contact-ID-bound but caller-supplied** +- duplicate prevention is not the same as authorization + +## Representation mutation boundary + +### Representation delete path + +Observed flow: + +```text +deleteMyRepresentations(myRepresentationsID) +-> /api/endpoint/deletemyrepresentations_api?myRepresentationsID=... +-> CRM delete pinswg_representationses(myRepresentationsID) +``` + +Evidence: + +- `actions/services/portalDirectService.js` + - `deleteMyRepresentations(myRepresentationsID)` forwards the record ID directly +- `pages/api/endpoint/deletemyrepresentations_api.js` + - accepts only `myRepresentationsID` + - deletes the target representation record directly + - does not resolve contact/session or verify ownership of the representation record first + +Assessment: + +- representation delete is **record-ID-bound with no visible ownership check** + +### Representation finalisation path + +Observed flow: + +```text +Representation complete page +-> setRepInvolvment(incidentid, contactid, involvement) +-> sendRepCompleteMessage(containerID, ticketnumber, repfile_name) +-> sendEmail(...) +-> createWatchedCases({ watched case bind, contact bind, representationsubmitted, representationtype }) +``` + +Evidence: + +- `components/case/representation/representationComplete.js` + - performs all of the above side effects in `useEffect` + - contact ID comes from account details state + - case ID comes from current representation/case state +- `pages/api/file/createrepcompletemessage_api.js` + - trusts `container`, `tempcaseref`, `repid`, `hash` + - does not resolve session or CRM contact + +Assessment: + +- representation finalisation is **session-bound upstream only** +- final side effects still rely on caller-supplied IDs / blob identifiers +- route-local user ownership proof is not visible + +## Awaiting submissions / draft completion observations + +### Appeal completion / finalisation path + +Observed flow: + +```text +sendCaseCompleteMessage(containerID, caseReference, typeOfInvolvement) +-> /api/file/createappealcompletemessage_api?container=...&tempcaseref=...&inv=...&hash=... +-> blob progress + case file loaded +-> contactId extracted from caseObj[customerid_contact@odata.bind] +-> updateAccount(contactId, { pinswg_typeofinvolvement: ... }) +-> createCaseCompleteMessage(...) +``` + +Evidence: + +- `lib/newappeal/journeyEffects.js` + - `sendCaseCompleteMessageEffect(...)` is a thin wrapper around portal service helper +- `pages/api/file/createappealcompletemessage_api.js` + - derives `contactId` from case blob content, not session + - calls `updateAccount(contactId, ...)` + - no session resolution or CRM-contact rebinding before updating contact role + +Assessment: + +- appeal finalisation is **session-bound upstream only** and then **caller/blob-identity-bound** +- it performs CRM contact mutation as a side effect without visible route-local session/contact verification + +### Blob draft helper variants + +`pages/api/file/createcase_api.js` and `pages/api/file/updatecase_api.js` appear to be blob-side draft helpers rather than the primary live CRM mutation boundary. + +Even so: + +- they also accept caller-supplied identifiers (`contactid`, `appealObj`, `incident`, `updateFormCollection`) +- they do not read session or verify ownership in-route + +## Pattern classification + +### A. session-bound at route + +- No reviewed mutation route in this pass provided clear evidence of route-local session binding + +### B. session-bound upstream only + +- `pages/api/file/createappealcompletemessage_api.js` +- `pages/api/file/createrepcompletemessage_api.js` +- practical case update flow leading to `updatecase_api.js` +- practical representation completion flow leading to watchlist/involvement/message side effects + +### C. contact-ID-bound but caller-supplied + +- `pages/api/endpoint/createwatchedcases_api.js` +- `pages/api/endpoint/createcase_api.js` +- `pages/api/file/createcaseinvolvement_api.js` +- `pages/api/file/createrepinvolvement_api.js` +- blob draft create helper `pages/api/file/createcase_api.js` + +### D. record-ID-bound with no visible ownership check + +- `pages/api/endpoint/deletewatchedcases_api.js` +- `pages/api/endpoint/deletemyrepresentations_api.js` +- `pages/api/endpoint/updatecase_api.js` +- `pages/api/endpoint/patchcase_api.js` +- blob draft update helper `pages/api/file/updatecase_api.js` + +### E. CRM-filter-bound + +- `createwatchedcases_api.js` duplicate/upsert detection for supplied `(incidentId, contactId)` +- `createrepinvolvement_api.js` duplicate detection for supplied `(contactid, incidentid, type)` + +Important note: + +- these are **not independent ownership proofs** +- they are filter/existence checks built from caller-supplied identifiers + +### F. unclear + +- `createrepcompletemessage_api.js` + - ownership of the blob-side representation identifiers is not re-proven in-route + - side effect is clear, but full user-ownership proof remains indirect + +## Mutation-family conclusion + +### Watchlist mutation map + +```text +User +-> UI builds watched case bind + contact bind +-> createWatchedCases(updateBody) +-> /api/endpoint/createwatchedcases_api +-> route extracts incidentId/contactId from body +-> duplicate check on supplied pair only +-> CRM watchlist create/patch + +User +-> deleteWatchedCases(watchedCaseID) +-> /api/endpoint/deletewatchedcases_api +-> CRM watchlist delete by record ID +``` + +### Case / appeal mutation map + +```text +New appeal +-> createNewCase(..., contactid, ...) +-> /api/endpoint/createcase_api?contactid=... +-> CRM incidents create with customerid_contact@odata.bind + +Appeal progress save +-> updateBody includes pinswg_Appellant@odata.bind from loggedinUserId +-> updateCase(...) +-> /api/endpoint/updatecase_api?appealObj=...&updateFormCollection=... +-> CRM patch target record by supplied record/collection identifiers + +Patch +-> patchCase(incidentid) +-> /api/endpoint/patchcase_api?incidentid=... +-> CRM patch incidents(incidentid) +``` + +### Involvement creation map + +```text +contactid + incidentid +-> createcaseinvolvement_api +-> CRM relationship ref create + +contactid + incidentid + involvement +-> createrepinvolvement_api +-> duplicate check on supplied values +-> CRM contact involvement create +``` + +### Representation mutation map + +```text +deleteMyRepresentations(myRepresentationsID) +-> /api/endpoint/deletemyrepresentations_api +-> CRM representation delete by record ID + +Representation completion +-> setRepInvolvment(incidentid, contactid, involvement) +-> sendRepCompleteMessage(container, caseRef, repid) +-> createWatchedCases(contact/case binds + rep submitted state) +-> CRM/blob side effects via supplied IDs +``` + +### Architectural answers + +#### 1. Is caller-supplied CRM identity trust isolated or systemic? + +It is **systemic across the reviewed user-owned CRM mutation families**. + +The same pattern appears in: + +- watchlist upsert +- case creation +- case involvement creation +- representation involvement creation +- account/profile mutation (previous pass) + +And a parallel caller-record-ID trust pattern appears in: + +- watchlist delete +- representation delete +- case update/patch + +#### 2. Which mutation families are strongest / weakest? + +Strongest visible family in this pass: + +- none of the reviewed mutation routes showed strong route-local session rebinding + +Relatively stronger upstream-only flows: + +- new appeal progress / completion flows where IDs are first derived in authenticated SSR/UI state + +Weakest route-local families: + +- delete routes keyed only by record ID (`deletewatchedcases_api`, `deletemyrepresentations_api`) +- update/patch routes keyed by supplied record identifiers (`updatecase_api`, `patchcase_api`) +- contact-binding mutation routes that trust supplied contact IDs (`createcase_api`, involvement routes) + +#### 3. Where is ownership actually enforced for mutation flows? + +Mostly **upstream in UI/bootstrap/state derivation**, not in the final route. + +The practical pattern is: + +```text +NextAuth session +-> session.user.email +-> CRM contact lookup (in bootstrap/page flow) +-> contactid stored in Redux/props/cookie/helper params +-> mutation route trusts stored/supplied identifiers +``` + +Some routes add: + +- duplicate/existence checks using supplied IDs +- signed hash validation for selected file/blob routes + +But these do not visibly replace route-local session-to-contact ownership proof. + +#### 4. Does the current architecture have a consistent mutation authorization boundary? + +No consistent route-local mutation authorization boundary is visible. + +Instead, the architecture appears to use a **distributed trust boundary**: + +- session and CRM-contact derivation happen upstream +- service helpers propagate identifiers +- API routes frequently trust those propagated identifiers directly +- CRM filtering / record target selection often acts on caller-supplied contact or record IDs + +## Recommended next assessment pass + +Next step only: + +**Trace hash-issuing and signed-route consumption together to determine whether `gethash_api` meaningfully strengthens the mutation boundary for record-ID and blob/container mutation routes, or whether it remains an integrity-only control layered on top of caller-supplied identity trust.** + +## Hash issuance and watchlist delete vertical slice + +This pass narrows to one question only: + +```text +Does the signed-hash model strengthen watchlist deletion beyond request integrity, +or does it remain a provenance/integrity control layered on top of upstream identity derivation? +``` + +Scope reviewed directly: + +- `pages/api/endpoint/gethash_api.js` +- `actions/clients/relayClient.js` +- `actions/clients/signedRequestClient.js` +- `actions/services/portalDirectService.js` +- `pages/api/endpoint/deletewatchedcases_api.js` +- `components/myportal/topthree.js` +- `components/myportal/viewall.js` +- `components/case/summary.js` +- `components/search/searchresults.js` + +## Hash issuance model + +### End-to-end issuance flow + +Observed path: + +```text +User action +-> service helper builds queryUrl +-> relayClient.buildHashedQueryUrl(queryUrl) +-> GET /api/endpoint/gethash_api?path= +-> gethash_api validates session and allowlisted path prefix +-> returns hash = hashAPIPath(rawQueryPath) +-> signed URL is queryUrl + hash +``` + +Evidence: + +- `actions/clients/relayClient.js` + - `buildHashedQueryUrl(queryUrl)` calls: + +```text +/api/endpoint/gethash_api?path=${encodeURIComponent(queryUrl)} +``` + + - appends returned `hash` directly to the original `queryUrl` + +- `actions/clients/signedRequestClient.js` + - `buildSignedUrl(queryUrl)` delegates to `buildHashedQueryUrl(queryUrl)` + - `deleteSignedJson(queryUrl)` performs request against the signed URL + +### Session influence on hash generation + +`pages/api/endpoint/gethash_api.js`: + +- calls `getSession({ req })` +- returns `401` if no session exists + +Assessment: + +- active session presence is a precondition for hash issuance +- however, the visible hash value is still produced from `hashAPIPath(rawQueryPath)` +- no reviewed code shows session identity being mixed into the signed payload itself + +### Allowlist model + +`gethash_api.js` allowlists path prefixes only: + +- `/api/endpoint/getportallogin_api` +- `/api/endpoint/deletemyrepresentations_api` +- `/api/endpoint/deletewatchedcases_api` +- selected upload / delete blob / completion message / PDF generation routes + +Assessment: + +- the allowlist constrains **which route families** may receive a signed hash +- it does not visibly encode per-user object ownership rules + +### Inputs signed + +`gethash_api.js`: + +- reads `req.query.path` as `rawQueryPath` +- validates only the prefix/path portion for allowlisting via `queryPath = rawQueryPath.split("?")[0]` +- returns: + +```text +hash = hashAPIPath(rawQueryPath) +``` + +Assessment: + +- the full raw query path is signed +- therefore signed material can include: + - route path + - query-string parameters + - record identifiers such as `watchedCaseID` +- in the reviewed watchlist-delete path, contact identifiers are **not** included because the delete URL only carries `watchedCaseID` + +### What the hash proves + +Based on reviewed code, the hash most clearly proves: + +- **A) route integrity** +- **B) route + identifier integrity** for identifiers embedded in the signed query string + +It does **not visibly prove**: + +- CRM contact ownership +- session-to-object relationship ownership +- route-local authorization to mutate the referenced record + +So for the requested classification: + +- **A) route integrity only** -> partially true +- **B) route + identifier integrity** -> strongest fit +- **C) route + ownership** -> not visibly supported by reviewed code + +## Watchlist delete trace + +### End-to-end flow + +Observed flow: + +```text +User +-> watched-case UI list +-> watchedCaseID obtained from already loaded watchlist data +-> deleteWatchedCases(watchedCaseID) +-> portalDirectService builds /api/endpoint/deletewatchedcases_api?watchedCaseID=... +-> signedRequestClient requests hash for that exact query URL +-> signed delete request sent +-> deletewatchedcases_api deletes pinswg_watchlists(watchedCaseID) +-> CRM delete executes directly +``` + +### Where `watchedCaseID` originates + +Evidence: + +- `components/myportal/topthree.js` + - delete button passes `showTopThreeArr[key].pinswg_watchlistid` +- `components/myportal/viewall.js` + - delete button passes `item.pinswg_watchlistid` +- `components/search/searchresults.js` + - uses `isWatchedCase(item.incidentid)[0].pinswg_watchlistid` +- `components/case/summary.js` + - delete flows also work from watchlist data already loaded into UI state + +Assessment: + +- normal portal flows obtain `watchedCaseID` from previously fetched watchlist records already associated with the user-facing journey +- identifier provenance is therefore tied to prior portal data retrieval and state propagation + +### How `watchedCaseID` reaches the delete route + +Evidence: + +- `actions/services/portalDirectService.js` + - `deleteWatchedCases(watchedCaseID)` builds: + +```text +/api/endpoint/deletewatchedcases_api?watchedCaseID=${watchedCaseID} +``` + + - then calls `deleteSignedJson(queryUrl)` + +- `actions/clients/signedRequestClient.js` + - signs the exact query URL before making the delete request + +### Does the hash include `watchedCaseID`? + +Yes. + +Reason: + +- `rawQueryPath` passed to `gethash_api` includes the full query string +- `hashAPIPath(rawQueryPath)` therefore covers: + +```text +/api/endpoint/deletewatchedcases_api?watchedCaseID= +``` + +Assessment: + +- the hash protects the integrity of the route + this record identifier in transit between client helper and route + +### Delete route behavior + +`pages/api/endpoint/deletewatchedcases_api.js`: + +- requires only `watchedCaseID` +- obtains token +- constructs: + +```text +pinswg_watchlists(watchedCaseID) +``` + +- performs direct CRM delete + +What was not visible in the reviewed route: + +- no session read +- no CRM contact lookup from `session.user.email` +- no fetch of the watchlist record to compare its contact relationship before delete + +Assessment: + +- CRM delete path is direct record deletion by supplied `watchedCaseID` + +## Referential ownership verification findings + +Reviewed target question: + +```text +Current Session +-> CRM Contact +-> Load watchedCaseID +-> Verify watchedCase.Contact == CRM Contact +-> Delete +``` + +Finding: + +**No referential ownership verification was visible in the reviewed watchlist delete flow.** + +More precisely: + +- session presence is required for hash issuance +- watchlist delete route itself does not visibly: + - resolve current session + - resolve CRM contact from session email + - load watchlist record for relationship comparison + - verify `pinswg_Contact/contactid == current CRM contact` + +## Identifier provenance assessment + +The reviewed design most strongly fits: + +- **A. provenance-based** + +Reason: + +- normal flow assumes valid `watchedCaseID` values come from prior portal watchlist retrievals and UI state +- signed hash protects the requested delete URL including `watchedCaseID` +- route-local referential ownership verification is not visible + +This is weaker evidence for: + +- **B. relationship-verified** + +because the reviewed delete route does not visibly perform a CRM relationship verification step before mutation. + +It is not the clearest fit for hybrid, because the visible control stack is: + +- session-gated hash issuance +- provenance of identifier through earlier portal flows +- route + identifier integrity protection + +rather than explicit relationship verification at delete time. + +## Hash security assessment + +### Protects route integrity? + +Yes, visibly. + +- `gethash_api` only issues hashes for allowlisted route prefixes +- downstream route compares supplied hash against the target route/query path shape through `hashAPIPath(...)` + +### Protects parameter integrity? + +Yes, for parameters included in the signed query path. + +- in watchlist delete flow, `watchedCaseID` is included in the signed URL + +### Protects identifier integrity? + +Yes, in the sense that the exact signed identifier value in the query string is protected from tampering without a new valid hash. + +### Protects ownership? + +No route-local ownership protection was visible from the hash mechanism alone. + +The reviewed code does not show the hash being derived from: + +- CRM contact relationship ownership +- session-derived object ownership mapping +- per-record authorization state + +### Protects authorization? + +Not visibly by itself. + +More precise statement: + +- the hash mechanism visibly participates in **request integrity control** +- session requirement for hash issuance adds an authenticated gateway to signing +- but object authorization or referential ownership verification is not visibly encoded into hash generation or the watchlist delete route itself + +## Vertical-slice conclusion + +### Hash issuance model + +```text +User +-> helper requests hash for exact query URL +-> gethash_api requires session +-> gethash_api checks allowlisted path prefix +-> hashAPIPath(rawQueryPath) +-> signed URL returned +``` + +Conclusion: + +- hash issuance is influenced by session presence only as a **gate to signing** +- reviewed code does not show session identity influencing the signed value itself + +### Watchlist delete model + +```text +User +-> watchedCaseID obtained from existing portal watchlist data +-> signed delete URL built for /api/endpoint/deletewatchedcases_api?watchedCaseID=... +-> deletewatchedcases_api +-> direct CRM delete pinswg_watchlists(watchedCaseID) +``` + +Conclusion: + +- watchlist deletion is **provenance-based** in the reviewed flow +- referential ownership verification at delete time was not visible + +### Architectural answers + +#### 1. What does the hash actually protect? + +In the reviewed slice, it protects: + +- allowlisted route use +- route integrity +- query/parameter integrity +- identifier integrity for identifiers present in the signed query path + +#### 2. Does hash issuance participate in authorization? + +Only indirectly and partially. + +More precise statement: + +- it requires an authenticated session before a hash is issued +- but reviewed code does not show it performing object-level or relationship-level authorization decisions + +#### 3. Is watchlist deletion provenance-based or relationship-verified? + +- **Provenance-based** in the reviewed flow + +#### 4. Where is ownership represented? + +Ownership is represented most visibly in: + +- CRM relationships on watchlist records (`pinswg_Contact`, `pinswg_WatchedCase`) +- prior portal retrieval flows that load watchlist data for the current user journey + +#### 5. Where is ownership verified? + +In this reviewed vertical slice: + +- **route-local referential ownership verification was not visible** in `deletewatchedcases_api.js` +- upstream identity derivation and identifier provenance are visible +- CRM relationship ownership exists as data model structure, but delete-time relationship verification was not visible + +## Recommended next assessment pass + +Next step only: + +**Perform the same vertical-slice integrity-vs-authorization trace for `deletemyrepresentations_api` and one blob/container mutation route, to determine whether the same provenance-based signed-request model is used consistently across CRM-record and blob/file deletion paths.** + +## Draft storage ownership assessment + +This pass focuses on the storage-owned boundary that exists before CRM submission. + +Key model under review: + +```text +NextAuth user +-> session.user.id +-> storage container +-> draft JSON + uploaded files +``` + +Scope reviewed directly: + +- `lib/newappeal/loadNewAppealPage.js` +- `lib/myportal/loadMyPortalAppealPage.js` +- `lib/representation/pageLoaders.js` +- `actions/services/documentDirectService.js` +- `actions/azurestorage.js` +- `pages/api/file/getprogressobjblob.js` +- `pages/api/file/getbloblist.js` +- `pages/api/file/getawaitingsubmissionfromblob.js` +- `pages/api/file/upload.js` +- `pages/api/file/uploadsinglefile.js` +- `pages/api/file/deleteblobcase.js` +- `pages/api/file/deleteblobrep.js` +- `pages/api/file/downloadblob.js` +- `pages/api/file/setupcontainer.js` +- `pages/api/file/createappealcompletemessage_api.js` +- `pages/api/file/createrepcompletemessage_api.js` +- `pages/api/file/editRepJson.js` + +## Container identity model + +### Root mapping + +The strongest visible storage ownership mapping is: + +```text +NextAuth user +-> session.user.id +-> container identity +``` + +Evidence: + +- `lib/newappeal/loadNewAppealPage.js` + - `loggedInUserIdent = session.user.id` + - draft progress is loaded with `getProgressFromBlob(loggedInUserIdent, query.id)` +- `lib/myportal/loadMyPortalAppealPage.js` + - `loggedInUserIdent = session.user.id` + - blob list and progress are loaded with: + - `getFilesFromBlob(loggedInUserIdent, query.casereference)` + - `getProgressFromBlob(loggedInUserIdent, query.casereference)` + - `getAwaitingSubmissionFromBlob(session.user.id)` +- `lib/representation/pageLoaders.js` + - draft representations are loaded with `getRepsFromBlob(thisSession.user.id)` + - representation file lists default to `result.containerID || thisSession.user.id` + +### Persistence / reuse properties + +Assessment: + +- container identity is based on the persistent NextAuth user ID, not the transient session token +- multiple sessions for the same user would resolve to the same container identity because the code repeatedly uses `session.user.id` +- container identity is also persisted in Redux/store state via `setContainerID(session.user.id)` in loader hydration paths + +### Are container names ever caller-supplied? + +Yes, at API route level many storage routes accept `container` or `containerID` as request input. + +Examples: + +- `pages/api/file/getprogressobjblob.js` + - `req.query.container` +- `pages/api/file/getbloblist.js` + - `req.query.container` +- `pages/api/file/getawaitingsubmissionfromblob.js` + - `req.query.container` +- `pages/api/file/upload.js` + - `req.body.containerID` +- `pages/api/file/uploadsinglefile.js` + - `req.body.containerID` +- `pages/api/file/deleteblobcase.js` + - `req.query.container` +- `pages/api/file/deleteblobrep.js` + - `req.query.container` +- `pages/api/file/downloadblob.js` + - `req.query.container` +- `pages/api/file/setupcontainer.js` + - `req.query.ident` + +Assessment: + +- container identity is strongly derived from `session.user.id` in normal SSR/page flows +- but many storage APIs operate on caller-provided container identifiers rather than deriving container identity inside the route + +## Draft creation trace + +### Draft appeal creation + +Observed path: + +```text +User starts new appeal +-> loader resolves session.user.id as loggedInUserIdent +-> client/service sends containerID + casefolderID +-> /api/file/upload or /api/file/createcase_api style draft writes +-> Azure blob write into containerID +``` + +Evidence: + +- `lib/newappeal/loadNewAppealPage.js` + - reads draft progress using `session.user.id` container identity +- `actions/services/documentDirectService.js` + - `uploadFiles(...)` appends `containerID` and `casefolderID` into form data +- `pages/api/file/upload.js` + - requires `containerID` and `casefolderID` + - calls `createBlob(appealData, containerID, casefolderID)` +- `actions/azurestorage.js` + - `createBlob(...)` writes `/_appeal.json` into the supplied container +- `pages/api/file/createcase_api.js` (reviewed earlier) + - writes draft case JSON into the supplied `containername` + +Assessment: + +- in normal portal flow, draft appeal creation uses a container that originates from `session.user.id` +- at the final storage API boundary, the container is supplied to the route rather than derived there + +### Draft representation creation + +Observed path: + +```text +User starts representation +-> representation loader uses session.user.id as container identity +-> draft rep JSON/files written into that container +``` + +Evidence: + +- `lib/representation/pageLoaders.js` + - `getRepsFromBlob(thisSession.user.id)` + - `setContainerID(thisSession.user.id)` +- `pages/api/file/upload.js` + - when `repOrAppeal` is true, route calls `createRepBlob(appealData, containerID, casefolderID)` +- `actions/azurestorage.js` + - `createRepBlob(...)` writes representation JSON into the supplied container under: + +```text +/_rep.json +``` + +Assessment: + +- draft representation creation follows the same model: session-derived container upstream, caller-supplied container at route level + +## Draft resume / read trace + +### Draft appeal resume / read + +Observed flow: + +```text +User resumes draft appeal +-> SSR loader gets session.user.id +-> getProgressFromBlob(session.user.id, caseReference) +-> /api/file/getprogressobjblob?container=&casefolderID=... +-> route validates hash +-> Azure read from supplied container + casefolderID +``` + +Evidence: + +- `lib/newappeal/loadNewAppealPage.js` + - `getProgressFromBlob(loggedInUserIdent, query.id)` +- `lib/myportal/loadMyPortalAppealPage.js` + - `getProgressFromBlob(loggedInUserIdent, query.casereference)` +- `actions/services/documentDirectService.js` + - `getProgressFromBlob(containerName, casereference)` builds route with caller-supplied `container` +- `pages/api/file/getprogressobjblob.js` + - accepts `container` and `casefolderID` + - validates hash for that route/query pair + - reads via `getProgressBlobs(containerName, casefolderIDTrimmed)` and `downloadProgressFile(containerName, ...)` + +Assessment: + +- draft resume/read is container-scoped in normal flow +- route-local container derivation from session is not visible; the route trusts supplied container once hash passes + +### Draft representation resume / read + +Observed flow: + +```text +User opens representation drafts +-> loader calls getRepsFromBlob(session.user.id) +-> representation details and files resolved from that container +``` + +Evidence: + +- `lib/representation/pageLoaders.js` + - `getRepsFromBlob(thisSession.user.id)` + - existing representation files use `getRepsFilesBlobs(result.containerID || thisSession.user.id, ...)` +- `actions/services/documentDirectService.js` + - `getRepsFromBlob(containerName)` builds `/api/file/getrepsblob?container=...` +- `pages/api/file/getrepsblob.js` (reviewed earlier in searches) + - accepts container as query input and validates hash +- `actions/azurestorage.js` + - `getRepsBlobs(containerName)` reads representation blobs by tags within the supplied container + +Assessment: + +- representation resume/read is also container-scoped in normal flow +- route-local session-to-container rebinding was not visible in the reviewed storage read routes + +## File upload / download trace + +### Upload + +Observed flow: + +```text +User uploads file +-> documentDirectService appends containerID + casefolderID +-> signed POST to /api/file/upload or /api/file/uploadsinglefile +-> route validates hash +-> route uses supplied containerID + casefolderID +-> Azure blob write into that container/path +``` + +Evidence: + +- `actions/services/documentDirectService.js` + - `uploadFiles`, `uploadSingleFile`, `uploadRepFiles` all append `containerID` and `casefolderID` +- `pages/api/file/upload.js` + - validates only route-level hash plus presence of `containerID` / `casefolderID` + - passes supplied values to `createBlob` / `createRepBlob` +- `pages/api/file/uploadsinglefile.js` + - validates route-level hash + - requires body `containerID` / `casefolderID` + - passes those values to `uploadSingleFile(...)` +- `actions/azurestorage.js` + - upload helpers write into paths built from supplied `containerName` and `foldername` + +Assessment: + +- uploads are container-scoped by the provided container value +- in normal application flow, that container value originates from `session.user.id` +- at the route itself, container identity is caller-supplied rather than freshly session-derived + +### Download / list + +Observed flow: + +```text +User requests files +-> helper builds route with container + casefolderID + blobname +-> route validates hash +-> route reads from supplied container/path +``` + +Evidence: + +- `actions/services/documentDirectService.js` + - `getFilesFromBlob(containerName, casefolderID)` + - `downloadBlob(containerName, blobName)` +- `pages/api/file/getbloblist.js` + - accepts `container` and `casefolderID` + - validates hash + - reads from supplied container via `getBlobs(...)` or `getRepsFilesBlobs(...)` +- `pages/api/file/downloadblob.js` + - accepts `container`, `casefolderID`, `blobname` + - validates hash + - reads from supplied container via `downloadFile(containerName, normalizedBlobName)` + +Assessment: + +- blob-path integrity is visibly protected by hash validation +- storage reads are container-scoped by supplied container/path +- route-local container derivation from session was not visible in these routes + +## Draft delete trace + +### Draft appeal delete + +Observed flow: + +```text +User deletes draft appeal +-> helper sends container + casefolderID +-> /api/file/deleteblobcase +-> route validates hash +-> Azure delete within supplied container, prefix = casefolderID +``` + +Evidence: + +- `actions/services/documentDirectService.js` + - `deleteAwaitingSubmissionsFromBlob(containerID, casefolderID)` +- `pages/api/file/deleteblobcase.js` + - accepts `container` and `casefolderID` + - validates hash + - calls `deleteBlobCase(containerName, casefolderIDTrimmed)` +- `actions/azurestorage.js` + - `deleteBlobCase(...)` lists blobs under prefix `blobName` within the supplied container and deletes them + +Assessment: + +- draft appeal deletion is container-scoped by supplied container +- route-local session-to-container verification was not visible + +### Draft representation delete + +Observed flow: + +```text +User deletes draft representation +-> helper sends container + casefolderID + repfile +-> /api/file/deleteblobrep +-> route validates hash +-> Azure delete within supplied container, prefix = casefolderID/repfile +``` + +Evidence: + +- `actions/services/documentDirectService.js` + - `deleteMyRepresentationsFromBlob(containerID, casefolderID, repfile)` +- `pages/api/file/deleteblobrep.js` + - accepts `container`, `casefolderID`, `repfile` + - validates hash + - calls `deleteBlobRep(containerName, normalizedRepPath)` +- `actions/azurestorage.js` + - `deleteBlobRep(...)` deletes blobs under the supplied prefix within the supplied container + +Assessment: + +- draft representation deletion follows the same model: container-scoped, caller-supplied container at route level + +## Submission boundary trace + +### Appeal submission transition + +Observed flow: + +```text +Draft in storage +-> /api/file/createappealcompletemessage_api?container=...&tempcaseref=...&hash=... +-> route reads draft JSON and case JSON from storage container +-> route rewrites blob progress / case data +-> route calls createCaseCompleteMessage(containerName, tempCaseRef) +-> queue message contains containerName + storage paths +-> downstream CRM creation happens after queue handoff +``` + +Evidence: + +- `pages/api/file/createappealcompletemessage_api.js` + - loads draft data from storage using supplied `containerName` and `tempCaseRef` + - calls `createCaseCompleteMessage(containerName, tempCaseRef)` +- `actions/azurestorage.js` + - `createCaseCompleteMessage(...)` sends queue message containing: + - `containerName` + - `appealpath` + - `casepath` + - `filespath` + +Queue message does **not visibly include**: + +- `session.user.id` + +It does include: + +- `containerName` (which in normal flow maps to `session.user.id`) + +It may indirectly carry CRM contact linkage later because case JSON contains CRM contact bindings, but the explicit queue payload is storage-path based. + +### Representation submission transition + +Observed flow: + +```text +Draft representation in storage +-> /api/file/createrepcompletemessage_api?container=...&tempcaseref=...&repid=...&hash=... +-> route calls createRepCompleteMessage(containerName, tempCaseRef, filename) +-> queue message contains containerName + representation storage paths +-> downstream CRM creation happens after queue handoff +``` + +Evidence: + +- `pages/api/file/createrepcompletemessage_api.js` + - passes supplied storage identifiers into queue helper +- `actions/azurestorage.js` + - `createRepCompleteMessage(...)` queue payload contains: + - `containerName` + - `caseref` + - `reppath` + - `filespath` + +### Ownership transition point + +The clearest visible transition is: + +```text +Storage ownership +-> queue message creation (`createCaseCompleteMessage` / `createRepCompleteMessage`) +-> downstream CRM creation / submitted-record model +``` + +Assessment: + +- before queue handoff, ownership is primarily storage/container-scoped +- after queue handoff, the model transitions toward CRM-owned submitted data +- the queue payloads are primarily storage-location based, not explicit session-ID payloads + +## Storage authorization classification + +### A. Session-derived container ownership + +Strongly visible in upstream loaders and store hydration: + +- `lib/newappeal/loadNewAppealPage.js` +- `lib/myportal/loadMyPortalAppealPage.js` +- `lib/representation/pageLoaders.js` + +These flows consistently use: + +```text +session.user.id -> container identity +``` + +### C. Container supplied and trusted + +Strongly visible at many storage API boundaries: + +- `getprogressobjblob` +- `getbloblist` +- `getawaitingsubmissionfromblob` +- `upload` +- `uploadsinglefile` +- `deleteblobcase` +- `deleteblobrep` +- `downloadblob` +- `setupcontainer` + +These routes accept container identifiers as request inputs and, in the reviewed code, do not visibly derive the container from session within the route itself. + +### D. Hybrid + +Best-fit overall classification for the storage model: + +- **D. Hybrid** + +Reason: + +- upstream application flow strongly derives container ownership from `session.user.id` +- many final file/blob routes then operate on caller-supplied container identity plus signed path/hash validation + +So the end-to-end draft authorization boundary is best described as: + +```text +session-derived container ownership upstream + + +caller-supplied container/path at route level + + +hash-protected blob-path integrity +``` + +## Storage boundary conclusion + +### Container identity model + +```text +NextAuth user +-> session.user.id +-> user storage container +``` + +### Draft creation / resume / delete / upload model + +```text +SSR/page loader +-> derives session.user.id +-> passes container identity into service helpers +-> service helpers send container/casefolder values to file APIs +-> file APIs validate hash and operate inside supplied container/path +``` + +### Architectural answers + +#### 1. What is the storage ownership root? + +The storage ownership root is most visibly: + +```text +session.user.id +``` + +#### 2. Is container ownership derived from `session.user.id`? + +Yes, strongly in the normal application loaders and store hydration flows. + +#### 3. Are storage operations consistently container-scoped? + +Yes, in the sense that the reviewed draft operations are all organized around container + folder/blob path. + +But an important precision: + +- they are not always route-locally session-derived +- many routes are container-scoped using caller-supplied container identifiers + +#### 4. Can container identity be influenced by callers? + +Yes, at the reviewed file-route boundaries container identity is commonly supplied by the caller. + +The reviewed routes do not visibly re-derive container identity from session inside the handler. + +#### 5. Where does ownership transition from storage ownership to CRM ownership? + +The clearest visible transition point is queue/finalisation: + +```text +storage-owned draft +-> completion route +-> queue message carrying storage paths/container +-> downstream submitted CRM record creation +``` + +## Recommended next assessment pass + +Next step only: + +**Perform a focused route-local verification pass on the highest-value storage APIs (`getprogressobjblob`, `getbloblist`, `downloadblob`, `uploadsinglefile`, `deleteblobcase`, `deleteblobrep`) to determine whether any of them derive container ownership from session server-side elsewhere in the stack, or whether they rely entirely on upstream container provenance plus hash-protected path integrity.** + +## PEDW authorization architecture model + +This section consolidates the completed investigation passes into one high-level authorization architecture model. + +It is intended to describe: + +- the visible authorization roots +- the main ownership / authorization patterns +- the integrity controls layered across those patterns +- what has been proven by code review +- what has not been proven + +It is not a new endpoint review and does not change prior evidence or conclusions. + +## Authorization roots + +### 1. Anonymous public root + +The public browsing root is: + +```text +Anonymous user +-> public search / case browsing +``` + +This root most clearly applies to: + +- public search +- public case browsing +- public document and case-discovery style routes where publication posture is the governing boundary + +### 2. NextAuth session root + +The primary authenticated root is: + +```text +NextAuth session +``` + +This is the most fundamental authenticated boundary visible in the application. + +From this session, two major downstream ownership models emerge. + +### 3. CRM contact root + +For CRM-owned portal data, the strongest visible business identity root is: + +```text +NextAuth session +-> session.user.email +-> getPortalLogin(email) +-> CRM Contact +``` + +This root then feeds CRM relationship scoping, CRM query filtering, and CRM record targeting in portal-owned user data flows. + +### 4. Storage container root + +For draft/blob-owned data, the strongest visible ownership root is: + +```text +NextAuth session +-> session.user.id +-> user-specific Azure Storage container +``` + +This root governs: + +- draft appeals +- draft representations +- uploaded draft files +- progress JSON and related storage-owned artefacts before submission + +## Pattern catalogue + +### A. Public anonymous pattern + +Model: + +```text +Anonymous user +-> public route/query inputs +-> published/public data access +``` + +Use case: + +- public portal search and browsing journeys + +### B. CRM contact scoped pattern + +Model: + +```text +NextAuth session +-> session.user.email +-> getPortalLogin(email) +-> CRM contactid +-> CRM query / CRM target record +``` + +Use case: + +- dashboard/account bootstrap +- my cases +- watched cases read path +- submitted representation list paths +- account/profile flows upstream of final API mutation route + +### C. CRM relationship scoped pattern + +Model: + +```text +CRM contact +-> CRM relationship +-> CRM query scoping / association records +``` + +Use case: + +- contact-linked CRM relationships +- watched-case relationships +- representation/contact involvement relationships + +Important note: + +- CRM relationship ownership is represented in the data model +- route-local referential verification of those relationships was not consistently visible in every reviewed mutation route + +### D. Record-ID provenance-based pattern + +Model: + +```text +Upstream identity derivation +-> identifier propagated through UI/service flow +-> route accepts record ID +-> mutation/read occurs by target ID +``` + +Use case: + +- watchlist delete +- representation delete +- several mutation routes where the visible route boundary trusts propagated record identifiers + +Preferred wording for this model: + +- **provenance-based trust** + +### E. Storage container scoped pattern + +Model: + +```text +NextAuth session +-> session.user.id +-> container identity +-> casefolder/blob path +-> Azure Storage operation +``` + +Use case: + +- draft appeal create/read/update/delete +- draft representation create/read/update/delete +- uploaded draft files +- pre-submission PDF/completion artefacts + +Important note: + +- storage ownership is strongly session-derived upstream +- many final file routes then operate on caller-supplied `container` / `containerID` values rather than deriving container identity in-route + +## Integrity controls + +### 1. Signed hash as route/query/path integrity control + +Visible model: + +```text +helper builds exact route/query path +-> gethash_api issues hash for allowlisted route family +-> downstream route validates signed path/query +``` + +What this most clearly protects: + +- route integrity +- query integrity +- parameter integrity +- path / identifier integrity where the identifier is part of the signed route/query path + +What it does not by itself visibly prove: + +- CRM object ownership +- storage object ownership +- route-local referential ownership verification + +Preferred wording: + +- **integrity control rather than object-authorization control** + +### 2. Azure SDK / server-mediated storage execution + +Visible model: + +```text +PEDW API +-> Azure SDK +-> storage account credentials / SAS generation +-> Azure Storage +``` + +Important conclusion: + +- users do not directly access Azure Storage in the reviewed architecture +- storage execution is server-mediated through PEDW API routes and Azure SDK helpers + +### 3. Relay path validation model + +Visible model: + +```text +PEDW API +-> signed path hash +-> Azure Relay +-> CRM +``` + +Important conclusion: + +- relay hash and signed path validation visibly strengthen request integrity +- they do not, by themselves, prove object ownership +- relay upstream authentication to CRM remains out of scope for this assessment + +## What is proven / not proven + +### What is proven by the reviewed code + +The completed assessment passes support the following conclusions: + +#### 1. The authorization model is distributed + +Authorization is not most clearly expressed as one route-local guard model. + +Instead, the visible model is a: + +- **distributed authorization model** + +spanning: + +- NextAuth session +- SSR/page-loader identity derivation +- CRM contact lookup +- session-derived storage container identity +- service/helper propagation of identifiers +- downstream CRM query scoping +- downstream storage container/path scoping + +#### 2. Route-local authorization is not consistently visible + +Across many reviewed CRM and storage routes: + +- route-local referential verification is not consistently visible +- ownership is often established upstream +- identifiers are then propagated into final handlers + +#### 3. Signed hash strengthens integrity, not ownership proof + +The reviewed signed-path model most clearly strengthens: + +- route/path/query integrity +- identifier integrity where applicable + +It does not, on reviewed evidence, independently prove object ownership. + +#### 4. Storage access is server-mediated + +Storage operations are visibly mediated by: + +- PEDW API routes +- Azure SDK helpers +- storage account credentials / generated SAS operations + +This is not a direct browser-to-storage model. + +#### 5. Ownership roots differ by domain + +The completed traces support two distinct ownership roots: + +##### CRM-owned domain + +```text +session.user.email +-> CRM contact +-> CRM relationships / query scoping +``` + +##### Draft/blob-owned domain + +```text +session.user.id +-> user-specific storage container +-> draft JSON / uploaded files +``` + +### What has not been proven + +The completed review does **not** prove the following: + +#### 1. No exploitability has been demonstrated + +The assessment has identified architectural visibility gaps and provenance-based trust patterns. + +It has **not** demonstrated a working exploit. + +#### 2. No confirmed User A -> User B mutation has been demonstrated + +The reviewed routes often accept propagated identifiers and do not always visibly re-bind them in-route. + +However: + +- no confirmed User A -> User B data mutation has been demonstrated in this assessment + +#### 3. No evidence that health checks or penetration tests are invalid + +This assessment does not invalidate prior testing posture. + +Specifically, it has produced: + +- no evidence that existing health checks are invalid +- no evidence that existing OWASP / pentest outcomes are invalid + +#### 4. No evidence of direct CRM or storage exposure + +The reviewed architecture does not show: + +- direct browser-to-CRM access +- direct browser-to-storage account access + +The execution model remains server-mediated. + +## Risk characterization + +Based on the completed investigation set, the best-supported characterization is: + +### 1. Architectural integrity / auditability risk + +Why: + +- ownership and authorization are frequently distributed across upstream derivation, helper propagation, query scoping, and integrity controls +- route-local referential verification is not consistently self-evident +- this can make the effective authorization boundary harder to audit quickly and confidently + +### 2. Maintainability risk + +Why: + +- multiple ownership models coexist: + - public anonymous + - CRM contact scoped + - CRM relationship scoped + - record-ID provenance based + - storage container scoped +- this increases the chance of misunderstanding or uneven implementation in future changes + +### 3. Future-change risk + +Why: + +- the architecture relies significantly on upstream identity derivation and propagated identifiers +- future modifications could weaken assumptions if contributors do not understand which routes rely on provenance-based trust versus route-local verification + +### 4. Not currently a confirmed exploitable vulnerability + +Based on reviewed evidence and wording discipline for this assessment: + +- this is **not currently a confirmed exploitable vulnerability** +- it is better understood as an architectural clarity, integrity, and future-hardening concern unless and until exploitability is independently demonstrated + +## Architectural conclusion + +The completed assessment supports the following high-level model: + +### Public anonymous + +```text +Anonymous user +-> public search / case browsing +``` + +### CRM-owned authorization + +```text +NextAuth session +-> session.user.email +-> getPortalLogin(email) +-> CRM Contact +-> CRM relationships / CRM query scoping +``` + +### Draft/blob-owned authorization + +```text +NextAuth session +-> session.user.id +-> user-specific Azure Storage container +-> JSON drafts / uploaded files +``` + +### Execution / integrity layers + +```text +PEDW API +-> signed path hash / route validation +-> Azure Relay or Azure SDK +-> CRM or Azure Storage +``` + +Most important synthesis statement: + +- PEDW currently exhibits a **distributed authorization model** +- ownership is usually established upstream +- identifiers are then propagated into downstream routes +- signed hash is an **integrity control rather than object-authorization control** +- no exploitability has been demonstrated by this assessment alone + +## Recommendation + +Next step only: + +**Capture this authorization architecture model as the baseline for future assessment and change review, and if later implementation work is explicitly approved, consider narrow helper/guard patterns that re-bind selected sensitive CRM mutation routes to session-derived CRM contact and selected storage mutation routes to session-derived container identity without changing current successful behaviour or testing posture by default.** + +## Programme status + +### Portal API Security & Access Boundary Assessment + +**Status: COMPLETE** + +The architecture is now understood sufficiently for this stream. + +Stable programme-level conclusions: + +- PEDW uses a **distributed authorization model** +- authorization is generally established upstream and propagated through later flows +- CRM-owned operations and draft/storage-owned operations use different ownership roots +- integrity controls are visible and meaningful, but they are not the same as route-local object-authorization proof +- no confirmed exploitability has been demonstrated by this assessment + +The dominant observed model is: + +```text +Identity established + ↓ +Ownership scope established + ↓ +Ownership identifier propagated + ↓ +Integrity controls applied + ↓ +Operation executed +``` + +rather than: + +```text +Operation + ↓ +Identity re-derived + ↓ +Ownership re-proven + ↓ +Operation executed +``` + +### Programme conclusion + +No immediate remediation programme is recommended on the basis of the current architecture evidence alone. + +If future work is commissioned in this area, it should be framed as: + +- authorization hardening +- consistency improvements +- maintainability improvements + +and **not** as emergency security remediation. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 22cdd5d5..b93a7603 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -18,6 +18,1044 @@ Follow-ups: --- +### CL-2026-06-22-JOURNEY-ARCHITECTURE-MAP-SLICE-8: case-messages-notices and related-published-case-communications lifecycle map + +date: 2026-06-22 +author: Cline +scope: `context/journey-architecture-map.md`, case messages/notices and related published case communications architecture documentation +type: milestone +rationale: Extend the Journey Architecture Map with the next bounded maintainability slice by documenting how public case-level notices are loaded and rendered on case pages, how adjacent published communication content such as SIPS events/media participates in the case-detail experience, and how the relevant routes classify under a future grouping view without proposing implementation changes. +impact: Documentation/context only; improves maintainer understanding of the case-message retrieval path, the case-page notice rendering model, the split ownership between message props and event/media Redux state, and the boundary between on-page case communications, document delivery, and outbound notifications; no runtime, CRM, relay, auth, storage, notification, or i18n behaviour change. +status: completed + +Summary: + +- Confirmed the required Slice 8 context was read before investigation: + - `context/journey-architecture-map.md` + - `context/api-route-map.md` + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Extended `context/journey-architecture-map.md` with Slice 8 covering: + - Case Messages / Notices + - Related Published Case Communications + - Case Communication Architecture + - Future API Grouping Assessment + - Messages vs Documents vs Notifications Comparison +- Documented, for each communication journey: + - purpose + - primary entry points + - loaders / initialisation + - state ownership + - service layer + - API layer + - integration boundaries + - ownership model + - architectural flow + - change entry set + - risk classification +- Recorded the clearest visible notice/message model as: + - case loader retrieves `getCaseMessage(incidentid)` + - `messagesObj` is passed as page props + - `CaseNoticeBanner` renders active banner-style notices on the case-details tab +- Recorded the clearest visible adjacent published communication model as: + - SIPS-only conditional event/media fetches + - Redux `eventDetailsObj` / `mediaDetailsObj` + - live-event banner plus events/media tab presentation in the case summary layer +- Added the requested future grouping classification for relevant case communication routes using: + - current location + - journey owner + - integration touched + - future grouping candidate + - migration caution +- Added the requested comparison showing: + - where case messages/notices overlap with published documents and notifications + - where they differ + - which one is case-page presentation + - which one is document delivery + - which one is outbound communication + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted searches across `pages`, `components/case`, `actions/services`, `store`, and `pages/api/endpoint` + - direct review of public/DNS/myportal case loaders, `components/case/summary.js`, `components/case/caseNoticeBanner.js`, `actions/services/caseDirectService.js`, `getcasemessage_api.js`, and directly relevant SIPS event/media routes +- Did not widen into notification email delivery or document download behaviour beyond the requested comparison. +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded documentation slice only: + - Case Status / Lifecycle Presentation and Related Published Timeline Signals + +--- + +### CL-2026-06-22-JOURNEY-ARCHITECTURE-MAP-SLICE-7: published-document-discovery and download lifecycle map + +date: 2026-06-22 +author: Cline +scope: `context/journey-architecture-map.md`, published document discovery and published document retrieval/download architecture documentation +type: milestone +rationale: Extend the Journey Architecture Map with the next bounded maintainability slice by documenting how published documents are surfaced on case pages, how published-document metadata is retrieved and shaped, how download links are generated, how the dedicated download proxy delivers files, and how the relevant routes classify under a future grouping view without proposing implementation changes or reopening authorization assessment. +impact: Documentation/context only; improves maintainer understanding of the case-document discovery path, the metadata-to-hashlink-to-download-proxy handoff, document-history route visibility boundaries, and journey ownership across the document route family; no runtime, CRM, relay, auth, storage, notification, or i18n behaviour change. +status: completed + +Summary: + +- Confirmed the required Slice 7 context was read before investigation: + - `context/journey-architecture-map.md` + - `context/api-route-map.md` + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Extended `context/journey-architecture-map.md` with Slice 7 covering: + - Published Document Discovery + - Published Document Retrieval / Download + - Document Delivery Architecture + - Future API Grouping Assessment + - Discovery vs Download Comparison +- Documented, for each document journey: + - purpose + - primary entry points + - loaders / initialisation + - state ownership + - service layer + - API layer + - integration boundaries + - ownership model + - architectural flow + - change entry set + - risk classification +- Recorded the clearest visible discovery model as: + - search-to-case navigation + - case documents component bootstrap + - relay-backed CRM metadata retrieval + - Redux `documentDetailsObj` + - filtered/sorted/paged document presentation +- Recorded the clearest visible delivery model as: + - metadata-generated `pinswg_hashlink` + - browser fetch of `/api/documents/download/[id]?hash=...` + - dedicated download proxy stream + - browser blob download +- Recorded that document history routes are present in the document route family but were not visibly surfaced by the reviewed case-document UI path. +- Added the requested future grouping classification for relevant document routes using: + - current location + - journey owner + - integration touched + - future grouping candidate + - migration caution +- Added the requested comparison showing: + - shared APIs + - shared integrations + - shared ownership assumptions + - where discovery and download diverge + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted searches across `pages`, `components`, `actions/services`, `store`, and `pages/api` + - direct review of case detail entry, case documents UI, search navigation handoff, document metadata routes, document type route, document history routes, and the dedicated download proxy route +- Did not reopen authorization or security posture assessment. +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded documentation slice only: + - Case Messages / Notices and Related Published Case Communications + +--- + +### CL-2026-06-21-JOURNEY-ARCHITECTURE-MAP-SLICE-6: watchlist-subscriptions and unsubscribe-watchlist-removal lifecycle map + +date: 2026-06-21 +author: Cline +scope: `context/journey-architecture-map.md`, watchlist/subscriptions and unsubscribe/watchlist-removal architecture documentation +type: milestone +rationale: Extend the Journey Architecture Map with the next bounded maintainability slice by documenting watched-case creation, viewing, removal, unsubscribe paths, the visible CRM contact↔watched-case relationship model, and future grouping candidates for watched-case APIs without proposing implementation changes or reopening the completed authorization assessment. +impact: Documentation/context only; improves maintainer understanding of the CRM watched-case relationship lifecycle, dashboard/search/case watch-state reuse, email-notification participation on watched-case records, and watched-case API ownership boundaries; no runtime, auth, CRM, Notify, storage, or i18n behaviour change. +status: completed + +Summary: + +- Confirmed the required Slice 6 context was read before investigation: + - `context/journey-architecture-map.md` + - `context/api-route-map.md` + - `context/portal-api-security-boundary-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Extended `context/journey-architecture-map.md` with Slice 6 covering: + - Watchlist Creation + - Watchlist Viewing + - Watchlist Removal + - CRM Relationship Ownership Model + - Future API Grouping Assessment + - Watchlist vs Dashboard Comparison +- Documented, for each watched-case journey: + - purpose + - primary entry points + - loaders / initialisation + - state ownership + - service layer + - API layer + - integration boundaries + - ownership model + - architectural flow + - change entry set + - risk classification +- Recorded the strongest visible CRM relationship model as: + - `CRM Contact ↔ Watched Case` + - represented through `pinswg_watchlists` + - with visible fields including watched-case id, contact id, case id, appeal type, and `pinswg_emailnotifications` +- Recorded that email-notification participation is visibly expressed on the watched-case relationship record itself rather than through a separate subscription entity in the reviewed frontend code. +- Added the requested future grouping classification for watched-case routes using: + - journey ownership + - integration touched + - future grouping candidate + - migration caution +- Added the requested comparison with My Portal Dashboard, documenting shared dependencies, shared state, shared APIs, and ownership relationship without proposing changes. + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted searches across `pages`, `components`, `actions/services`, `store`, and `pages/api` + - direct review of watched-case portal bootstrap pages, search/case watch-action components, dashboard/view-all watched-case components, watched-case routes, unsubscribe pages, and watched-case Redux ownership files +- Did not reopen exploitability or security posture assessment. +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded documentation slice only: + - Documents / Published Document Retrieval and Download + +--- + +### CL-2026-06-21-JOURNEY-ARCHITECTURE-MAP-SLICE-5: authentication-sign-in and notifications-email lifecycle map + +date: 2026-06-21 +author: Cline +scope: `context/journey-architecture-map.md`, authentication/sign-in and notifications/email architecture documentation +type: milestone +rationale: Extend the Journey Architecture Map with the next bounded maintainability slice by documenting authentication/sign-in and notifications/email as cross-cutting journeys, including future grouping classification for auth/email-related APIs and an explicit auth-vs-notification comparison, without reopening the completed authorization assessment or proposing implementation changes. +impact: Documentation/context only; improves maintainer understanding of NextAuth-to-CRM bootstrap continuity, locale-aware sign-in behaviour, Notify usage across auth and business notifications, and journey ownership/grouping boundaries for auth/email routes; no runtime, auth, CRM, Notify, storage, or i18n behaviour change. +status: completed + +Summary: + +- Confirmed the required Slice 5 context was read before investigation: + - `context/journey-architecture-map.md` + - `context/api-route-map.md` + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Extended `context/journey-architecture-map.md` with Slice 5 covering: + - Authentication / Sign-In + - Notifications / Email + - Ownership / Identity Model + - Future API Grouping Assessment + - Auth vs Notification Comparison +- Documented, for each auth/email journey: + - purpose + - primary entry points + - loaders / initialisation + - state ownership + - service layer + - API layer + - integration boundaries + - ownership / identity model + - architectural flow + - change entry set + - risk classification +- Recorded the strongest visible sign-in continuity model as: + - sign-in page locale resolution + - NextAuth verification + callback + - session creation + - homepage bootstrap + - `getPortalLogin(session.user.email)` + - `/myportal` or `/account/register` +- Recorded the strongest visible notifications model as split between: + - thin direct Notify sends via `pages/api/email/notify.js` + - orchestration-heavy aggregation/batch sends via `pages/api/email/getall.js` and supporting data routes +- Added the requested future grouping classification for auth/email-related routes using: + - current location + - journey ownership + - integration touched + - future grouping candidate + - migration caution +- Added the requested comparison showing: + - where auth and notifications are independent + - where they overlap + - where Notify acts as auth-support integration + - where Notify acts as business-notification integration +- Kept the grouping and comparison sections explicitly framed as documentation/discovery only and not implementation recommendations. + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted searches across `pages`, `components`, `actions/services`, `lib`, and `pages/api` + - direct review of sign-in pages, verify-request page, homepage bootstrap, logout helpers, NextAuth handler, locale resolution route, email routes, Notify service helpers, and completion/watchlist notification callers +- Did not reopen the completed authorization risk/security assessment. +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded documentation slice only: + - Watchlist / Subscriptions and Unsubscribe Flows + +--- + +### CL-2026-06-21-JOURNEY-ARCHITECTURE-MAP-SLICE-4: account registration and personal-details/account-management lifecycle map + +date: 2026-06-21 +author: Cline +scope: `context/journey-architecture-map.md`, account registration and personal-details/account-management architecture documentation +type: milestone +rationale: Extend the Journey Architecture Map with the next bounded maintainability slice by documenting account registration and personal-details/account-management journeys, and by classifying relevant APIs against a future journey-based grouping model without proposing implementation changes. +impact: Documentation/context only; improves maintainer understanding of the NextAuth-session-to-CRM-contact bootstrap model, account journey ownership, and future grouping candidates for account/auth/shared/platform-level APIs; no runtime, auth, storage, CRM, or i18n behaviour change. +status: completed + +Summary: + +- Re-read required Slice 4 context before investigation: + - `context/journey-architecture-map.md` + - `context/api-route-map.md` + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Extended `context/journey-architecture-map.md` with Slice 4 covering: + - Account Registration + - Personal Details / Account Management + - Ownership Model + - Future API Grouping Assessment +- Documented, for each account journey: + - purpose + - primary entry points + - loaders / initialisation + - state ownership + - service layer + - API layer + - integration boundaries + - ownership model + - architectural flows + - change entry sets + - risk classification +- Recorded how the journeys strengthen the identity model: + - registration bridges `NextAuth session.user.email -> CRM Contact` + - account management depends on that bridge and reuses CRM contact identity for account reads/updates +- Added a future grouping classification for relevant account/auth routes using: + - journey owner + - integration touched + - future grouping candidate + - migration caution +- Kept the grouping section explicitly framed as a **future grouping assessment** only and **not an implementation recommendation**. + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted searches across `pages`, `components/account`, `actions/services`, `pages/api`, and `store` + - direct review of homepage bootstrap, registration page/components, personal-details page/components, account service helpers, account endpoint handlers, and relevant auth support routes +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded documentation slice only: + - Authentication / Sign-In and Notifications / Email + +--- + +### CL-2026-06-21-JOURNEY-ARCHITECTURE-MAP-SLICE-3: draft representation and representation submission/finalisation lifecycle map + +date: 2026-06-21 +author: Cline +scope: `context/journey-architecture-map.md`, representation draft creation and representation submission/finalisation architecture documentation +type: milestone +rationale: Extend the Journey Architecture Map with the next bounded maintainability slice by documenting the representation draft lifecycle and the representation submission/finalisation lifecycle, including an explicit comparison against the already documented appeal lifecycle, without proposing implementation changes. +impact: Documentation/context only; improves maintainer understanding of representation storage ownership, queue handoff, CRM representation boundaries, and appeal-vs-representation lifecycle differences; no runtime, auth, storage, queue, CRM, or i18n behaviour change. +status: completed + +Summary: + +- Re-read required Slice 3 context before investigation: + - `context/journey-architecture-map.md` + - `context/api-route-map.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Extended `context/journey-architecture-map.md` with Slice 3 covering: + - Draft Representation Creation + - Representation Submission / Finalisation + - Appeal vs Representation Lifecycle Comparison +- Documented, for each representation journey: + - purpose + - primary entry points + - loaders / initialisation + - state ownership + - service layer + - API layer + - integration boundaries + - ownership model + - architectural flows + - change entry sets + - risk classification +- Added the requested comparison across: + - ownership + - storage + - queue + - CRM + - maintainability +- Recorded the main visible distinction from appeals as: + - representation drafts are more tightly case-linked and `repfile_name`-keyed + - representation completion concentrates more client-side side effects around involvement, queue handoff, email, and watched/submitted status updates + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted searches across `pages`, `lib`, `actions/services`, `pages/api`, and `store` + - direct review of representation loaders, representation completion component, storage routes, queue/completion routes, and representation-specific Redux state +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded documentation slice only: + - Account Registration and Personal Details / Account Management + +--- + +### CL-2026-06-21-JOURNEY-ARCHITECTURE-MAP-SLICE-2: draft appeal and appeal submission/finalisation lifecycle map + +date: 2026-06-21 +author: Cline +scope: `context/journey-architecture-map.md`, draft appeal creation and appeal submission/finalisation architecture documentation +type: milestone +rationale: Extend the Journey Architecture Map with the next bounded maintainability slice by documenting the draft appeal lifecycle and the appeal submission/finalisation lifecycle from page entry through state, services, APIs, storage, queue, and CRM transition without proposing implementation changes. +impact: Documentation/context only; improves maintainer understanding of draft ownership, storage container boundaries, queue handoff, and submitted-appeal transition; no runtime, auth, storage, queue, CRM, or i18n behaviour change. +status: completed + +Summary: + +- Re-read required Slice 2 context before investigation: + - `context/journey-architecture-map.md` + - `context/api-route-map.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Extended `context/journey-architecture-map.md` with Slice 2 covering: + - Draft Appeal Creation + - Appeal Submission / Finalisation +- Documented, for each journey: + - purpose + - primary entry points + - loaders / initialisation + - state ownership + - service layer + - API layer + - integration boundaries + - ownership model + - architectural flows + - change entry sets + - risk classification +- Recorded the strongest visible draft ownership model as: + - `NextAuth session.user.id -> containerID -> Azure Storage container -> casefolder / draft blob state` +- Recorded the clearest visible submitted-appeal transition as: + - draft blob state + - finalisation route + - Azure Queue message + - adjacent CRM mutation boundary +- Added Slice 2-specific investigation method, risks/cautions, validation summary, and a documentation-only recommendation for the next journey slice. + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted searches across `pages`, `lib`, `components/newappeal`, `actions/services`, `actions`, `pages/api`, and `store` + - direct review of draft/resume loaders, Redux hydration helpers, new appeal flow components, storage helpers, finalisation handlers, and adjacent CRM mutation routes +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded documentation slice only: + - Representation Draft Creation and Representation Submission / Finalisation + +--- + +### CL-2026-06-20-PORTAL-API-PLATFORM-ASSESSMENT-SLICE-A: API family classification sample + +date: 2026-06-20 +author: Cline +scope: `context/portal-api-platform-assessment.md`, sampled API families under `pages/api/{endpoint,file,email,documents,auth}` +type: milestone +rationale: Record the first bounded slice of the Portal Integration Contract & API Platform Assessment by classifying representative API families, integration responsibilities, repeated patterns, duplication candidates, and contract-critical areas without widening into a full inventory. +impact: Documentation/context only; improves visibility of the portal API platform shape and integration-family boundaries; no runtime, contract, auth, storage, email, or i18n behaviour change. +status: completed + +Summary: + +- Added `context/portal-api-platform-assessment.md`. +- Confirmed the required context files were read before starting the slice: + - `context/architecture.md` + - `context/portal-api-security-boundary-assessment.md` + - `context/integration-map.md` + - `memory-bank/debt-list.md` + - `memory-bank/change-log.md` +- Assessed only the requested first slice folders: + - `pages/api/endpoint` + - `pages/api/file` + - `pages/api/email` + - `pages/api/documents` + - `pages/api/auth` +- Used a representative bounded sample to classify the visible API platform families into: + - CRM relay read + - CRM relay write + - storage/blob read + - storage/blob write + - queue/finalisation + - email/notification + - document download + - auth/session + - local utility/meta + - middleware/helper +- Recorded the integration responsibility map from the sample, showing a mix of: + - CRM relay-backed + - Azure Storage SDK-backed + - GOV.UK Notify-backed + - NextAuth-backed + - local-only + - mixed/orchestration routes +- Identified repeated sample patterns including: + - hash validation + - helper-based relay forwarding + - required query/body validation + - `respondSuccess` / `respondError` response envelopes + - raw CRM URL construction in older handlers + - Azure blob path construction + - mixed logging styles +- Marked only high-level duplication candidates from the sample, distinguishing likely intentional vs likely historical vs unclear families without recommending consolidation yet. +- Identified contract-critical sampled families including public search, myportal/dashboard, submission/finalisation, file upload/download, document download, auth/session, and email/notification. + +Validation: + +- Documentation-only assessment; no runtime code changed. +- Non-destructive evidence gathering only: + - small directory listings of sampled API folders and `pages/api/middleware` + - direct file inspection of representative sample handlers + - targeted grep/search limited to sampled folders for relay helper usage, hash usage, axios/direct integration usage, Notify usage, and auth/locale/redirect patterns +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded slice: `Slice B — Endpoint CRM Relay Shape Sample`, restricted to a representative subset of `pages/api/endpoint` to distinguish newer helper-based relay routes from older direct relay wrappers and to classify main endpoint contract shapes without proposing implementation. + +--- + +### CL-2026-06-20-PORTAL-API-PLATFORM-ASSESSMENT-SLICE-B: endpoint CRM relay contract shape sample + +date: 2026-06-20 +author: Cline +scope: `context/portal-api-platform-assessment.md`, representative `pages/api/endpoint` route sample +type: milestone +rationale: Record the second bounded slice of the Portal Integration Contract & API Platform Assessment by classifying endpoint contract shapes inside `pages/api/endpoint`, comparing helper-oriented read contracts with older direct-wrapper mutation contracts, and assessing contract consistency without widening into a full endpoint inventory. +impact: Documentation/context only; improves visibility of endpoint contract structure, implementation-style split, and maintainability shape in the CRM relay layer; no runtime, auth, storage, i18n, or API behaviour change. +status: completed + +Summary: + +- Re-read required Slice B context before inspection: + - `context/portal-api-platform-assessment.md` + - `context/portal-api-security-boundary-assessment.md` + - `context/architecture.md` + - `memory-bank/change-log.md` +- Assessed only a bounded representative subset inside `pages/api/endpoint` across the requested route groups: + - public read/search + - authenticated/myportal read + - CRM create + - CRM update/patch/delete + - proxy/pass-through +- Classified the sampled endpoint layer into a small number of recurring contract shapes: + - public CRM read + - user-owned CRM read + - CRM create + - CRM patch/update + - CRM delete + - proxy/pass-through + - lookup/config/support + - hybrid upsert/orchestration +- Recorded the strongest visible implementation split in `pages/api/endpoint`: + - newer helper-oriented read routes using `relayGet(...)`, shared response helpers, transforms, and relay policy presets + - older direct-wrapper mutation routes using `getToken()`, `axios(config)`, `WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)`, and manual CRM method configuration +- Recorded that response envelope style is more consistent than integration implementation style: + - `respondError(...)` / `respondSuccess(...)` are broadly used across both newer and older route styles +- Recorded that paged/search/document read routes repeatedly use transform/pagination sub-patterns: + - `transformData` + - `@odata.nextLink` normalization + - custom request headers for paged reads +- Concluded that the `endpoint/` route count likely overstates uniqueness, because the sampled evidence suggests relatively few repeated contract shapes rather than many fully bespoke contracts. + +Validation: + +- Documentation-only assessment; no runtime code changed. +- Non-destructive evidence gathering only: + - direct inspection of representative endpoint routes including: + - `getbasicsearch_api.js` + - `getbasicsearchpaged_api.js` + - `getadvancedsearch_api.js` + - `getsearchdocumentdetails_api.js` + - `getsearchdocumenthistory_api.js` + - `getmycases_api.js` + - `getmyrepresentations_api.js` + - `getwatchedcases_api.js` + - `getawaitingsubmission_api.js` + - `getpersonalaccount_api.js` + - `createcase_api.js` + - `createaccount_api.js` + - `createwatchedcases_api.js` + - `updateaccount_api.js` + - `patchcase_api.js` + - `deletewatchedcases_api.js` + - `deletemyrepresentations_api.js` + - `getwatchedcasesproxy_api.js` + - targeted endpoint-only searches for helper/direct-wrapper indicators: + - `relayGet(...)` + - `relayGetData(...)` + - `axios(...)` + - `hashAPIPath(...)` + - `respondSuccess(...)` / `respondError(...)` + - `transformData` + - `@odata.nextLink` + - common status-code patterns +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded slice: `Slice C — Endpoint Transform and Pagination Pattern Sample`, restricted to representative `pages/api/endpoint` routes that use transforms, `@odata.nextLink` normalization, paged/unpaged variants, and supplementary `relayGetData(...)` enrichment so the transform/pagination sub-patterns can be classified without proposing implementation. + +--- + +### CL-2026-06-20-PORTAL-API-PLATFORM-ASSESSMENT-SLICE-C: API maintenance map and reuse baseline + +date: 2026-06-20 +author: Cline +scope: `context/portal-api-platform-assessment.md`, top-level `pages/api` maintenance structure and existing reuse primitives +type: milestone +rationale: Record the third bounded slice of the Portal Integration Contract & API Platform Assessment by mapping current folder responsibility, feature-to-API maintenance entry points, folder drift, reusable building blocks, and a future-facing consistency baseline without proposing implementation or file movement. +impact: Documentation/context only; improves API findability and maintainability guidance for future work planning; no runtime, auth, storage, email, i18n, or API behaviour change. +status: completed + +Summary: + +- Re-read required Slice C context before inspection: + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/debt-list.md` + - `memory-bank/change-log.md` +- Built a bounded maintenance map for the top-level API areas: + - `endpoint` + - `file` + - `email` + - `documents` + - `auth` + - `admin` + - `middleware` + - top-level utility/meta files +- Recorded that current API maintenance difficulty is driven primarily by: + - folder responsibility overlap + - historical naming drift + - split document/account/auth-support behaviors across multiple areas + - coexistence of older direct-wrapper and newer shared-helper implementation styles +- Added a feature-to-API maintenance map covering major portal areas including: + - search + - case details + - document download + - my portal/dashboard + - watched cases + - representations + - drafts + - submission/finalisation + - account/auth + - email/notifications + - admin/internal reporting +- Identified reusable building blocks already present, including: + - `relayGet(...)` + - `relayGetData(...)` + - `respondSuccess(...)` / `respondError(...)` + - relay policy presets + - signed request client helpers + - hash/path validation helpers + - Azure storage helpers + - logging helpers +- Added a future-facing consistency baseline as guidance only, emphasizing reuse of existing shared helpers for new work where suitable and avoiding blind copying of stable but older direct `axios + hashAPIPath` patterns. + +Validation: + +- Documentation-only assessment; no runtime code changed. +- Non-destructive evidence gathering only: + - small top-level listings of `pages/api` and `pages/api/admin` + - direct representative inspection of: + - `pages/api/admin/getnewappeals_api.js` + - `pages/api/admin/getlatestdocuments_api.js` + - `pages/api/health.js` + - `pages/api/doc.ts` + - `pages/api/notices/index.js` + - `pages/api/middleware/{apiResponse,relayForwarding,relayPolicyPresets,middleware}.js` + - `actions/azurestorage.js` + - `actions/clients/signedRequestClient.js` + - targeted searches across `pages/api` for existing helper and integration-building-block usage + - reuse of already completed bounded evidence from Slices A and B for folder/feature classification +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded step: `Slice D — API Route Ownership & Change-Entry Sample`, focused on a small set of high-value journeys (watched cases, account/personal details, public document retrieval, appeal submission/finalisation) to identify the minimum safe API entry set a maintainer would need to inspect for future changes. + +--- + +### CL-2026-06-20-PORTAL-API-PLATFORM-ASSESSMENT-SLICE-D: route ownership and change-entry sample + +date: 2026-06-20 +author: Cline +scope: `context/portal-api-platform-assessment.md`, bounded journey change-entry mapping across watched cases, account/personal details, public document retrieval, and appeal submission/finalisation +type: milestone +rationale: Record the fourth bounded slice of the Portal Integration Contract & API Platform Assessment by identifying maintainer-first change-entry sets, route ownership, change risk, first-look checklists, and reuse points for four high-value journeys without widening into a full dependency inventory. +impact: Documentation/context only; improves maintainability and change-entry clarity for future API work; no runtime, auth, storage, queue, i18n, or API behaviour change. +status: completed + +Summary: + +- Re-read required Slice D context before inspection: + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Assessed only the requested four journeys: + - watched cases + - account / personal details + - public document retrieval + - appeal submission / finalisation +- Built maintainer-first change-entry sets for each journey, covering: + - primary UI/component entry points + - likely service helpers + - API routes + - shared clients/helpers + - relevant state/store modules + - integrations touched +- Classified route ownership for sampled journey routes using: + - feature-owned + - integration-owned + - orchestration-owned + - helper/support + - ambiguous/historical +- Recorded journey-level change risk: + - watched cases -> high + - account/personal details -> high + - public document retrieval -> medium-high + - appeal submission/finalisation -> very high +- Added short first-look checklists so a maintainer can identify the minimum safe inspection set before changing behavior in each sampled journey. +- Identified reuse points already present for each journey, including relay helpers, response helpers, signing/hash helpers, Azure storage helpers, and logging helpers. + +Validation: + +- Documentation-only assessment; no runtime code changed. +- Non-destructive evidence gathering only: + - targeted bounded searches across `components`, `actions/services`, `pages`, `lib`, and `store` for the four sampled journeys + - direct representative inspection of: + - `components/myportal/viewall.js` + - `pages/account/personaldetails.js` + - `components/account/personaldetails.js` + - `components/search/searchresults.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `components/case/representation/representationComplete.js` + - reuse of already reviewed route/service evidence from prior slices for linked API ownership and helper classification +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded step: `Slice E — API Maintainer Decision Guide Sample`, focused on a small sample of maintenance intents (for example: adding a CRM read route, extending a watched-case mutation, adding a document-related route, or extending a finalisation orchestration path) so preferred reusable patterns can be distinguished from older compatibility-driven ones without proposing implementation. + +--- + +### CL-2026-06-20-PORTAL-API-PLATFORM-ASSESSMENT-SLICE-E: wider API surface pattern validation + +date: 2026-06-20 +author: Cline +scope: `context/portal-api-platform-assessment.md`, top-level API surface validation across wider route families and implementation styles +type: milestone +rationale: Record the fifth bounded slice of the Portal Integration Contract & API Platform Assessment by validating whether the route-family, contract-shape, implementation-style, and folder-drift conclusions from earlier representative slices generalise across the wider API surface without generating a full endpoint inventory. +impact: Documentation/context only; increases confidence that the architecture model is representative of the wider API platform; no runtime, auth, storage, queue, i18n, or API behaviour change. +status: completed + +Summary: + +- Re-read required Slice E context before inspection: + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Performed bounded top-level API folder sizing and validation: + - `endpoint` -> 74 files + - `file` -> 26 files + - `email` -> 6 files + - `documents` -> 1 file + - `auth` -> 2 files + - `admin` -> 5 files + - `middleware` -> 4 files + - top-level files -> 4 +- Confirmed that the earlier API platform model still appears representative: + - `endpoint` remains the dominant historical CRM relay catch-all + - `file` remains the main secondary mixed storage/orchestration folder + - smaller folders (`email`, `documents`, `auth`, `admin`, `middleware`) did not reveal a materially new route family or implementation style +- Validated that the existing route-family model still covers the wider surface, including: + - search/read + - myportal reads + - account/profile + - create/update/delete + - document retrieval + - storage/blob + - queue/finalisation + - notifications + - admin/reporting + - auth/session + - lookup/config/support +- Confirmed that no genuinely new major contract shape was needed beyond the existing set: + - Public CRM read + - User-owned CRM read + - CRM create + - CRM update/patch + - CRM delete + - Proxy/pass-through + - Lookup/config/support + - Hybrid upsert/orchestration +- Confirmed that the implementation-style model still holds across the wider surface: + - helper-oriented relay routes + - direct-wrapper routes + - orchestration routes + - with local/static support routes treated as small support variants, not a separate major style +- Classified folder drift: + - low drift: `documents`, `admin`, `middleware`, top-level utility routes + - low-to-moderate drift: `auth` + - moderate drift: `email` + - high drift: `endpoint`, `file` +- Identified main maintenance hotspots: + - high: `endpoint`, `file` + - medium-high: `auth`, `middleware` + - medium: `email` + - low/medium-low: `documents`, `admin` +- Recorded a high-confidence conclusion that Slices A–D are representative at the **platform pattern level**, even though route-local nuance still exists. + +Validation: + +- Documentation-only assessment; no runtime code changed. +- Non-destructive evidence gathering only: + - bounded top-level API folder-size count pass + - targeted route-name searches across `pages/api`, `pages/api/endpoint`, and `pages/api/file` + - direct representative inspection of selected wider-surface outlier routes: + - `pages/api/endpoint/getmandatoryfields_api.js` + - `pages/api/endpoint/createcrmtask_api.js` + - `pages/api/file/createcaseinvolvement_api.js` + - `pages/api/file/generateappealpdf.js` + - `pages/api/email/getdocuments.js` + - `pages/api/admin/getStatusCountsByAppealAndLPA_api.js` + - comparison against the documented conclusions from Slices A–D +- No full route inventory generated. +- No repo-wide automated analysis performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next bounded step: `Slice F — API Platform Final Synthesis`, consolidating Slices A–E into one stable architecture-level API platform view covering route families, contract shapes, implementation styles, folder drift, maintainer guidance, and final assessment conclusions without proposing implementation. + +--- + +### CL-2026-06-20-PORTAL-API-PLATFORM-ASSESSMENT-SLICE-F: final synthesis and stream closure + +date: 2026-06-20 +author: Cline +scope: `context/portal-api-platform-assessment.md`, `context/architecture.md`, API platform assessment stream closure +type: milestone +rationale: Consolidate Slices A–E into one concise architecture-level API platform view, record the stable route-family / contract-shape / implementation-style model, and formally close the Portal Integration Contract & API Platform Assessment stream. +impact: Documentation/context only; improves long-term maintainability guidance and architectural clarity for future API planning work; no runtime, auth, storage, queue, i18n, or API behaviour change. +status: completed + +Summary: + +- Re-read required Slice F context before synthesis: + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `memory-bank/change-log.md` +- Added a final synthesis section to `context/portal-api-platform-assessment.md` and marked the stream complete. +- Recorded the stable architecture-level API platform model as: + - large route surface + - small route-family vocabulary + - small contract-shape vocabulary + - small implementation-style vocabulary +- Consolidated the stable route-family model: + - CRM relay routes + - storage/blob routes + - finalisation/orchestration routes + - email/notification routes + - document download routes + - auth/session routes + - admin/internal routes + - middleware/helper routes + - local utility/meta routes +- Consolidated the stable contract-shape model: + - Public CRM read + - User-owned CRM read + - CRM create + - CRM update/patch + - CRM delete + - Proxy/pass-through + - Lookup/config/support + - Hybrid upsert/orchestration + - Storage read/write/delete + - Queue/finalisation + - Notify send / notification orchestration +- Consolidated the stable implementation-style model: + - newer helper-oriented + - older direct-wrapper + - orchestration-heavy +- Recorded the stable folder-drift and maintenance-hotspot model: + - highest drift/hotspots remain `endpoint` and `file` + - smaller folders are more coherent but still require sensitivity in `auth` and `middleware` +- Added a maintainer guidance baseline emphasizing: + - identify owning feature/journey first + - identify integration touched + - identify contract-criticality + - prefer existing shared helpers where suitable + - avoid blindly copying older direct-wrapper patterns into new work + - avoid refactoring stable legacy routes without explicit approval and characterization +- Updated `context/architecture.md` so the core architecture reference now includes the completed Portal API Platform Assessment and its stable final model. +- Formally recorded that the Portal Integration Contract & API Platform Assessment should now be considered complete. + +Validation: + +- Documentation-only synthesis and closure; no runtime code changed. +- Non-destructive evidence base reused from completed Slices A–E. +- No new route inventory or automated analysis performed for closure. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Recommended future planning stream only: + - `API Route Map / Maintainer Guide` + - or `API Rationalisation Planning` +- No implementation work is recommended from this assessment alone. + +--- + +### CL-2026-06-20-API-ROUTE-MAP-MAINTAINER-GUIDE: first-generation journey-owned route map + +date: 2026-06-20 +author: Cline +scope: `context/api-route-map.md`, maintainability-focused journey-owned API documentation +type: milestone +rationale: Create a first-generation maintainer guide that converts the completed API platform assessment into a journey-owned route map so future contributors can identify where to start, which APIs/helpers matter, what integrations are touched, and where change risk is highest without reopening discovery. +impact: Documentation/context only; improves API maintainability, findability, and ownership clarity for future planning and change review; no runtime, auth, storage, queue, i18n, or API behaviour change. +status: completed + +Summary: + +- Re-read required context before drafting the route map: + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `context/integration-map.md` + - `memory-bank/change-log.md` +- Added new document: `context/api-route-map.md`. +- Structured the guide around **journey ownership**, not folder ownership. +- Documented the requested major journey catalogue, including: + - Public Search + - Case Details + - Documents + - My Portal Dashboard + - Watched Cases + - Representations + - Draft Appeals + - Draft Representations + - Appeal Submission / Finalisation + - Representation Submission / Finalisation + - Account Registration + - Personal Details / Account Management + - Authentication / Sign-In + - Notifications / Email + - Admin / Reporting +- For each journey, recorded: + - purpose + - primary UI entry points + - service layer helpers + - primary API routes a maintainer should inspect first + - integrations touched + - ownership type + - change risk + - first-look checklist +- Added a shared platform section documenting reusable API building blocks such as: + - `relayGet` + - `relayGetData` + - `respondSuccess` + - `respondError` + - relay policy helpers + - hash helpers + - signed request helpers + - Azure storage helpers + - Notify helpers + - auth/session helpers +- Added maintainer guidance describing the recommended decision sequence when adding a new API: + - which journey owns it + - which integration it touches + - whether an existing route family exists + - whether existing helpers can be reused + - whether the route is contract-critical +- Recorded that the resulting document is sufficient as a **first-generation maintainer guide** and that any future expansion should be treated as a separate planning/documentation stream rather than renewed platform discovery. + +Validation: + +- Documentation-only work; no runtime code changed. +- Non-destructive consolidation based on completed architecture streams and existing API platform conclusions. +- No new discovery or route inventory performed. +- No Python used. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Optional future documentation stream only: + - deeper `API Route Map / Maintainer Guide v2` + - or `API Rationalisation Planning` +- No implementation work recommended from this guide alone. + +--- + ### CL-2026-06-19-ARCHITECTURE-PROGRAMME-MILESTONE: discovery close-out and transition to adoption planning date: 2026-06-19 @@ -55,6 +1093,527 @@ Follow-ups: --- +### CL-2026-06-19-PORTAL-API-SECURITY-FIRST-PASS: endpoint inventory and access-boundary assessment + +date: 2026-06-19 +author: Cline +scope: `pages/api/**`, `context/portal-api-security-boundary-assessment.md` +type: milestone +rationale: Record the first-pass architecture assessment of the portal API security and access boundary, focusing on endpoint inventory, authentication visibility, trust boundaries, and ownership enforcement evidence. +impact: Documentation/context only; improves visibility of API security posture and next-step prioritisation; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Added `context/portal-api-security-boundary-assessment.md` as a concise first-pass assessment of `pages/api/**`. +- Recorded a route-family inventory across auth, endpoint, file, email, documents, admin, and utility API areas. +- Identified the main current architectural concern: + - route-level authentication and ownership enforcement are not consistently visible in sensitive handlers + - many routes trust caller-supplied identifiers such as `loggedInUserId`, `contactId`, `incidentId`, blob/container/path values, and record IDs + - file/blob routes rely heavily on signed hash validation, but that is not equivalent to explicit per-route ownership verification +- Recorded that the only explicit session gate found during the first-pass scan was `pages/api/endpoint/gethash_api.js`, which protects hash issuance for a narrow allowlist of sensitive downstream paths. +- Prioritised the next pass as end-to-end tracing of authenticated hash issuance and downstream consumption in sensitive file/blob and user-owned CRM routes. + +Validation: + +- Documentation-only assessment; no runtime code changed. +- Read required architecture and memory-bank context files before assessment. +- Non-destructive inventory and evidence commands used: + - recursive listing of `pages/api/**` + - route-family count command across `pages/api` + - targeted code searches for session/auth usage and trusted identifiers + - direct review of representative high-risk handlers in auth, file, email, documents, middleware, and endpoint families +- Lint/tests not run because no implementation files were changed. + +Follow-ups: + +- Next assessment pass: trace `gethash_api` consumers and determine whether sensitive downstream routes are effectively session-bound and ownership-bound, or only path-hash protected. + +--- + +### CL-2026-06-19-PORTAL-API-OWNERSHIP-TRACE: session-to-contact and ownership-enforcement trace + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, portal SSR loaders, account/portal/document service flows, selected user-owned API handlers +type: milestone +rationale: Record the ownership-enforcement trace for the Portal API Security & Access Boundary Assessment by identifying how the authenticated session becomes portal identity, CRM contact identity, blob/container identity, and user-owned API access. +impact: Documentation/context only; clarifies current authorization architecture and ownership-enforcement locations; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Extended `context/portal-api-security-boundary-assessment.md` with an ownership-enforcement trace focused on: + - session -> email -> portal user -> CRM contact flow + - split identity model between CRM contact ownership and blob/container ownership + - the role of `pinsUser` cookie as a CRM-contact shortcut in some SSR flows + - location mapping for where ownership is established vs merely propagated +- Documented that ownership enforcement is not primarily route-local in sampled APIs. +- Recorded the dominant pattern as: + - SSR/page loaders establish identity from session and/or cookie + - service helpers propagate that identity into API calls + - CRM ownership is often expressed as query filtering by contact ID + - blob ownership is often expressed as container scoping by `session.user.id` +- Traced high-risk flows end-to-end: + - My Cases + - My Representations + - Watched Cases + - Draft Appeals + - Draft Representations + - Document Retrieval +- Concluded that ownership enforcement is partially centralized at session/bootstrap time but operationally distributed across SSR loaders, cookies, query construction, CRM filtering, and blob container naming. + +Validation: + +- Documentation-only assessment update; no runtime code changed. +- Non-destructive evidence gathering performed via targeted searches and direct code review of: + - `lib/auth/resolveMyPortalAuthContext.js` + - `lib/representation/pageLoaders.js` + - `lib/newappeal/loadNewAppealPage.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `pages/myportal/index.js` + - `pages/myportal/case/[ticketnumber].js` + - `pages/myportal/representation.js` + - `actions/services/{accountDirectService,portalDirectService,documentDirectService}.js` + - `pages/api/endpoint/createwatchedcases_api.js` + - previously reviewed user-owned API handlers from the first-pass assessment +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next assessment pass: trace the provenance and lifecycle of the `pinsUser` cookie and compare it against the `getPortalLogin(session.user.email)` path to determine which identity source is canonical and where divergence risk exists. + +--- + +### CL-2026-06-19-PORTAL-API-PINSUSER-TRACE: cookie identity provenance and comparison with CRM contact resolution + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, `pinsUser` cookie lifecycle, SSR loaders, homepage/login routing, account/search/myportal flows +type: milestone +rationale: Record the architectural role of `pinsUser` by tracing where it is created, refreshed, cleared, and consumed, and by comparing it to the canonical session-to-CRM-contact identity path. +impact: Documentation/context only; clarifies current identity and trust-boundary model; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Extended the assessment with a dedicated `pinsUser` trace covering: + - cookie contents + - creation source + - overwrite/refresh behavior + - clear/delete paths + - consumption across SSR and client flows +- Determined that `pinsUser` stores a CRM `contactid` value. +- Identified the primary creation path in `pages/index.js`: + - `NextAuth session` -> `session.user.email` -> `getPortalLogin(email)` -> CRM `contactid` -> `setCookie("pinsUser", contactid)` +- Identified that later flows frequently consume `pinsUser` directly without re-resolving CRM contact from the current session. +- Recorded the architectural conclusion that `pinsUser` behaves as: + - a cached CRM contact identity + - a convenience shortcut + - and likely a compatibility mechanism in cookie-driven flows + rather than the canonical business authorization authority. +- Confirmed that CRM Contact remains the strongest visible business authorization boundary for CRM-owned portal data, while `session.user.id` remains the visible ownership boundary for blob/draft flows. + +Validation: + +- Documentation-only assessment update; no runtime code changed. +- Non-destructive evidence gathering performed via targeted searches and direct review of: + - `pages/index.js` + - `pages/account/personaldetails.js` + - `components/search/searchresults.js` + - `components/search/addresssearchresults.js` + - `lib/auth/sessionClient.js` + - `lib/auth/resolveMyPortalAuthContext.js` + - `lib/newappeal/loadNewAppealPage.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `pages/error.js` + - `pages/_error.js` + - `pages`/`components` search results for `pinsUser`, `setCookie`, `destroyCookie`, and `parseCookies` +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next assessment pass: trace registration and post-registration bootstrap to determine exactly how a NextAuth-authenticated user becomes a CRM Contact and where dashboard authorization is granted after contact creation. + +--- + +### CL-2026-06-19-PORTAL-API-ACCOUNT-MUTATION-TRACE: account/profile read and mutation boundary assessment + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, account/profile pages, account direct services, account endpoint handlers +type: milestone +rationale: Record the account/profile mutation assessment by tracing how account pages establish identity, how personal details are read and updated, whether email can diverge from session identity, and whether account/password APIs re-bind caller-supplied contact IDs to the authenticated session. +impact: Documentation/context only; clarifies account/profile authorization posture and legacy-route risk; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Extended `context/portal-api-security-boundary-assessment.md` with a dedicated account/profile mutation trace. +- Documented that normal account entry is session-gated at page level, but account identity is typically established earlier during dashboard SSR bootstrap: + - `getSession(ctx)` + - `getPortalLogin(session.user.email)` + - CRM `contactid` + - `getPersonalAccount(contactid)` + - Redux `loggedinUserId` / `accountDetails` +- Recorded that the live personal-details flow reads and mutates CRM contacts using a Redux-held `loggedinUserId`, then passes that ID into: + - `/api/endpoint/getpersonalaccount_api?contactid=...` + - `/api/endpoint/updateaccount_api?contactId=...` +- Confirmed that sampled account read/write routes do not visibly: + - resolve current session server-side + - resolve CRM contact from `session.user.email` + - compare caller-supplied `contactId/contactid` to a session-derived CRM contact +- Confirmed that the reviewed personal-details UI does not allow direct email editing because `emailaddress1` is rendered disabled, but also recorded that `updateaccount_api.js` forwards request bodies to CRM without a visible field allowlist or email/session reconciliation step. +- Confirmed that the live change-password UI does not use `updatepassword_api.js`; it writes `pinswg_custom_password` through `updateaccount_api.js`, making the standalone password route appear exposed but likely legacy/inconsistent with passwordless NextAuth. +- Concluded that account/profile mutation is strongest at session/bootstrap derivation time but weaker at the final API route boundary, where caller-supplied contact IDs are trusted. + +Validation: + +- Documentation-only assessment update; no runtime code changed. +- Non-destructive evidence gathering performed via targeted searches and direct review of: + - `pages/account/personaldetails.js` + - `pages/account/changepassword.js` + - `components/account/{personaldetails,personaldetailsCheck,personaldetailsComplete,changepassword}.js` + - `components/myportal/youraccount.js` + - `pages/index.js` + - `pages/myportal/index.js` + - `actions/services/accountDirectService.js` + - `pages/api/endpoint/{getpersonalaccount_api,updateaccount_api,updatepassword_api,getemailaccountcheck_api,getportallogin_api}.js` + - `store/accountDetails/{action,reducer}.js` +- Additional targeted searches performed for `setLoggedInUserId`, `setAccountDetails`, `updatePassword`, `pinswg_custom_password`, and `updatepassword_api` usage. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next assessment pass: trace other user-owned CRM mutation routes to determine whether the caller-supplied contact-ID trust pattern repeats across watchlists, involvements, representations, and case mutation endpoints. + +--- + +### CL-2026-06-19-PORTAL-API-USER-OWNED-MUTATION-SURFACE-TRACE: systemic caller-ID trust across watchlists, cases, involvements, and representations + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, watchlist/case/involvement/representation mutation routes, related service helpers, and completion/finalisation flows +type: milestone +rationale: Extend the security boundary assessment beyond account/profile routes to determine whether caller-supplied CRM identity and record-ID trust is isolated or systemic across other user-owned CRM mutation families. +impact: Documentation/context only; clarifies systemic authorization-boundary posture for user-owned CRM mutations; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Extended `context/portal-api-security-boundary-assessment.md` with a dedicated user-owned CRM mutation route trace covering: + - watchlist create/update and delete + - case/appeal create, update, and patch + - case and representation involvement creation + - representation delete and completion/finalisation side effects + - blob-side draft helper variants where relevant +- Added a mutation-route inventory table recording, for each route: + - purpose + - primary callers/helpers + - accepted identifiers + - CRM entity affected + - whether the route reads session + - whether it resolves CRM contact from `session.user.email` + - whether it trusts caller-supplied contact/record identifiers + - whether it performs visible ownership checks before mutation + - a classification label +- Confirmed the account/profile pattern is not isolated: + - contact-ID-bound caller trust appears in watchlist upsert, case creation, case involvement creation, and representation involvement creation + - record-ID-bound mutation without visible ownership proof appears in watchlist delete, representation delete, case update, and case patch routes +- Recorded that some flows perform duplicate/existence checks or CRM filtering using supplied IDs, but these checks do not amount to independent user-ownership verification. +- Concluded that the current mutation authorization model is distributed and upstream-heavy: + - session-to-contact derivation commonly happens in SSR/UI/bootstrap layers + - service helpers propagate those identifiers + - final mutation routes frequently trust supplied IDs directly rather than re-binding them to the authenticated session + +Validation: + +- Documentation-only assessment update; no runtime code changed. +- Non-destructive evidence gathering performed via targeted searches and direct review of: + - `pages/api/endpoint/{createwatchedcases_api,deletewatchedcases_api,createcase_api,updatecase_api,patchcase_api,deletemyrepresentations_api}.js` + - `pages/api/file/{createcaseinvolvement_api,createrepinvolvement_api,createappealcompletemessage_api,createrepcompletemessage_api,createcase_api,updatecase_api}.js` + - `actions/services/{portalDirectService,caseDirectService}.js` + - `components/{case/summary,myportal/viewall,newappeal/createCase,newappeal/buildsection,case/representation/representationComplete}.js` + - `lib/newappeal/journeyEffects.js` +- Additional targeted searches performed across `pages/api`, `actions/services`, `components`, and `lib` for mutation helpers and identifier propagation (`contactid`, `contactId`, `loggedInUserId`, `watchedCaseID`, `myRepresentationsID`, `incidentid`, mutation helper names). +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next assessment pass: trace `gethash_api` and signed-route consumption together to determine whether signed hash issuance materially strengthens mutation authorization boundaries or remains an integrity-only control layered on top of caller-supplied identity trust. + +--- + +### CL-2026-06-19-PORTAL-API-HASH-WATCHLIST-DELETE-VERTICAL-SLICE: request integrity vs referential ownership verification + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, `gethash_api`, signing helpers, watchlist delete route, and watched-case delete UI/service flow +type: milestone +rationale: Perform a focused vertical-slice assessment of signed hash issuance and watchlist deletion to determine whether the hash mechanism materially strengthens the mutation boundary beyond request/identifier integrity. +impact: Documentation/context only; clarifies what the signed-hash model does and does not visibly protect; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Extended `context/portal-api-security-boundary-assessment.md` with a dedicated vertical-slice trace for: + - `pages/api/endpoint/gethash_api.js` + - client signing helpers (`relayClient`, `signedRequestClient`) + - watchlist delete service + UI flows + - `pages/api/endpoint/deletewatchedcases_api.js` +- Recorded the visible hash issuance model as: + - authenticated session required to obtain a hash + - allowlisted route-prefix check in `gethash_api` + - hash generated from the full raw query path (`hashAPIPath(rawQueryPath)`) + - signed URL built client-side as `queryUrl + hash` +- Recorded that in the watchlist delete flow, the signed query path includes `watchedCaseID`, so the hash visibly protects: + - route integrity + - query/parameter integrity + - identifier integrity for `watchedCaseID` +- Recorded that the reviewed code does **not visibly** show hash issuance encoding CRM ownership or object authorization state. +- Traced watchlist deletion end-to-end and concluded that, in the reviewed flow: + - `watchedCaseID` originates from previously loaded watchlist data in normal portal UI flows + - delete route performs direct CRM delete by record ID + - no route-local referential ownership verification was visible before delete +- Classified the current watchlist delete design as **provenance-based** rather than relationship-verified. + +Validation: + +- Documentation-only assessment update; no runtime code changed. +- Non-destructive evidence gathering performed via direct review of: + - `pages/api/endpoint/gethash_api.js` + - `actions/clients/relayClient.js` + - `actions/clients/signedRequestClient.js` + - `actions/services/portalDirectService.js` + - `pages/api/endpoint/deletewatchedcases_api.js` + - `components/myportal/topthree.js` + - `components/myportal/viewall.js` + - `components/case/summary.js` + - `components/search/searchresults.js` +- Additional targeted searches performed for `gethash_api`, `buildHashedQueryUrl`, `buildSignedUrl`, `deleteWatchedCases`, `watchedCaseID`, and `pinswg_watchlistid` across `actions`, `components`, and `pages/api`. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next assessment pass: perform the same integrity-vs-authorization vertical slice for `deletemyrepresentations_api` and one blob/container mutation route to compare whether the provenance-based signed-request model is consistent across CRM-record and blob/file deletion paths. + +--- + +### CL-2026-06-19-PORTAL-API-DRAFT-STORAGE-OWNERSHIP-ASSESSMENT: session user ID to container boundary + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, draft appeal/representation loaders, storage service helpers, Azure storage helpers, and file/blob draft APIs +type: milestone +rationale: Assess the draft-storage ownership boundary by tracing how `session.user.id` becomes container identity, how draft data/files are created/read/deleted/submitted, and whether storage operations remain container-scoped through the queue handoff to CRM creation. +impact: Documentation/context only; clarifies pre-submission storage ownership architecture and the storage-to-CRM ownership transition; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Extended `context/portal-api-security-boundary-assessment.md` with a dedicated draft storage ownership assessment covering: + - container identity generation + - draft appeal and draft representation creation/resume flows + - file upload/download/list/delete storage routes + - draft delete flows + - submission/finalisation queue handoff +- Confirmed the strongest visible upstream storage ownership root is: + - `NextAuth session.user.id -> container identity` +- Recorded that SSR/page-loader flows consistently use `session.user.id` as the container identity for draft reads and store hydration. +- Confirmed many file/blob APIs accept `container` / `containerID` as caller-supplied inputs and do not visibly re-derive container identity from session inside the reviewed handlers. +- Classified the overall draft-storage model as **hybrid**: + - upstream session-derived container ownership + - downstream caller-supplied container/path values + - hash-protected blob-path integrity +- Identified the clearest ownership transition point as queue/finalisation: + - storage-owned draft + - completion route + - queue message carrying container/storage paths + - downstream CRM submitted-record creation + +Validation: + +- Documentation-only assessment update; no runtime code changed. +- Non-destructive evidence gathering performed via targeted searches and direct review of: + - `lib/newappeal/loadNewAppealPage.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `lib/representation/pageLoaders.js` + - `actions/services/documentDirectService.js` + - `actions/azurestorage.js` + - `pages/api/file/{getprogressobjblob,getbloblist,getawaitingsubmissionfromblob,upload,uploadsinglefile,deleteblobcase,deleteblobrep,downloadblob,setupcontainer,createappealcompletemessage_api,createrepcompletemessage_api,editRepJson}.js` +- Additional searches performed across `lib`, `pages`, `actions`, and `pages/api/file` for `session.user.id`, `containerID`, `getProgressFromBlob`, `getFilesFromBlob`, `getRepsFromBlob`, `getAwaitingSubmissionFromBlob`, and queue helper names. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next assessment pass: perform a focused route-local verification pass on the highest-value storage APIs to determine whether any of them derive container ownership from session server-side elsewhere in the stack, or whether they rely entirely on upstream container provenance plus hash-protected path integrity. + +--- + +### CL-2026-06-19-PORTAL-API-AUTHORIZATION-ARCHITECTURE-MODEL: consolidated authorization roots, patterns, integrity controls, and risk framing + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, `memory-bank/change-log.md` +type: milestone +rationale: Consolidate the completed portal API assessment passes into one PEDW authorization architecture model that clearly distinguishes authorization roots, ownership patterns, integrity controls, proven findings, and non-proven findings without widening scope into a new endpoint review. +impact: Documentation/context only; improves architectural clarity, auditability, and future review discipline; no runtime, contract, auth, storage, relay, or i18n behaviour change. +status: completed + +Summary: + +- Added a dedicated synthesis section to `context/portal-api-security-boundary-assessment.md` titled **PEDW authorization architecture model**. +- Consolidated the completed investigations into four visible authorization roots: + - anonymous public + - NextAuth session + - CRM Contact (`session.user.email -> getPortalLogin(email) -> CRM Contact`) + - storage container (`session.user.id -> user-specific Azure Storage container`) +- Added a pattern catalogue describing the main visible ownership / authorization models: + - public anonymous + - CRM contact scoped + - CRM relationship scoped + - record-ID provenance-based + - storage container scoped +- Added an integrity controls section clarifying that the signed hash most clearly protects route/query/path integrity and identifier integrity, and should be understood as an **integrity control rather than object-authorization control**. +- Recorded the server-mediated execution model for storage and relay paths: + - PEDW API -> Azure SDK -> Azure Storage + - PEDW API -> signed path hash -> Azure Relay -> CRM +- Added explicit synthesis of what has been proven: + - distributed authorization model + - route-local authorization not consistently visible + - ownership usually established upstream and identifiers then propagated + - signed hash strengthens integrity rather than object ownership proof + - storage is server-mediated rather than direct browser-to-storage +- Added explicit synthesis of what has not been proven: + - no confirmed exploitability + - no demonstrated User A -> User B mutation + - no evidence invalidating existing health-check / OWASP / pentest posture + - no evidence of direct browser-to-CRM or direct browser-to-storage exposure +- Added high-level risk framing as: + - architectural integrity / auditability risk + - maintainability risk + - future-change risk + - not currently a confirmed exploitable vulnerability + +Validation: + +- Documentation-only synthesis update; no runtime code changed. +- Consolidation based on previously completed evidence in the assessment document covering: + - endpoint inventory and risk classification + - ownership-enforcement trace + - `pinsUser` lifecycle and identity comparison + - registration/bootstrap trace + - account/profile mutation trace + - user-owned CRM mutation trace + - signed-hash watchlist delete vertical slice + - draft storage ownership assessment +- No new endpoint review performed for this synthesis step. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Next step: use this authorization architecture model as the baseline for future assessment/change review, and only if implementation work is later explicitly approved, consider narrow helper/guard patterns that re-bind selected sensitive CRM mutation routes to session-derived CRM contact and selected storage mutation routes to session-derived container identity while preserving existing behaviour and successful test posture. + +--- + +### CL-2026-06-19-PORTAL-API-SECURITY-STREAM-CLOSURE: formal close-out of authorization architecture assessment + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, `context/architecture.md`, `memory-bank/change-log.md` +type: milestone +rationale: Formally close the Portal API Security & Access Boundary Assessment stream and record the resulting stable architectural conclusions, programme status, and future framing in core project documentation. +impact: Documentation/context only; clarifies that the stream is complete and records the final authorization architecture model for future review work; no runtime, contract, auth, relay, storage, or i18n behaviour change. +status: completed + +Summary: + +- Updated `context/portal-api-security-boundary-assessment.md` with a dedicated **Programme status** section stating: + - `Portal API Security & Access Boundary Assessment` + - `Status: COMPLETE` +- Recorded stable stream conclusions in the assessment document: + - PEDW uses a distributed authorization model + - authorization is generally established upstream and propagated through later flows + - integrity controls are meaningful but distinct from route-local object-authorization proof + - no confirmed exploitability has been demonstrated by the assessment +- Updated `context/architecture.md` to reflect that the portal authorization assessment stream is complete and to record the consolidated authorization architecture model as part of the architecture reference. +- Added architecture-level conclusions covering: + - principal authorization roots (public anonymous, CRM-owned, draft/storage-owned) + - signed hash as an integrity control rather than object-authorization control + - server-mediated storage execution via PEDW API and Azure SDK + - relay path validation model via PEDW API and Azure Relay + - proven vs not-proven findings + - risk characterisation as architectural integrity/auditability/maintainability/future-change risk +- Recorded that no immediate remediation programme is recommended and that any future work should be framed as authorization hardening, consistency improvement, and maintainability improvement rather than emergency remediation. + +Validation: + +- Documentation-only closure update; no runtime code changed. +- Reviewed existing assessment synthesis in `context/portal-api-security-boundary-assessment.md` before adding stream close-out language. +- Reviewed `context/architecture.md` and aligned the architecture reference with the now-complete authorization assessment stream. +- Reviewed `memory-bank/open-questions.md`; no update made because no genuinely new unresolved architecture/security question emerged from closure work. +- Lint/tests not run because this was documentation-only work. + +Follow-ups: + +- Portal authorization architecture stream should now be considered closed. +- Recommended next architecture stream: authorization hardening / consistency planning, using the completed authorization architecture model as the baseline and only proceeding to implementation work by explicit approval. + +--- + +### CL-2026-06-19-PORTAL-API-REGISTRATION-TRACE: session-to-CRM-contact registration and dashboard bootstrap + +date: 2026-06-19 +author: Cline +scope: `context/portal-api-security-boundary-assessment.md`, registration pages/components, account service helpers, `createaccount_api`, homepage signed-in bootstrap +type: milestone +rationale: Record how the portal transitions an authenticated NextAuth user with no CRM contact into a CRM-contact-backed portal user, and identify where dashboard access becomes valid. +impact: Documentation/context only; improves architectural clarity around registration, identity binding, and dashboard authorization bootstrap; no runtime, contract, auth, or i18n behaviour change. +status: completed + +Summary: + +- Extended the assessment with a dedicated registration and post-registration bootstrap trace. +- Identified the primary registration entry condition in `pages/index.js`: + - signed-in session exists + - `getPortalLogin(session.user.email)` returns no CRM contact + - user is redirected to `/account/register` +- Traced the registration flow across: + - `pages/account/register.js` + - `components/account/registerform.js` + - `components/account/registerCheck.js` + - `components/account/registerComplete.js` + - `actions/services/accountDirectService.createAccount` + - `pages/api/endpoint/createaccount_api.js` +- Determined that registration uses the authenticated session email as the intended identity source: + - `loggedInUserEmail` comes from `getServerSideProps` + - form `emailaddress1` is prefilled from session email + - form email field is disabled in the reviewed UI +- Determined that CRM contact creation is performed by forwarding the form body directly to CRM `contacts` via `createaccount_api`, with `pinswg_typeofinvolvement` set client-side in the registration completion flow. +- Determined that dashboard access is not visibly granted immediately by registration completion itself. +- Instead, dashboard access becomes valid after a subsequent signed-in bootstrap pass on `/` re-runs `getPortalLogin(session.user.email)`, finds the newly created contact, sets `pinsUser`, and redirects to `/myportal`. + +Validation: + +- Documentation-only assessment update; no runtime code changed. +- Non-destructive evidence gathering performed via targeted searches and direct review of: + - `pages/index.js` + - `pages/account/register.js` + - `components/account/registerform.js` + - `components/account/registerCheck.js` + - `components/account/registerComplete.js` + - `actions/services/accountDirectService.js` + - `pages/api/endpoint/createaccount_api.js` + - `pages/api/endpoint/getemailaccountcheck_api.js` + - `pages/api/endpoint/getportallogin_api.js` +- No lint/tests run because this was documentation-only work. + +Follow-ups: + +- Next assessment pass: trace account/profile mutation flows after registration to determine whether CRM-contact-backed users mutate account data through session-derived identity, `pinsUser`, or caller-supplied contact IDs. + +--- + ### CL-2026-06-18-CRM-CASE-PROGRESS-CONTEXT: record CRM Case Progress Display investigation conclusions date: 2026-06-18