TASK22224: modernize generatepdf handlers

This commit is contained in:
2026-03-23 18:39:44 +00:00
parent 411190e403
commit cc4fb36439
4 changed files with 250 additions and 133 deletions
+40
View File
@@ -476,3 +476,43 @@ Validation:
Follow-ups: Follow-ups:
- Remaining major modernization candidate is `pages/api/file/generateappealpdf.js` (+ optional `pages/api/file/generatepdf.js`) if we continue final closure slices. - 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.
+27 -22
View File
@@ -112,16 +112,16 @@ import { respondError, respondSuccess } from "../middleware/apiResponse";
// ApiProxy.post(async (req, res) => { // ApiProxy.post(async (req, res) => {
export default async function handler(req, res) { export default async function handler(req, res) {
var checkHash = req.query.hash; const checkHash = req.query.hash;
var appealType = req.query.appealType; const appealType = req.query.appealType;
var isDownload = req.query?.download === "true"; const isDownload = req.query?.download === "true";
let appealBodyObj = let appealBodyObj =
typeof req.body === "string" ? JSON.parse(req.body) : req.body; typeof req.body === "string" ? JSON.parse(req.body) : req.body;
var containerID = appealBodyObj.containerID; const containerID = appealBodyObj.containerID;
var casefolderID = appealBodyObj.casefolderID; const casefolderID = appealBodyObj.casefolderID;
var caseRef = appealBodyObj.caseRef; const caseRef = appealBodyObj.caseRef;
var checkquerypath = "/api/file/generateappealpdf"; let checkquerypath = "/api/file/generateappealpdf";
if ( if (
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
@@ -184,18 +184,20 @@ export default async function handler(req, res) {
} }
}; };
try {
const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID); const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID);
const blobProgress = await getProgressBlobs(containerID, casefolderID).then( const progressBlob = await getProgressBlobs(containerID, casefolderID);
(data) => { const blobProgress = await downloadProgressFile(
//console.log("Progress blob path:", data); containerID,
return downloadProgressFile(containerID, data.path, casefolderID); progressBlob.path,
} casefolderID
); );
const uniqueArray = appealBodyObj.filesList.filter((obj, index, self) => { const uniqueArray = appealBodyObj.filesList.filter(
// Check if the name has already been encountered (obj, index, self) => {
return index === self.findIndex((t) => t.name === obj.name); return index === self.findIndex((t) => t.name === obj.name);
}); }
);
Object.assign(blobProgress, { Object.assign(blobProgress, {
"filesList": uniqueArray, "filesList": uniqueArray,
@@ -225,7 +227,10 @@ export default async function handler(req, res) {
}; };
const renderedPDF = await ReactPDF.renderToStream( const renderedPDF = await ReactPDF.renderToStream(
<MyDocument docProps={blobProgress} pickListData={pickListData} /> React.createElement(MyDocument, {
docProps: blobProgress,
pickListData: pickListData
})
); );
const repDate = new Date(); const repDate = new Date();
@@ -244,12 +249,12 @@ export default async function handler(req, res) {
const renderedPDFBuffer = await streamToBuffer(renderedPDF); const renderedPDFBuffer = await streamToBuffer(renderedPDF);
await createAppealPDFBlob( const data = await createAppealPDFBlob(
renderedPDFBuffer, renderedPDFBuffer,
containerID, containerID,
blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber
) );
.then((data) => {
if (isDownload) { if (isDownload) {
const filename = `${pdfFileName}.pdf`; const filename = `${pdfFileName}.pdf`;
res.setHeader("Content-Type", "application/pdf"); res.setHeader("Content-Type", "application/pdf");
@@ -259,17 +264,17 @@ export default async function handler(req, res) {
); );
return res.status(200).send(renderedPDFBuffer); return res.status(200).send(renderedPDFBuffer);
} }
return respondSuccess(res, { return respondSuccess(res, {
status: "success", status: "success",
data: data, data: data,
path: `/files/${pdfFileName}.pdf` path: `/files/${pdfFileName}.pdf`
}); });
}) } catch (error) {
.catch(() => {
return respondError(res, { return respondError(res, {
status: 400, status: 400,
code: "GENERATE_APPEAL_PDF_FAILED", code: "GENERATE_APPEAL_PDF_FAILED",
message: "Failed to generate appeal PDF" message: "Failed to generate appeal PDF"
}); });
}); }
} }
+15 -15
View File
@@ -109,7 +109,7 @@ import { respondError, respondSuccess } from "../middleware/apiResponse";
export default async function handler(req, res) { export default async function handler(req, res) {
const shouldDownload = req.query.download === "true"; const shouldDownload = req.query.download === "true";
var checkHash = req.query.hash; const checkHash = req.query.hash;
let reqBodyobj = let reqBodyobj =
typeof req.body === "string" ? JSON.parse(req.body) : req.body; typeof req.body === "string" ? JSON.parse(req.body) : req.body;
@@ -169,15 +169,15 @@ export default async function handler(req, res) {
// "\n//////////////////////\n" // "\n//////////////////////\n"
// ); // );
var containerID = reqBodyobj.containerID; const containerID = reqBodyobj.containerID;
var casefolderID = reqBodyobj.casefolderID; const casefolderID = reqBodyobj.casefolderID;
var caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef; const caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef;
var representationType = reqBodyobj.representationType; const representationType = reqBodyobj.representationType;
var repRaiser = reqBodyobj.lastname; const repRaiser = reqBodyobj.lastname;
var localeSelect = reqBodyobj.locale; const localeSelect = reqBodyobj.locale;
//console.log("there are files:", Object.keys(req.files).length); //console.log("there are files:", Object.keys(req.files).length);
var checkquerypath = "/api/file/generatepdf"; let checkquerypath = "/api/file/generatepdf";
if (shouldDownload) { if (shouldDownload) {
checkquerypath += "?download=true"; checkquerypath += "?download=true";
@@ -336,18 +336,19 @@ export default async function handler(req, res) {
}); });
}; };
try {
const renderedPDFStream = await ReactPDF.renderToStream( const renderedPDFStream = await ReactPDF.renderToStream(
<MyDocument docProps={reqBodyobj} /> React.createElement(MyDocument, { docProps: reqBodyobj })
); );
const renderedPDFBuffer = await streamToBuffer(renderedPDFStream); const renderedPDFBuffer = await streamToBuffer(renderedPDFStream);
await createRepPDFBlob( const data = await createRepPDFBlob(
renderedPDFBuffer, renderedPDFBuffer,
containerID, containerID,
caseRef, caseRef,
reqBodyobj.repfile_name reqBodyobj.repfile_name
) );
.then((data) => {
if (shouldDownload) { if (shouldDownload) {
const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`; const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`;
res.setHeader("Content-Type", "application/pdf"); res.setHeader("Content-Type", "application/pdf");
@@ -363,13 +364,12 @@ export default async function handler(req, res) {
data: data, data: data,
path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf` path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf`
}); });
}) } catch (error) {
.catch((error) => {
consoleLogger(error); consoleLogger(error);
return respondError(res, { return respondError(res, {
status: 400, status: 400,
code: "GENERATE_PDF_FAILED", code: "GENERATE_PDF_FAILED",
message: "Failed to generate PDF" message: "Failed to generate PDF"
}); });
}); }
} }
@@ -1350,6 +1350,78 @@ test("generateappealpdfcopy catch path returns 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", {
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 () => { const run = async () => {
let passed = 0; let passed = 0;
for (const currentTest of tests) { for (const currentTest of tests) {