From 94b19f96978dfe3af90b7971c085ca385c8516d3 Mon Sep 17 00:00:00 2001 From: Robert Bond Date: Thu, 25 Jun 2026 09:37:01 +0000 Subject: [PATCH] Merged PR 2432: Add myportal API grouping facade read slice Add myportal API grouping facade read slice Related work items: #23754 --- actions/services/caseDirectService.js | 2 +- actions/services/portalDirectService.js | 4 +- context/api-grouping-adoption-roadmap.md | 48 ++++++ memory-bank/change-log.md | 78 +++++++++ pages/api/myportal/get-my-cases.js | 5 + pages/api/myportal/get-my-lpa-cases.js | 5 + .../api/myportal/get-portal-module-details.js | 5 + ...pi-grouping-facade-myportal-reads.test.cjs | 162 ++++++++++++++++++ 8 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 pages/api/myportal/get-my-cases.js create mode 100644 pages/api/myportal/get-my-lpa-cases.js create mode 100644 pages/api/myportal/get-portal-module-details.js create mode 100644 tests/phase22/api-grouping-facade-myportal-reads.test.cjs diff --git a/actions/services/caseDirectService.js b/actions/services/caseDirectService.js index 89e2bc9f..04c4649d 100644 --- a/actions/services/caseDirectService.js +++ b/actions/services/caseDirectService.js @@ -251,7 +251,7 @@ export const getAppealPDFDocument = async (incidentid) => { }; export const getPortalModuleDetails = async (appealType, caseReference) => { - const route = buildFileQuery("/api/endpoint/getportalmoduledetails_api", { + const route = buildFileQuery("/api/myportal/get-portal-module-details", { appealType, caseReference: encodeURI(caseReference) }); diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 2db90b28..6e315a41 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -12,7 +12,7 @@ import { } from "../clients/fileRouteBuilder"; export const getMyCases = (loggedInUserId) => { - const route = buildFileQuery("/api/endpoint/getmycases_api", { + const route = buildFileQuery("/api/myportal/get-my-cases", { loggedInUserId }); @@ -32,7 +32,7 @@ export const getMyInvolvements = async (loggedInUserId) => { }; export const getMyLPACases = (lpaid) => { - const route = buildFileQuery("/api/endpoint/getmylpacases_api", { + const route = buildFileQuery("/api/myportal/get-my-lpa-cases", { lpaid }); diff --git a/context/api-grouping-adoption-roadmap.md b/context/api-grouping-adoption-roadmap.md index d1b4d8cd..41f5043d 100644 --- a/context/api-grouping-adoption-roadmap.md +++ b/context/api-grouping-adoption-roadmap.md @@ -371,6 +371,54 @@ UI journey - legacy endpoint handlers remain canonical - no route migration, deletion, or contract change has occurred +#### Additional vertical-slice proof point — My Portal dashboard read / enrichment + +- The active My Portal dashboard read / enrichment journey now has a fifth additive grouping slice for the proven CRM-backed dashboard read/enrichment family only: + - grouped façade routes exist under `pages/api/myportal/` for the proven active reads: + - `get-my-cases` + - `get-my-lpa-cases` + - `get-portal-module-details` + - the existing owning service-layer adoption is split across the current dashboard read services: + - `actions/services/portalDirectService.js` now targets grouped myportal routes for: + - `getMyCases` + - `getMyLPACases` + - `actions/services/caseDirectService.js` now targets the grouped myportal route for: + - `getPortalModuleDetails` + - legacy endpoint handlers remain present and canonical behind the façade: + - `pages/api/endpoint/getmycases_api.js` + - `pages/api/endpoint/getmylpacases_api.js` + - `pages/api/endpoint/getportalmoduledetails_api.js` +- The bounded audit established that the true active My Portal dashboard read/enrichment slice is narrower than the whole dashboard-related API surface: + - `pages/myportal/index.js` is the principal authenticated dashboard loader + - the active CRM read branch on that page is: + - `getMyCases(loggedInUser)` or `getMyLPACases(lpaId)` + - `getPortalModuleDetails(collectionName, caseID)` for detail-card enrichment fan-out + - watched cases are already covered by the subscriptions slice through `getWatchedCases(loggedInUser)` + - representation and awaiting-submission lists on this loader are blob-backed in the active path: + - `getRepsFromBlob(thisSession.user.id)` + - `getAwaitingSubmissionFromBlob(thisSession.user.id)` + - account/bootstrap/auth routes remain upstream supporting concerns rather than part of this grouped read slice: + - `getPortalLogin(...)` + - `getPersonalAccount(...)` + - `getSession(ctx)` +- The following routes were intentionally left outside this grouped adoption scope: + - `getwatchedcases_api.js` + - `getmyrepresentations_api.js` + - `getawaitingsubmission_api.js` + - storage/draft routes under `pages/api/file/**` + - account/auth/session routes +- Exclusion reasoning: + - watched-case reads are already owned by the completed subscriptions slice + - `getmyrepresentations_api.js` and `getawaitingsubmission_api.js` were candidate routes to investigate, but they are not part of the proven active `pages/myportal/index.js` read path, which currently uses blob-backed representation and awaiting-submission retrieval instead + - storage/blob routes remain explicitly out of scope for this read-only CRM façade slice + - account/bootstrap/auth concerns remain out of scope by requirement and because they are not journey-owned dashboard CRM reads +- For this My Portal slice, a complete vertical grouping slice means: + - the proven active CRM dashboard read/enrichment family has grouped façade routes + - current owning service calls for that family target the grouped routes + - subscriptions, storage/draft, and account/auth routes remain unchanged + - legacy endpoint handlers remain canonical + - no route migration, deletion, contract change, auth change, storage change, or behaviour change has occurred + --- ### Stage 3 — Documentation Maturity diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index ecf73dd9..e953b3b4 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -20,6 +20,84 @@ Follow-ups: ### CL-2026-06-25-API-GROUPING-FACADE-SEARCH-VERTICAL-SLICE: active public search results grouping and service adoption +### CL-2026-06-25-API-GROUPING-FACADE-MYPORTAL-READS-VERTICAL-SLICE: bounded myportal dashboard CRM read/enrichment grouping and service adoption + +date: 2026-06-25 +author: Cline +scope: `pages/api/myportal/get-my-cases.js`, `pages/api/myportal/get-my-lpa-cases.js`, `pages/api/myportal/get-portal-module-details.js`, `actions/services/portalDirectService.js`, `actions/services/caseDirectService.js`, `tests/phase22/api-grouping-facade-myportal-reads.test.cjs`, `context/api-grouping-adoption-roadmap.md` +type: change +rationale: Implement the next additive API grouping façade vertical slice for the bounded My Portal dashboard read / enrichment journey by grouping only the proven active CRM read/enrichment routes under `pages/api/myportal/` and adopting those grouped routes in the existing owning service layers without changing dashboard storage, auth, watched-case, submission, or account behaviour. +impact: Runtime behaviour is intended to remain unchanged; grouped myportal façade routes now cover the proven active dashboard CRM read/enrichment family, legacy endpoint handlers remain canonical, watched-case behaviour remains owned by subscriptions, and no route migration, deletion, contract change, CRM query change, auth/session change, storage/draft change, or submission/orchestration change has occurred. +status: completed + +Summary: + +- Confirmed the required context was read before implementation: + - `context/api-grouping-plan.md` + - `context/api-grouping-adoption-roadmap.md` + - `context/api-route-map.md` + - `context/journey-architecture-map.md` + - `context/architecture.md` + - `memory-bank/change-log.md` +- Performed a bounded audit of the active My Portal dashboard read / enrichment journey across the dashboard loader, service layers, and endpoint usage. +- Audit findings established that: + - the active entry page is `pages/myportal/index.js` + - the active owning service layers for the proven CRM read/enrichment path are: + - `actions/services/portalDirectService.js` for `getMyCases` / `getMyLPACases` + - `actions/services/caseDirectService.js` for `getPortalModuleDetails` + - the active CRM-backed dashboard path is: + - `getMyCases(loggedInUser)` or `getMyLPACases(lpaId)` + - detail-card enrichment fan-out through `getPortalModuleDetails(collectionName, caseID)` + - watched cases are already handled by the earlier subscriptions façade slice and were left alone + - representation and awaiting-submission data on the active dashboard loader are blob-backed in the current path via: + - `getRepsFromBlob(thisSession.user.id)` + - `getAwaitingSubmissionFromBlob(thisSession.user.id)` + - `getmyrepresentations_api.js` and `getawaitingsubmission_api.js` were investigated but were not proven active in the bounded dashboard loader path for this slice + - account/auth/bootstrap routes (`getPortalLogin`, `getPersonalAccount`, `getSession`) remain upstream support concerns and out of scope +- Added grouped façade routes under `pages/api/myportal/` for the proven active CRM dashboard family only: + - `get-my-cases.js` + - `get-my-lpa-cases.js` + - `get-portal-module-details.js` +- Implemented each façade as the smallest safe compatibility wrapper: + - import the existing legacy endpoint handler + - delegate `req` and `res` directly to that existing handler +- Updated only the existing owning service call sites needed for the bounded slice: + - `actions/services/portalDirectService.js` + - `getMyCases` + - `getMyLPACases` + - `actions/services/caseDirectService.js` + - `getPortalModuleDetails` +- Left the following unchanged: + - legacy endpoint handlers + - watched-case grouped subscriptions routes + - representation routes + - awaiting-submission routes + - storage/blob routes + - account/profile routes + - auth/session behaviour + - CRM queries and transforms + - query parameter names and response contracts +- Added focused characterization coverage to prove: + - grouped myportal façade routes delegate correctly + - legacy endpoint handlers remain present + - adopted service methods now target grouped myportal routes + - adopted methods no longer target the legacy endpoint URLs directly + - request-contract cues remain unchanged + - excluded route families remain outside this slice +- Updated the adoption roadmap to record the audited scope boundary, included routes, excluded routes, service adoption status, and behaviour-preservation notes for the My Portal dashboard read / enrichment slice. + +Validation: + +- Focused validation only intended for this slice: + - `npx eslint actions/services/portalDirectService.js actions/services/caseDirectService.js pages/api/myportal/*.js tests/phase22/api-grouping-facade-myportal-reads.test.cjs` + - `node tests/phase22/api-grouping-facade-myportal-reads.test.cjs` +- No repository-wide validation run. + +Follow-ups: + +- The bounded My Portal dashboard read / enrichment family is now a complete façade grouping slice only for the proven active CRM dashboard read/enrichment routes adopted here. +- Adjacent subscriptions, storage/draft, representation, awaiting-submission, and account/auth support routes remain intentionally outside this slice and should only be considered in separate bounded audits if needed. + ### CL-2026-06-25-API-GROUPING-FACADE-CASE-DETAILS-VERTICAL-SLICE: bounded case-details read grouping and service adoption date: 2026-06-25 diff --git a/pages/api/myportal/get-my-cases.js b/pages/api/myportal/get-my-cases.js new file mode 100644 index 00000000..38eafa8e --- /dev/null +++ b/pages/api/myportal/get-my-cases.js @@ -0,0 +1,5 @@ +import legacyGetMyCasesHandler from "../endpoint/getmycases_api"; + +export default async function getMyCasesFacade(req, res) { + return legacyGetMyCasesHandler(req, res); +} diff --git a/pages/api/myportal/get-my-lpa-cases.js b/pages/api/myportal/get-my-lpa-cases.js new file mode 100644 index 00000000..dae75ee6 --- /dev/null +++ b/pages/api/myportal/get-my-lpa-cases.js @@ -0,0 +1,5 @@ +import legacyGetMyLpaCasesHandler from "../endpoint/getmylpacases_api"; + +export default async function getMyLpaCasesFacade(req, res) { + return legacyGetMyLpaCasesHandler(req, res); +} diff --git a/pages/api/myportal/get-portal-module-details.js b/pages/api/myportal/get-portal-module-details.js new file mode 100644 index 00000000..7f0acfcf --- /dev/null +++ b/pages/api/myportal/get-portal-module-details.js @@ -0,0 +1,5 @@ +import legacyGetPortalModuleDetailsHandler from "../endpoint/getportalmoduledetails_api"; + +export default async function getPortalModuleDetailsFacade(req, res) { + return legacyGetPortalModuleDetailsHandler(req, res); +} diff --git a/tests/phase22/api-grouping-facade-myportal-reads.test.cjs b/tests/phase22/api-grouping-facade-myportal-reads.test.cjs new file mode 100644 index 00000000..a9464293 --- /dev/null +++ b/tests/phase22/api-grouping-facade-myportal-reads.test.cjs @@ -0,0 +1,162 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const { loadModule, createRes } = require("../phase21/_shared.cjs"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +const read = (relativePath) => + fs.readFileSync(path.join(rootDir, relativePath), "utf8"); + +test("myportal get-my-cases facade delegates to legacy handler", async () => { + const calls = []; + const mod = loadModule("pages/api/myportal/get-my-cases.js", { + legacyGetMyCasesHandler: async (req, res) => { + calls.push({ req, res }); + return res.status(200).json({ ok: true, route: "my-cases" }); + } + }); + + const req = { method: "GET", query: { loggedInUserId: "user-1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].req, req); + assert.strictEqual(calls[0].res, res); +}); + +test("myportal get-my-lpa-cases facade delegates to legacy handler", async () => { + const calls = []; + const mod = loadModule("pages/api/myportal/get-my-lpa-cases.js", { + legacyGetMyLpaCasesHandler: async (req, res) => { + calls.push({ req, res }); + return res.status(200).json({ ok: true, route: "my-lpa-cases" }); + } + }); + + const req = { method: "GET", query: { lpaid: "lpa-1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].req, req); + assert.strictEqual(calls[0].res, res); +}); + +test("myportal get-portal-module-details facade delegates to legacy handler", async () => { + const calls = []; + const mod = loadModule("pages/api/myportal/get-portal-module-details.js", { + legacyGetPortalModuleDetailsHandler: async (req, res) => { + calls.push({ req, res }); + return res + .status(200) + .json({ ok: true, route: "portal-module-details" }); + } + }); + + const req = { + method: "GET", + query: { + appealType: "pinswg_planningappeals78s", + caseReference: "CAS-1" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].req, req); + assert.strictEqual(calls[0].res, res); +}); + +test("legacy myportal endpoint handlers remain present", async () => { + const legacyFiles = [ + "pages/api/endpoint/getmycases_api.js", + "pages/api/endpoint/getmylpacases_api.js", + "pages/api/endpoint/getportalmoduledetails_api.js", + "pages/api/endpoint/getmyrepresentations_api.js", + "pages/api/endpoint/getawaitingsubmission_api.js" + ]; + + legacyFiles.forEach((filePath) => { + assert.strictEqual( + fs.existsSync(path.join(rootDir, filePath)), + true, + filePath + ); + }); +}); + +test("active service layers now target grouped myportal routes", async () => { + const portalSource = read("actions/services/portalDirectService.js"); + const caseSource = read("actions/services/caseDirectService.js"); + + assert.match(portalSource, /\/api\/myportal\/get-my-cases/); + assert.match(portalSource, /\/api\/myportal\/get-my-lpa-cases/); + assert.match(caseSource, /\/api\/myportal\/get-portal-module-details/); +}); + +test("adopted service methods no longer target legacy endpoint URLs", async () => { + const portalSource = read("actions/services/portalDirectService.js"); + const caseSource = read("actions/services/caseDirectService.js"); + + assert.doesNotMatch(portalSource, /\/api\/endpoint\/getmycases_api/); + assert.doesNotMatch(portalSource, /\/api\/endpoint\/getmylpacases_api/); + assert.doesNotMatch( + caseSource, + /\/api\/endpoint\/getportalmoduledetails_api/ + ); +}); + +test("runtime contract strings and parameter names are preserved where structurally testable", async () => { + const portalSource = read("actions/services/portalDirectService.js"); + const caseSource = read("actions/services/caseDirectService.js"); + + assert.match(portalSource, /loggedInUserId/); + assert.match(portalSource, /lpaid/); + assert.match(caseSource, /appealType/); + assert.match(caseSource, /caseReference: encodeURI\(caseReference\)/); +}); + +test("excluded route families remain outside this myportal slice", async () => { + const portalSource = read("actions/services/portalDirectService.js"); + const caseSource = read("actions/services/caseDirectService.js"); + const pageSource = read("pages/myportal/index.js"); + + assert.match(portalSource, /\/api\/subscriptions\/get-watched-cases/); + assert.match(pageSource, /getRepsFromBlob\(thisSession.user.id\)/); + assert.match( + pageSource, + /getAwaitingSubmissionFromBlob\(thisSession.user.id\)/ + ); + assert.match(portalSource, /\/api\/endpoint\/getmyrepresentations_api/); + assert.match(portalSource, /\/api\/endpoint\/getawaitingsubmission_api/); + assert.doesNotMatch(caseSource, /\/api\/cases\/get-case"/); + assert.doesNotMatch(caseSource, /\/api\/cases\/get-case-by-id/); +}); + +async function run() { + let failures = 0; + + for (const { name, fn } of tests) { + try { + await fn(); + console.log(`✓ ${name}`); + } catch (error) { + failures += 1; + console.error(`✗ ${name}`); + console.error(error); + } + } + + if (failures > 0) { + process.exit(1); + } +} + +run();