@gov-cy/govcy-express-services 1.3.0-alpha.1 → 1.3.0-alpha.3

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
+ ];
@@ -100,6 +100,16 @@ export async function handleFileUpload({
100
100
 
101
101
  // deep copy the page template to avoid modifying the original
102
102
  const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
103
+
104
+ // If mode is `single` make sure it has no multipleThings
105
+ if (mode === "single" && page?.multipleThings) {
106
+ return {
107
+ status: 400,
108
+ dataStatus: 413,
109
+ errorMessage: 'Single mode upload not allowed on multipleThings pages'
110
+ };
111
+ }
112
+
103
113
  // Validate the field: Only allow upload if the page contains a fileInput with the given name
104
114
  const isAllowed = pageContainsFileInput(pageTemplateCopy, elementName);
105
115
  if (!isAllowed) {
@@ -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
  {
@@ -96,13 +96,39 @@ function validateValue(value, rules) {
96
96
  }
97
97
  return normalizedVal <= max;
98
98
  },
99
- // ✅ New rule: maxCurrentYear
99
+ // ✅ Year based current rules
100
100
  maxCurrentYear: (val) => {
101
101
  const normalizedVal = normalizeNumber(val);
102
102
  if (isNaN(normalizedVal)) return false;
103
103
  const currentYear = new Date().getFullYear();
104
104
  return normalizedVal <= currentYear;
105
105
  },
106
+ minCurrentYear: (val) => {
107
+ const normalizedVal = normalizeNumber(val);
108
+ if (isNaN(normalizedVal)) return false;
109
+ const currentYear = new Date().getFullYear();
110
+ return normalizedVal >= currentYear;
111
+ },
112
+ // ✅ Date-based current rules
113
+ minCurrentDate: (val) => {
114
+ const valueDate = parseDate(val);
115
+ const today = new Date();
116
+ if (isNaN(valueDate)) return false;
117
+ // strip time components from both
118
+ const valueOnly = new Date(valueDate.getFullYear(), valueDate.getMonth(), valueDate.getDate());
119
+ const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
120
+ return valueOnly >= todayOnly;
121
+ },
122
+
123
+ maxCurrentDate: (val) => {
124
+ const valueDate = parseDate(val);
125
+ const today = new Date();
126
+ if (isNaN(valueDate)) return false;
127
+ const valueOnly = new Date(valueDate.getFullYear(), valueDate.getMonth(), valueDate.getDate());
128
+ const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
129
+ return valueOnly <= todayOnly;
130
+ },
131
+
106
132
  minValueDate: (val, minDate) => {
107
133
  const valueDate = parseDate(val); // Parse the input date
108
134
  const min = parseDate(minDate); // Parse the minimum date
@@ -139,7 +165,7 @@ function validateValue(value, rules) {
139
165
  // Skip validation if the value is empty
140
166
  if (value === null || value === undefined || (typeof value === 'string' && value.trim() === "")) {
141
167
  continue; // let "required" handle emptiness
142
- }
168
+ }
143
169
  // Check for "valid" rules (e.g., numeric, telCY, etc.)
144
170
  if (check === "valid" && validationRules[checkValue]) {
145
171
  const isValid = validationRules[checkValue](value);
@@ -168,12 +194,12 @@ function validateValue(value, rules) {
168
194
  if (check === 'minValue' && !validationRules.minValue(value, checkValue)) {
169
195
  return message;
170
196
  }
171
-
197
+
172
198
  // Check for "maxValue"
173
199
  if (check === 'maxValue' && !validationRules.maxValue(value, checkValue)) {
174
200
  return message;
175
201
  }
176
-
202
+
177
203
  // Check for "minValueDate"
178
204
  if (check === 'minValueDate' && !validationRules.minValueDate(value, checkValue)) {
179
205
  return message;
@@ -368,4 +394,77 @@ export function validateFormElements(elements, formData, pageUrl) {
368
394
  }
369
395
  });
370
396
  return validationErrors;
371
- }
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
+