@gov-cy/govcy-express-services 1.3.0-alpha.2 → 1.3.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,4 +5,16 @@ export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios
5
5
  export const ALLOWED_FILE_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
6
6
  export const ALLOWED_FILE_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png'];
7
7
  export const ALLOWED_FILE_SIZE_MB = 4; // Maximum file size in MB
8
- export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // Maximum file size in MB
8
+ export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // Maximum file size in MB
9
+ // UPDATE MY DETAILS
10
+ // Only allow certain hosts
11
+ export const UPDATE_MY_DETAILS_ALLOWED_HOSTS = [
12
+ "update-my-details.staging.service.gov.cy",
13
+ "update-my-details.service.gov.cy",
14
+ "localhost"
15
+ ];
16
+ // Possible incoming routes
17
+ export const UPDATE_MY_DETAILS_REDIRECT_HOSTS = [
18
+ "update-my-details.staging.service.gov.cy",
19
+ "update-my-details.service.gov.cy"
20
+ ];
@@ -248,6 +248,34 @@ export function getSiteEligibilityResult(store, siteId, endpointKey, maxAgeMs =
248
248
  return entry.result;
249
249
  }
250
250
 
251
+ /**
252
+ * Stores the update my details data for contact purposes, outside the scope of formData
253
+ *
254
+ * For example `store.siteData[siteId].inputData[pageUrl]["updateMyDetails"] = value`
255
+ *
256
+ * @param {object} store The session store
257
+ * @param {string} siteId The site id
258
+ * @param {string} pageUrl The page url
259
+ * @param {object} userData The user's update my details data
260
+ */
261
+ export function storePageUpdateMyDetails(store, siteId, pageUrl, userData) {
262
+ // Ensure session structure is initialized
263
+ initializeSiteData(store, siteId, pageUrl);
264
+
265
+ store.siteData[siteId].inputData[pageUrl]["updateMyDetails"] = userData;
266
+ }
267
+
268
+ /**
269
+ * Get the update my details data for contact purposes, outside the scope of formData
270
+ * @param {object} store The session store
271
+ * @param {string} siteId The site id
272
+ * @param {string} pageUrl The page url
273
+ * @returns The user's update my details data
274
+ */
275
+ export function getPageUpdateMyDetails(store, siteId, pageUrl) {
276
+ return store?.siteData?.[siteId]?.inputData?.[pageUrl]?.["updateMyDetails"] || null;
277
+ }
278
+
251
279
  /**
252
280
  * Get the page validation errors from the store and clear them
253
281
  *
@@ -6,6 +6,7 @@ import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
6
6
  import { evaluatePageConditions } from "./govcyExpressions.mjs";
7
7
  import { getPageConfigData } from "./govcyLoadConfigData.mjs";
8
8
  import { logger } from "./govcyLogger.mjs";
9
+ import { createUmdManualPageTemplate } from "../middleware/govcyUpdateMyDetails.mjs"
9
10
  import nunjucks from "nunjucks";
10
11
 
11
12
  /**
@@ -43,9 +44,30 @@ export function prepareSubmissionData(req, siteId, service) {
43
44
 
44
45
  // Find the <form> element in the page
45
46
  let formElement = null;
46
- for (const section of page.pageTemplate.sections || []) {
47
- formElement = section.elements.find(el => el.element === "form");
48
- if (formElement) break;
47
+
48
+ // ----- `updateMyDetails` handling
49
+ // 🔹 Case C: updateMyDetails
50
+ if (page.updateMyDetails) {
51
+ logger.debug("Preparing submission data for UpdateMyDetails page", { siteId, pageUrl });
52
+ // Build the manual UMD page template
53
+ const umdTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
54
+
55
+ // Extract the form element
56
+ formElement = umdTemplate .sections
57
+ .flatMap(section => section.elements)
58
+ .find(el => el.element === "form");
59
+
60
+ if (!formElement) {
61
+ logger.error("🚨 UMD form element not found during prepareSubmissionData", { siteId, pageUrl });
62
+ return handleMiddlewareError("🚨 UMD form element not found during prepareSubmissionData", 500, next);
63
+ }
64
+ // ----- `updateMyDetails` handling
65
+ } else {
66
+ // Normal flow
67
+ for (const section of page.pageTemplate.sections || []) {
68
+ formElement = section.elements.find(el => el.element === "form");
69
+ if (formElement) break;
70
+ }
49
71
  }
50
72
 
51
73
  if (!formElement) continue; // ⛔ Skip pages without a <form> element
@@ -235,8 +257,20 @@ export function preparePrintFriendlyData(req, siteId, service) {
235
257
  continue; // ⛔ Skip this page from print-friendly data
236
258
  }
237
259
 
260
+ let pageTemplate = page.pageTemplate;
261
+ let pageTitle = page.pageData.title || {};
262
+
263
+
264
+ // ----- MultipleThings hub handling
265
+ if (page.updateMyDetails) {
266
+ // create the page template
267
+ pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req );
268
+ // set the page title
269
+ pageTitle = govcyResources.staticResources.text.updateMyDetailsTitle;
270
+ }
271
+
238
272
  // find the form element in the page template
239
- for (const section of page.pageTemplate.sections || []) {
273
+ for (const section of pageTemplate.sections || []) {
240
274
  for (const element of section.elements || []) {
241
275
  if (element.element !== "form") continue;
242
276
 
@@ -282,6 +316,7 @@ export function preparePrintFriendlyData(req, siteId, service) {
282
316
 
283
317
  // Special case: multipleThings page → extract item titles // ✅ new
284
318
  if (page.multipleThings) {
319
+ pageTitle = page.multipleThings?.listPage?.title || pageTitle;
285
320
  let mtItems = dataLayer.getPageData(req.session, siteId, page.pageData.url);
286
321
  if (Array.isArray(mtItems)) {
287
322
  const env = new nunjucks.Environment(null, { autoescape: false });
@@ -295,7 +330,7 @@ export function preparePrintFriendlyData(req, siteId, service) {
295
330
  if (fields.length > 0) {
296
331
  submissionData.push({
297
332
  pageUrl: page.pageData.url,
298
- pageTitle: page.pageData.title,
333
+ pageTitle: pageTitle,
299
334
  fields,
300
335
  items: (page.multipleThings ? items : null) // ✅ new
301
336
  });
@@ -674,8 +709,12 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
674
709
  // For each page in the submission data
675
710
  for (const page of submissionData) {
676
711
  // Get the page URL, title, and fields
677
- const { pageUrl, pageTitle, fields, items } = page;
712
+ let { pageUrl, pageTitle, fields, items } = page;
678
713
 
714
+ // 🔹 MultipleThings page
715
+ if (page.multipleThings) {
716
+ pageTitle = page.multipleThings?.listPage?.title || pageTitle;
717
+ }
679
718
  // Add data title to the body
680
719
  body.push(
681
720
  {
@@ -394,4 +394,77 @@ export function validateFormElements(elements, formData, pageUrl) {
394
394
  }
395
395
  });
396
396
  return validationErrors;
397
- }
397
+ }
398
+
399
+
400
+ /**
401
+ * Checks if a user is an Individual with a valid Cypriot citizen identifier.
402
+ * Rules:
403
+ * - profile_type must be "Individual"
404
+ * - unique_identifier must be a string
405
+ * - must start with "00"
406
+ * - must be 10 characters long
407
+ *
408
+ * @param {object} user - The user object (e.g. req.session.user)
409
+ * @returns {boolean} true if valid, false otherwise
410
+ */
411
+ export function isValidCypriotCitizen(user = {}) {
412
+ const { profile_type, unique_identifier } = user;
413
+
414
+ if (
415
+ typeof profile_type === "string" &&
416
+ profile_type === "Individual" &&
417
+ typeof unique_identifier === "string" &&
418
+ unique_identifier.startsWith("00") &&
419
+ unique_identifier.length === 10
420
+ ) {
421
+ return true;
422
+ }
423
+
424
+ return false;
425
+ }
426
+
427
+ /**
428
+ * Checks if the given user represents a valid foreign resident (ARC holder).
429
+ * Conditions:
430
+ * - profile_type must equal "Individual"
431
+ * - unique_identifier must be a string
432
+ * - unique_identifier must start with "05"
433
+ * - unique_identifier must be exactly 10 characters long
434
+ *
435
+ * @param {object} user - e.g. req.session.user
436
+ * @returns {boolean} True if valid foreign resident, otherwise false
437
+ */
438
+ export function isValidForeignResident(user = {}) {
439
+ const { profile_type, unique_identifier } = user;
440
+
441
+ return (
442
+ typeof profile_type === "string" &&
443
+ profile_type === "Individual" &&
444
+ typeof unique_identifier === "string" &&
445
+ unique_identifier.startsWith("05") &&
446
+ unique_identifier.length === 10
447
+ );
448
+ }
449
+
450
+ /**
451
+ * Checks if the user is under 18 years old based on their date of birth.
452
+ * @param {string} dobString - The date of birth in the format "YYYY-MM-DD".
453
+ * @returns {boolean} True if the user is under 18 years old, otherwise false.
454
+ * @throws {Error} If the date of birth is missing or invalid.
455
+ * */
456
+ export function isUnder18(dobString) {
457
+ if (!dobString) throw new Error("DOB is missing");
458
+ const dob = new Date(dobString);
459
+ if (isNaN(dob)) throw new Error("Invalid DOB format");
460
+
461
+ const today = new Date();
462
+ const ageDiff = today.getFullYear() - dob.getFullYear();
463
+ const hasHadBirthday =
464
+ today.getMonth() > dob.getMonth() ||
465
+ (today.getMonth() === dob.getMonth() && today.getDate() >= dob.getDate());
466
+
467
+ return (hasHadBirthday ? ageDiff : ageDiff - 1) < 18;
468
+ }
469
+
470
+