diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 7a788d60..9c0af5ec 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -476,3 +476,43 @@ Validation: 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. diff --git a/pages/api/file/generateappealpdf.js b/pages/api/file/generateappealpdf.js index e90284c9..2bb5eac8 100644 --- a/pages/api/file/generateappealpdf.js +++ b/pages/api/file/generateappealpdf.js @@ -112,16 +112,16 @@ 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; + const caseRef = appealBodyObj.caseRef; + let checkquerypath = "/api/file/generateappealpdf"; if ( typeof checkHash === "undefined" || @@ -184,92 +184,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( + React.createElement(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/generatepdf.js b/pages/api/file/generatepdf.js index 981d4f0e..ff2871ec 100644 --- a/pages/api/file/generatepdf.js +++ b/pages/api/file/generatepdf.js @@ -109,7 +109,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 +169,15 @@ 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 casefolderID = reqBodyobj.casefolderID; + const caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef; + const representationType = reqBodyobj.representationType; + const repRaiser = reqBodyobj.lastname; + const localeSelect = reqBodyobj.locale; //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"; @@ -336,40 +336,40 @@ export default async function handler(req, res) { }); }; - const renderedPDFStream = await ReactPDF.renderToStream( - - ); - const renderedPDFBuffer = await streamToBuffer(renderedPDFStream); + try { + const renderedPDFStream = await ReactPDF.renderToStream( + React.createElement(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/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index b22ff772..8306a0f4 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -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 () => { let passed = 0; for (const currentTest of tests) {