TASK22224: modernize generatepdf handlers
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,92 +184,97 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID);
|
try {
|
||||||
const blobProgress = await getProgressBlobs(containerID, casefolderID).then(
|
const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID);
|
||||||
(data) => {
|
const progressBlob = await getProgressBlobs(containerID, casefolderID);
|
||||||
//console.log("Progress blob path:", data);
|
const blobProgress = await downloadProgressFile(
|
||||||
return downloadProgressFile(containerID, data.path, casefolderID);
|
containerID,
|
||||||
}
|
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, {
|
|
||||||
"filesList": uniqueArray,
|
|
||||||
"caseObj": tempCaseBlob
|
|
||||||
});
|
|
||||||
|
|
||||||
const pickListData = await getPickLists(appealType);
|
|
||||||
|
|
||||||
// ReactPDF.render(
|
|
||||||
// <MyDocument docProps={blobProgress} pickListData={pickListData} />,
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
return respondSuccess(res, {
|
);
|
||||||
status: "success",
|
|
||||||
data: data,
|
Object.assign(blobProgress, {
|
||||||
path: `/files/${pdfFileName}.pdf`
|
"filesList": uniqueArray,
|
||||||
});
|
"caseObj": tempCaseBlob
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return respondError(res, {
|
|
||||||
status: 400,
|
|
||||||
code: "GENERATE_APPEAL_PDF_FAILED",
|
|
||||||
message: "Failed to generate appeal PDF"
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pickListData = await getPickLists(appealType);
|
||||||
|
|
||||||
|
// ReactPDF.render(
|
||||||
|
// <MyDocument docProps={blobProgress} pickListData={pickListData} />,
|
||||||
|
// 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"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,40 +336,40 @@ export default async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderedPDFStream = await ReactPDF.renderToStream(
|
try {
|
||||||
<MyDocument docProps={reqBodyobj} />
|
const renderedPDFStream = await ReactPDF.renderToStream(
|
||||||
);
|
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) {
|
|
||||||
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, {
|
if (shouldDownload) {
|
||||||
status: "success",
|
const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`;
|
||||||
data: data,
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf`
|
res.setHeader(
|
||||||
});
|
"Content-Disposition",
|
||||||
})
|
`attachment; filename="${filename}"`
|
||||||
.catch((error) => {
|
);
|
||||||
consoleLogger(error);
|
return res.status(200).send(renderedPDFBuffer);
|
||||||
return respondError(res, {
|
}
|
||||||
status: 400,
|
|
||||||
code: "GENERATE_PDF_FAILED",
|
return respondSuccess(res, {
|
||||||
message: "Failed to generate PDF"
|
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user