UNPKG

@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.

622 lines (539 loc) 25.2 kB
import * as govcyResources from "../resources/govcyResources.mjs"; import * as dataLayer from "./govcyDataLayer.mjs"; import { DSFEmailRenderer } from '@gov-cy/dsf-email-templates'; import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs"; import { evaluatePageConditions } from "./govcyExpressions.mjs"; import { logger } from "./govcyLogger.mjs"; /** * Prepares the submission data for the service, including raw data, print-friendly data, and renderer data. * * @param {object} req The request object containing session data * @param {string} siteId The site ID * @param {object} service The service object containing site and page data * @returns {object} The submission data object containing raw data, print-friendly data, and renderer data */ export function prepareSubmissionData(req, siteId, service) { // Get the raw data from the session store // const rawData = dataLayer.getSiteInputData(req.session, siteId); // ----- Conditional logic comes here // Filter site input data based on active pages only // const rawData = {}; // for (const page of service.pages) { // const shouldInclude = evaluatePageConditions(page, req.session, siteId, req).result === true; // if (shouldInclude) { // const pageUrl = page.pageData.url; // const formData = dataLayer.getPageData(req.session, siteId, pageUrl); // if (formData && Object.keys(formData).length > 0) { // rawData[pageUrl] = { formData }; // } // } // } // ----- consistent data model for submission_data (CONFIG-BASED) const submissionData = {}; // Loop through every page in the service definition for (const page of service.pages) { const pageUrl = page.pageData.url || ""; // Find the <form> element in the page let formElement = null; for (const section of page.pageTemplate.sections || []) { formElement = section.elements.find(el => el.element === "form"); if (formElement) break; } if (!formElement) continue; // ⛔ Skip pages without a <form> element submissionData[pageUrl] = { formData: {} }; // ✅ Now initialize only if a form is present // Traverse the form elements inside the form for (const element of formElement.params.elements || []) { const elType = element.element; // ✅ Skip non-input elements like buttons if (!ALLOWED_FORM_ELEMENTS.includes(elType)) continue; const elId = element.params?.id || element.params?.name; if (!elId) continue; // ⛔ Skip elements with no id/name // 🟢 Use helper to get session value (or "" fallback if missing) const value = getValue(element, pageUrl, req, siteId) ?? ""; // Store in submissionData submissionData[pageUrl].formData[elId] = value; // 🔄 If radios with conditionalElements, walk ALL options if (elType === "radios" && Array.isArray(element.params?.items)) { for (const radioItem of element.params.items) { const condEls = radioItem.conditionalElements; if (!Array.isArray(condEls)) continue; for (const condElement of condEls) { const condType = condElement.element; if (!ALLOWED_FORM_ELEMENTS.includes(condType)) continue; const condId = condElement.params?.id || condElement.params?.name; if (!condId) continue; // Again: read from session or fallback to "" const condValue = getValue(condElement, pageUrl, req, siteId) ?? ""; // Store even if the field was not visible to user submissionData[pageUrl].formData[condId] = condValue; } } } } } logger.debug("Submission Data prepared:", submissionData); // ----- END config-based stable submission_data block // Get the print-friendly data from the session store const printFriendlyData = preparePrintFriendlyData(req, siteId, service); // Get the renderer data from the session store const reviewSummaryList = generateReviewSummary(printFriendlyData, req, siteId, false); // Prepare the submission data object return { submission_username: dataLayer.getUser(req.session).name, submission_email: dataLayer.getUser(req.session).email, submission_data: submissionData, // Raw data as submitted by the user in each page submission_data_version: service.site?.submission_data_version || "", // The submission data version print_friendly_data: printFriendlyData, // Print-friendly data renderer_data: reviewSummaryList, // Renderer data of the summary list renderer_version: service.site?.renderer_version || "", // The renderer version design_systems_version: service.site?.design_systems_version || "", // The design systems version service: { // Service info id: service.site.id, // Service ID title: service.site.title // Service title multilingual object }, referenceNumber: "", // Reference number // timestamp: new Date().toISOString(), // Timestamp `new Date().toISOString();` }; } /** * Prepares the submission data for the API, stringifying all relevant fields. * * @param {object} data data prepared by `prepareSubmissionData` * @returns {object} The API-ready submission data object with all fields as strings */ export function prepareSubmissionDataAPI(data) { return { submission_username: String(data.submission_username ?? ""), submission_email: String(data.submission_email ?? ""), submission_data: JSON.stringify(data.submission_data ?? {}), submission_data_version: String(data.submission_data_version ?? ""), print_friendly_data: JSON.stringify(data.print_friendly_data ?? []), renderer_data: JSON.stringify(data.renderer_data ?? {}), renderer_version: String(data.renderer_version ?? ""), design_systems_version: String(data.design_systems_version ?? ""), service: JSON.stringify(data.service ?? {}) }; } /** * Prepares the print-friendly data for the service, including page data and field labels. * * @param {object} req The request object containing session data * @param {string} siteId The site ID * @param {object} service The service object containing site and page data * @returns The print-friendly data for the service, including page data and field labels. */ export function preparePrintFriendlyData(req, siteId, service) { const submissionData = []; const allowedElements = ALLOWED_FORM_ELEMENTS; // loop through each page in the service // and extract the form data from the session for (const page of service.pages) { const fields = []; // const currentPageUrl = page.pageData.url; // ----- Conditional logic comes here // Skip page if conditions indicate it should redirect (i.e. not be shown) const conditionResult = evaluatePageConditions(page, req.session, siteId, req); if (conditionResult.result === false) { continue; // ⛔ Skip this page from print-friendly data } // find the form element in the page template for (const section of page.pageTemplate.sections || []) { for (const element of section.elements || []) { if (element.element !== "form") continue; // loop through each form element and get the data from the session for (const formElement of element.params.elements || []) { if (!allowedElements.includes(formElement.element)) continue; // handle raw value let rawValue = getValue(formElement, page.pageData.url, req, siteId); //create the field object and push it to the fields array // value of the field is handled by getValueLabel function const field = createFieldObject(formElement, rawValue, getValueLabel(formElement, rawValue, page.pageData.url, req, siteId, service), service); fields.push(field); // Handle conditional elements (only for radios for now) if (formElement.element === "radios") { //find the selected radio based on the raw value const selectedRadio = formElement.params.items.find(i => i.value === rawValue); //check if the selected radio has conditional elements if (selectedRadio?.conditionalElements) { //loop through each conditional element and get the data for (const condEl of selectedRadio.conditionalElements) { if (!allowedElements.includes(condEl.element)) continue; // handle raw value let condValue = getValue(condEl, page.pageData.url, req, siteId); //create the field object and push it to the fields array // value of the field is handled by getValueLabel function const field = createFieldObject(condEl, condValue, getValueLabel(condEl, condValue, page.pageData.url, req, siteId, service), service); fields.push(field); } } } } } } if (fields.length > 0) { submissionData.push({ pageUrl: page.pageData.url, pageTitle: page.pageData.title, fields }); } } return submissionData ; } //------------------------------- Helper Functions -------------------------------// /** * Helper function to retrieve date raw input. * * @param {string} pageUrl The page URL * @param {string} name The name of the form element * @param {object} req The request object * @param {string} siteId The site ID * @returns {string} The raw date input in ISO format (YYYY-MM-DD) or an empty string if not found */ function getDateInputISO(pageUrl, name, req, siteId) { const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day`); const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month`); const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year`); if (!day || !month || !year) return ""; // Pad day and month with leading zero if needed const paddedDay = String(day).padStart(2, "0"); const paddedMonth = String(month).padStart(2, "0"); return `${year}-${paddedMonth}-${paddedDay}`; // ISO format: YYYY-MM-DD } /** * Helper function to retrieve date input in DMY format. * * @param {string} pageUrl The page URL * @param {string} name The name of the form element * @param {object} req The request object * @param {string} siteId The site ID * @returns {string} The raw date input in DMY format (DD/MM/YYYY) or an empty string if not found */ function getDateInputDMY(pageUrl, name, req, siteId) { const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day`); const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month`); const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year`); if (!day || !month || !year) return ""; return `${day}/${month}/${year}`; // EU format: DD/MM/YYYY } /** * Helper function to create a field object. * * @param {object} formElement The form element object * @param {string} value The value of the form element * @param {object} valueLabel The label of the form element * @param {object} service The service object * @returns {object} The field object containing id, label, value, and valueLabel */ function createFieldObject(formElement, value, valueLabel, service) { return { id: formElement.params?.id || "", name: formElement.params?.name || "", label: formElement.params.label || formElement.params.legend || govcyResources.getSameMultilingualObject(service.site.languages, formElement.params.name), value: value, valueLabel: valueLabel }; } /** * Helper function to retrieve the value of a form element from the session. * * @param {object} formElement The form element object * @param {string} pageUrl The page URL * @param {object} req The request object * @param {string} siteId The site ID * @returns {string} The value of the form element from the session or an empty string if not found */ function getValue(formElement, pageUrl, req, siteId) { // handle raw value let value = "" if (formElement.element === "dateInput") { value = getDateInputISO(pageUrl, formElement.params.name, req, siteId); } else { value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name); } return value; } /** * Helper function to get the label of a form element based on its value and type. * * @param {object} formElement The form element object * @param {string} value The value of the form element * @param {string} pageUrl The page URL * @param {object} req The request object * @param {string} siteId The site ID * @param {object} service The service object * @returns {object} The label of the form element based on the value and element type */ function getValueLabel(formElement, value, pageUrl, req, siteId, service) { //handle checkboxes label if (formElement.element === "checkboxes") { if (Array.isArray(value)) { // loop through each value and find the corresponding item return value.map(v => { // find the item const item = formElement.params.items.find(i => i.value === v); return item?.text || govcyResources.getSameMultilingualObject(service.site.languages, ""); }); } else if (typeof value === "string") { const matchedItem = formElement.params.items.find(item => item.value === value); if (matchedItem) { return matchedItem.text; } else { return govcyResources.getSameMultilingualObject(service.site.languages, "") } } } // handle radios and select labels if (formElement.element === "radios" || formElement.element === "select") { const item = formElement.params.items.find(i => i.value === value); return item?.text || govcyResources.getSameMultilingualObject(service.site.languages, ""); } // handle dateInput if (formElement.element === "dateInput") { const formattedDate = getDateInputDMY(pageUrl, formElement.params.name, req, siteId); return govcyResources.getSameMultilingualObject(service.site.languages, formattedDate); } // textInput, textArea, etc. return govcyResources.getSameMultilingualObject(service.site.languages, value); } /** * Helper function to get the item value of checkboxes based on the selected value. * @param {object} valueLabel the value * @param {string} lang the language * @returns {string} the item value of checkboxes */ function getSubmissionValueLabelString(valueLabel, lang, fallbackLang = "en") { if (!valueLabel) return ""; // Helper to get the desired language string or fallback const getText = (obj) => obj?.[lang]?.trim() || obj?.[fallbackLang]?.trim() || ""; // Case 1: Array of multilingual objects if (Array.isArray(valueLabel)) { return valueLabel .map(getText) // get lang/fallback string for each item .filter(Boolean) // remove empty strings .join(", "); // join with comma } // Case 2: Single multilingual object if (typeof valueLabel === "object") { return getText(valueLabel); } // Graceful fallback return ""; } //------------------------------- Review Summary -------------------------------// /** * Generates a review summary for the submission data, ready to be rendered. * * @param {object} submissionData The submission data object containing page data and fields * @param {object} req The request object containing global language and session data * @param {string} siteId The site ID * @param {boolean} showChangeLinks Flag to show change links or not * @returns {object} The review summary to be rendered by the renderer */ export function generateReviewSummary(submissionData, req, siteId, showChangeLinks = true) { // Base summary list structure let summaryList = { element: "summaryList", params: { items: [] } }; /** * Helper function to create a summary list item. * @param {object} key the key of multilingual object * @param {string} value the value * @returns {object} the summary list item */ function createSummaryListItem(key, value) { return { "key": key, "value": [ { "element": "textElement", "params": { "text": { "en": value, "el": value, "tr": value }, "type": "span", "showNewLine": true } } ] }; } // Loop through each page in the submission data for (const page of submissionData) { // Get the page URL, title, and fields const { pageUrl, pageTitle, fields } = page; let summaryListInner = { element: "summaryList", params: { items: [] } }; // loop through each field and add it to the summary entry for (const field of fields) { const label = field.label; const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang); // add the field to the summary entry summaryListInner.params.items.push(createSummaryListItem(label, valueLabel)); } // Add inner summary list to the main summary list let outerSummaryList = { "key": pageTitle, "value": [summaryListInner], "actions": [ //add change link { text: govcyResources.staticResources.text.change, classes: govcyResources.staticResources.other.noPrintClass, href: govcyResources.constructPageUrl(siteId, pageUrl, "review"), visuallyHiddenText: pageTitle } ] }; // If showChangeLinks is false, remove the change link if (!showChangeLinks) { delete outerSummaryList.actions; } //push to the main summary list summaryList.params.items.push(outerSummaryList); } return summaryList; } //------------------------------- Email Generation -------------------------------// /** * Generates an email HTML body for the submission data, ready to be sent. * * @param {object} service The service object * @param {object} submissionData The submission data object containing page data and fields * @param {string} submissionId The submission id * @param {object} req The request object containing global language and session data * @returns {string} The email HTML body */ export function generateSubmitEmail(service, submissionData, submissionId, req) { let body = []; //check if there is submission Id if (submissionId) { // Add success message to the body body.push( { component: "bodySuccess", params: { title: govcyResources.getLocalizeContent(govcyResources.staticResources.text.submissionSuccessTitle, req.globalLang), body: `${govcyResources.getLocalizeContent(govcyResources.staticResources.text.yourSubmissionId, req.globalLang)} ${submissionId}` } } ); } // Add data title to the body body.push( { component: "bodyParagraph", body: govcyResources.getLocalizeContent(govcyResources.staticResources.text.theDataFromYourRequest, req.globalLang) } ); // For each page in the submission data for (const page of submissionData) { // Get the page URL, title, and fields const { pageUrl, pageTitle, fields } = page; // Add data title to the body body.push( { component: "bodyHeading", params: {"headingLevel":2}, body: govcyResources.getLocalizeContent(pageTitle, req.globalLang) } ); let dataUl = []; // loop through each field and add it to the summary entry for (const field of fields) { const label = govcyResources.getLocalizeContent(field.label, req.globalLang); const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang); dataUl.push({key: label, value: valueLabel}); } // add data to the body body.push( { component: "bodyKeyValue", params: {type:"ul", items: dataUl}, }); } let emailObject = govcyResources.getEmailObject( service.site.title, govcyResources.staticResources.text.emailSubmissionPreHeader, service.site.title, dataLayer.getUser(req.session).name, body, service.site.title, req.globalLang ) // Create an instance of DSFEmailRenderer const emailRenderer = new DSFEmailRenderer(); return emailRenderer.renderFromJson(emailObject); } /* { "bank-details": { "formData": { "AccountName": "asd", "Iban": "CY12 0020 0123 0000 0001 2345 6789", "Swift": "BANKCY2NXXX", "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh" } }, "answer-bank-boc": { "formData": { "Objection": "Object", "country": "Azerbaijan", "ObjectionReason": "ObjectionReasonCode1", "ObjectionExplanation": "asdsa", "DepositsBOCAttachment": "", "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh" } }, "bank-settlement": { "formData": { "ReceiveSettlementExplanation": "", "ReceiveSettlementDate_day": "", "ReceiveSettlementDate_month": "", "ReceiveSettlementDate_year": "", "ReceiveSettlement": "no", "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh" } } } [ { pageUrl: "personal-details", pageTitle: { en: "Personal data", el: "Προσωπικά στοιχεία" }, // from pageData.title in correct language fields: [ [ { id: "firstName", label: { en: "First Name", el: "Όνομα" }, value: "John", // The actual user input value valueLabel: { en: "John", el: "John" } // Same label as the value for text inputs }, { id: "lastName", label: { en: "Last Name", el: "Επίθετο" }, value: "Doe", // The actual user input value valueLabel: { en: "Doe", el: "Doe" } // Same label as the value for text inputs }, { id: "gender", label: { en: "Gender", el: "Φύλο" }, value: "m", // The actual value ("male") valueLabel: { en: "Male", el: "Άντρας" } // The corresponding label for "male" }, { id: "languages", label: { en: "Languages", el: "Γλώσσες" }, value: ["en", "el"], // The selected values ["en", "el"] valueLabel: [ { en: "English", el: "Αγγλικά" }, // Labels corresponding to "en" and "el" { en: "Greek", el: "Ελληνικά" } ] }, { id: "birthDate", label: { en: "Birth Date", el: "Ημερομηνία Γέννησης" }, value: "1990-01-13", // The actual value based on user input valueLabel: "13/1/1990" // Date inputs label will be conveted to D/M/YYYY } ] }, ... ] */