diff --git a/actions/azurestorage.js b/actions/azurestorage.js
index ea104db1..f75d6000 100644
--- a/actions/azurestorage.js
+++ b/actions/azurestorage.js
@@ -1384,14 +1384,27 @@ export const getRepsBlobs = async (containerName) => {
listOptions
)) {
const blobClient = containerClient.getBlobClient(blob.name);
- //console.log("getreps blob:", blob);
- blob.name.split("/")[2].indexOf("_rep.json") > 0 &&
- blob.name.split("/")[2].indexOf("undefined") < 0 &&
+
+ // Filter out soft-deleted/stale tag entries and malformed names.
+ const namePart = blob.name.split("/")[2] ?? "";
+ if (
+ namePart.indexOf("_rep.json") <= 0 ||
+ namePart.indexOf("undefined") >= 0
+ )
+ continue;
+
+ try {
+ const properties = await blobClient.getProperties();
+
blobObj.push({
- "name": blob.name.split("/")[2],
+ "name": namePart,
"path": blob.name,
- "size": blobClient.getProperties().contentLength
+ "size": properties.contentLength
});
+ } catch (error) {
+ if (error?.statusCode === 404) continue;
+ throw error;
+ }
}
//console.log("blobObjwwwww:", blobObj);
diff --git a/components/myportal.js b/components/myportal.js
index 526df56a..4597b5a8 100644
--- a/components/myportal.js
+++ b/components/myportal.js
@@ -130,7 +130,6 @@ const MyPortal = (props) => {
{!isLPA && (
)}
-
{/* {props.awaitingSubmission.awaitingSubmission[
"@odata.count"
] > 0 && (
@@ -147,9 +146,8 @@ const MyPortal = (props) => {
"@odata.count"
]) &&
props.awaitingSubmission
- .awaitingSubmissionFromBlob[
- "@odata.count"
- ] > 0 && (
+ .awaitingSubmissionFromBlob.case.length >
+ 0 && (
{
isLPA={isLPA}
/>
)}
-
{props.myRepresentations.mySubmittedReps
.mySubmittedReps.length > 0 && (
pass
+ - helper: 4/4
+ - file-handler: 17/17
+ - email-handler: 12/12
+ - endpoint-handler: 149/149
+- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors)
+
+Follow-ups:
+
+- If desired, next slice can target remaining file-route parity candidates outside this bundle, but this closes the planned TASK22224 scope.
+
+---
+
+### CL-005: TASK22224 downloadblob hotfix closure (path normalization + hash compatibility)
+
+date: 2026-03-23
+author: Cline
+scope: `pages/api/file/downloadblob.js`
+type: change
+rationale: Close post-merge runtime regressions reported on live links where download URLs alternated between filename-only/full-path blob names and mixed encoded/raw hash input variants.
+impact: Restored reliable blob downloads without relaxing hash security guarantees (still HMAC validated), and preserved existing caller compatibility across legacy/new URL encodings.
+status: completed
+
+Summary:
+
+- Hotfix 1 (`f09f3b7`): normalized blob path resolution
+ - accepts both forms of `blobname` input:
+ - filename only (legacy)
+ - full prefixed path (already includes `casefolderID/...`)
+ - prevents double-prefix lookup failures
+ - sets attachment filename from final path segment only
+- Hotfix 2 (`bd3bf68`): hash compatibility validation
+ - validates against a bounded set of canonical query-path variants (raw/encoded combinations for `casefolderID` and `blobname`)
+ - fixes `INVALID_HASH` false negatives for legitimate caller-generated links
+ - keeps strict HMAC requirement in place (no unauthenticated bypass)
+
+Validation:
+
+- `node tests/phase21/file-handler-contract.test.cjs` -> pass (17/17)
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 17/17
+ - email-handler: 12/12
+ - endpoint-handler: 149/149
+- User confirmation: "downloadblob now works"
+
+Follow-ups:
+
+- Next recommended slice on this branch: complete file-route guard parity for `deleteblob.js`, `deleteblobcase.js`, and `deleteblobrep.js` by aligning hash validation canonicalization and explicit `respondError` contracts (`MISSING_REQUIRED_QUERY`, `INVALID_HASH`, operation-specific `*_FAILED`).
+- Extend `tests/phase21/file-handler-contract.test.cjs` for the above routes with mixed encoded/raw hash cases to lock compatibility.
+
+---
+
+### CL-006: TASK22224 file delete-route guard parity slice
+
+date: 2026-03-23
+author: Cline
+scope: `pages/api/file/{deleteblob,deleteblobcase,deleteblobrep}.js`, `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Execute the next planned slice to align hash/canonicalization behavior and negative-path contracts across high-risk file delete routes, matching the compatibility posture established for `downloadblob`.
+impact: Reduces false `INVALID_HASH` failures for legitimate encoded/raw caller variants while preserving strict hash enforcement and improving resilience via explicit catch-path contracts.
+status: completed
+
+Summary:
+
+- `deleteblob.js`
+ - added bounded hash candidate validation for encoded/raw combinations of `casefolderID` and `blobname`
+ - normalized delete path handling for both filename-only and already-prefixed blob paths
+ - added explicit catch-path contract: `DELETE_BLOB_FAILED`
+- `deleteblobcase.js`
+ - added hash candidate validation for raw/encoded `casefolderID`
+ - added explicit catch-path contract: `DELETE_BLOB_CASE_FAILED`
+- `deleteblobrep.js`
+ - added hash candidate validation for encoded/raw `casefolderID` + `repfile`
+ - added explicit catch-path contract: `DELETE_BLOB_REP_FAILED`
+- Phase21 tests expanded (`file-handler-contract.test.cjs`):
+ - encoded hash-variant acceptance cases for all three delete routes
+ - explicit dependency-failure contract assertions for all three delete routes
+
+Validation:
+
+- `node tests/phase21/file-handler-contract.test.cjs` -> pass (23/23)
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 23/23
+ - email-handler: 12/12
+ - endpoint-handler: 149/149
+
+Follow-ups:
+
+- Optional next slice: apply same bounded hash-canonicalization parity to remaining high-sensitivity file routes where mixed encoded/raw callers may exist (`getbloblist`, `getprogressobjblob`) and add regression cases to phase21.
+
+---
+
+### CL-007: TASK22224 getrepsblob stability hotfix after delete representation flow
+
+date: 2026-03-23
+author: Cline
+scope: `actions/azurestorage.js` (`getRepsBlobs`), `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Resolve reported runtime 400 (`GET_REPS_BLOB_FAILED`) after delete representation actions, caused by stale soft-deleted blob tag hits during representation blob enumeration.
+impact: Prevents transient/stale Azure tag index entries from breaking representation retrieval, improving reliability of post-delete refresh without relaxing route security contracts.
+status: completed
+
+Summary:
+
+- Hardened `getRepsBlobs(containerName)` in `actions/azurestorage.js`:
+ - fixed async misuse (`blobClient.getProperties().contentLength` without await)
+ - added existence/property guard with explicit `await blobClient.getProperties()`
+ - skips 404s (soft-deleted/stale tag index results) instead of throwing
+ - preserves behavior for non-404 failures (rethrow for proper error visibility)
+ - kept existing `_rep.json`/`undefined` name filtering intact
+- Added phase21 contract coverage for `getrepsblob` route:
+ - success payload contract test
+ - dependency failure contract test (`GET_REPS_BLOB_FAILED`)
+
+Validation:
+
+- `node tests/phase21/file-handler-contract.test.cjs` -> pass (25/25)
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 25/25
+ - email-handler: 12/12
+ - endpoint-handler: 149/149
+
+Follow-ups:
+
+- Optional: add the same stale-tag existence guard pattern to any remaining Azure tag-list readers that still consume `findBlobsByTags` results without property existence verification.
+
+---
+
+### CL-008: TASK22224 awaiting-submission route resilience parity hardening
+
+date: 2026-03-23
+author: Cline
+scope: `pages/api/file/getawaitingsubmissionfromblob.js`, `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Add explicit catch-path contract parity for awaiting-submission blob retrieval route so unexpected dependency failures return consistent, actionable error envelopes.
+impact: Improves reliability/diagnostics for post-delete case refresh and aligns file-route error handling style without changing success payload contract or hash verification behavior.
+status: completed
+
+Summary:
+
+- Refactored `getawaitingsubmissionfromblob` handler to structured `try/catch` flow.
+- Preserved existing guard behavior:
+ - `MISSING_REQUIRED_QUERY` for missing container/hash
+ - `INVALID_HASH` for signature mismatch
+- Added explicit dependency failure contract:
+ - `GET_AWAITING_SUBMISSION_BLOB_FAILED` (400)
+ - message: `Failed to retrieve awaiting submission blobs`
+- Added phase21 coverage for this route:
+ - success payload pass-through contract
+ - dependency failure contract assertion
+
+Validation:
+
+- `node tests/phase21/file-handler-contract.test.cjs` -> pass (27/27)
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 27/27
+ - email-handler: 12/12
+ - endpoint-handler: 149/149
+
+Follow-ups:
+
+- Optional parity sweep: apply the same explicit catch-path contract pattern to remaining file routes that still rely on implicit promise-chain errors.
+
+---
+
+### CL-009: TASK22224 proxy-route resilience and encoding parity bundle
+
+date: 2026-03-23
+author: Cline
+scope: `pages/api/file/{getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy,createappealcompletemessageproxy_api}.js`, `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Continue larger-slice hardening by aligning proxy handlers with explicit async error handling and safer encoded upstream query forwarding for hash-based downstream calls.
+impact: Improves proxy reliability and compatibility for encoded query values while preserving existing proxy error contracts and response behavior.
+status: completed
+
+Summary:
+
+- `getbloblistproxy.js`
+ - converted `.then/.catch` chain to explicit `try/catch`
+ - encoded forwarded `container` and `casefolderID` query values
+ - preserved error contract: `GET_BLOB_LIST_PROXY_FAILED`
+- `getrepsblobproxy.js`
+ - converted `.then/.catch` chain to explicit `try/catch`
+ - encoded forwarded `container`
+ - preserved error contract: `GET_REPS_BLOB_PROXY_FAILED`
+- `getawaitingsubmissionfromblobproxy.js`
+ - converted `.then/.catch` chain to explicit `try/catch`
+ - preserved error contract: `GET_AWAITING_SUBMISSION_PROXY_FAILED`
+- `createappealcompletemessageproxy_api.js`
+ - converted `.then/.catch` chain to explicit `try/catch`
+ - encoded forwarded `container` and `tempcaseref`
+ - preserved error contract: `CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED`
+- Phase21 tests expanded for proxy paths:
+ - getbloblistproxy success + dependency failure
+ - getrepsblobproxy success
+ - getawaitingsubmissionfromblobproxy dependency failure
+ - createappealcompletemessageproxy dependency failure
+
+Validation:
+
+- `node tests/phase21/file-handler-contract.test.cjs` -> pass (46/46)
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 46/46
+ - email-handler: 12/12
+ - endpoint-handler: 149/149
+
+Follow-ups:
+
+- Optional next big slice: bring remaining proxy/message routes using raw axios promise chains (`createcaseinvolvement_api.js`, `createrepinvolvement_api.js`, `updatecase_api.js`) onto the same async/await + explicit contract pattern.
+
+---
+
+### CL-010: TASK22224 involvement/update route async contract hardening bundle
+
+date: 2026-03-23
+author: Cline
+scope: `pages/api/file/{createcaseinvolvement_api,createrepinvolvement_api,updatecase_api}.js`, `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Execute the next requested slice by modernizing remaining relay-backed involvement/update handlers that still used axios promise chains and legacy dead imports.
+impact: Improves reliability/readability and preserves existing response contracts, including 412 "record exists" semantics for involvement creation flows.
+status: completed
+
+Summary:
+
+- `createcaseinvolvement_api.js`
+ - removed unused `CryptoJS` import
+ - refactored axios `.then/.catch` to explicit `try/catch`
+ - preserved conflict behavior: status 412 -> success `{ record: "exists" }`
+ - preserved failure contract: `CREATE_CASE_INVOLVEMENT_FAILED`
+- `createrepinvolvement_api.js`
+ - removed unused `CryptoJS` import
+ - refactored axios `.then/.catch` to explicit `try/catch`
+ - preserved conflict behavior: status 412 -> success `{ record: "exists" }`
+ - preserved failure contract: `CREATE_REP_INVOLVEMENT_FAILED`
+- `updatecase_api.js`
+ - removed unused `CryptoJS` import
+ - refactored axios `.then/.catch` to explicit `try/catch`
+ - preserved failure contract: `UPDATE_CASE_FAILED`
+- Phase21 tests expanded:
+ - createcaseinvolvement 412 conflict success contract
+ - createrepinvolvement dependency failure contract
+ - updatecase dependency failure contract
+
+Validation:
+
+- `node tests/phase21/file-handler-contract.test.cjs` -> pass (49/49)
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 49/49
+ - email-handler: 12/12
+ - endpoint-handler: 149/149
+
+Follow-ups:
+
+- Optional: apply equivalent modernization to any remaining relay-backed handlers outside `pages/api/file/` that still use raw axios promise chains and have no explicit phase21 contract assertions.
+
+---
+
+### CL-011: TASK22224 aggressive non-file bundle (email/admin/endpoint parity)
+
+date: 2026-03-23
+author: Cline
+scope: `pages/api/email/{getmailinglist,getcaseref,notify}.js`, `pages/api/admin/{getnewappeals_api,getlatestdocuments_api}.js`, `pages/api/endpoint/getportallogin_api.js`, `tests/phase21/endpoint-handler-contract.test.cjs`
+type: change
+rationale: Execute requested aggressive bundling for remaining non-file modernization/parity candidates: remove legacy promise chains and improve hash compatibility on login endpoint while preserving existing contracts.
+impact: Improves consistency and resilience across email/admin/endpoint routes with no contract regressions; adds encoded hash-variant compatibility for portal login hash checks.
+status: completed
+
+Summary:
+
+- `pages/api/email/getmailinglist.js`
+ - converted axios `.then/.catch` to `try/catch`
+ - preserved flattening behavior and error contract `MAILING_LIST_FETCH_FAILED`
+- `pages/api/email/getcaseref.js`
+ - converted axios `.then/.catch` to `try/catch`
+ - preserved flattening behavior and error contract `CASE_REF_FETCH_FAILED`
+- `pages/api/email/notify.js`
+ - converted notify client `.then/.catch` to `try/catch`
+ - preserved success payload and error contract `EMAIL_NOTIFY_FAILED`
+- `pages/api/admin/getnewappeals_api.js`
+ - removed unused `CryptoJS` import
+ - converted axios `.then/.catch` to `try/catch`
+ - preserved `@odata.nextLink` normalization and error contract `ADMIN_NEW_APPEALS_FETCH_FAILED`
+- `pages/api/admin/getlatestdocuments_api.js`
+ - converted axios `.then/.catch` to `try/catch`
+ - preserved flatten/enrich behavior and error contract `ADMIN_LATEST_DOCS_FETCH_FAILED`
+- `pages/api/endpoint/getportallogin_api.js`
+ - retained required query/hash guards
+ - expanded hash validation to accept raw + encoded `emailAddress` query-path candidates
+ - preserved error contract `PORTAL_LOGIN_FETCH_FAILED`
+- phase21 endpoint tests expanded:
+ - `getportallogin` encoded hash variant success path
+ - `getnewappeals_api` catch contract
+ - `getlatestdocuments_api` catch contract
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 49/49
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+
+Follow-ups:
+
+- Remaining major modernization candidate is `pages/api/file/generateappealpdf.js` (+ optional `pages/api/file/generatepdf.js`) if we continue final closure slices.
+
+---
+
+### CL-012: TASK22224 generatepdf/generateappealpdf async hardening slice
+
+date: 2026-03-23
+author: Cline
+scope: `pages/api/file/{generateappealpdf,generatepdf}.js`, `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Execute next requested slice to modernize remaining file PDF-generation handlers still using promise chains, while preserving existing hash/response behavior.
+impact: Improves maintainability and error-path consistency for PDF generation routes; phase21 coverage now includes explicit failure contracts for both handlers.
+status: completed
+
+Summary:
+
+- `pages/api/file/generateappealpdf.js`
+ - converted mixed promise-chain flow to `async/await` + `try/catch`
+ - preserved existing guard contracts: `MISSING_REQUIRED_QUERY`, `INVALID_HASH`
+ - preserved generation failure contract: `GENERATE_APPEAL_PDF_FAILED`
+ - replaced JSX render call with `React.createElement(...)` compatibility form used by test loader
+- `pages/api/file/generatepdf.js`
+ - converted create/upload promise-chain to `async/await` + `try/catch`
+ - preserved existing guard contracts: `HASH_REQUIRED`, `INVALID_HASH`
+ - preserved generation failure contract: `GENERATE_PDF_FAILED`
+ - replaced JSX render call with `React.createElement(...)` compatibility form used by test loader
+- phase21 file tests expanded:
+ - `generatepdf` catch-path contract (`GENERATE_PDF_FAILED`)
+ - `generateappealpdf` catch-path contract (`GENERATE_APPEAL_PDF_FAILED`)
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 51/51
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+
+Follow-ups:
+
+- Remaining optional cleanup in these handlers is dead import/unused local pruning (non-behavioral) if we want a final low-risk tidy pass.
+
+---
+
+### CL-013: TASK22224 completion-message route parity closure slice
+
+date: 2026-03-24
+author: Cline
+scope: `pages/api/file/createappealcompletemessage_api.js`, `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Continue next requested slice by closing the final promise-chain parity outlier in file completion-message flow and strengthening phase21 contract coverage.
+impact: Improves maintainability and async error hygiene while preserving route behavior and existing error contracts.
+status: completed
+
+Summary:
+
+- `pages/api/file/createappealcompletemessage_api.js`
+ - replaced inline `.catch(...)` on fire-and-forget `updateAccount(...)` with explicit async IIFE + `try/catch` and `void` invocation
+ - preserved non-blocking behavior and logging semantics for account-update failure path
+ - preserved primary route contracts and success payload (`{ status: "success" }`)
+- phase21 file contract tests expanded:
+ - success path for encoded hash candidate on `createappealcompletemessage_api`
+ - dependency-failure contract assertion for `CREATE_APPEAL_COMPLETE_MESSAGE_FAILED`
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 53/53
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+
+Follow-ups:
+
+- Optional final low-risk tidy sweep: remove dead imports/unused locals in legacy file handlers now that contract hardening stream is functionally complete.
+
+---
+
+### CL-014: TASK22224 pdf render compatibility tidy slice
+
+date: 2026-03-24
+author: Cline
+scope: `pages/api/file/{generatepdf,generateappealpdf}.js`, `tests/phase21/file-handler-contract.test.cjs`
+type: change
+rationale: Apply a low-risk compatibility tidy so PDF render invocation remains stable across runtime and contract-test VM contexts.
+impact: Keeps functional behavior unchanged while reducing test/runtime mismatch risk in render path setup.
+status: completed
+
+Summary:
+
+- `pages/api/file/generatepdf.js`
+ - switched render call input from `React.createElement(MyDocument, ...)` to direct `MyDocument(...)` invocation in `ReactPDF.renderToStream(...)`
+- `pages/api/file/generateappealpdf.js`
+ - switched render call input from `React.createElement(MyDocument, ...)` to direct `MyDocument(...)` invocation in `ReactPDF.renderToStream(...)`
+- `tests/phase21/file-handler-contract.test.cjs`
+ - added `Buffer` injection for `generatepdf` catch-path test harness to align VM context expectations
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 53/53
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+
+Follow-ups:
+
+- Optional: dead import cleanup (`Document/Page/Text/View/StyleSheet/PDFViewer`, `middleware`, `nextConnect`, `fs`, etc.) can be done in a dedicated non-behavioral hygiene PR.
+
+---
+
+### CL-015: TASK22224 pdf handler dead-code hygiene slice
+
+date: 2026-03-24
+author: Cline
+scope: `pages/api/file/{generatepdf,generateappealpdf}.js`
+type: change
+rationale: Execute the requested next low-risk slice by removing dead imports and unused locals in recently hardened PDF handlers.
+impact: Non-behavioral maintainability cleanup; reduces lint noise and future edit risk while preserving existing contracts.
+status: completed
+
+Summary:
+
+- `pages/api/file/generatepdf.js`
+ - pruned unused Azure storage imports, leaving only `createRepPDFBlob`
+ - pruned unused `@react-pdf/renderer` named imports
+ - removed unused imports (`middleware`, `nextConnect`, `fs`)
+ - removed unused locals (`casefolderID`, `representationType`, `repRaiser`, `localeSelect`, `repCapacity`, `repType`)
+- `pages/api/file/generateappealpdf.js`
+ - pruned unused Azure storage imports to only required functions
+ - pruned unused `@react-pdf/renderer` named imports
+ - removed unused imports (`middleware`, `nextConnect`, `fs`, `path`, unused pdf templates)
+ - removed unused local (`caseRef`)
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 53/53
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+
+Follow-ups:
+
+- Optional: run full repo lint in a separate pass for broader non-slice hygiene now that targeted contract suite is stable.
+
+---
+
+### CL-016: TASK22224 documents download contract slice
+
+date: 2026-03-24
+author: Cline
+scope: `pages/api/documents/download/[id].js`, `tests/phase21/{documents-handler-contract,api-contract-slice1}.test.cjs`
+type: change
+rationale: Execute next aggressive slice by standardizing document download guard behavior and bringing the route under phase21 contract coverage.
+impact: Improves reliability on invalid input and relay-failure paths while preserving existing user-visible fallback behavior (`/filenotavailable`) for download failures.
+status: completed
+
+Summary:
+
+- `pages/api/documents/download/[id].js`
+ - added explicit required-query guard for `id` and `hash`
+ - unified fallback redirect path via constant (`/filenotavailable`)
+ - preserved streaming download behavior and retry flow
+- added `tests/phase21/documents-handler-contract.test.cjs` covering:
+ - missing query -> redirect contract
+ - success -> attachment/content-type headers + stream pipe contract
+ - relay failure -> redirect contract
+- updated combined runner (`tests/phase21/api-contract-slice1.test.cjs`) to include documents handler contract suite
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 53/53
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+ - documents-handler: 3/3
+
+Follow-ups:
+
+- Optional future hardening: migrate documents route onto shared `respondError/respondSuccess` envelope if product requirements allow replacing redirect-style fallback.
+
+---
+
+### CL-017: TASK22224 endpoint legacy-comment hygiene slice
+
+date: 2026-03-24
+author: Cline
+scope: `pages/api/endpoint/{createwatchedcases_api,getadvancedsearchpaged_api}.js`
+type: change
+rationale: Complete second requested slice with low-risk maintainability cleanup by removing large obsolete commented legacy handler blocks.
+impact: Non-behavioral cleanup only; improves readability and reduces maintenance noise with no runtime contract changes.
+status: completed
+
+Summary:
+
+- `createwatchedcases_api.js`
+ - removed obsolete commented promise-chain implementation block
+- `getadvancedsearchpaged_api.js`
+ - removed obsolete commented legacy implementation block retained below active handler
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 53/53
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+ - documents-handler: 3/3
+
+Follow-ups:
+
+- Optional further hygiene pass can target remaining oversized commented historical sections in non-sensitive handlers.
+
+---
+
+### CL-018: TASK22224 nextauth notify micro-refactor
+
+date: 2026-03-24
+author: Cline
+scope: `pages/api/auth/[...nextauth].js`
+type: change
+rationale: Execute the explicitly approved auth micro-slice by replacing inline promise `.catch(...)` with explicit `try/catch` while preserving existing auth behavior.
+impact: Auth-sensitive non-functional refactor only; keeps current sign-in flow, template/locale routing, and error-handling semantics unchanged.
+status: completed
+
+Summary:
+
+- `pages/api/auth/[...nextauth].js`
+ - replaced:
+ - `await notifyClient.sendEmail(...).catch((error) => consoleLogger(error))`
+ - with explicit:
+ - `try { await notifyClient.sendEmail(...) } catch (error) { consoleLogger(error) }`
+- preserved behavior contracts:
+ - Notify failures are still logged and do not throw through auth handler
+ - no changes to callback URL construction, locale/template selection, NextAuth options, session/cookies/pages config
+
+Validation:
+
+- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
+ - helper: 4/4
+ - file-handler: 53/53
+ - email-handler: 12/12
+ - endpoint-handler: 152/152
+ - documents-handler: 3/3
+
+Follow-ups:
+
+- Optional future auth hygiene (separate guarded slice): replace verbose auth `console.log` diagnostics with structured logger usage once production logging requirements are confirmed.
diff --git a/memory-bank/refactor-backlog.md b/memory-bank/refactor-backlog.md
index 5f7109d1..cabb30c9 100644
--- a/memory-bank/refactor-backlog.md
+++ b/memory-bank/refactor-backlog.md
@@ -32,6 +32,28 @@ Last updated: 2026-03-12
- **Target outcome:** Shared guard middleware for hash/auth/input validation + consistent negative-path responses.
- **Initial scope:** enforce a common pre-handler contract on high-risk routes first.
+### Priority 4 — Current slice status (2026-03-23)
+
+- Completed on `TASK22224-file-endpoint-contract-bundle`:
+ - `pages/api/file/downloadblob.js` hardening + hotfix closure
+ - path normalization compatibility (filename-only + prefixed path)
+ - encoded/raw hash validation compatibility guard
+- Confirmed outcome:
+ - user-reported download failures resolved
+ - phase21 file + combined contract suites passing
+
+### Priority 4 — Next recommended slice on this branch
+
+- Target routes:
+ - `pages/api/file/deleteblob.js`
+ - `pages/api/file/deleteblobcase.js`
+ - `pages/api/file/deleteblobrep.js`
+- Scope:
+ - align hash canonicalization strategy with `downloadblob.js`
+ - enforce explicit `respondError` contracts (`MISSING_REQUIRED_QUERY`, `INVALID_HASH`, route-specific `*_FAILED`)
+ - preserve existing success payload shapes
+ - extend `tests/phase21/file-handler-contract.test.cjs` with mixed encoded/raw hash variants and negative paths
+
## Priority 5 — Establish minimum automated regression baseline
- **Problem:** Limited automated tests for high-risk logic.
diff --git a/pages/api/admin/getlatestdocuments_api.js b/pages/api/admin/getlatestdocuments_api.js
index 686352b3..a699550a 100644
--- a/pages/api/admin/getlatestdocuments_api.js
+++ b/pages/api/admin/getlatestdocuments_api.js
@@ -43,18 +43,18 @@ const encryptDocReference = (documentRef) => {
};
export default async function ApiProxy(req, res) {
- var pageNumber = req.query.pageNumber || 1;
- var token = await getToken();
+ const pageNumber = req.query.pageNumber || 1;
+ const token = await getToken();
- var orderby = req.query.orderby || "createdon";
- var fieldSort = req.query.fieldSort || "desc";
- var showNumberOfRecords = req.query.showNumberOfRecords || 10;
- var documentType = req.query.documentType || "all";
- var documentOrigin = req.query.documentOrigin || "all";
- var numberOfWeeks = req.query.numberWeeks || 1;
+ const orderby = req.query.orderby || "createdon";
+ const fieldSort = req.query.fieldSort || "desc";
+ const showNumberOfRecords = req.query.showNumberOfRecords || 10;
+ const documentType = req.query.documentType || "all";
+ const documentOrigin = req.query.documentOrigin || "all";
+ const numberOfWeeks = req.query.numberWeeks || 1;
- var docTypeQueryString = "";
- var docOriginQueryString = "";
+ let docTypeQueryString = "";
+ let docOriginQueryString = "";
if (documentType != "all") {
if (documentType.indexOf(",") >= 0) {
@@ -108,7 +108,7 @@ export default async function ApiProxy(req, res) {
return resultDate.toISOString().replace(/\.000Z$/, "Z"); // format as 'YYYY-MM-DDT00:00:00Z'
}
- var queryUrl =
+ const queryUrl =
"pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " +
daysAgoISO(numberOfWeeks) +
docTypeQueryString +
@@ -131,40 +131,37 @@ export default async function ApiProxy(req, res) {
"\n==========================================\n"
);
- var apiResponse = axios
- .get(
+ try {
+ const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeadersPagedCustom(token.access_token, showNumberOfRecords)
- )
- .then(({ data }) => {
- let flattened = data.value.map((r) => ({
- ...r,
- ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null,
- publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null
- }));
+ );
- data.value = flattened;
- data.value.forEach(function (element) {
- element.pinswg_hashlink = encryptDocReference(
- element.pinswg_isharedocumentreference
- );
- });
+ const flattened = data.value.map((r) => ({
+ ...r,
+ ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null,
+ publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null
+ }));
- var dataStr;
-
- _.has(data, "@odata.nextLink") == true &&
- ((dataStr = JSON.stringify(data["@odata.nextLink"])),
- (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "ADMIN_LATEST_DOCS_FETCH_FAILED",
- message: "Failed to fetch latest documents"
- });
+ data.value = flattened;
+ data.value.forEach(function (element) {
+ element.pinswg_hashlink = encryptDocReference(
+ element.pinswg_isharedocumentreference
+ );
});
- return apiResponse;
+ let dataStr;
+
+ _.has(data, "@odata.nextLink") == true &&
+ ((dataStr = JSON.stringify(data["@odata.nextLink"])),
+ (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "ADMIN_LATEST_DOCS_FETCH_FAILED",
+ message: "Failed to fetch latest documents"
+ });
+ }
}
diff --git a/pages/api/admin/getnewappeals_api.js b/pages/api/admin/getnewappeals_api.js
index 595ed467..01c321eb 100644
--- a/pages/api/admin/getnewappeals_api.js
+++ b/pages/api/admin/getnewappeals_api.js
@@ -16,7 +16,6 @@
*/
import axios from "axios";
-import CryptoJS from "crypto-js";
import _ from "lodash";
import { azureHeadersPagedCustom } from "../../../actions/core/headers";
import { getToken } from "../../../actions/core/token";
@@ -29,15 +28,14 @@ const WEBAPI_URL =
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
export default async function ApiProxy(req, res) {
- var searchString = req.query.searchString;
- var pageNumber = req.query.pageNumber || 1;
- var token = await getToken();
+ const pageNumber = req.query.pageNumber || 1;
+ const token = await getToken();
- var orderby = req.query.orderby || "createdon";
- var fieldSort = req.query.fieldSort || "desc";
- var showNumberOfRecords = req.query.showNumberOfRecords || 10;
+ const orderby = req.query.orderby || "createdon";
+ const fieldSort = req.query.fieldSort || "desc";
+ const showNumberOfRecords = req.query.showNumberOfRecords || 10;
- var queryUrl =
+ const queryUrl =
"incidents?$select=caseorigincode,pinswg_publishtoweb,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title, _primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$orderby=" +
orderby +
" " +
@@ -55,26 +53,23 @@ export default async function ApiProxy(req, res) {
"\n==========================================\n"
);
- var apiResponse = axios
- .get(
+ try {
+ const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeadersPagedCustom(token.access_token, showNumberOfRecords)
- )
- .then(({ data }) => {
- var dataStr;
- _.has(data, "@odata.nextLink") == true &&
- ((dataStr = JSON.stringify(data["@odata.nextLink"])),
- (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "ADMIN_NEW_APPEALS_FETCH_FAILED",
- message: "Failed to fetch new appeals"
- });
- });
+ );
- return apiResponse;
+ let dataStr;
+ _.has(data, "@odata.nextLink") == true &&
+ ((dataStr = JSON.stringify(data["@odata.nextLink"])),
+ (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "ADMIN_NEW_APPEALS_FETCH_FAILED",
+ message: "Failed to fetch new appeals"
+ });
+ }
}
diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js
index 75939546..7a713e47 100644
--- a/pages/api/auth/[...nextauth].js
+++ b/pages/api/auth/[...nextauth].js
@@ -139,8 +139,8 @@ const authOptions = (req, res) => {
process.env.NOTIFY_API_KEY
);
- await notifyClient
- .sendEmail(
+ try {
+ await notifyClient.sendEmail(
effectiveLocale === "cy"
? templateIdcy
: templateId,
@@ -149,8 +149,10 @@ const authOptions = (req, res) => {
personalisation,
reference
}
- )
- .catch((error) => consoleLogger(error));
+ );
+ } catch (error) {
+ consoleLogger(error);
+ }
}
})
],
diff --git a/pages/api/documents/download/[id].js b/pages/api/documents/download/[id].js
index aab6804b..4fbd0752 100644
--- a/pages/api/documents/download/[id].js
+++ b/pages/api/documents/download/[id].js
@@ -116,6 +116,8 @@ const WEBAPI_URL =
process.env.RELAY_ROOT ||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
+const FILE_NOT_AVAILABLE_PATH = "/filenotavailable";
+
// Retry utility with logging
async function retry(fn, retries = 3, delay = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
@@ -133,6 +135,18 @@ async function retry(fn, retries = 3, delay = 1000) {
const ApiProxy = async (req, res) => {
const docRef = req.query;
+ if (
+ typeof docRef?.id !== "string" ||
+ docRef.id.length === 0 ||
+ typeof docRef?.hash !== "string" ||
+ docRef.hash.length === 0
+ ) {
+ if (!res.headersSent) {
+ res.redirect(FILE_NOT_AVAILABLE_PATH);
+ }
+ return;
+ }
+
try {
const token = await getToken();
const startTime = Date.now();
@@ -186,11 +200,11 @@ const ApiProxy = async (req, res) => {
response.data.on("error", (err) => {
consoleLogger(err);
- if (!res.headersSent) res.redirect("/filenotavailable");
+ if (!res.headersSent) res.redirect(FILE_NOT_AVAILABLE_PATH);
});
} catch (error) {
consoleLogger(error);
- if (!res.headersSent) res.redirect("/filenotavailable");
+ if (!res.headersSent) res.redirect(FILE_NOT_AVAILABLE_PATH);
}
};
diff --git a/pages/api/email/getcaseref.js b/pages/api/email/getcaseref.js
index ddc59198..52e07578 100644
--- a/pages/api/email/getcaseref.js
+++ b/pages/api/email/getcaseref.js
@@ -22,27 +22,25 @@ function flattenWatchlistEntry(entry) {
}
export default async function ApiProxy(req, res) {
- var token = await getToken();
+ const token = await getToken();
- var queryUrl =
+ const queryUrl =
"pinswg_watchlists?$count=true&$filter=pinswg_emailnotifications ne null&$expand=pinswg_Contact($select=contactid,emailaddress1)&$select=pinswg_emailnotifications,_pinswg_watchedcase_value";
- return axios
- .get(
+ try {
+ const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
- )
- .then(({ data }) => {
- const flattenedResults = data.value.map(flattenWatchlistEntry);
+ );
+ const flattenedResults = data.value.map(flattenWatchlistEntry);
- return respondSuccess(res, flattenedResults);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "CASE_REF_FETCH_FAILED",
- message: "Failed to fetch case references"
- });
+ return respondSuccess(res, flattenedResults);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "CASE_REF_FETCH_FAILED",
+ message: "Failed to fetch case references"
});
+ }
}
diff --git a/pages/api/email/getmailinglist.js b/pages/api/email/getmailinglist.js
index 3a980c57..6c368fb0 100644
--- a/pages/api/email/getmailinglist.js
+++ b/pages/api/email/getmailinglist.js
@@ -22,27 +22,25 @@ function flattenWatchlistEntry(entry) {
}
export default async function ApiProxy(req, res) {
- var token = await getToken();
+ const token = await getToken();
- var queryUrl =
+ const queryUrl =
"pinswg_watchlists?$count=true&$filter=pinswg_emailnotifications ne null&$expand=pinswg_Contact($select=contactid,emailaddress1)&$select=pinswg_emailnotifications,_pinswg_watchedcase_value";
- return axios
- .get(
+ try {
+ const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
- )
- .then(({ data }) => {
- const flattenedResults = data.value.map(flattenWatchlistEntry);
+ );
+ const flattenedResults = data.value.map(flattenWatchlistEntry);
- return respondSuccess(res, flattenedResults);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "MAILING_LIST_FETCH_FAILED",
- message: "Failed to fetch mailing list"
- });
+ return respondSuccess(res, flattenedResults);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "MAILING_LIST_FETCH_FAILED",
+ message: "Failed to fetch mailing list"
});
+ }
}
diff --git a/pages/api/email/notify.js b/pages/api/email/notify.js
index edf8a7d0..a03b1aa7 100644
--- a/pages/api/email/notify.js
+++ b/pages/api/email/notify.js
@@ -31,7 +31,7 @@ import { getPreferredLanguage } from "../../../actions/services/accountService";
import { respondError, respondSuccess } from "../middleware/apiResponse";
export default async function ApiProxy(req, res) {
- var data = req.body;
+ const data = req.body;
const emailAddress = sanitizeString(data?.emailAddress);
if (!isNonEmptyString(emailAddress)) {
@@ -58,7 +58,7 @@ export default async function ApiProxy(req, res) {
}
}
- var NotifyClient = require("notifications-node-client").NotifyClient;
+ const NotifyClient = require("notifications-node-client").NotifyClient;
const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY);
//const emailReplyToId = process.env.EMAIL_REPLY_TO_ID;
@@ -68,20 +68,19 @@ export default async function ApiProxy(req, res) {
payload: redactSensitive(data)
});
- notifyClient
- .sendEmail(data.templateId, data.emailAddress, {
+ try {
+ await notifyClient.sendEmail(data.templateId, data.emailAddress, {
personalisation: data.personalisation,
reference: data.reference
- })
- .then(() => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "EMAIL_NOTIFY_FAILED",
- message: "Failed to send notify email"
- });
});
+
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "EMAIL_NOTIFY_FAILED",
+ message: "Failed to send notify email"
+ });
+ }
}
diff --git a/pages/api/endpoint/createwatchedcases_api.js b/pages/api/endpoint/createwatchedcases_api.js
index 55662558..da199830 100644
--- a/pages/api/endpoint/createwatchedcases_api.js
+++ b/pages/api/endpoint/createwatchedcases_api.js
@@ -20,35 +20,6 @@ const WEBAPI_URL =
process.env.RELAY_ROOT ||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
-// export default async function ApiProxy(req, res) {
-// var token = await getToken();
-// var data = JSON.stringify(req.body);
-// var queryUrl = "pinswg_watchlists";
-
-// console.log(data);
-// var config = {
-// method: "put",
-// url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
-// headers: {
-// "OData-MaxVersion": "4.0",
-// "OData-Version": "4.0",
-// "Accept": "application/json",
-// "Prefer": 'odata.include-annotations="*",return=representation',
-// "Authorization": "Bearer " + token.access_token,
-// "Content-Type": "application/json",
-// },
-// data: data,
-// };
-
-// return axios(config)
-// .then(({ data }) => {
-// res.status(200).json(data);
-// })
-// .catch((error) => {
-// consoleLogger(Object.assign(err, data));
-// res.status(400).json(error);
-// });
-// }
const recordExists = async (incidentId, contactId, token) => {
const filter = `$filter=pinswg_WatchedCase/incidentid eq ${incidentId} and pinswg_Contact/contactid eq ${contactId}`;
const queryUrl = `pinswg_watchlists?${filter}`;
diff --git a/pages/api/endpoint/getadvancedsearchpaged_api.js b/pages/api/endpoint/getadvancedsearchpaged_api.js
index 915efdeb..3150880e 100644
--- a/pages/api/endpoint/getadvancedsearchpaged_api.js
+++ b/pages/api/endpoint/getadvancedsearchpaged_api.js
@@ -258,161 +258,3 @@ export default async function ApiProxy(req, res) {
});
}
}
-
-// export default async function ApiProxy(req, res) {
-// var searchString = req.query.searchstring;
-// var pageNumber = req.query.pageNumber;
-// var token = await getToken();
-
-// var orderby = req.query.orderby;
-// var fieldSort = req.query.fieldSort;
-// var showNumberOfRecords = req.query.showNumberOfRecords;
-
-// searchString = _.isEmpty(searchString)
-// ? searchString
-// : JSON.parse(decodeURI(searchString));
-
-// var queryString = "";
-
-// queryString +=
-// _.has(searchString, "q") && searchString.q != null
-// ? "(contains(title, '" +
-// searchString.q.replace(/\'/g, "''") +
-// "') or contains(ticketnumber,'" +
-// searchString.q.replace(/\'/g, "''") +
-// "')) and"
-// : "";
-
-// queryString +=
-// (_.has(searchString, "lpa") || _.has(searchString, "LPA")) &&
-// searchString.lpa != null
-// ? " _pinswg_associatedlpa_value eq " + searchString.lpa + " and"
-// : "";
-
-// queryString +=
-// _.has(searchString, "apt") && searchString.apt != null
-// ? " pinswg_appealcasetype eq " + searchString.apt + " and"
-// : "";
-
-// queryString +=
-// _.has(searchString, "statuscode") && searchString.statuscode != null
-// ? " statuscode eq " + searchString.statuscode + " and"
-// : "";
-
-// var queryUrl =
-// "incidents?$select=pinswg_environmentalstatementlocation,pinswg_caseaddress,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title,_primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$filter=" +
-// queryString +
-// " pinswg_appealcasetype ne null and pinswg_publishtoweb eq true&$orderby=" +
-// orderby +
-// " " +
-// fieldSort +
-// "&$count=true";
-
-// console.log(WEBAPI_URL + queryUrl + hashAPIPath(queryUrl));
-
-// var apiResponse = _.isEmpty(searchString)
-// ? res.status(400).json()
-// : axios
-// .get(
-// WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
-// azureHeadersPagedCustom(
-// token.access_token,
-// showNumberOfRecords
-// )
-// )
-// .then(async ({ data }) => {
-// var dataStr;
-// _.has(data, "@odata.nextLink") == true &&
-// ((dataStr = JSON.stringify(data["@odata.nextLink"])),
-// (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
-
-// if (_.has(searchString, "projecttype")) {
-// await updateValueArray(data, token);
-
-// async function updateValueArray(data, token) {
-// let hasNextPage = true;
-// let nextPageUrl = data["@odata.nextLink"];
-
-// // Loop through all pages
-// while (hasNextPage) {
-// // Iterate through current data and process it
-// for (let i = 0; i < data.value.length; i++) {
-// const item = data.value[i];
-
-// try {
-// // Axios call using the `incidentid` to fetch additional data
-// const response = await axios.get(
-// WEBAPI_URL +
-// "pinswg_sipses?$filter=_pinswg_sipscase_value eq " +
-// item.incidentid +
-// " and _pinswg_projecttype_value eq " +
-// searchString.projecttype +
-// "&$select=_pinswg_projecttype_value" +
-// hashAPIPath(
-// "pinswg_sipses?$filter=_pinswg_sipscase_value eq " +
-// item.incidentid +
-// " and _pinswg_projecttype_value eq " +
-// searchString.projecttype +
-// "&$select=_pinswg_projecttype_value"
-// ),
-// azureHeadersPaged(token.access_token)
-// );
-
-// if (
-// response.data.value.length > 0 &&
-// response.data.value[0]
-// ._pinswg_projecttype_value != null
-// ) {
-// Object.assign(
-// item,
-// response.data.value[0]
-// ); // Update item with new data
-// } else {
-// console.log(
-// `No project type found for incident ID ${item.incidentid}`
-// );
-// }
-// } catch (error) {
-// console.error(
-// `Error fetching data for incident ID ${item.incidentid}:`,
-// error
-// );
-// }
-// }
-
-// // If there's a next page, fetch it
-// if (nextPageUrl) {
-// const nextPageData = await axios.get(
-// WEBAPI_URL +
-// nextPageUrl +
-// hashAPIPath(nextPageUrl),
-// azureHeadersPaged(token.access_token)
-// );
-// data.value = [
-// ...data.value,
-// ...nextPageData.data.value,
-// ];
-// nextPageUrl =
-// nextPageData.data["@odata.nextLink"];
-// } else {
-// hasNextPage = false;
-// }
-// }
-// }
-
-// data.value = data.value.filter(
-// (item) => item._pinswg_projecttype_value != null
-// );
-
-// data["@odata.count"] = data.value.length;
-// }
-
-// res.status(200).json(data);
-// })
-// .catch((error) => {
-// consoleLogger(error);
-// res.status(400).json(error);
-// });
-
-// return apiResponse;
-// }
diff --git a/pages/api/endpoint/getappealtypesfornewappeal_api.js b/pages/api/endpoint/getappealtypesfornewappeal_api.js
index 06dde026..bbc4afc3 100644
--- a/pages/api/endpoint/getappealtypesfornewappeal_api.js
+++ b/pages/api/endpoint/getappealtypesfornewappeal_api.js
@@ -1,4 +1,4 @@
-import { respondSuccess } from "../middleware/apiResponse";
+import { respondError, respondSuccess } from "../middleware/apiResponse";
/**
* @swagger
@@ -13,6 +13,14 @@ import { respondSuccess } from "../middleware/apiResponse";
*/
const ApiResponse = (req, res) => {
+ if (req.method && req.method !== "GET") {
+ return respondError(res, {
+ status: 405,
+ code: "METHOD_NOT_ALLOWED",
+ message: "Only GET is supported"
+ });
+ }
+
return respondSuccess(res, {
"value": [
{
diff --git a/pages/api/endpoint/getportallogin_api.js b/pages/api/endpoint/getportallogin_api.js
index e1adb714..c3f52aae 100644
--- a/pages/api/endpoint/getportallogin_api.js
+++ b/pages/api/endpoint/getportallogin_api.js
@@ -49,8 +49,16 @@ export default async function ApiProxy(req, res) {
const checkquerypath =
"/api/endpoint/getportallogin_api?emailAddress=" + emailAddress;
+ const encodedCheckquerypath =
+ "/api/endpoint/getportallogin_api?emailAddress=" +
+ encodeURIComponent(emailAddress);
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const expectedHashCandidates = [
+ hashAPIPath(checkquerypath),
+ hashAPIPath(encodedCheckquerypath)
+ ];
+
+ if (!expectedHashCandidates.includes("&hash=" + checkHash)) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
diff --git a/pages/api/endpoint/getsipsmedia_api.js b/pages/api/endpoint/getsipsmedia_api.js
index 1ef461b4..deb143ad 100644
--- a/pages/api/endpoint/getsipsmedia_api.js
+++ b/pages/api/endpoint/getsipsmedia_api.js
@@ -1,29 +1,14 @@
-import { respondSuccess } from "../middleware/apiResponse";
+import { respondError, respondSuccess } from "../middleware/apiResponse";
-// export default async function ApiProxy(req, res) {
-// var caseid = req.query.caseid;
-// var token = await getToken();
+export default function ApiProxy(req, res) {
+ if (req.method && req.method !== "GET") {
+ return respondError(res, {
+ status: 405,
+ code: "METHOD_NOT_ALLOWED",
+ message: "Only GET is supported"
+ });
+ }
-// var queryUrl =
-// "pinswg_sipsevents?$filter=_pinswg_sipseventsid_value eq " +
-// caseid +
-// "&$count=true";
-
-// return axios
-// .get(
-// WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
-// azureHeaders(token.access_token),
-// )
-// .then(({ data }) => {
-// res.status(200).json(data);
-// })
-// .catch((error) => {
-// consoleLogger(error);
-// res.status(400).json(error);
-// });
-// }
-
-export default function ApiProx(req, res) {
const data = {
"@odata.count": 4,
"value": [
diff --git a/pages/api/file/createappealcompletemessage_api.js b/pages/api/file/createappealcompletemessage_api.js
index 7b211519..17c43b1e 100644
--- a/pages/api/file/createappealcompletemessage_api.js
+++ b/pages/api/file/createappealcompletemessage_api.js
@@ -18,10 +18,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var tempCaseRef = req.query.tempcaseref;
- var typeofinvolvement = req.query.inv;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const tempCaseRef = req.query.tempcaseref;
+ const typeofinvolvement = req.query.inv;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -38,13 +38,22 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
+ const hashCandidatePaths = [
"/api/file/createappealcompletemessage_api?container=" +
- containerName +
- "&tempcaseref=" +
- tempCaseRef;
+ containerName +
+ "&tempcaseref=" +
+ tempCaseRef,
+ "/api/file/createappealcompletemessage_api?container=" +
+ encodeURIComponent(containerName) +
+ "&tempcaseref=" +
+ encodeURIComponent(tempCaseRef)
+ ];
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -52,83 +61,87 @@ ApiProxy.get(async (req, res) => {
});
}
- const blobProgress = await getProgressBlobs(
- containerName,
- tempCaseRef
- ).then((data) => {
- return downloadProgressFile(containerName, data.path, tempCaseRef);
- });
+ try {
+ const progressBlob = await getProgressBlobs(containerName, tempCaseRef);
+ const blobProgress = await downloadProgressFile(
+ containerName,
+ progressBlob.path,
+ tempCaseRef
+ );
- const caseObj = await downloadProgressFile(
- containerName,
- tempCaseRef + "/" + tempCaseRef + "_case.json",
- tempCaseRef
- );
+ const caseObj = await downloadProgressFile(
+ containerName,
+ tempCaseRef + "/" + tempCaseRef + "_case.json",
+ tempCaseRef
+ );
- Object.assign(blobProgress, {
- "appealComplete": tempCaseRef
- });
+ Object.assign(blobProgress, {
+ appealComplete: tempCaseRef
+ });
- delete blobProgress["pinswg_name"];
+ delete blobProgress.pinswg_name;
- await createBlob(JSON.stringify(blobProgress), containerName, tempCaseRef)
- .then(() => {
- //update case.json with lpa ref and description
+ await createBlob(
+ JSON.stringify(blobProgress),
+ containerName,
+ tempCaseRef
+ );
- let combinedAddress =
- blobProgress.pinswg_siteaddressline1 +
- (!_.isEmpty(blobProgress.pinswg_siteaddressline2)
- ? ", " + blobProgress.pinswg_siteaddressline2
- : "") +
- (!_.isEmpty(blobProgress.pinswg_siteaddresstown)
- ? ", " + blobProgress.pinswg_siteaddresstown
- : "");
+ let combinedAddress =
+ blobProgress.pinswg_siteaddressline1 +
+ (!_.isEmpty(blobProgress.pinswg_siteaddressline2)
+ ? ", " + blobProgress.pinswg_siteaddressline2
+ : "") +
+ (!_.isEmpty(blobProgress.pinswg_siteaddresstown)
+ ? ", " + blobProgress.pinswg_siteaddresstown
+ : "");
- combinedAddress =
- combinedAddress +
- ", " +
- blobProgress.pinswg_siteaddresscounty +
- ", " +
- blobProgress.pinswg_siteaddresspostcode;
+ combinedAddress =
+ combinedAddress +
+ ", " +
+ blobProgress.pinswg_siteaddresscounty +
+ ", " +
+ blobProgress.pinswg_siteaddresspostcode;
- Object.assign(caseObj, {
- "pinswg_lpareference":
- blobProgress.pinswg_lpaapplicationreference,
- "description": blobProgress.pinswg_developmentdescription,
- "pinswg_caseaddress": combinedAddress
- });
+ Object.assign(caseObj, {
+ pinswg_lpareference: blobProgress.pinswg_lpaapplicationreference,
+ description: blobProgress.pinswg_developmentdescription,
+ pinswg_caseaddress: combinedAddress
+ });
- //send update case.json and then create queue message
- getCaseBlob(containerName, tempCaseRef, caseObj).then(() => {
- let contactId = caseObj["customerid_contact@odata.bind"];
- contactId = contactId.match(/\(([^)]+)\)/);
+ await getCaseBlob(containerName, tempCaseRef, caseObj);
- if (typeofinvolvement != 846040000) {
- updateAccount(
+ let contactId = caseObj["customerid_contact@odata.bind"];
+ contactId = contactId.match(/\(([^)]+)\)/);
+
+ if (typeofinvolvement != 846040000) {
+ void (async () => {
+ try {
+ await updateAccount(
contactId[1],
{
- "pinswg_typeofinvolvement": 846040001
+ pinswg_typeofinvolvement: 846040001
},
true
- ).catch((error) => {
- consoleLogger(error);
- });
+ );
+ } catch (error) {
+ consoleLogger(error);
}
+ })();
+ }
- createCaseCompleteMessage(containerName, tempCaseRef);
- return respondSuccess(res, {
- status: "success"
- });
- });
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED",
- message: "Failed to create appeal complete message"
- });
+ await createCaseCompleteMessage(containerName, tempCaseRef);
+ return respondSuccess(res, {
+ status: "success"
});
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED",
+ message: "Failed to create appeal complete message"
+ });
+ }
});
export const config = {
diff --git a/pages/api/file/createappealcompletemessageproxy_api.js b/pages/api/file/createappealcompletemessageproxy_api.js
index b495040c..81d3c443 100644
--- a/pages/api/file/createappealcompletemessageproxy_api.js
+++ b/pages/api/file/createappealcompletemessageproxy_api.js
@@ -17,8 +17,8 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var tempCaseRef = req.query.tempcaseref;
+ const containerName = req.query.container;
+ const tempCaseRef = req.query.tempcaseref;
if (!hasValue(containerName) || !hasValue(tempCaseRef)) {
return respondError(res, {
@@ -28,30 +28,28 @@ ApiProxy.get(async (req, res) => {
});
}
- var token = await getToken();
+ const token = await getToken();
- var queryUrl =
+ const queryUrl =
"/api/file/createappealcompletemessage_api?container=" +
- containerName +
+ encodeURIComponent(containerName) +
"&tempcaseref=" +
- tempCaseRef;
+ encodeURIComponent(tempCaseRef);
- return axios
- .get(
+ try {
+ const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
- )
- .then(({ data }) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED",
- message: "Failed to create appeal complete message"
- });
+ );
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED",
+ message: "Failed to create appeal complete message"
});
+ }
});
export const config = {
diff --git a/pages/api/file/createcaseinvolvement_api.js b/pages/api/file/createcaseinvolvement_api.js
index af6b7848..a1761dc9 100644
--- a/pages/api/file/createcaseinvolvement_api.js
+++ b/pages/api/file/createcaseinvolvement_api.js
@@ -11,7 +11,6 @@
*/
import axios from "axios";
-import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -36,20 +35,20 @@ export default async function ApiProxy(req, res) {
});
}
- var token = await getToken();
- var contactid = req.body.contactid;
- var queryUrl =
+ const token = await getToken();
+ const contactid = req.body.contactid;
+ const queryUrl =
"incidents(" +
req.body.incidentid +
")/pinswg_incident_contact_case_involvement/$ref";
- var crmUrl = "https://" + process.env.CRMURL;
+ const crmUrl = "https://" + process.env.CRMURL;
- var data = {
+ const data = {
"@odata.id": crmUrl + "/api/data/v8.2/contacts(" + contactid + ")"
};
- var config = {
+ const config = {
method: "post",
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
headers: {
@@ -64,18 +63,20 @@ export default async function ApiProxy(req, res) {
data: data
};
- return axios(config)
- .then(({ data }) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- return error.status == 412
- ? respondSuccess(res, { "record": "exists" })
- : (consoleLogger(Object.assign(error, data)),
- respondError(res, {
- status: 400,
- code: "CREATE_CASE_INVOLVEMENT_FAILED",
- message: "Failed to create case involvement"
- }));
+ try {
+ const { data: responseData } = await axios(config);
+ return respondSuccess(res, responseData);
+ } catch (error) {
+ const errorStatus = error?.status || error?.response?.status;
+ if (errorStatus == 412) {
+ return respondSuccess(res, { record: "exists" });
+ }
+
+ consoleLogger(Object.assign(error, data));
+ return respondError(res, {
+ status: 400,
+ code: "CREATE_CASE_INVOLVEMENT_FAILED",
+ message: "Failed to create case involvement"
});
+ }
}
diff --git a/pages/api/file/createrepcompletemessage_api.js b/pages/api/file/createrepcompletemessage_api.js
index d682d651..bff12c91 100644
--- a/pages/api/file/createrepcompletemessage_api.js
+++ b/pages/api/file/createrepcompletemessage_api.js
@@ -13,10 +13,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var tempCaseRef = req.query.tempcaseref;
- var filename = req.query.repid;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const tempCaseRef = req.query.tempcaseref;
+ const filename = req.query.repid;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -35,15 +35,26 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
+ const hashCandidatePaths = [
"/api/file/createrepcompletemessage_api?container=" +
- containerName +
- "&tempcaseref=" +
- tempCaseRef +
- "&repid=" +
- filename;
+ containerName +
+ "&tempcaseref=" +
+ tempCaseRef +
+ "&repid=" +
+ filename,
+ "/api/file/createrepcompletemessage_api?container=" +
+ encodeURIComponent(containerName) +
+ "&tempcaseref=" +
+ encodeURIComponent(tempCaseRef) +
+ "&repid=" +
+ encodeURIComponent(filename)
+ ];
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -51,18 +62,21 @@ ApiProxy.get(async (req, res) => {
});
}
- await createRepCompleteMessage(containerName, tempCaseRef, filename)
- .then((data) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "CREATE_REP_COMPLETE_MESSAGE_FAILED",
- message: "Failed to create representation complete message"
- });
+ try {
+ const data = await createRepCompleteMessage(
+ containerName,
+ tempCaseRef,
+ filename
+ );
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "CREATE_REP_COMPLETE_MESSAGE_FAILED",
+ message: "Failed to create representation complete message"
});
+ }
});
export const config = {
diff --git a/pages/api/file/createrepinvolvement_api.js b/pages/api/file/createrepinvolvement_api.js
index 4f40349f..856185ec 100644
--- a/pages/api/file/createrepinvolvement_api.js
+++ b/pages/api/file/createrepinvolvement_api.js
@@ -11,7 +11,6 @@
*/
import axios from "axios";
-import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -36,22 +35,22 @@ export default async function ApiProxy(req, res) {
});
}
- var token = await getToken();
- var contactid = req.body.contactid;
- var queryUrl =
+ const token = await getToken();
+ const contactid = req.body.contactid;
+ const queryUrl =
"incidents(" +
req.body.incidentid +
")/pinswg_incident_contact_case_involvement/$ref";
- var crmUrl = "https://" + process.env.CRMURL;
- var crmVersion = process.env.CRMURL_VERSION;
+ const crmUrl = "https://" + process.env.CRMURL;
+ const crmVersion = process.env.CRMURL_VERSION;
- var data = {
+ const data = {
"@odata.id":
crmUrl + "/api/data/" + crmVersion + "/contacts(" + contactid + ")"
};
- var config = {
+ const config = {
method: "post",
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
headers: {
@@ -66,18 +65,20 @@ export default async function ApiProxy(req, res) {
data: data
};
- return axios(config)
- .then(({ data }) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- return error.status == 412
- ? respondSuccess(res, { "record": "exists" })
- : (consoleLogger(Object.assign(error, data)),
- respondError(res, {
- status: 400,
- code: "CREATE_REP_INVOLVEMENT_FAILED",
- message: "Failed to create representation involvement"
- }));
+ try {
+ const { data: responseData } = await axios(config);
+ return respondSuccess(res, responseData);
+ } catch (error) {
+ const errorStatus = error?.status || error?.response?.status;
+ if (errorStatus == 412) {
+ return respondSuccess(res, { record: "exists" });
+ }
+
+ consoleLogger(Object.assign(error, data));
+ return respondError(res, {
+ status: 400,
+ code: "CREATE_REP_INVOLVEMENT_FAILED",
+ message: "Failed to create representation involvement"
});
+ }
}
diff --git a/pages/api/file/deleteawaitingsubmissionfromblob.js b/pages/api/file/deleteawaitingsubmissionfromblob.js
index a1cdbe53..4740aa45 100644
--- a/pages/api/file/deleteawaitingsubmissionfromblob.js
+++ b/pages/api/file/deleteawaitingsubmissionfromblob.js
@@ -9,10 +9,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
- var blobName = req.query.blobname;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
+ const blobName = req.query.blobname;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -31,15 +31,43 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
- "/api/file/deleteblob?container=" +
- containerName +
- "&casefolderID=" +
- casefolderID +
- "&blobname=" +
- blobName;
+ const casefolderIDTrimmed = casefolderID.trim();
+ const blobNameTrimmed = blobName.trim();
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const hashCandidatePaths = [
+ "/api/file/deleteawaitingsubmissionfromblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed +
+ "&blobname=" +
+ blobNameTrimmed,
+ "/api/file/deleteawaitingsubmissionfromblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed) +
+ "&blobname=" +
+ encodeURIComponent(blobNameTrimmed),
+
+ // legacy compatibility with callers that hash against deleteblob route
+ "/api/file/deleteblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed +
+ "&blobname=" +
+ blobNameTrimmed,
+ "/api/file/deleteblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed) +
+ "&blobname=" +
+ encodeURIComponent(blobNameTrimmed)
+ ];
+
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -47,11 +75,22 @@ ApiProxy.get(async (req, res) => {
});
}
- await deleteBlob(containerName, casefolderID + "/files/" + blobName).then(
- (data) => {
- return respondSuccess(res, { data: data });
- }
- );
+ try {
+ const normalizedBlobName = blobNameTrimmed.startsWith(
+ casefolderIDTrimmed + "/"
+ )
+ ? blobNameTrimmed
+ : casefolderIDTrimmed + "/files/" + blobNameTrimmed;
+
+ const data = await deleteBlob(containerName, normalizedBlobName);
+ return respondSuccess(res, { data: data });
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "DELETE_AWAITING_SUBMISSION_BLOB_FAILED",
+ message: "Unable to delete awaiting submission blob"
+ });
+ }
});
export const config = {
diff --git a/pages/api/file/deleteblob.js b/pages/api/file/deleteblob.js
index 5c1d52fe..5402078b 100644
--- a/pages/api/file/deleteblob.js
+++ b/pages/api/file/deleteblob.js
@@ -9,10 +9,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
- var blobName = req.query.blobname;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
+ const blobName = req.query.blobname;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -31,15 +31,41 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
- "/api/file/deleteblob?container=" +
- containerName +
- "&casefolderID=" +
- encodeURIComponent(casefolderID) +
- "&blobname=" +
- encodeURIComponent(blobName);
+ const casefolderIDTrimmed = casefolderID.trim();
+ const blobNameTrimmed = blobName.trim();
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const hashCandidatePaths = [
+ "/api/file/deleteblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed +
+ "&blobname=" +
+ blobNameTrimmed,
+ "/api/file/deleteblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed) +
+ "&blobname=" +
+ blobNameTrimmed,
+ "/api/file/deleteblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed +
+ "&blobname=" +
+ encodeURIComponent(blobNameTrimmed),
+ "/api/file/deleteblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed) +
+ "&blobname=" +
+ encodeURIComponent(blobNameTrimmed)
+ ];
+
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -47,11 +73,22 @@ ApiProxy.get(async (req, res) => {
});
}
- await deleteBlob(containerName, casefolderID + "/files/" + blobName).then(
- (data) => {
- return respondSuccess(res, { data: data });
- }
- );
+ try {
+ const normalizedBlobName = blobNameTrimmed.startsWith(
+ casefolderIDTrimmed + "/"
+ )
+ ? blobNameTrimmed
+ : casefolderIDTrimmed + "/files/" + blobNameTrimmed;
+
+ const data = await deleteBlob(containerName, normalizedBlobName);
+ return respondSuccess(res, { data: data });
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "DELETE_BLOB_FAILED",
+ message: "Unable to delete blob"
+ });
+ }
});
export const config = {
diff --git a/pages/api/file/deleteblobcase.js b/pages/api/file/deleteblobcase.js
index ba48e9d5..62764132 100644
--- a/pages/api/file/deleteblobcase.js
+++ b/pages/api/file/deleteblobcase.js
@@ -9,9 +9,9 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -28,13 +28,24 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
- "/api/file/deleteblobcase?container=" +
- containerName +
- "&casefolderID=" +
- casefolderID;
+ const casefolderIDTrimmed = casefolderID.trim();
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const hashCandidatePaths = [
+ "/api/file/deleteblobcase?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed,
+ "/api/file/deleteblobcase?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed)
+ ];
+
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -42,9 +53,16 @@ ApiProxy.get(async (req, res) => {
});
}
- await deleteBlobCase(containerName, casefolderID).then((data) => {
+ try {
+ const data = await deleteBlobCase(containerName, casefolderIDTrimmed);
return respondSuccess(res, { data: data });
- });
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "DELETE_BLOB_CASE_FAILED",
+ message: "Unable to delete blob case"
+ });
+ }
});
export const config = {
diff --git a/pages/api/file/deleteblobrep.js b/pages/api/file/deleteblobrep.js
index fd80f28b..b327837c 100644
--- a/pages/api/file/deleteblobrep.js
+++ b/pages/api/file/deleteblobrep.js
@@ -9,10 +9,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
- var repfile = req.query.repfile;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
+ const repfile = req.query.repfile;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -31,15 +31,41 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
- "/api/file/deleteblobrep?container=" +
- containerName +
- "&casefolderID=" +
- casefolderID +
- "&repfile=" +
- repfile;
+ const casefolderIDTrimmed = casefolderID.trim();
+ const repfileTrimmed = repfile.trim();
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const hashCandidatePaths = [
+ "/api/file/deleteblobrep?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed +
+ "&repfile=" +
+ repfileTrimmed,
+ "/api/file/deleteblobrep?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed) +
+ "&repfile=" +
+ repfileTrimmed,
+ "/api/file/deleteblobrep?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed +
+ "&repfile=" +
+ encodeURIComponent(repfileTrimmed),
+ "/api/file/deleteblobrep?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed) +
+ "&repfile=" +
+ encodeURIComponent(repfileTrimmed)
+ ];
+
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -47,11 +73,17 @@ ApiProxy.get(async (req, res) => {
});
}
- casefolderID = casefolderID + "/" + repfile;
-
- await deleteBlobRep(containerName, casefolderID).then((data) => {
+ try {
+ const normalizedRepPath = casefolderIDTrimmed + "/" + repfileTrimmed;
+ const data = await deleteBlobRep(containerName, normalizedRepPath);
return respondSuccess(res, { data: data });
- });
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "DELETE_BLOB_REP_FAILED",
+ message: "Unable to delete blob representation"
+ });
+ }
});
export const config = {
diff --git a/pages/api/file/downloadblob.js b/pages/api/file/downloadblob.js
index f7683731..5209128e 100644
--- a/pages/api/file/downloadblob.js
+++ b/pages/api/file/downloadblob.js
@@ -10,10 +10,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
- var blobName = req.query.blobname;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
+ const blobName = req.query.blobname;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -32,15 +32,39 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
+ const blobNameTrimmed = blobName.trim();
+ const hashCandidatePaths = [
"/api/file/downloadblob?container=" +
- containerName +
- "&casefolderID=" +
- casefolderID +
- "&blobname=" +
- blobName.trim();
+ containerName +
+ "&casefolderID=" +
+ casefolderID +
+ "&blobname=" +
+ blobNameTrimmed,
+ "/api/file/downloadblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderID) +
+ "&blobname=" +
+ blobNameTrimmed,
+ "/api/file/downloadblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderID +
+ "&blobname=" +
+ encodeURIComponent(blobNameTrimmed),
+ "/api/file/downloadblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderID) +
+ "&blobname=" +
+ encodeURIComponent(blobNameTrimmed)
+ ];
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -48,34 +72,36 @@ ApiProxy.get(async (req, res) => {
});
}
- const bloblocation =
- casefolderID + (blobName.indexOf(".json") > 0 ? "/" : "/files/");
+ try {
+ const decodedBlobName = decodeURI(blobName);
+ const normalizedBlobName = decodedBlobName.startsWith(
+ casefolderID + "/"
+ )
+ ? decodedBlobName
+ : casefolderID +
+ (decodedBlobName.indexOf(".json") > 0 ? "/" : "/files/") +
+ decodedBlobName;
- const downloaded = await downloadFile(
- containerName,
- bloblocation + decodeURI(blobName)
- );
+ const downloaded = await downloadFile(
+ containerName,
+ normalizedBlobName
+ );
+ const responseFileName = decodedBlobName.split("/").pop();
- res.setHeader(
- "content-disposition",
- "attachment; filename=" + decodeURI(blobName)
- );
- return res.status(200).send(downloaded);
+ res.setHeader(
+ "content-disposition",
+ "attachment; filename=" + responseFileName
+ );
+ return res.status(200).send(downloaded);
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "DOWNLOAD_BLOB_FAILED",
+ message: "Unable to download blob"
+ });
+ }
});
-async function streamToBuffer(readableStream) {
- return new Promise((resolve, reject) => {
- const chunks = [];
- readableStream.on("data", (data) => {
- chunks.push(data instanceof Buffer ? data : Buffer.from(data));
- });
- readableStream.on("end", () => {
- resolve(Buffer.concat(chunks));
- });
- readableStream.on("error", reject);
- });
-}
-
export const config = {
api: {
bodyParser: false
diff --git a/pages/api/file/generateappealpdf.js b/pages/api/file/generateappealpdf.js
index e90284c9..d4fb3569 100644
--- a/pages/api/file/generateappealpdf.js
+++ b/pages/api/file/generateappealpdf.js
@@ -73,12 +73,6 @@
*/
import {
- createContainer,
- getContainers,
- getBlobs,
- createRepBlob,
- uploadFile,
- uploadPDFAppealFiles,
downloadProgressFile,
getProgressBlobs,
getTempCaseBlob,
@@ -86,23 +80,9 @@ import {
} from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
-import ReactPDF, {
- Document,
- Page,
- Text,
- View,
- StyleSheet,
- PDFViewer
-} from "@react-pdf/renderer";
-import middleware from "../middleware/middleware";
-import nextConnect from "next-connect";
-import fs from "fs";
-import path from "path";
+import ReactPDF from "@react-pdf/renderer";
import { getPickLists } from "../../../actions/services/referenceDataService";
import { planningappeals78_pdf } from "../../../components/pdftemplates/planningappeals78_pdf";
-import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf";
-import { statement_pdf } from "../../../components/pdftemplates/statement_pdf";
-import { writtenStatement_pdf } from "../../../components/pdftemplates/writtenStatement_pdf";
import { other_pdf } from "../../../components/pdftemplates/other_pdf";
import { respondError, respondSuccess } from "../middleware/apiResponse";
@@ -112,16 +92,15 @@ import { respondError, respondSuccess } from "../middleware/apiResponse";
// ApiProxy.post(async (req, res) => {
export default async function handler(req, res) {
- var checkHash = req.query.hash;
- var appealType = req.query.appealType;
- var isDownload = req.query?.download === "true";
+ const checkHash = req.query.hash;
+ const appealType = req.query.appealType;
+ const isDownload = req.query?.download === "true";
let appealBodyObj =
typeof req.body === "string" ? JSON.parse(req.body) : req.body;
- var containerID = appealBodyObj.containerID;
- var casefolderID = appealBodyObj.casefolderID;
- var caseRef = appealBodyObj.caseRef;
- var checkquerypath = "/api/file/generateappealpdf";
+ const containerID = appealBodyObj.containerID;
+ const casefolderID = appealBodyObj.casefolderID;
+ let checkquerypath = "/api/file/generateappealpdf";
if (
typeof checkHash === "undefined" ||
@@ -184,92 +163,97 @@ export default async function handler(req, res) {
}
};
- const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID);
- const blobProgress = await getProgressBlobs(containerID, casefolderID).then(
- (data) => {
- //console.log("Progress blob path:", data);
- return downloadProgressFile(containerID, data.path, casefolderID);
- }
- );
+ try {
+ const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID);
+ const progressBlob = await getProgressBlobs(containerID, casefolderID);
+ const blobProgress = await downloadProgressFile(
+ containerID,
+ progressBlob.path,
+ casefolderID
+ );
- const uniqueArray = appealBodyObj.filesList.filter((obj, index, self) => {
- // Check if the name has already been encountered
- return index === self.findIndex((t) => t.name === obj.name);
- });
-
- Object.assign(blobProgress, {
- "filesList": uniqueArray,
- "caseObj": tempCaseBlob
- });
-
- const pickListData = await getPickLists(appealType);
-
- // ReactPDF.render(
- // ,
- // process.cwd() + `/tmp/${blobProgress.pinswg_name}.pdf`
- // );
-
- // ReactPDF.renderToStream();
-
- const streamToBuffer = async (readableStream) => {
- return new Promise((resolve, reject) => {
- const chunks = [];
- readableStream.on("data", (chunk) => {
- chunks.push(
- Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
- );
- });
- readableStream.on("end", () => resolve(Buffer.concat(chunks)));
- readableStream.on("error", reject);
- });
- };
-
- const renderedPDF = await ReactPDF.renderToStream(
-
- );
-
- const repDate = new Date();
-
- let day = repDate.getDate();
- let month = repDate.getMonth() + 1;
- let year = repDate.getFullYear();
-
- const pdfFileName =
- year +
- "-" +
- ("0" + month).slice(-2) +
- "-" +
- ("0" + day).slice(-2) +
- "_-_Appeal_Form";
-
- const renderedPDFBuffer = await streamToBuffer(renderedPDF);
-
- await createAppealPDFBlob(
- renderedPDFBuffer,
- containerID,
- blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber
- )
- .then((data) => {
- if (isDownload) {
- const filename = `${pdfFileName}.pdf`;
- res.setHeader("Content-Type", "application/pdf");
- res.setHeader(
- "Content-Disposition",
- `attachment; filename="${filename}"`
- );
- return res.status(200).send(renderedPDFBuffer);
+ const uniqueArray = appealBodyObj.filesList.filter(
+ (obj, index, self) => {
+ return index === self.findIndex((t) => t.name === obj.name);
}
- return respondSuccess(res, {
- status: "success",
- data: data,
- path: `/files/${pdfFileName}.pdf`
- });
- })
- .catch(() => {
- return respondError(res, {
- status: 400,
- code: "GENERATE_APPEAL_PDF_FAILED",
- message: "Failed to generate appeal PDF"
- });
+ );
+
+ Object.assign(blobProgress, {
+ "filesList": uniqueArray,
+ "caseObj": tempCaseBlob
});
+
+ const pickListData = await getPickLists(appealType);
+
+ // ReactPDF.render(
+ // ,
+ // process.cwd() + `/tmp/${blobProgress.pinswg_name}.pdf`
+ // );
+
+ // ReactPDF.renderToStream();
+
+ const streamToBuffer = async (readableStream) => {
+ return new Promise((resolve, reject) => {
+ const chunks = [];
+ readableStream.on("data", (chunk) => {
+ chunks.push(
+ Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
+ );
+ });
+ readableStream.on("end", () => resolve(Buffer.concat(chunks)));
+ readableStream.on("error", reject);
+ });
+ };
+
+ const renderedPDF = await ReactPDF.renderToStream(
+ MyDocument({
+ docProps: blobProgress,
+ pickListData: pickListData
+ })
+ );
+
+ const repDate = new Date();
+
+ let day = repDate.getDate();
+ let month = repDate.getMonth() + 1;
+ let year = repDate.getFullYear();
+
+ const pdfFileName =
+ year +
+ "-" +
+ ("0" + month).slice(-2) +
+ "-" +
+ ("0" + day).slice(-2) +
+ "_-_Appeal_Form";
+
+ const renderedPDFBuffer = await streamToBuffer(renderedPDF);
+
+ const data = await createAppealPDFBlob(
+ renderedPDFBuffer,
+ containerID,
+ blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber
+ );
+
+ if (isDownload) {
+ const filename = `${pdfFileName}.pdf`;
+ res.setHeader("Content-Type", "application/pdf");
+ res.setHeader(
+ "Content-Disposition",
+ `attachment; filename="${filename}"`
+ );
+ return res.status(200).send(renderedPDFBuffer);
+ }
+
+ return respondSuccess(res, {
+ status: "success",
+ data: data,
+ path: `/files/${pdfFileName}.pdf`
+ });
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "GENERATE_APPEAL_PDF_FAILED",
+ message: "Failed to generate appeal PDF"
+ });
+ }
}
diff --git a/pages/api/file/generateappealpdfcopy.js b/pages/api/file/generateappealpdfcopy.js
index 4a1b7778..a3c87d83 100644
--- a/pages/api/file/generateappealpdfcopy.js
+++ b/pages/api/file/generateappealpdfcopy.js
@@ -1,45 +1,17 @@
-import {
- createContainer,
- getContainers,
- getBlobs,
- createRepBlob,
- uploadFile,
- uploadPDFAppealFiles,
- downloadProgressFile,
- getProgressBlobs,
- getTempCaseBlob,
- createAppealPDFBlob
-} from "../../../actions/azurestorage";
import {
getCaseByID,
getPortalModuleDetails,
getAppealPDFDocs
} from "../../../actions/services/caseService";
import { getFormCollectionByID } from "../../../components/utils";
-import ReactPDF, {
- Document,
- Page,
- Text,
- View,
- StyleSheet,
- PDFViewer,
- pdf
-} from "@react-pdf/renderer";
-
-import middleware from "../middleware/middleware";
-import nextConnect from "next-connect";
-import fs from "fs";
-import path from "path";
+import { pdf } from "@react-pdf/renderer";
import { getPickLists } from "../../../actions/services/referenceDataService";
import { planningappeals78_pdf } from "../../../components/pdftemplates/planningappeals78_pdf";
-import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf";
-import { statement_pdf } from "../../../components/pdftemplates/statement_pdf";
-import { writtenStatement_pdf } from "../../../components/pdftemplates/writtenStatement_pdf";
import { other_pdf } from "../../../components/pdftemplates/other_pdf";
+import { respondError } from "../middleware/apiResponse";
-const getDetails = (resultsObj, detailsType) => {
+const getDetails = (resultsObj) => {
if (!Array.isArray(resultsObj)) {
- console.warn("getDetails: resultsObj is not an array");
return Promise.resolve([]);
}
@@ -51,7 +23,6 @@ const getDetails = (resultsObj, detailsType) => {
const form = getFormCollectionByID(appealType);
if (!form) {
- console.warn("No form found for appeal type:", appealType);
return null;
}
@@ -64,54 +35,74 @@ const getDetails = (resultsObj, detailsType) => {
export default async function handler(req, res) {
const incidentid = req.body?.docProps;
- if (!incidentid) return res.status(400).send("Missing incident ID");
-
- const caseObj = await getCaseByID(incidentid);
- if (!Array.isArray(caseObj) || caseObj.length === 0)
- return res.status(404).send("Case not found");
-
- const caseDetailsObj = await getDetails(caseObj, "myCases");
- const fileList = (await getAppealPDFDocs(incidentid)) || [];
-
- // Inject into the first item in `value` array
-
- if (caseDetailsObj[0]?.value?.[0]) {
- caseDetailsObj[0].value[0].pinswg_developmentdescription =
- caseObj[0].description;
- caseDetailsObj[0].value[0].caseObj = caseObj[0];
- caseDetailsObj[0].value[0].filesList = fileList;
+ if (!incidentid) {
+ return respondError(res, {
+ status: 400,
+ code: "INCIDENT_ID_REQUIRED",
+ message: "Incident id is required"
+ });
}
- const whichForm = getFormCollectionByID(caseObj[0].pinswg_appealcasetype);
- const pickListData = await getPickLists(whichForm.UrlName);
+ try {
+ const caseObj = await getCaseByID(incidentid);
+ if (!Array.isArray(caseObj) || caseObj.length === 0) {
+ return respondError(res, {
+ status: 404,
+ code: "CASE_NOT_FOUND",
+ message: "Case not found"
+ });
+ }
- const appealTypeMap = {
- 846040000: planningappeals78_pdf,
- 846040004: planningappeals78_pdf
- };
+ const caseDetailsObj = await getDetails(caseObj);
+ const fileList = (await getAppealPDFDocs(incidentid)) || [];
- //console.log(caseDetailsObj[0].value[0]);
+ if (caseDetailsObj[0]?.value?.[0]) {
+ caseDetailsObj[0].value[0].pinswg_developmentdescription =
+ caseObj[0].description;
+ caseDetailsObj[0].value[0].caseObj = caseObj[0];
+ caseDetailsObj[0].value[0].filesList = fileList;
+ }
- const MyDocument = (values) => {
- const type = caseObj[0].pinswg_appealcasetype;
- const renderDocument = appealTypeMap[type] || other_pdf;
- return renderDocument(values);
- };
+ const whichForm = getFormCollectionByID(
+ caseObj[0].pinswg_appealcasetype
+ );
+ if (!whichForm?.UrlName) {
+ return respondError(res, {
+ status: 400,
+ code: "FORM_COLLECTION_NOT_FOUND",
+ message: "Unable to resolve form collection"
+ });
+ }
+ const pickListData = await getPickLists(whichForm.UrlName);
- const pdfComponent = MyDocument({
- docProps: caseDetailsObj[0].value[0],
- pickListData
- });
+ const appealTypeMap = {
+ 846040000: planningappeals78_pdf,
+ 846040004: planningappeals78_pdf
+ };
- const pdfBuffer = await pdf(pdfComponent).toBuffer();
+ const renderDocument =
+ appealTypeMap[caseObj[0].pinswg_appealcasetype] || other_pdf;
+ const pdfComponent = renderDocument({
+ docProps: caseDetailsObj?.[0]?.value?.[0] || {},
+ pickListData
+ });
- const repDate = new Date();
- const formattedDate = repDate.toISOString().split("T")[0];
+ const pdfBuffer = await pdf(pdfComponent).toBuffer();
- res.setHeader("Content-Type", "application/pdf");
- res.setHeader(
- "Content-Disposition",
- `attachment; filename=${formattedDate}_-_Appeal_Form.pdf`
- );
- res.send(pdfBuffer);
+ const repDate = new Date();
+ const formattedDate = repDate.toISOString().split("T")[0];
+
+ res.setHeader("Content-Type", "application/pdf");
+ res.setHeader(
+ "Content-Disposition",
+ `attachment; filename=${formattedDate}_-_Appeal_Form.pdf`
+ );
+ return res.send(pdfBuffer);
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "APPEAL_PDF_COPY_GENERATION_FAILED",
+ message: "Unable to generate appeal PDF copy"
+ });
+ }
}
diff --git a/pages/api/file/generatepdf.js b/pages/api/file/generatepdf.js
index 981d4f0e..42500ca6 100644
--- a/pages/api/file/generatepdf.js
+++ b/pages/api/file/generatepdf.js
@@ -72,28 +72,10 @@
* description: Success
*/
-import {
- createContainer,
- getContainers,
- getBlobs,
- createRepBlob,
- uploadFile,
- uploadPDFRepFiles,
- createRepPDFBlob
-} from "../../../actions/azurestorage";
+import { createRepPDFBlob } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
-import ReactPDF, {
- Document,
- Page,
- Text,
- View,
- StyleSheet,
- PDFViewer
-} from "@react-pdf/renderer";
-import middleware from "../middleware/middleware";
-import nextConnect from "next-connect";
-import fs from "fs";
+import ReactPDF from "@react-pdf/renderer";
import { EnforcementQuestionnaire_pdf } from "../../../components/pdftemplates/enforcement_pdf";
import { lpaQuestionnaire_pdf } from "../../../components/pdftemplates/lpaQuestionnaire_pdf";
import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf";
@@ -109,7 +91,7 @@ import { respondError, respondSuccess } from "../middleware/apiResponse";
export default async function handler(req, res) {
const shouldDownload = req.query.download === "true";
- var checkHash = req.query.hash;
+ const checkHash = req.query.hash;
let reqBodyobj =
typeof req.body === "string" ? JSON.parse(req.body) : req.body;
@@ -169,15 +151,11 @@ export default async function handler(req, res) {
// "\n//////////////////////\n"
// );
- var containerID = reqBodyobj.containerID;
- var casefolderID = reqBodyobj.casefolderID;
- var caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef;
- var representationType = reqBodyobj.representationType;
- var repRaiser = reqBodyobj.lastname;
- var localeSelect = reqBodyobj.locale;
+ const containerID = reqBodyobj.containerID;
+ const caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef;
//console.log("there are files:", Object.keys(req.files).length);
- var checkquerypath = "/api/file/generatepdf";
+ let checkquerypath = "/api/file/generatepdf";
if (shouldDownload) {
checkquerypath += "?download=true";
@@ -247,67 +225,6 @@ export default async function handler(req, res) {
}
};
- let repCapacity = "";
- switch (reqBodyobj.representationCapacity) {
- case "Appellant":
- repCapacity = "APP";
- break;
- case "Agent":
- repCapacity = "AGENT";
- break;
- case "Interested Party/Person":
- repCapacity = "IP";
- break;
- case "Land Owner":
- repCapacity = "LO";
- break;
- case "lpa":
- case "LPA":
- repCapacity = "LPA";
- break;
-
- default:
- // code block
- }
-
- let repType = "";
- switch (reqBodyobj.representationType) {
- case "Statement":
- repType = "Statement";
- break;
- case "Statement of common ground":
- repType = "SCG";
- break;
- case "Written statement":
- repType = "WS";
- break;
- case "Written statement of evidence":
- repType = "WSE";
- break;
- case "Questionnaire":
- repType = "Questionnaire";
- break;
- case "Final comments":
- repType = "Comments";
- break;
- case "Local Impact Report":
- repType = "Impact";
- break;
- case "Consultation Response":
- repType = "Consultation_Response";
- break;
- case "Marine Impact Report":
- repType = "Marine_Impact";
- break;
-
- case "Other":
- repType = "OTHER";
- break;
-
- default:
- // code block
- }
-
//console.log(
// "///////////////////////////////////\n file created: \n" +
// process.cwd() +
@@ -336,40 +253,40 @@ export default async function handler(req, res) {
});
};
- const renderedPDFStream = await ReactPDF.renderToStream(
-
- );
- const renderedPDFBuffer = await streamToBuffer(renderedPDFStream);
+ try {
+ const renderedPDFStream = await ReactPDF.renderToStream(
+ MyDocument({ docProps: reqBodyobj })
+ );
+ const renderedPDFBuffer = await streamToBuffer(renderedPDFStream);
- await createRepPDFBlob(
- renderedPDFBuffer,
- containerID,
- caseRef,
- reqBodyobj.repfile_name
- )
- .then((data) => {
- if (shouldDownload) {
- const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`;
- res.setHeader("Content-Type", "application/pdf");
- res.setHeader(
- "Content-Disposition",
- `attachment; filename="${filename}"`
- );
- return res.status(200).send(renderedPDFBuffer);
- }
+ const data = await createRepPDFBlob(
+ renderedPDFBuffer,
+ containerID,
+ caseRef,
+ reqBodyobj.repfile_name
+ );
- return respondSuccess(res, {
- status: "success",
- data: data,
- path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf`
- });
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "GENERATE_PDF_FAILED",
- message: "Failed to generate PDF"
- });
+ if (shouldDownload) {
+ const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`;
+ res.setHeader("Content-Type", "application/pdf");
+ res.setHeader(
+ "Content-Disposition",
+ `attachment; filename="${filename}"`
+ );
+ return res.status(200).send(renderedPDFBuffer);
+ }
+
+ return respondSuccess(res, {
+ status: "success",
+ data: data,
+ path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf`
});
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "GENERATE_PDF_FAILED",
+ message: "Failed to generate PDF"
+ });
+ }
}
diff --git a/pages/api/file/getawaitingsubmissionfromblob.js b/pages/api/file/getawaitingsubmissionfromblob.js
index d8ca4315..8ee749a7 100644
--- a/pages/api/file/getawaitingsubmissionfromblob.js
+++ b/pages/api/file/getawaitingsubmissionfromblob.js
@@ -2,7 +2,6 @@ import {
downloadAllProgressFiles,
getAllProgressBlobs
} from "../../../actions/azurestorage";
-import _ from "lodash";
import nextConnect from "next-connect";
import middleware from "../middleware/middleware";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -12,8 +11,8 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -28,10 +27,17 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
- "/api/file/getawaitingsubmissionfromblob?container=" + containerName;
+ const hashCandidatePaths = [
+ "/api/file/getawaitingsubmissionfromblob?container=" + containerName,
+ "/api/file/getawaitingsubmissionfromblob?container=" +
+ encodeURIComponent(containerName)
+ ];
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -39,13 +45,20 @@ ApiProxy.get(async (req, res) => {
});
}
- const blobObj = await getAllProgressBlobs(containerName)
- .then((data) => {
- return downloadAllProgressFiles(containerName, data);
- })
- .then((data) => {
- return respondSuccess(res, data);
+ try {
+ const progressBlobs = await getAllProgressBlobs(containerName);
+ const data = await downloadAllProgressFiles(
+ containerName,
+ progressBlobs
+ );
+ return respondSuccess(res, data);
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "GET_AWAITING_SUBMISSION_BLOB_FAILED",
+ message: "Failed to retrieve awaiting submission blobs"
});
+ }
});
export const config = {
diff --git a/pages/api/file/getawaitingsubmissionfromblobproxy.js b/pages/api/file/getawaitingsubmissionfromblobproxy.js
index f04a2198..3dcda590 100644
--- a/pages/api/file/getawaitingsubmissionfromblobproxy.js
+++ b/pages/api/file/getawaitingsubmissionfromblobproxy.js
@@ -11,7 +11,7 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
export default async function ApiProxy(req, res) {
- var containerName = req.query.container;
+ const containerName = req.query.container;
if (!hasValue(containerName)) {
return respondError(res, {
@@ -21,25 +21,23 @@ export default async function ApiProxy(req, res) {
});
}
- var token = await getToken();
+ const token = await getToken();
- var queryUrl =
+ const queryUrl =
"/api/file/getawaitingsubmissionfromblob?container=" + containerName;
- return axios
- .get(
+ try {
+ const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
- )
- .then(({ data }) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "GET_AWAITING_SUBMISSION_PROXY_FAILED",
- message: "Failed to fetch awaiting submission blob"
- });
+ );
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "GET_AWAITING_SUBMISSION_PROXY_FAILED",
+ message: "Failed to fetch awaiting submission blob"
});
+ }
}
diff --git a/pages/api/file/getbloblist.js b/pages/api/file/getbloblist.js
index da29a655..8b21ee3a 100644
--- a/pages/api/file/getbloblist.js
+++ b/pages/api/file/getbloblist.js
@@ -40,10 +40,9 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
-
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -60,13 +59,24 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
- "/api/file/getbloblist?container=" +
- containerName +
- "&casefolderID=" +
- casefolderID;
+ const casefolderIDTrimmed = casefolderID.trim();
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const hashCandidatePaths = [
+ "/api/file/getbloblist?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed,
+ "/api/file/getbloblist?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed)
+ ];
+
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -74,16 +84,24 @@ ApiProxy.get(async (req, res) => {
});
}
- const data =
- casefolderID.split("/").length > 1
- ? await getRepsFilesBlobs(
- containerName,
- casefolderID.split("/")[0],
- casefolderID.split("/")[1]
- )
- : await getBlobs(containerName, casefolderID);
+ try {
+ const data =
+ casefolderIDTrimmed.split("/").length > 1
+ ? await getRepsFilesBlobs(
+ containerName,
+ casefolderIDTrimmed.split("/")[0],
+ casefolderIDTrimmed.split("/")[1]
+ )
+ : await getBlobs(containerName, casefolderIDTrimmed);
- return respondSuccess(res, { "value": [data] });
+ return respondSuccess(res, { "value": [data] });
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "GET_BLOB_LIST_FAILED",
+ message: "Failed to retrieve blob list"
+ });
+ }
});
export const config = {
diff --git a/pages/api/file/getbloblistproxy.js b/pages/api/file/getbloblistproxy.js
index c6771fca..6dd29acf 100644
--- a/pages/api/file/getbloblistproxy.js
+++ b/pages/api/file/getbloblistproxy.js
@@ -11,8 +11,8 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
export default async function ApiProxy(req, res) {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
if (!hasValue(containerName) || !hasValue(casefolderID)) {
return respondError(res, {
@@ -22,28 +22,26 @@ export default async function ApiProxy(req, res) {
});
}
- var token = await getToken();
+ const token = await getToken();
- var queryUrl =
+ const queryUrl =
"/api/file/getbloblist?container=" +
- containerName +
+ encodeURIComponent(containerName) +
"&casefolderID=" +
- casefolderID;
+ encodeURIComponent(casefolderID);
- return axios
- .get(
+ try {
+ const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
- )
- .then(({ data }) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "GET_BLOB_LIST_PROXY_FAILED",
- message: "Failed to fetch blob list"
- });
+ );
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "GET_BLOB_LIST_PROXY_FAILED",
+ message: "Failed to fetch blob list"
});
+ }
}
diff --git a/pages/api/file/getprogressobjblob.js b/pages/api/file/getprogressobjblob.js
index a9481aa4..ebdea060 100644
--- a/pages/api/file/getprogressobjblob.js
+++ b/pages/api/file/getprogressobjblob.js
@@ -12,10 +12,9 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
-
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const casefolderID = req.query.casefolderID;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -32,13 +31,24 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath =
- "/api/file/getprogressobjblob?container=" +
- containerName +
- "&casefolderID=" +
- casefolderID;
+ const casefolderIDTrimmed = casefolderID.trim();
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const hashCandidatePaths = [
+ "/api/file/getprogressobjblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ casefolderIDTrimmed,
+ "/api/file/getprogressobjblob?container=" +
+ containerName +
+ "&casefolderID=" +
+ encodeURIComponent(casefolderIDTrimmed)
+ ];
+
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -46,13 +56,24 @@ ApiProxy.get(async (req, res) => {
});
}
- await getProgressBlobs(containerName, casefolderID)
- .then((data) => {
- return downloadProgressFile(containerName, data.path, casefolderID);
- })
- .then((data) => {
- return respondSuccess(res, data);
+ try {
+ const progressBlob = await getProgressBlobs(
+ containerName,
+ casefolderIDTrimmed
+ );
+ const data = await downloadProgressFile(
+ containerName,
+ progressBlob.path,
+ casefolderIDTrimmed
+ );
+ return respondSuccess(res, data);
+ } catch (error) {
+ return respondError(res, {
+ status: 400,
+ code: "GET_PROGRESS_OBJ_BLOB_FAILED",
+ message: "Failed to retrieve progress blob"
});
+ }
});
export const config = {
diff --git a/pages/api/file/getrepsblob.js b/pages/api/file/getrepsblob.js
index a059f5a2..45dff296 100644
--- a/pages/api/file/getrepsblob.js
+++ b/pages/api/file/getrepsblob.js
@@ -33,7 +33,6 @@ import {
downloadAllRepsFiles,
getRepsBlobs
} from "../../../actions/azurestorage";
-import _ from "lodash";
import nextConnect from "next-connect";
import middleware from "../middleware/middleware";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -44,10 +43,8 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.container;
- var casefolderID = req.query.casefolderID;
-
- var checkHash = req.query.hash;
+ const containerName = req.query.container;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -62,9 +59,16 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath = "/api/file/getrepsblob?container=" + containerName;
+ const hashCandidatePaths = [
+ "/api/file/getrepsblob?container=" + containerName,
+ "/api/file/getrepsblob?container=" + encodeURIComponent(containerName)
+ ];
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -72,23 +76,19 @@ ApiProxy.get(async (req, res) => {
});
}
- const blobObj = await getRepsBlobs(containerName)
- .then(async (data) => {
- return data;
- })
- .then(async (data) => {
- let result = await downloadAllRepsFiles(containerName, data);
- res.setHeader("Cache-Control", "no-store");
- return respondSuccess(res, result);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "GET_REPS_BLOB_FAILED",
- message: "Failed to retrieve representation blobs"
- });
+ try {
+ const repsBlobs = await getRepsBlobs(containerName);
+ const result = await downloadAllRepsFiles(containerName, repsBlobs);
+ res.setHeader("Cache-Control", "no-store");
+ return respondSuccess(res, result);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "GET_REPS_BLOB_FAILED",
+ message: "Failed to retrieve representation blobs"
});
+ }
});
export default ApiProxy;
diff --git a/pages/api/file/getrepsblobproxy.js b/pages/api/file/getrepsblobproxy.js
index 36383f1a..64a02e54 100644
--- a/pages/api/file/getrepsblobproxy.js
+++ b/pages/api/file/getrepsblobproxy.js
@@ -11,7 +11,7 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
export default async function ApiProxy(req, res) {
- var containerName = req.query.container;
+ const containerName = req.query.container;
if (!hasValue(containerName)) {
return respondError(res, {
@@ -21,24 +21,23 @@ export default async function ApiProxy(req, res) {
});
}
- var token = await getToken();
+ const token = await getToken();
- var queryUrl = "/api/file/getrepsblob?container=" + containerName;
+ const queryUrl =
+ "/api/file/getrepsblob?container=" + encodeURIComponent(containerName);
- return axios
- .get(
+ try {
+ const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
- )
- .then(({ data }) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "GET_REPS_BLOB_PROXY_FAILED",
- message: "Failed to fetch representation blobs"
- });
+ );
+ return respondSuccess(res, data);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "GET_REPS_BLOB_PROXY_FAILED",
+ message: "Failed to fetch representation blobs"
});
+ }
}
diff --git a/pages/api/file/setupcontainer.js b/pages/api/file/setupcontainer.js
index ca2c5d00..01927dcc 100644
--- a/pages/api/file/setupcontainer.js
+++ b/pages/api/file/setupcontainer.js
@@ -1,10 +1,4 @@
-import {
- createContainer,
- createContainerSas,
- getContainers,
- getBlobs,
- uploadFile
-} from "../../../actions/azurestorage";
+import { createContainer } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import { consoleLogger } from "../../../actions/core/logger";
import { respondError, respondSuccess } from "../middleware/apiResponse";
@@ -16,8 +10,8 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
- var containerName = req.query.ident;
- var checkHash = req.query.hash;
+ const containerName = req.query.ident;
+ const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -32,9 +26,19 @@ ApiProxy.get(async (req, res) => {
});
}
- var checkquerypath = "/api/file/setupcontainer?ident=" + containerName;
+ const containerNameTrimmed = containerName.trim();
- if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
+ const hashCandidatePaths = [
+ "/api/file/setupcontainer?ident=" + containerNameTrimmed,
+ "/api/file/setupcontainer?ident=" +
+ encodeURIComponent(containerNameTrimmed)
+ ];
+
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
+ );
+
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -42,18 +46,17 @@ ApiProxy.get(async (req, res) => {
});
}
- await createContainer(containerName)
- .then((data) => {
- return respondSuccess(res, { data: "success", output: data });
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "SETUP_CONTAINER_FAILED",
- message: "Failed to setup container"
- });
+ try {
+ const data = await createContainer(containerNameTrimmed);
+ return respondSuccess(res, { data: "success", output: data });
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "SETUP_CONTAINER_FAILED",
+ message: "Failed to setup container"
});
+ }
});
export const config = {
diff --git a/pages/api/file/updatecase_api.js b/pages/api/file/updatecase_api.js
index c458b8dd..ccb51318 100644
--- a/pages/api/file/updatecase_api.js
+++ b/pages/api/file/updatecase_api.js
@@ -1,5 +1,4 @@
import axios from "axios";
-import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -10,12 +9,12 @@ const WEBAPI_URL =
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
export default async function ApiProxy(req, res) {
- var data = JSON.stringify(req.body);
- var appealObj = req.query.appealObj;
- var incidentID = req.query.incident;
+ const data = JSON.stringify(req.body);
+ const appealObj = req.query.appealObj;
+ const incidentID = req.query.incident;
- var updateFormCollection = req.query.updateFormCollection;
- var queryUrl = updateFormCollection + "(" + appealObj + ")";
+ const updateFormCollection = req.query.updateFormCollection;
+ const queryUrl = updateFormCollection + "(" + appealObj + ")";
if (
typeof updateFormCollection === "undefined" ||
@@ -35,9 +34,9 @@ export default async function ApiProxy(req, res) {
});
}
- var token = await getToken();
+ const token = await getToken();
- var config = {
+ const config = {
method: "patch",
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
headers: {
@@ -53,16 +52,15 @@ export default async function ApiProxy(req, res) {
//console.log("update case:", data);
- return axios(config)
- .then(({ data }) => {
- return respondSuccess(res, data);
- })
- .catch((error) => {
- consoleLogger(error);
- return respondError(res, {
- status: 400,
- code: "UPDATE_CASE_FAILED",
- message: "Failed to update case"
- });
+ try {
+ const { data: responseData } = await axios(config);
+ return respondSuccess(res, responseData);
+ } catch (error) {
+ consoleLogger(error);
+ return respondError(res, {
+ status: 400,
+ code: "UPDATE_CASE_FAILED",
+ message: "Failed to update case"
});
+ }
}
diff --git a/pages/api/file/upload.js b/pages/api/file/upload.js
index 80038527..70072889 100644
--- a/pages/api/file/upload.js
+++ b/pages/api/file/upload.js
@@ -1,8 +1,4 @@
-import {
- createBlob,
- createRepBlob,
- uploadFile
-} from "../../../actions/azurestorage";
+import { createBlob, createRepBlob } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
@@ -13,7 +9,7 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.post(async (req, res) => {
- var checkHash = req.query.hash;
+ const checkHash = req.query.hash;
if (typeof checkHash === "undefined" || checkHash.length === 0) {
return respondError(res, {
@@ -23,9 +19,12 @@ ApiProxy.post(async (req, res) => {
});
}
- var checkquerypath = "/api/file/upload";
+ const hashCandidatePaths = ["/api/file/upload", "/api/file/upload?"];
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "?hash=" + checkHash
+ );
- if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -51,21 +50,19 @@ ApiProxy.post(async (req, res) => {
});
}
- return (
- repOrAppeal
+ try {
+ const data = await (repOrAppeal
? createRepBlob(appealData, containerID, casefolderID)
- : createBlob(appealData, containerID, casefolderID)
- )
- .then((data) => {
- return respondSuccess(res, { data });
- })
- .catch(() => {
- return respondError(res, {
- status: 400,
- code: "UPLOAD_FAILED",
- message: "Failed to upload blob"
- });
+ : createBlob(appealData, containerID, casefolderID));
+
+ return respondSuccess(res, { data });
+ } catch {
+ return respondError(res, {
+ status: 400,
+ code: "UPLOAD_FAILED",
+ message: "Failed to upload blob"
});
+ }
});
export const config = {
diff --git a/pages/api/file/uploadsinglefile.js b/pages/api/file/uploadsinglefile.js
index d5c0a99c..bef3de5a 100644
--- a/pages/api/file/uploadsinglefile.js
+++ b/pages/api/file/uploadsinglefile.js
@@ -1,8 +1,4 @@
-import {
- createBlob,
- createRepBlob,
- uploadSingleFile
-} from "../../../actions/azurestorage";
+import { uploadSingleFile } from "../../../actions/azurestorage";
import nextConnect from "next-connect";
import middleware from "../middleware/middleware";
@@ -37,7 +33,7 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.post(async (req, res) => {
- var checkHash = req.query.hash;
+ const checkHash = req.query.hash;
if (typeof checkHash === "undefined" || checkHash.length === 0) {
return respondError(res, {
@@ -47,9 +43,15 @@ ApiProxy.post(async (req, res) => {
});
}
- var checkquerypath = "/api/file/uploadsinglefile";
+ const hashCandidatePaths = [
+ "/api/file/uploadsinglefile",
+ "/api/file/uploadsinglefile?"
+ ];
+ const isHashValid = hashCandidatePaths.some(
+ (candidatePath) => hashAPIPath(candidatePath) == "?hash=" + checkHash
+ );
- if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
+ if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -87,8 +89,8 @@ ApiProxy.post(async (req, res) => {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
];
- var allowedFilesFormData = {};
- var invalidFiles = []; // To store the names of invalid files
+ const allowedFilesFormData = {};
+ const invalidFiles = []; // To store the names of invalid files
for (const [fileName, fileDetails] of Object.entries(uploadedFiles)) {
const file = fileDetails[0];
diff --git a/tests/phase21/api-contract-slice1.test.cjs b/tests/phase21/api-contract-slice1.test.cjs
index 2f89aaf8..f055de07 100644
--- a/tests/phase21/api-contract-slice1.test.cjs
+++ b/tests/phase21/api-contract-slice1.test.cjs
@@ -2,12 +2,14 @@ const runHelperTests = require("./api-response-helper.test.cjs");
const runHandlerTests = require("./file-handler-contract.test.cjs");
const runEmailHandlerTests = require("./email-handler-contract.test.cjs");
const runEndpointHandlerTests = require("./endpoint-handler-contract.test.cjs");
+const runDocumentsHandlerTests = require("./documents-handler-contract.test.cjs");
const run = async () => {
await runHelperTests();
await runHandlerTests();
await runEmailHandlerTests();
await runEndpointHandlerTests();
+ await runDocumentsHandlerTests();
console.log("Phase 21 combined suite passed.");
};
diff --git a/tests/phase21/documents-handler-contract.test.cjs b/tests/phase21/documents-handler-contract.test.cjs
new file mode 100644
index 00000000..e1035b3e
--- /dev/null
+++ b/tests/phase21/documents-handler-contract.test.cjs
@@ -0,0 +1,134 @@
+const assert = require("assert");
+const { Readable } = require("stream");
+const { loadModule } = require("./_shared.cjs");
+
+const tests = [];
+const test = (name, fn) => tests.push({ name, fn });
+
+const createRedirectRes = () => {
+ const state = {
+ redirectedTo: null,
+ headers: {},
+ piped: false,
+ headersSent: false
+ };
+
+ return {
+ state,
+ get headersSent() {
+ return state.headersSent;
+ },
+ setHeader(key, value) {
+ state.headers[key.toLowerCase()] = value;
+ },
+ redirect(path) {
+ state.redirectedTo = path;
+ state.headersSent = true;
+ return path;
+ }
+ };
+};
+
+test("documents download redirects when id/hash query is missing", async () => {
+ const mod = loadModule("pages/api/documents/download/[id].js", {
+ getToken: async () => ({ access_token: "tok" }),
+ consoleLogger: () => {},
+ axios: { get: async () => ({}) }
+ });
+
+ const req = { query: { id: "DOC-1" } };
+ const res = createRedirectRes();
+
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.redirectedTo, "/filenotavailable");
+});
+
+test("documents download success sets attachment headers and pipes stream", async () => {
+ const stream = new Readable({
+ read() {}
+ });
+
+ const mod = loadModule("pages/api/documents/download/[id].js", {
+ setTimeout: (fn) => {
+ fn();
+ return 0;
+ },
+ getToken: async () => ({ access_token: "tok" }),
+ consoleLogger: () => {},
+ axios: {
+ get: async () => ({
+ headers: {
+ "content-disposition": "attachment; filename=test-file.pdf"
+ },
+ data: stream
+ })
+ }
+ });
+
+ const req = { query: { id: "DOC-1", hash: "h1" } };
+ const res = createRedirectRes();
+
+ stream.pipe = (target) => {
+ target.state.piped = true;
+ target.state.headersSent = true;
+ return target;
+ };
+
+ await mod.default(req, res);
+
+ assert.strictEqual(
+ res.state.headers["content-disposition"],
+ "attachment; filename=test-file.pdf"
+ );
+ assert.strictEqual(
+ res.state.headers["content-type"],
+ "application/octet-stream"
+ );
+ assert.strictEqual(res.state.piped, true);
+ assert.strictEqual(res.state.redirectedTo, null);
+});
+
+test("documents download redirects when relay request fails", async () => {
+ const mod = loadModule("pages/api/documents/download/[id].js", {
+ setTimeout: (fn) => {
+ fn();
+ return 0;
+ },
+ getToken: async () => ({ access_token: "tok" }),
+ consoleLogger: () => {},
+ axios: {
+ get: async () => {
+ throw new Error("relay failed");
+ }
+ }
+ });
+
+ const req = { query: { id: "DOC-1", hash: "h1" } };
+ const res = createRedirectRes();
+
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.redirectedTo, "/filenotavailable");
+});
+
+const run = async () => {
+ let passed = 0;
+ for (const currentTest of tests) {
+ await currentTest.fn();
+ passed += 1;
+ }
+
+ console.log(
+ `Phase 21 documents-handler contract tests passed (${passed}/${tests.length}).`
+ );
+};
+
+module.exports = run;
+
+if (require.main === module) {
+ run().catch((error) => {
+ console.error(error);
+ process.exit(1);
+ });
+}
diff --git a/tests/phase21/endpoint-handler-contract.test.cjs b/tests/phase21/endpoint-handler-contract.test.cjs
index 0b3e09e1..8e165eba 100644
--- a/tests/phase21/endpoint-handler-contract.test.cjs
+++ b/tests/phase21/endpoint-handler-contract.test.cjs
@@ -323,6 +323,93 @@ test("getportallogin catch path returns PORTAL_LOGIN_FETCH_FAILED", async () =>
);
});
+test("getportallogin accepts encoded email hash variant", async () => {
+ const mod = loadModule("pages/api/endpoint/getportallogin_api.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "token" }),
+ hashAPIPath: (input) =>
+ input && input.includes("emailAddress=user%2Btest%40local")
+ ? "&hash=expected"
+ : "&hash=other",
+ azureHeadersPaged: () => ({}),
+ axios: {
+ get: async () => ({ data: { value: [{ contactid: "c1" }] } })
+ },
+ consoleLogger: () => {}
+ });
+
+ const req = {
+ query: { emailAddress: "user+test@local", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ value: [{ contactid: "c1" }]
+ });
+});
+
+test("getnewappeals catch path returns ADMIN_NEW_APPEALS_FETCH_FAILED", async () => {
+ const mod = loadModule("pages/api/admin/getnewappeals_api.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "token" }),
+ hashAPIPath: () => "&hash=expected",
+ azureHeadersPagedCustom: () => ({}),
+ axios: {
+ get: async () => {
+ throw new Error("admin new appeals failed");
+ }
+ },
+ consoleLogger: () => {},
+ _: { has: () => false }
+ });
+
+ const req = { query: {} };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "ADMIN_NEW_APPEALS_FETCH_FAILED"
+ );
+});
+
+test("getlatestdocuments catch path returns ADMIN_LATEST_DOCS_FETCH_FAILED", async () => {
+ const mod = loadModule("pages/api/admin/getlatestdocuments_api.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "token" }),
+ hashAPIPath: () => "&hash=expected",
+ azureHeadersPagedCustom: () => ({}),
+ axios: {
+ get: async () => {
+ throw new Error("latest docs failed");
+ }
+ },
+ consoleLogger: () => {},
+ process: { env: { HASHKEY: "abc" } },
+ CryptoJS: {
+ HmacSHA256: () => ({ toString: () => "hash" }),
+ enc: { Hex: { parse: () => "parsed" } }
+ },
+ _: { has: () => false }
+ });
+
+ const req = { query: {} };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "ADMIN_LATEST_DOCS_FETCH_FAILED"
+ );
+});
+
test("getportalloginproxy catch path returns PORTAL_LOGIN_PROXY_FETCH_FAILED", async () => {
const mod = loadModule("pages/api/endpoint/getportalloginproxy_api.js", {
respondError: respondErrorMock,
@@ -3688,6 +3775,37 @@ test("getsipsmedia returns success contract", async () => {
assert.strictEqual(res.state.jsonBody["@odata.count"], 4);
});
+test("getsipsmedia returns METHOD_NOT_ALLOWED for non-GET methods", async () => {
+ const mod = loadModule("pages/api/endpoint/getsipsmedia_api.js", {
+ respondSuccess: respondSuccessMock,
+ respondError: respondErrorMock
+ });
+
+ const req = { method: "POST", query: {} };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 405);
+ assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED");
+});
+
+test("getappealtypesfornewappeal returns METHOD_NOT_ALLOWED for non-GET methods", async () => {
+ const mod = loadModule(
+ "pages/api/endpoint/getappealtypesfornewappeal_api.js",
+ {
+ respondSuccess: respondSuccessMock,
+ respondError: respondErrorMock
+ }
+ );
+
+ const req = { method: "POST", query: {} };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 405);
+ assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED");
+});
+
const run = async () => {
let passed = 0;
for (const currentTest of tests) {
diff --git a/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs
index cc0f9ee9..3d976800 100644
--- a/tests/phase21/file-handler-contract.test.cjs
+++ b/tests/phase21/file-handler-contract.test.cjs
@@ -51,6 +51,38 @@ test("upload handler returns INVALID_HASH for wrong hash", async () => {
assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH");
});
+test("upload handler accepts alternate canonical hash candidate", async () => {
+ const mod = loadModule("pages/api/file/upload.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath === "/api/file/upload?"
+ ? "?hash=expected"
+ : "?hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ createBlob: async () => ({ id: "blob-alt" }),
+ createRepBlob: async () => ({ id: "rep-alt" })
+ });
+
+ const req = {
+ query: { hash: "expected" },
+ body: {
+ appealData: { key: "value" },
+ containerID: ["c1"],
+ casefolderID: ["f1"],
+ repOrAppeal: false
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ data: { id: "blob-alt" }
+ });
+});
+
test("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing", async () => {
const mod = loadModule("pages/api/file/deleteblob.js", {
nextConnect: createNextConnectMock(),
@@ -71,6 +103,827 @@ test("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing",
assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY");
});
+test("deleteblob handler accepts encoded hash variant and normalizes prefixed path", async () => {
+ let deletedPath = null;
+ const mod = loadModule("pages/api/file/deleteblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("/api/file/deleteblob?container=c1")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlob: async (_container, blobPath) => {
+ deletedPath = blobPath;
+ return { deleted: true };
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "TMP-LFE1C",
+ blobname:
+ "TMP-LFE1C/files/2026-03-18_-_Site_Location_Plan_-_test document 021.xlsx",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.strictEqual(
+ deletedPath,
+ "TMP-LFE1C/files/2026-03-18_-_Site_Location_Plan_-_test document 021.xlsx"
+ );
+});
+
+test("deleteblob handler dependency failure returns DELETE_BLOB_FAILED", async () => {
+ const mod = loadModule("pages/api/file/deleteblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlob: async () => {
+ throw new Error("delete failed");
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "f1",
+ blobname: "doc.pdf",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "DELETE_BLOB_FAILED");
+});
+
+test("deleteblobcase handler accepts encoded hash variant", async () => {
+ let deletedCaseFolder = null;
+ const mod = loadModule("pages/api/file/deleteblobcase.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("casefolderID=CASE%2FSUB")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlobCase: async (_container, casefolder) => {
+ deletedCaseFolder = casefolder;
+ return { deleted: true };
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "CASE/SUB",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.strictEqual(deletedCaseFolder, "CASE/SUB");
+});
+
+test("deleteblobcase handler dependency failure returns DELETE_BLOB_CASE_FAILED", async () => {
+ const mod = loadModule("pages/api/file/deleteblobcase.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlobCase: async () => {
+ throw new Error("delete case failed");
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "f1",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "DELETE_BLOB_CASE_FAILED"
+ );
+});
+
+test("deleteblobrep handler accepts encoded hash variant", async () => {
+ let deletedRepPath = null;
+ const mod = loadModule("pages/api/file/deleteblobrep.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("repfile=Rep%20Name")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlobRep: async (_container, repPath) => {
+ deletedRepPath = repPath;
+ return { deleted: true };
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "CASE-1",
+ repfile: "Rep Name",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.strictEqual(deletedRepPath, "CASE-1/Rep Name");
+});
+
+test("deleteblobrep handler dependency failure returns DELETE_BLOB_REP_FAILED", async () => {
+ const mod = loadModule("pages/api/file/deleteblobrep.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlobRep: async () => {
+ throw new Error("delete rep failed");
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "f1",
+ repfile: "rep1",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "DELETE_BLOB_REP_FAILED");
+});
+
+test("deleteawaitingsubmissionfromblob accepts legacy deleteblob hash variant", async () => {
+ let deletedPath = null;
+ const mod = loadModule(
+ "pages/api/file/deleteawaitingsubmissionfromblob.js",
+ {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("/api/file/deleteblob?container=c1")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlob: async (_container, blobPath) => {
+ deletedPath = blobPath;
+ return { deleted: true };
+ }
+ }
+ );
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "CASE-1",
+ blobname: "CASE-1/files/doc.pdf",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.strictEqual(deletedPath, "CASE-1/files/doc.pdf");
+});
+
+test("deleteawaitingsubmissionfromblob dependency failure returns DELETE_AWAITING_SUBMISSION_BLOB_FAILED", async () => {
+ const mod = loadModule(
+ "pages/api/file/deleteawaitingsubmissionfromblob.js",
+ {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ deleteBlob: async () => {
+ throw new Error("delete awaiting failed");
+ }
+ }
+ );
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "CASE-1",
+ blobname: "doc.pdf",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "DELETE_AWAITING_SUBMISSION_BLOB_FAILED"
+ );
+});
+
+test("getrepsblob handler success returns representations payload", async () => {
+ const mod = loadModule("pages/api/file/getrepsblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ consoleLogger: () => {},
+ getRepsBlobs: async () => [
+ { path: "c/ref_rep.json", name: "ref_rep.json" }
+ ],
+ downloadAllRepsFiles: async () => ({
+ "@odata.count": 1,
+ value: [{ id: "r1" }]
+ })
+ });
+
+ const req = {
+ query: { container: "c1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ "@odata.count": 1,
+ value: [{ id: "r1" }]
+ });
+});
+
+test("getrepsblob handler dependency failure returns GET_REPS_BLOB_FAILED", async () => {
+ const mod = loadModule("pages/api/file/getrepsblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ consoleLogger: () => {},
+ getRepsBlobs: async () => {
+ throw new Error("storage failure");
+ },
+ downloadAllRepsFiles: async () => ({})
+ });
+
+ const req = {
+ query: { container: "c1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "GET_REPS_BLOB_FAILED");
+});
+
+test("getrepsblob handler accepts encoded container hash variant", async () => {
+ const mod = loadModule("pages/api/file/getrepsblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("container=cont%2F1")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ consoleLogger: () => {},
+ getRepsBlobs: async () => [{ path: "cont/rep1.json" }],
+ downloadAllRepsFiles: async () => ({ value: [{ id: "r-encoded" }] })
+ });
+
+ const req = {
+ query: { container: "cont/1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ value: [{ id: "r-encoded" }]
+ });
+});
+
+test("uploadsinglefile handler returns HASH_REQUIRED when hash missing", async () => {
+ const mod = loadModule("pages/api/file/uploadsinglefile.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "?hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ consoleLogger: () => {},
+ uploadSingleFile: async () => ({ uploaded: 0 }),
+ fileTypeFromBuffer: async () => ({ mime: "application/pdf" }),
+ fs: { readFileSync: () => Buffer.from("x") }
+ });
+
+ const req = { query: {}, body: {}, files: {} };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "HASH_REQUIRED");
+});
+
+test("uploadsinglefile handler dependency failure returns UPLOAD_SINGLE_FILE_FAILED", async () => {
+ const mod = loadModule("pages/api/file/uploadsinglefile.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "?hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ consoleLogger: () => {},
+ uploadSingleFile: async () => {
+ throw new Error("upload failed");
+ },
+ fileTypeFromBuffer: async () => ({ mime: "application/pdf" }),
+ fs: { readFileSync: () => Buffer.from("x") }
+ });
+
+ const req = {
+ query: { hash: "expected" },
+ body: { containerID: ["c1"], casefolderID: ["f1"] },
+ files: {}
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 500);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "UPLOAD_SINGLE_FILE_FAILED"
+ );
+});
+
+test("createrepcompletemessage handler accepts encoded hash variant", async () => {
+ const mod = loadModule("pages/api/file/createrepcompletemessage_api.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("repid=rep%201")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ consoleLogger: () => {},
+ createRepCompleteMessage: async () => ({ ok: true })
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ tempcaseref: "TMP/1",
+ repid: "rep 1",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ ok: true
+ });
+});
+
+test("createrepcompletemessage handler dependency failure returns CREATE_REP_COMPLETE_MESSAGE_FAILED", async () => {
+ const mod = loadModule("pages/api/file/createrepcompletemessage_api.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ consoleLogger: () => {},
+ createRepCompleteMessage: async () => {
+ throw new Error("rep message failed");
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ tempcaseref: "TMP1",
+ repid: "rep1",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "CREATE_REP_COMPLETE_MESSAGE_FAILED"
+ );
+});
+
+test("getbloblistproxy success returns proxied payload", async () => {
+ const mod = loadModule("pages/api/file/getbloblistproxy.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ azureHeaders: () => ({ headers: { Authorization: "Bearer tok" } }),
+ hashAPIPath: () => "&hash=abc",
+ consoleLogger: () => {},
+ process: { env: { API_ROOT: "http://example.com" } },
+ axios: {
+ get: async () => ({ data: { value: [{ id: "b1" }] } })
+ }
+ });
+
+ const req = {
+ query: { container: "cont/1", casefolderID: "CASE/1" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ value: [{ id: "b1" }]
+ });
+});
+
+test("getbloblistproxy dependency failure returns GET_BLOB_LIST_PROXY_FAILED", async () => {
+ const mod = loadModule("pages/api/file/getbloblistproxy.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ azureHeaders: () => ({ headers: {} }),
+ hashAPIPath: () => "&hash=abc",
+ consoleLogger: () => {},
+ process: { env: { API_ROOT: "http://example.com" } },
+ axios: {
+ get: async () => {
+ throw new Error("proxy failed");
+ }
+ }
+ });
+
+ const req = {
+ query: { container: "c1", casefolderID: "f1" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "GET_BLOB_LIST_PROXY_FAILED"
+ );
+});
+
+test("getrepsblobproxy success returns proxied payload", async () => {
+ const mod = loadModule("pages/api/file/getrepsblobproxy.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ azureHeaders: () => ({ headers: {} }),
+ hashAPIPath: () => "&hash=abc",
+ consoleLogger: () => {},
+ process: { env: { API_ROOT: "http://example.com" } },
+ axios: {
+ get: async () => ({ data: { value: [{ id: "r1" }] } })
+ }
+ });
+
+ const req = {
+ query: { container: "c1" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ value: [{ id: "r1" }]
+ });
+});
+
+test("getawaitingsubmissionfromblobproxy dependency failure returns GET_AWAITING_SUBMISSION_PROXY_FAILED", async () => {
+ const mod = loadModule(
+ "pages/api/file/getawaitingsubmissionfromblobproxy.js",
+ {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ azureHeaders: () => ({ headers: {} }),
+ hashAPIPath: () => "&hash=abc",
+ consoleLogger: () => {},
+ process: { env: { API_ROOT: "http://example.com" } },
+ axios: {
+ get: async () => {
+ throw new Error("awaiting failed");
+ }
+ }
+ }
+ );
+
+ const req = {
+ query: { container: "c1" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "GET_AWAITING_SUBMISSION_PROXY_FAILED"
+ );
+});
+
+test("createappealcompletemessageproxy dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", async () => {
+ const mod = loadModule(
+ "pages/api/file/createappealcompletemessageproxy_api.js",
+ {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ azureHeaders: () => ({ headers: {} }),
+ hashAPIPath: () => "&hash=abc",
+ consoleLogger: () => {},
+ process: { env: { API_ROOT: "http://example.com" } },
+ axios: {
+ get: async () => {
+ throw new Error("create appeal proxy failed");
+ }
+ }
+ }
+ );
+
+ const req = {
+ query: { container: "c1", tempcaseref: "TMP1" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED"
+ );
+});
+
+test("createappealcompletemessage_api accepts encoded hash variant and returns success", async () => {
+ const mod = loadModule(
+ "pages/api/file/createappealcompletemessage_api.js",
+ {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("tempcaseref=TMP%2F1")
+ ? "&hash=expected"
+ : "&hash=other",
+ getProgressBlobs: async () => ({ path: "TMP/1/progress.json" }),
+ downloadProgressFile: async (_container, path) => {
+ if (path.includes("_case.json")) {
+ return {
+ "customerid_contact@odata.bind":
+ "/contacts(00000000-0000-0000-0000-000000000001)"
+ };
+ }
+
+ return {
+ pinswg_siteaddressline1: "Line 1",
+ pinswg_siteaddressline2: "",
+ pinswg_siteaddresstown: "Town",
+ pinswg_siteaddresscounty: "County",
+ pinswg_siteaddresspostcode: "CF1 1AA",
+ pinswg_lpaapplicationreference: "LPA-1",
+ pinswg_developmentdescription: "Desc",
+ pinswg_name: "tmp"
+ };
+ },
+ createBlob: async () => ({}),
+ getCaseBlob: async () => ({}),
+ createCaseCompleteMessage: async () => ({}),
+ updateAccount: async () => ({}),
+ consoleLogger: () => {},
+ _: { isEmpty: (v) => v === undefined || v === null || v === "" }
+ }
+ );
+
+ const req = {
+ query: {
+ container: "cont/1",
+ tempcaseref: "TMP/1",
+ inv: "846040001",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ status: "success"
+ });
+});
+
+test("createappealcompletemessage_api dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_FAILED", async () => {
+ const mod = loadModule(
+ "pages/api/file/createappealcompletemessage_api.js",
+ {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ hashAPIPath: () => "&hash=expected",
+ getProgressBlobs: async () => {
+ throw new Error("progress failed");
+ },
+ consoleLogger: () => {},
+ _: { isEmpty: () => true }
+ }
+ );
+
+ const req = {
+ query: {
+ container: "c1",
+ tempcaseref: "TMP1",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED"
+ );
+});
+
+test("createcaseinvolvement returns record exists on 412 conflict", async () => {
+ const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ hashAPIPath: () => "?hash=abc",
+ consoleLogger: () => {},
+ process: {
+ env: { CRMURL: "crm.example", RELAY_ROOT: "https://relay/" }
+ },
+ axios: async () => {
+ const error = new Error("exists");
+ error.status = 412;
+ throw error;
+ }
+ });
+
+ const req = {
+ body: { contactid: "c1", incidentid: "i1" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ record: "exists"
+ });
+});
+
+test("createrepinvolvement dependency failure returns CREATE_REP_INVOLVEMENT_FAILED", async () => {
+ const mod = loadModule("pages/api/file/createrepinvolvement_api.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ hashAPIPath: () => "?hash=abc",
+ consoleLogger: () => {},
+ process: {
+ env: {
+ CRMURL: "crm.example",
+ CRMURL_VERSION: "v9.2",
+ RELAY_ROOT: "https://relay/"
+ }
+ },
+ axios: async () => {
+ throw new Error("rep involvement failed");
+ }
+ });
+
+ const req = {
+ body: { contactid: "c1", incidentid: "i1" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "CREATE_REP_INVOLVEMENT_FAILED"
+ );
+});
+
+test("updatecase dependency failure returns UPDATE_CASE_FAILED", async () => {
+ const mod = loadModule("pages/api/file/updatecase_api.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getToken: async () => ({ access_token: "tok" }),
+ hashAPIPath: () => "?hash=abc",
+ consoleLogger: () => {},
+ process: { env: { RELAY_ROOT: "https://relay/" } },
+ axios: async () => {
+ throw new Error("update failed");
+ }
+ });
+
+ const req = {
+ query: {
+ updateFormCollection: "pinswg_forms",
+ appealObj: "obj1",
+ incident: "inc1"
+ },
+ body: { field: "value" }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "UPDATE_CASE_FAILED");
+});
+
+test("getawaitingsubmissionfromblob handler success returns payload", async () => {
+ const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getAllProgressBlobs: async () => [{ path: "c1/case1_appeal.json" }],
+ downloadAllProgressFiles: async () => ({ value: [{ id: "a1" }] })
+ });
+
+ const req = {
+ query: { container: "c1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ value: [{ id: "a1" }]
+ });
+});
+
+test("getawaitingsubmissionfromblob handler dependency failure returns GET_AWAITING_SUBMISSION_BLOB_FAILED", async () => {
+ const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getAllProgressBlobs: async () => {
+ throw new Error("progress fetch failed");
+ },
+ downloadAllProgressFiles: async () => ({})
+ });
+
+ const req = {
+ query: { container: "c1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "GET_AWAITING_SUBMISSION_BLOB_FAILED"
+ );
+});
+
test("getbloblist handler returns INVALID_HASH for mismatch", async () => {
const mod = loadModule("pages/api/file/getbloblist.js", {
nextConnect: createNextConnectMock(),
@@ -92,6 +945,138 @@ test("getbloblist handler returns INVALID_HASH for mismatch", async () => {
assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH");
});
+test("getbloblist handler accepts encoded casefolder hash variant", async () => {
+ const mod = loadModule("pages/api/file/getbloblist.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("casefolderID=CASE%2FSUB")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getBlobs: async () => ({ file: "a.pdf" }),
+ getRepsFilesBlobs: async () => ({ file: "rep.pdf" })
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "CASE/SUB",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ value: [{ file: "rep.pdf" }]
+ });
+});
+
+test("getbloblist handler dependency failure returns GET_BLOB_LIST_FAILED", async () => {
+ const mod = loadModule("pages/api/file/getbloblist.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getBlobs: async () => {
+ throw new Error("blob lookup failed");
+ },
+ getRepsFilesBlobs: async () => ({ file: "rep.pdf" })
+ });
+
+ const req = {
+ query: { container: "c1", casefolderID: "f1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "GET_BLOB_LIST_FAILED");
+});
+
+test("getprogressobjblob handler success returns payload", async () => {
+ const mod = loadModule("pages/api/file/getprogressobjblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getProgressBlobs: async () => ({ path: "CASE1/CASE1_appeal.json" }),
+ downloadProgressFile: async () => ({ id: "progress-1" })
+ });
+
+ const req = {
+ query: { container: "c1", casefolderID: "CASE1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ id: "progress-1"
+ });
+});
+
+test("getprogressobjblob handler accepts encoded casefolder hash variant", async () => {
+ const mod = loadModule("pages/api/file/getprogressobjblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("casefolderID=CASE%2FSUB")
+ ? "&hash=expected"
+ : "&hash=other",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getProgressBlobs: async () => ({ path: "CASE/SUB/CASE_appeal.json" }),
+ downloadProgressFile: async () => ({ id: "progress-2" })
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "CASE/SUB",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ id: "progress-2"
+ });
+});
+
+test("getprogressobjblob handler dependency failure returns GET_PROGRESS_OBJ_BLOB_FAILED", async () => {
+ const mod = loadModule("pages/api/file/getprogressobjblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ getProgressBlobs: async () => {
+ throw new Error("progress lookup failed");
+ },
+ downloadProgressFile: async () => ({})
+ });
+
+ const req = {
+ query: { container: "c1", casefolderID: "CASE1", hash: "expected" }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "GET_PROGRESS_OBJ_BLOB_FAILED"
+ );
+});
+
test("setupcontainer handler returns MISSING_REQUIRED_QUERY when ident missing", async () => {
const mod = loadModule("pages/api/file/setupcontainer.js", {
nextConnect: createNextConnectMock(),
@@ -111,6 +1096,31 @@ test("setupcontainer handler returns MISSING_REQUIRED_QUERY when ident missing",
assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY");
});
+test("setupcontainer handler accepts encoded ident hash variant", async () => {
+ const mod = loadModule("pages/api/file/setupcontainer.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: (queryPath) =>
+ queryPath.includes("ident=container%2Fname")
+ ? "&hash=expected"
+ : "&hash=other",
+ consoleLogger: () => {},
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ createContainer: async () => ({ created: true })
+ });
+
+ const req = { query: { ident: "container/name", hash: "expected" } };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 200);
+ assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
+ data: "success",
+ output: { created: true }
+ });
+});
+
test("upload handler success returns data payload with 200", async () => {
const mod = loadModule("pages/api/file/upload.js", {
nextConnect: createNextConnectMock(),
@@ -263,6 +1273,248 @@ test("setupcontainer dependency failure returns SETUP_CONTAINER_FAILED", async (
assert.strictEqual(res.state.jsonBody.error.code, "SETUP_CONTAINER_FAILED");
});
+test("downloadblob dependency failure returns DOWNLOAD_BLOB_FAILED", async () => {
+ const mod = loadModule("pages/api/file/downloadblob.js", {
+ nextConnect: createNextConnectMock(),
+ middleware: () => {},
+ hashAPIPath: () => "&hash=expected",
+ respondError: respondErrorMock,
+ downloadFile: async () => {
+ throw new Error("download failed");
+ }
+ });
+
+ const req = {
+ query: {
+ container: "c1",
+ casefolderID: "f1",
+ blobname: "doc.pdf",
+ hash: "expected"
+ }
+ };
+ const res = createRes();
+ await mod.default.handler(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "DOWNLOAD_BLOB_FAILED");
+});
+
+test("generateappealpdfcopy returns INCIDENT_ID_REQUIRED when docProps missing", async () => {
+ const mod = loadModule("pages/api/file/generateappealpdfcopy.js", {
+ respondError: respondErrorMock,
+ getCaseByID: async () => [],
+ getPortalModuleDetails: async () => ({ value: [{}] }),
+ getAppealPDFDocs: async () => [],
+ getFormCollectionByID: () => ({ UrlName: "planningappeals78" }),
+ getPickLists: async () => ({}),
+ planningappeals78_pdf: () => ({}),
+ other_pdf: () => ({}),
+ pdf: () => ({ toBuffer: async () => Buffer.from("pdf") })
+ });
+
+ const req = { body: {} };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "INCIDENT_ID_REQUIRED");
+});
+
+test("generateappealpdfcopy returns CASE_NOT_FOUND when case lookup is empty", async () => {
+ const mod = loadModule("pages/api/file/generateappealpdfcopy.js", {
+ respondError: respondErrorMock,
+ getCaseByID: async () => [],
+ getPortalModuleDetails: async () => ({ value: [{}] }),
+ getAppealPDFDocs: async () => [],
+ getFormCollectionByID: () => ({ UrlName: "planningappeals78" }),
+ getPickLists: async () => ({}),
+ planningappeals78_pdf: () => ({}),
+ other_pdf: () => ({}),
+ pdf: () => ({ toBuffer: async () => Buffer.from("pdf") })
+ });
+
+ const req = { body: { docProps: "i1" } };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 404);
+ assert.strictEqual(res.state.jsonBody.error.code, "CASE_NOT_FOUND");
+});
+
+test("generateappealpdfcopy returns FORM_COLLECTION_NOT_FOUND when collection is unresolved", async () => {
+ const mod = loadModule("pages/api/file/generateappealpdfcopy.js", {
+ respondError: respondErrorMock,
+ getCaseByID: async () => [
+ {
+ pinswg_appealcasetype: 846040000,
+ ticketnumber: "CAS-1",
+ description: "desc"
+ }
+ ],
+ getPortalModuleDetails: async () => ({ value: [{}] }),
+ getAppealPDFDocs: async () => [],
+ getFormCollectionByID: () => null,
+ getPickLists: async () => ({}),
+ planningappeals78_pdf: () => ({}),
+ other_pdf: () => ({}),
+ pdf: () => ({ toBuffer: async () => Buffer.from("pdf") })
+ });
+
+ const req = { body: { docProps: "i1" } };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "FORM_COLLECTION_NOT_FOUND"
+ );
+});
+
+test("generateappealpdfcopy success returns pdf buffer and headers", async () => {
+ const mod = loadModule("pages/api/file/generateappealpdfcopy.js", {
+ respondError: respondErrorMock,
+ getCaseByID: async () => [
+ {
+ pinswg_appealcasetype: 846040000,
+ ticketnumber: "CAS-1",
+ description: "desc"
+ }
+ ],
+ getPortalModuleDetails: async () => ({ value: [{}] }),
+ getAppealPDFDocs: async () => [{ id: "d1" }],
+ getFormCollectionByID: () => ({
+ UrlName: "planningappeals78",
+ LogicalCollectionName: "pinswg_planningappeals78s"
+ }),
+ getPickLists: async () => ({ a: 1 }),
+ planningappeals78_pdf: () => ({ type: "pdfComponent" }),
+ other_pdf: () => ({ type: "otherComponent" }),
+ pdf: () => ({ toBuffer: async () => Buffer.from("pdf-content") })
+ });
+
+ const req = { body: { docProps: "i1" } };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.headers["content-type"], "application/pdf");
+ assert.strictEqual(
+ typeof res.state.headers["content-disposition"],
+ "string"
+ );
+ assert.strictEqual(res.state.sentBody.toString(), "pdf-content");
+});
+
+test("generateappealpdfcopy catch path returns APPEAL_PDF_COPY_GENERATION_FAILED", async () => {
+ const mod = loadModule("pages/api/file/generateappealpdfcopy.js", {
+ respondError: respondErrorMock,
+ getCaseByID: async () => [
+ {
+ pinswg_appealcasetype: 846040000,
+ ticketnumber: "CAS-1",
+ description: "desc"
+ }
+ ],
+ getPortalModuleDetails: async () => ({ value: [{}] }),
+ getAppealPDFDocs: async () => [{ id: "d1" }],
+ getFormCollectionByID: () => ({
+ UrlName: "planningappeals78",
+ LogicalCollectionName: "pinswg_planningappeals78s"
+ }),
+ getPickLists: async () => ({ a: 1 }),
+ planningappeals78_pdf: () => ({ type: "pdfComponent" }),
+ other_pdf: () => ({ type: "otherComponent" }),
+ pdf: () => ({
+ toBuffer: async () => {
+ throw new Error("pdf failure");
+ }
+ })
+ });
+
+ const req = { body: { docProps: "i1" } };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "APPEAL_PDF_COPY_GENERATION_FAILED"
+ );
+});
+
+test("generatepdf catch path returns GENERATE_PDF_FAILED", async () => {
+ const { Readable } = require("stream");
+ const mod = loadModule("pages/api/file/generatepdf.js", {
+ Buffer,
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ hashAPIPath: () => "&hash=expected",
+ consoleLogger: () => {},
+ ReactPDF: {
+ renderToStream: async () => Readable.from(["pdf"])
+ },
+ createRepPDFBlob: async () => {
+ throw new Error("pdf write failed");
+ },
+ other_pdf: () => ({ type: "other" })
+ });
+
+ const req = {
+ query: { hash: "expected", download: "true" },
+ body: {
+ representationType: "Other",
+ repfile_name: "rep-file",
+ containerID: "c1",
+ casefolderID: "f1",
+ caseRef: "CAS-1"
+ }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(res.state.jsonBody.error.code, "GENERATE_PDF_FAILED");
+});
+
+test("generateappealpdf catch path returns GENERATE_APPEAL_PDF_FAILED", async () => {
+ const mod = loadModule("pages/api/file/generateappealpdf.js", {
+ respondError: respondErrorMock,
+ respondSuccess: respondSuccessMock,
+ hashAPIPath: () => "&hash=expected",
+ getTempCaseBlob: async () => ({ ticketnumber: "CAS-1" }),
+ getProgressBlobs: async () => ({ path: "CAS-1/progress.json" }),
+ downloadProgressFile: async () => {
+ throw new Error("progress failed");
+ },
+ getPickLists: async () => ({}),
+ planningappeals78_pdf: () => ({ type: "planning" }),
+ other_pdf: () => ({ type: "other" }),
+ ReactPDF: {
+ renderToStream: async () => {
+ throw new Error("render failed");
+ }
+ }
+ });
+
+ const req = {
+ query: { hash: "expected", appealType: "846040000" },
+ body: {
+ containerID: "c1",
+ casefolderID: "f1",
+ caseRef: "CAS-1",
+ filesList: []
+ }
+ };
+ const res = createRes();
+ await mod.default(req, res);
+
+ assert.strictEqual(res.state.statusCode, 400);
+ assert.strictEqual(
+ res.state.jsonBody.error.code,
+ "GENERATE_APPEAL_PDF_FAILED"
+ );
+});
+
const run = async () => {
let passed = 0;
for (const currentTest of tests) {