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) {