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

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.
package/README.md CHANGED
@@ -33,6 +33,7 @@ The APIs used for submission, temporary save and file uploads are not part of th
33
33
  - [🧩 Dynamic services](#-dynamic-services)
34
34
  - [Pages](#pages)
35
35
  - [Form vs static pages](#form-vs-static-pages)
36
+ - [Update my details pages](#update-my-details-pages)
36
37
  - [Multiple things pages (repeating group of inputs)](#multiple-things-pages-repeating-group-of-inputs)
37
38
  - [Review page](#review-page)
38
39
  - [Success page](#success-page)
@@ -755,6 +756,8 @@ flowchart LR
755
756
 
756
757
  Some pages are generated automatically by the project, such as the `review` and `success` pages.
757
758
 
759
+ ------------------------------------------
760
+
758
761
  #### Pages
759
762
 
760
763
  Pages defined in the JSON file under the `pages` array, they rendered based on the [govcy-frontend-renderer](https://github.com/gov-cy/govcy-frontend-renderer) library, and they are served by the `/:siteId/:pageUrl` route. The `pageData.nextPage` field is used to determine the next page to render.
@@ -876,6 +879,8 @@ Lets break down the JSON config for this page:
876
879
  - `sections` is an array of sections, which is an array of elements. Sections allowed: `beforeMain`, `main`, `afterMain`.
877
880
  - `elements` is an array of elements for the said section. Seem more details on the [govcy-frontend-renderer's design elements documentation](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md).
878
881
 
882
+ ------------------------------------------
883
+
879
884
  #### Form vs static pages
880
885
 
881
886
  - If the `pageTemplate` includes a `form` element in the `main` section and `button` element, the system will treat it as form and will:
@@ -909,6 +914,147 @@ The [start page](https://gov-cy.github.io/govcy-design-system-docs/patterns/serv
909
914
  - Check out the [govcy-frontend-renderer's design elements](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md) for more details on the supported elements and their parameters.
910
915
  - Check out the [input validations section](#-input-validations) for more details on how to add validations to the JSON file.
911
916
 
917
+ -------------------------------------------
918
+
919
+ #### Update my details pages
920
+
921
+ ![Update my details seamless](docs/img/express-UMD-Seamless.png)
922
+
923
+ Update my details pages are pages that can integrate the [Update My Personal Details service](https://update-my-details.service.gov.cy/) to fetch or update a user’s name, email, mobile number, or correspondence address, without breaking the user journey. The implementation of these pages follow the instructuctions described in the [Use ‘Update my personal details’ in Your Service](https://dsf.dmrid.gov.cy/2025/05/20/use-update-my-personal-details-in-your-service/) post.
924
+
925
+ If the `scope` also includes the `email` element, the system will also use that email address to send the email to the user on submition.
926
+
927
+ **Update my details - example JSON config**
928
+ ```json
929
+ {
930
+ "pageData": {
931
+ "url": "index", // Page URL
932
+ "layout": "layouts/govcyBase.njk",
933
+ "mainLayout": "two-third",
934
+ "nextPage": "next-page"
935
+ },
936
+ "updateMyDetails": {
937
+ "APIEndpoint": { // API endpoint for fetching user details from the Update My Details service
938
+ "url": "CIVIL_REGISTRY_CONTACT_API_URL", // URL
939
+ "method": "GET", // HTTP method
940
+ "clientKey": "DSF_API_GTW_CLIENT_ID", // Client key
941
+ "serviceId": "DSF_API_GTW_SERVICE_ID", // Service ID
942
+ "dsfgtwApiKey": "DSF_API_GTW_SECRET" // DSF GTW API key
943
+ },
944
+ "updateMyDetailsURL": "UPDATE_MY_DETAILS_URL", // DOMAIN URL for redirecting to the Update My Details service
945
+ "topElements": [ // Elements to be displayed on the top of the page
946
+ {
947
+ "element": "progressList",
948
+ "params": {
949
+ "id": "steps",
950
+ "current": "4",
951
+ "total": "4",
952
+ "showSteps": true
953
+ }
954
+ }
955
+ ],
956
+ "scope": [ // Scope of the form. What elenents to collect
957
+ "fullName",
958
+ "email",
959
+ "mobile",
960
+ "address"
961
+ ],
962
+ "hasBackLink": true // Whether the hub page has a back link
963
+ }
964
+ }
965
+
966
+ ```
967
+
968
+ Lets break down the JSON config for Update my details:
969
+
970
+ - **updateMyDetails** are the page's definition for the integration with the Update My Details service.
971
+ - `updateMyDetails.APIEndpoint` is the API endpoint for fetching user details from the Update My Details service.
972
+ - `updateMyDetails.updateMyDetailsURL` is the DOMAIN URL for redirecting to the Update My Details service.
973
+ - `updateMyDetails.scope` is the scope of the form. What elenents to collect. It can be one or more of the following:
974
+ - `fullName`
975
+ - `dob`
976
+ - `email`
977
+ - `mobile`
978
+ - `address`
979
+ - `updateMyDetails.hasBackLink` is a boolean that indicates whether the hub page has a back link.
980
+
981
+
982
+ The above config references the following environment variables that need to be set:
983
+
984
+ ```dotenv
985
+ ### Update my details
986
+ CIVIL_REGISTRY_CONTACT_API_URL=http://localhost:3002/get-update-my-details
987
+ DSF_API_GTW_CLIENT_ID=your-DSF-API-gateway-client-id
988
+ DSF_API_GTW_SERVICE_ID=your-DSF-API-gateway-service-id
989
+ DSF_API_GTW_SECRET=your-DSF-API-gateway-secret
990
+
991
+ UPDATE_MY_DETAILS_URL=https://update-my-details.staging.service.gov.cy # FOR TESTING
992
+ ```
993
+
994
+ The DSF team has developed an API that performs standard eligibility checks against the Civil Registry. More details at [Update-my-details.md](docs/Update-my-details.md)
995
+
996
+ **Update my details - users' flow**
997
+
998
+ The update my details behaves like a normal page and it is accessed through the url `:siteId/:pageUrl`. As a best practice you should set this as your `index` page and the first one in your pages array. Depending on the user it can have 3 variants:
999
+
1000
+ **Variant 1: Manual form for non-eligible users (no access to UMD)**
1001
+
1002
+ When a user is either:
1003
+ - Not a Cypriot citizen
1004
+ - Or Cypriot citizen under 18
1005
+
1006
+ The user gets a data entry page.
1007
+
1008
+ ![variant 1 screenshot](docs/img/express-UMD-variant1.png)
1009
+
1010
+ **Variant 2: Eligible users (access to UMD) with existing details**
1011
+
1012
+ When a user is :
1013
+ - A Cypriot citizen over 18
1014
+ - AND has data in Update my Details
1015
+
1016
+ The users get a page with the data from UMD and asks if its ok to use those data.
1017
+
1018
+ ![variant 2 screenshot](docs/img/express-UMD-variant2.png)
1019
+
1020
+ If the user selects:
1021
+ - `YES`: the data are stored in the data layer and continues to the next page (or review page depending where the user came from)
1022
+ - `NO`: the users are redirected to the Update my details service in seamless mode. When users finish with that, they are redirected back to the page with variant 2 and are asked again if it's ok to use the updated data.
1023
+
1024
+ **Variant 3: Eligible users (access to UMD) without existing details**
1025
+
1026
+ When a user is :
1027
+ - A Cypriot citizen over 18
1028
+ - AND has no data in Update my Details
1029
+
1030
+ The users get a continue button that redirects to the Update my details service in seamless mode.
1031
+
1032
+ ![variant 3 screenshot](docs/img/express-UMD-variant3.png)
1033
+
1034
+ When users finish with that, they are redirected back to the page with variant 2 and are asked if it's ok to use the updated data.
1035
+
1036
+ **Update my details - data storage**
1037
+
1038
+ The form data for a `updateMyDetails` page is stored as a normal page as an **object** in the session data layer:
1039
+ ```json
1040
+ {
1041
+ "index": {
1042
+ "formData":
1043
+ {
1044
+ "fullName": "John Smith",
1045
+ "email": "email@example.com",
1046
+ "mobile": "+35712345678",
1047
+ "address": "123 Some Street\n Nicosia\nCyprus",
1048
+ }
1049
+ }
1050
+ }
1051
+ ```
1052
+
1053
+ **Notes on Update my details**
1054
+ - There `pageData.title` is not used. Instead the page title is generated by the system.
1055
+
1056
+ ------------------------------------------
1057
+
912
1058
  #### Multiple things pages (repeating group of inputs)
913
1059
  ![Multiple things pages](docs/img/express-mt.png)
914
1060
 
@@ -1129,6 +1275,8 @@ Form data for a `multipleThings` page is stored as an **array** in the session d
1129
1275
  - `multipleDraft` is used internally to store file's data while the user is adding a new item.
1130
1276
 
1131
1277
 
1278
+ ------------------------------------------
1279
+
1132
1280
  #### Review page
1133
1281
 
1134
1282
  The `review` page is automatically generated by the project and includes the following sections:
@@ -1145,6 +1293,8 @@ When the user clicks a change link, the user is redirected to the corresponding
1145
1293
 
1146
1294
  When the user clicks the `Submit` button, all the data gathered from the site's forms within this session are validated based on the validation definition in the JSON file, and if they pass they are submitted to the configured API endpoint.
1147
1295
 
1296
+ ------------------------------------------
1297
+
1148
1298
  #### Success page
1149
1299
 
1150
1300
  The `success` page is automatically generated by the project, is accessible only when a submission is made successfully, and includes the following sections:
@@ -1157,6 +1307,8 @@ Here's an example screenshot of success page
1157
1307
 
1158
1308
  ![Screenshot of success page](docs/img/express-success.png)
1159
1309
 
1310
+ --------------------------------
1311
+
1160
1312
  ### 🛡️ Site eligibility checks
1161
1313
 
1162
1314
  The project uses an array of API endpoints to check the eligibility of a service/site. To use this feature, you need to configure the following in your JSON file under the `site` object:
@@ -2479,7 +2631,7 @@ The environment variables are defined in:
2479
2631
  - `.env.production` for production
2480
2632
 
2481
2633
  #### Secret environment variables
2482
- The following secret environment variables are used to configure the server:
2634
+ The following secret environment variables are used to configure the server (note the values below are examples, you need to replace them with your own values):
2483
2635
 
2484
2636
  ```dotenv
2485
2637
  # 🔐 Session
@@ -2553,9 +2705,13 @@ TEST_FILE_DELETE_API_URL=http://localhost:3002/fileDelete
2553
2705
  # Eligibility checks (optional test APIs)
2554
2706
  TEST_ELIGIBILITY_1_API_URL=http://localhost:3002/eligibility1
2555
2707
  TEST_ELIGIBILITY_2_API_URL=http://localhost:3002/eligibility2
2708
+
2709
+ # Update my details
2710
+ CIVIL_REGISTRY_CONTACT_API_URL=http://localhost:3002/get-update-my-details
2711
+ UPDATE_MY_DETAILS_URL=https://update-my-details.staging.service.gov.cy
2556
2712
  ```
2557
2713
  #### Non secret environment variables
2558
- The following non-secret environment variables are used to configure the server defined in `.env.development` for local development, `.env.staging` for staging, and `.env.production` for production.:
2714
+ The following non-secret environment variables are used to configure the server defined in `.env.development` for local development, `.env.staging` for staging, and `.env.production` for production:
2559
2715
 
2560
2716
  ```dotenv
2561
2717
  #### Matomo web analytics environment variables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.3.0-alpha.4",
3
+ "version": "1.3.0-alpha.6",
4
4
  "description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
5
5
  "author": "DMRID - DSF Team",
6
6
  "license": "MIT",
@@ -4,6 +4,7 @@ import { successResponse, errorResponse } from '../utils/govcyApiResponse.mjs';
4
4
  import { ALLOWED_MULTER_FILE_SIZE_MB } from "../utils/govcyConstants.mjs";
5
5
  import { handleFileUpload } from "../utils/govcyHandleFiles.mjs";
6
6
 
7
+ /* c8 ignore start */
7
8
  // Configure multer to store the file in memory (not disk) and limit the size to 10MB
8
9
  const upload = multer({
9
10
  storage: multer.memoryStorage(),
@@ -46,4 +47,5 @@ export const govcyFileUpload = [
46
47
 
47
48
  return res.json(successResponse(result.data));
48
49
  }
49
- ];
50
+ ];
51
+ /* c8 ignore end */
@@ -14,7 +14,7 @@ import { getEnvVariable, getEnvVariableBool, isProdOrStaging } from "../utils/go
14
14
  import * as govcyResources from "../resources/govcyResources.mjs";
15
15
  import * as dataLayer from "../utils/govcyDataLayer.mjs";
16
16
  import { logger } from '../utils/govcyLogger.mjs';
17
- import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
17
+ import { handleMiddlewareError, dateStringISOtoDMY } from "../utils/govcyUtils.mjs";
18
18
  import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
19
19
  import { isUnder18, isValidCypriotCitizen, validateFormElements } from "../utils/govcyValidator.mjs";
20
20
  import { populateFormData, getFormData } from "../utils/govcyFormHandling.mjs";
@@ -128,8 +128,21 @@ export async function govcyUpdateMyDetailsHandler(req, res, next, page, serviceC
128
128
  // Special case for address
129
129
  if (element === "address") {
130
130
  key = "addressInfo";
131
- value = response.Data?.addressInfo?.[0]?.addressText || "";
132
- }
131
+ //if response.Data.addressInfo is an array
132
+ if (response.Data.addressInfo && Array.isArray(response.Data.addressInfo)) {
133
+ value = response.Data?.addressInfo?.[0]?.addressText || "";
134
+ }
135
+ // else if response.Data.addressInfoUnstructured is not null and is an array
136
+ else if (response.Data.addressInfoUnstructured && Array.isArray(response.Data.addressInfoUnstructured)) {
137
+ value = response.Data?.addressInfoUnstructured?.[0]?.addressText || "";
138
+ }
139
+ // else if response.Data.poBoxAddress is not null and is not an array
140
+ else if (response.Data.poBoxAddress && Array.isArray(response.Data.poBoxAddress)) {
141
+ value = response.Data?.poBoxAddress?.[0]?.poBoxText || "";
142
+ } else {
143
+ value = "";
144
+ }
145
+ }
133
146
 
134
147
  // Check if the key exists
135
148
  if (!Object.prototype.hasOwnProperty.call(response.Data || {}, key)) {
@@ -162,6 +175,9 @@ export async function govcyUpdateMyDetailsHandler(req, res, next, page, serviceC
162
175
  }
163
176
  }
164
177
 
178
+ // Deep copy pageTemplate to avoid modifying the original
179
+ const pageTemplateCopy = JSON.parse(JSON.stringify(pageTemplate));
180
+
165
181
  // if the page variant is 1 or 2 which means it has a form
166
182
  if (pageVariant === 1 || pageVariant === 2) {
167
183
  // Handle form data
@@ -181,7 +197,7 @@ export async function govcyUpdateMyDetailsHandler(req, res, next, page, serviceC
181
197
 
182
198
 
183
199
  populateFormData(
184
- pageTemplate.sections[0].elements[0].params.elements,
200
+ pageTemplateCopy.sections[0].elements[0].params.elements,
185
201
  theData,
186
202
  validationErrors,
187
203
  req.session,
@@ -194,18 +210,18 @@ export async function govcyUpdateMyDetailsHandler(req, res, next, page, serviceC
194
210
 
195
211
  // if there are validation errors, add an error summary
196
212
  if (validationErrors?.errorSummary?.length > 0) {
197
- pageTemplate.sections[0].elements[0].params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
213
+ pageTemplateCopy.sections[0].elements[0].params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
198
214
  }
199
215
  }
200
216
 
201
217
  // Add topElements if provided
202
218
  if (Array.isArray(umdConfig.topElements)) {
203
- pageTemplate.sections[0].elements[0].params.elements.unshift(...umdConfig.topElements);
219
+ pageTemplateCopy.sections[0].elements[0].params.elements.unshift(...umdConfig.topElements);
204
220
  }
205
221
 
206
222
  //if hasBackLink == true add section beforeMain with backlink element
207
223
  if (umdConfig?.hasBackLink == true) {
208
- pageTemplate.sections.unshift({
224
+ pageTemplateCopy.sections.unshift({
209
225
  name: "beforeMain",
210
226
  elements: [
211
227
  {
@@ -226,7 +242,7 @@ export async function govcyUpdateMyDetailsHandler(req, res, next, page, serviceC
226
242
  mainLayout: page?.pageData?.mainLayout || "two-third"
227
243
  }
228
244
  },
229
- pageTemplate: pageTemplate
245
+ pageTemplate: pageTemplateCopy
230
246
  };
231
247
 
232
248
  logger.debug("Processed `govcyUpdateMyDetailsHandler` page data:", req.processedPage, req);
@@ -372,8 +388,21 @@ export function govcyUpdateMyDetailsPostHandler() {
372
388
  // Special case for address
373
389
  if (element === "address") {
374
390
  key = "addressInfo";
375
- value = response.Data?.addressInfo?.[0]?.addressText || "";
376
- }
391
+ //if response.Data.addressInfo is an array
392
+ if (response.Data.addressInfo && Array.isArray(response.Data.addressInfo)) {
393
+ value = response.Data?.addressInfo?.[0]?.addressText || "";
394
+ }
395
+ // else if response.Data.addressInfoUnstructured is not null and is an array
396
+ else if (response.Data.addressInfoUnstructured && Array.isArray(response.Data.addressInfoUnstructured)) {
397
+ value = response.Data?.addressInfoUnstructured?.[0]?.addressText || "";
398
+ }
399
+ // else if response.Data.poBoxAddress is not null and is not an array
400
+ else if (response.Data.poBoxAddress && Array.isArray(response.Data.poBoxAddress)) {
401
+ value = response.Data?.poBoxAddress?.[0]?.poBoxText || "";
402
+ } else {
403
+ value = "";
404
+ }
405
+ }
377
406
 
378
407
  // Check if the key exists
379
408
  if (!Object.prototype.hasOwnProperty.call(response.Data || {}, key)) {
@@ -387,8 +416,18 @@ export function govcyUpdateMyDetailsPostHandler() {
387
416
  hasData = false;
388
417
  userDetails[element] = "";
389
418
  } else {
390
- // Set the value
391
- userDetails[element] = value;
419
+ if (element === "dob") {
420
+ key = "dob";
421
+ // value = response.Data?.[key] || "";
422
+ // Store different for ${element}_day, ${element}_month, ${element}_year
423
+ const [year, month, day] = value.split("-").map(Number);
424
+ userDetails[`${element}_day`] = day;
425
+ userDetails[`${element}_month`] = month;
426
+ userDetails[`${element}_year`] = year;
427
+ } else {
428
+ // Set the value as it is
429
+ userDetails[element] = value;
430
+ }
392
431
  }
393
432
  }
394
433
 
@@ -567,6 +606,12 @@ function createUmdHasDataPageTemplate(siteId, lang, page, req, userDetails) {
567
606
  }
568
607
  //for each element in the scope array
569
608
  umdConfig?.scope.forEach(element => {
609
+ let value = userDetails?.[element] || "";
610
+
611
+ //if element is dob
612
+ if (element === "dob") {
613
+ value = dateStringISOtoDMY(value);
614
+ }
570
615
  // add the key and value to the summaryList
571
616
  summaryList.params.items.push({
572
617
  key: govcyResources.staticResources.text.updateMyDetailsScopes[element],
@@ -576,7 +621,7 @@ function createUmdHasDataPageTemplate(siteId, lang, page, req, userDetails) {
576
621
  params: {
577
622
  type: "span",
578
623
  classes: "govcy-whitespace-pre-line",
579
- text: govcyResources.getSameMultilingualObject(null, userDetails[element])
624
+ text: govcyResources.getSameMultilingualObject(null, value)
580
625
  }
581
626
  }
582
627
  ]
@@ -744,3 +789,4 @@ export function constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, return
744
789
  // Construct redirect URL
745
790
  return `${umdBaseURL}/ReturnUrl/SetReturnUrl?Url=${encodedReturnUrl}&UserProfileId=${encodedUserId}&lang=${lang}`;
746
791
  }
792
+
@@ -239,7 +239,7 @@ export const staticResources = {
239
239
  updateMyDetailsScopes : {
240
240
  fullName :
241
241
  {
242
- el: "Ονοματεπωνυμο",
242
+ el: "Ονοματεπώνυμο",
243
243
  en: "Full name",
244
244
  tr: "Full name"
245
245
  },
@@ -261,6 +261,12 @@ export const staticResources = {
261
261
  en: "Mailing address",
262
262
  tr: "Mailing address"
263
263
  },
264
+ dob :
265
+ {
266
+ el: "Ημερομηνία γέννησης",
267
+ en: "Date of birth",
268
+ tr: "Date of birth"
269
+ },
264
270
  }
265
271
  },
266
272
  //remderer sections
@@ -312,7 +318,7 @@ export const staticResources = {
312
318
  name: "useTheseDetails",
313
319
  legend: {
314
320
  el: "Να χρησιμοποιήσουμε αυτά τα στοιχεία;",
315
- en: "Should we use these elements?"
321
+ en: "Should we use these details?"
316
322
  },
317
323
  items: [
318
324
  {
@@ -329,6 +335,11 @@ export const staticResources = {
329
335
  el: "Όχι, θα αλλάξω αυτά τα στοιχεία",
330
336
  en: "No, I will change these details",
331
337
  tr: ""
338
+ },
339
+ hint: {
340
+ el: "Μπορείτε να αλλάξετε μόνο τα στοιχεία επικοινωνίας",
341
+ en: "You can only change your contact details",
342
+ tr: ""
332
343
  }
333
344
  }
334
345
  ],
@@ -603,6 +614,68 @@ export const staticResources = {
603
614
  }
604
615
  }
605
616
  ]
617
+ },
618
+ "dob" : {
619
+ element: "dateInput",
620
+ params: {
621
+ id: "dob",
622
+ name: "dob",
623
+ legend: {
624
+ el: "Ημερομηνία γέννησης",
625
+ en: "Date of birth"
626
+ },
627
+ isPageHeading: false,
628
+ hint: {
629
+ el: "Για παράδειγμα, 13 8 2001",
630
+ en: "For example, 13 8 2001"
631
+ }
632
+ },
633
+ validations: [
634
+ {
635
+ check: "required",
636
+ params: {
637
+ checkValue: "",
638
+ message: {
639
+ el: "Εισαγάγετε την ημερομηνία γέννησης",
640
+ en: "Enter the date of birth",
641
+ tr: ""
642
+ }
643
+ }
644
+ },
645
+ {
646
+ check: "valid",
647
+ params: {
648
+ checkValue: "dateISO",
649
+ message: {
650
+ el: "Η ημερομηνία γέννησης πρέπει να είναι σωστή ημερομηνία",
651
+ en: "The date of birth must be an valid date",
652
+ tr: ""
653
+ }
654
+ }
655
+ },
656
+ {
657
+ check: "minValueDate",
658
+ params: {
659
+ checkValue: "1900-01-01",
660
+ message: {
661
+ el: "Η ημερομηνία γέννησης πρέπει να είναι μετά από τις 1/1/1990",
662
+ en: "The date of birth must be after 1/1/1900",
663
+ tr: ""
664
+ }
665
+ }
666
+ },
667
+ {
668
+ check: "valid",
669
+ params: {
670
+ checkValue: "maxCurrentDate",
671
+ message: {
672
+ el: "Η ημερομηνία γέννησης δεν πρέπει να είναι στο μέλλον",
673
+ en: "The date of birth must not be in the future",
674
+ tr: ""
675
+ }
676
+ }
677
+ }
678
+ ]
606
679
  }
607
680
  },
608
681
  govcyFormsJs: {
@@ -10,4 +10,16 @@ export function handleMiddlewareError(message, status, next) {
10
10
  const error = new Error(message);
11
11
  error.status = status;
12
12
  return next(error);
13
+ }
14
+
15
+ /**
16
+ * Helper function to format a date in the format D/M/YYYY.
17
+ *
18
+ * @param {string} dateString - The date string in the format YYYY-MM-DD.
19
+ * @returns {string} The formatted date in the format D/M/YYYY.
20
+ */
21
+ export function dateStringISOtoDMY(dateString) {
22
+ if (typeof dateString !== "string" || !dateString.trim()) return "";
23
+ const [year, month, day] = dateString.trim().split("-");
24
+ return `${parseInt(day)}/${parseInt(month)}/${year}`;
13
25
  }