@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
295 lines (253 loc) • 15.3 kB
JavaScript
import * as govcyResources from "../resources/govcyResources.mjs";
import { validateFormElements } from "../utils/govcyValidator.mjs"; // Import your validator
import * as dataLayer from "../utils/govcyDataLayer.mjs";
import { logger } from "../utils/govcyLogger.mjs";
import { prepareSubmissionData, prepareSubmissionDataAPI, generateSubmitEmail } from "../utils/govcySubmitData.mjs";
import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
import { getEnvVariable, getEnvVariableBool } from "../utils/govcyEnvVariables.mjs";
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
import { sendEmail } from "../utils/govcyNotification.mjs"
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
import { createUmdManualPageTemplate } from "./govcyUpdateMyDetails.mjs"
import { validateMultipleThings } from "../utils/govcyMultipleThingsValidation.mjs";
import { resetCustomPages } from "../utils/govcyCustomPages.mjs";
/**
* Middleware to handle review page form submission
* This middleware processes the review page, validates form data, and shows validation errors.
*/
export function govcyReviewPostHandler() {
return async (req, res, next) => {
try {
const { siteId } = req.params;
// ✅ Load service and check if it exists
const service = req.serviceData;
let validationErrors = {};
// to be used for sending email
let updateMyDetailsData = null;
// Loop through all pages in the service
for (const page of service.pages) {
//get page url
const pageUrl = page.pageData.url;
// to be used for sending email
// get updateMyDetails data if not found before
if (!updateMyDetailsData) {
updateMyDetailsData = dataLayer.getPageUpdateMyDetails(req.session, siteId, pageUrl);
}
// ----- taskList handling
// Task list pages are navigation wrappers; skip validation here
if (page.taskList) {
continue;
}
// ----- Conditional logic comes here
// ✅ Skip validation if page is conditionally excluded
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
if (conditionResult.result === false) {
logger.debug(`⏩ Skipping validation for conditionally excluded page '${pageUrl}' → Redirects to '${conditionResult.redirect}'`, req);
continue;
}
// Find the form definition inside `pageTemplate.sections`
let formElement = null;
// ----- `updateMyDetails` handling
if (page.updateMyDetails) {
logger.debug("Validating UpdateMyDetails page during review POST", { siteId, pageUrl });
// Build the manual UMD page template
const umdTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req, true);
// Extract the form element
formElement = umdTemplate.sections
.flatMap(section => section.elements)
.find(el => el.element === "form");
if (!formElement) {
logger.error("🚨 UMD form element not found during review validation", { siteId, pageUrl });
return handleMiddlewareError("🚨 UMD form element not found during review validation", 500, next);
}
// ----- `updateMyDetails` handling
} else {
// Normal flow
for (const section of page.pageTemplate.sections) {
formElement = section.elements.find(el => el.element === "form");
if (formElement) break;
}
}
if (!formElement) continue; // Skip pages without forms
// Get stored form data for this page (or default to empty)
const formData = dataLayer.getPageData(req.session, siteId, pageUrl) || {};
let errors = {};
// ----- MultipleThings hub handling -----
if (page.multipleThings) {
// Use multiple things validator
const items = Array.isArray(formData) ? formData : [];
const mtErrors = validateMultipleThings(page, items, service.site.lang);
if (Object.keys(mtErrors).length > 0) {
errors[pageUrl] = {
type: "multipleThings", // ✅ mark it
hub: { errors: mtErrors } // keep hub-style structure
};
}
} else { // ----- Normal form handling -----
// Normal page validation
const v = validateFormElements(formElement.params.elements, formData, pageUrl);
if (Object.keys(v).length > 0) {
errors[pageUrl] = {
type: "normal", // ✅ mark it
...v
};
}
// // Run validations
// errors = validateFormElements(formElement.params.elements, formData, pageUrl);
}
// Add errors to the validationErrors object
validationErrors = { ...validationErrors, ...errors };
}
// ==========================================================
// Custom pages
// ==========================================================
// Handle custom summary validation blocks from session
const customPages = dataLayer.getSiteCustomPages(req.session, siteId);
// Convert existing validationErrors object to ordered array of [pageUrl, errorObj]
// so we can splice into it in the correct order.
let orderedErrors = Object.entries(validationErrors);
for (const [key, block] of Object.entries(customPages)) {
if (Array.isArray(block.errors) && block.errors.length > 0) {
const customErrObj = {};
for (const err of block.errors) {
customErrObj[err.id] = {
id: err.id,
message: err.text, // ⚙️ uses your structure
pageUrl: err.pageUrl || key
};
}
const newErrorEntry = [
key,
{
type: "custom", // mark it as custom for debug clarity
...customErrObj
}
];
// If block.insertAfter exists, insert after that page’s index
if (block.insertAfterPageUrl) {
const idx = orderedErrors.findIndex(([pageUrl]) => pageUrl === block.insertAfterPageUrl);
if (idx >= 0) {
orderedErrors.splice(idx + 1, 0, newErrorEntry); // insert after
continue; // move to next custom block
}
}
// Fallback: append to top if insertAfter not found or not defined
orderedErrors.unshift(newErrorEntry);
}
}
// Convert back into a normal object
validationErrors = Object.fromEntries(orderedErrors);
// ==========================================================
// ❌ Return validation errors if any exist
if (Object.keys(validationErrors).length > 0) {
logger.debug("🚨 Validation errors:", validationErrors, req);
logger.info("🚨 Validation errors:", req.originalUrl);
dataLayer.storeSiteValidationErrors(req.session, siteId, validationErrors);
//redirect to the same page with error summary
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
} else {
// ------------ DO SUBMISSION ---------------------
// get the submission API endpoint URL, clientKey, serviceId from the environment variable (handle edge cases)
const submissionUrl = getEnvVariable(service?.site?.submissionAPIEndpoint?.url || "", false);
const clientKey = getEnvVariable(service?.site?.submissionAPIEndpoint?.clientKey || "", false);
const serviceId = getEnvVariable(service?.site?.submissionAPIEndpoint?.serviceId || "", false);
const dsfGtwApiKey = getEnvVariable(service?.site?.submissionAPIEndpoint?.dsfgtwApiKey || "", "");
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false); // Default to false if not set
if (!submissionUrl) {
return handleMiddlewareError("🚨 Submission API endpoint URL is missing", 500, next);
}
if (!clientKey) {
return handleMiddlewareError("🚨 Submission API clientKey is missing", 500, next);
}
if (!serviceId) {
return handleMiddlewareError("🚨 Submission API serviceId is missing", 500, next);
}
// Prepare submission data
const submissionData = prepareSubmissionData(req, siteId, service);
// Prepare submission data for API
const submissionDataAPI = prepareSubmissionDataAPI(submissionData, service, req);
logger.debug("Prepared submission data for API:", submissionDataAPI);
// Call the API to submit the data
const response = await govcyApiRequest(
"post", // Use POST method
submissionUrl, // Use the submission URL from the environment variable
submissionDataAPI, // Pass the prepared submission data
true, // Use access token authentication
dataLayer.getUser(req.session), // Get the user from the session
{
accept: "text/plain", // Set Accept header to text/plain
"client-key": clientKey, // Set the client key header
"service-id": serviceId, // Set the service ID header
...(dsfGtwApiKey !== '' && { "dsfgtw-api-key": dsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
},
3,
allowSelfSignedCerts
);
// Check if the response is successful
if (response.Succeeded) {
let referenceNo = response?.Data?.referenceValue || "";
// Add the reference number to the submission data
submissionData.referenceNumber = referenceNo;
logger.info("✅ Data submitted", siteId, referenceNo);
// Get the user email address
let emailAddress = "";
// if Update my details not provided the use user email
if (!updateMyDetailsData || !updateMyDetailsData.email) {
emailAddress = dataLayer.getUser(req.session).email;
} else {
emailAddress = updateMyDetailsData.email;
}
// add contact email to submission data
submissionData.contactEmailAddress = emailAddress;
// handle data layer submission
dataLayer.storeSiteSubmissionData(
req.session,
siteId,
submissionData);
//-- Send email to user
// Generate the email body
let emailBody = generateSubmitEmail(service, submissionData.printFriendlyData, referenceNo, req);
// ==========================================================
// Custom pages
// =========================================================
// 🆕 Reset per-session custom pages from the global app definition
const app = req.app; // ✅ Express automatically provides this
resetCustomPages(app, req.session, siteId);
// ==========================================================
// Send the email
sendEmail(
service.site.title[service.site.lang],
emailBody,
[emailAddress],
"eMail").catch(err => {
logger.error("Email sending failed (async):", err);
});
// --- End of email sending
logger.debug("🔄 Redirecting to success page:", req);
// redirect to success
return res.redirect(govcyResources.constructPageUrl(siteId, `success`));
// logger.debug("The submission data prepared:", printFriendlyData);
// let reviewSummary = generateReviewSummary(printFriendlyData,req, siteId, false);
// res.send(emailBody);
// // Clear any existing submission errors from the session
// dataLayer.clearSiteSubmissionErrors(req.session, siteId);
} else {
// Handle submission failure
const errorCode = response.ErrorCode;
const errorPage = service.site?.submissionAPIEndpoint?.response?.errorResponse?.[errorCode]?.page;
if (errorPage) {
logger.info("🚨 Submission returned failed:", response.ErrorCode);
return res.redirect(errorPage);
} else {
return handleMiddlewareError("🚨 Unknown error code received from API.", 500, next);
}
}
}
// Proceed to final submission if no errors
// return next();
// print the submission data to the page
} catch (error) {
return next(error);
}
};
}