@gov-cy/govcy-express-services 0.1.1

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1157 -0
  3. package/package.json +72 -0
  4. package/src/auth/cyLoginAuth.mjs +123 -0
  5. package/src/index.mjs +188 -0
  6. package/src/middleware/cyLoginAuth.mjs +131 -0
  7. package/src/middleware/govcyConfigSiteData.mjs +38 -0
  8. package/src/middleware/govcyCsrf.mjs +36 -0
  9. package/src/middleware/govcyFormsPostHandler.mjs +83 -0
  10. package/src/middleware/govcyHeadersControl.mjs +22 -0
  11. package/src/middleware/govcyHttpErrorHandler.mjs +63 -0
  12. package/src/middleware/govcyLanguageMiddleware.mjs +19 -0
  13. package/src/middleware/govcyLogger.mjs +15 -0
  14. package/src/middleware/govcyManifestHandler.mjs +46 -0
  15. package/src/middleware/govcyPDFRender.mjs +30 -0
  16. package/src/middleware/govcyPageHandler.mjs +110 -0
  17. package/src/middleware/govcyPageRender.mjs +14 -0
  18. package/src/middleware/govcyRequestTimer.mjs +29 -0
  19. package/src/middleware/govcyReviewPageHandler.mjs +102 -0
  20. package/src/middleware/govcyReviewPostHandler.mjs +147 -0
  21. package/src/middleware/govcyRoutePageHandler.mjs +37 -0
  22. package/src/middleware/govcyServiceEligibilityHandler.mjs +101 -0
  23. package/src/middleware/govcySessionData.mjs +9 -0
  24. package/src/middleware/govcySuccessPageHandler.mjs +112 -0
  25. package/src/public/img/Certificate_A4.svg +30 -0
  26. package/src/public/js/govcyForms.js +21 -0
  27. package/src/resources/govcyResources.mjs +430 -0
  28. package/src/standalone.mjs +7 -0
  29. package/src/utils/govcyApiRequest.mjs +114 -0
  30. package/src/utils/govcyConstants.mjs +4 -0
  31. package/src/utils/govcyDataLayer.mjs +311 -0
  32. package/src/utils/govcyEnvVariables.mjs +45 -0
  33. package/src/utils/govcyFormHandling.mjs +148 -0
  34. package/src/utils/govcyLoadConfigData.mjs +135 -0
  35. package/src/utils/govcyLogger.mjs +30 -0
  36. package/src/utils/govcyNotification.mjs +85 -0
  37. package/src/utils/govcyPdfMaker.mjs +27 -0
  38. package/src/utils/govcyReviewSummary.mjs +205 -0
  39. package/src/utils/govcySubmitData.mjs +530 -0
  40. package/src/utils/govcyUtils.mjs +13 -0
  41. package/src/utils/govcyValidator.mjs +352 -0
@@ -0,0 +1,430 @@
1
+ export const staticResources = {
2
+ //text content
3
+ text: {
4
+ submit: {
5
+ en: "Submit",
6
+ el: "Υποβολή",
7
+ tr: "Gönder"
8
+ },
9
+ cancel: {
10
+ en: "Cancel",
11
+ el: "Ακύρωση",
12
+ tr: "İptal"
13
+ },
14
+ back: {
15
+ en: "Back",
16
+ el: "Πίσω",
17
+ tr: "Geri"
18
+ },
19
+ change: {
20
+ en: "Change",
21
+ el: "Αλλαγή",
22
+ tr: "Değişiklik"
23
+ },
24
+ formSuccess: {
25
+ en: "Your form has been submitted!",
26
+ el: "Η φόρμα σας έχει υποβληθεί!" ,
27
+ tr: "Formunuz gönderild"
28
+ },
29
+ errorOccurred: {
30
+ en: "An error occurred. Please try again.",
31
+ el: "Παρουσιάστηκε σφάλμα. Παρακαλώ δοκιμάστε ξανά.",
32
+ tr: "Bir hata oluştu. Lutfen tekrar deneyiniz."
33
+ },
34
+ errorPage404Title: {
35
+ el: "Δεν βρέθηκε η σελίδα",
36
+ en: "Page not found",
37
+ tr: "Sayfa bulunamadı"
38
+ },
39
+ errorPage404Body: {
40
+ el: "<p>Αν πληκτρολογήσατε την ηλεκτρονική διεύθυνση, ελέγξετε ότι είναι σωστή.</p><p>Αν αντιγράψατε την ηλεκτρονική διεύθυνση, ελέγξετε ότι επικολλήσατε ολόκληρη τη διεύθυνση.</p>",
41
+ en: "<p>If you typed the web address, check it is correct.</p><p>If you copied and pasted the web address, check that you copied the entire address.</p>",
42
+ tr: "<p>Web adresini yazdıysanız, doğru olduğunu kontrol edin.</p><p>Web adresini kopyalayıp yapıştırdıysanız, adresin tamamını kopyaladığınızdan emin olun.</p>"
43
+ },
44
+ errorPage403Title: {
45
+ el: "Απαγορευμένη προσβαση",
46
+ en: "Forbidden access",
47
+ tr: "Yasaklı erişim"
48
+ },
49
+ errorPage403Body: {
50
+ el: "<p><a href=\"/logout\">Αποσυνδεθείτε</a> και δοκιμάστε ξανά αργότερα.</p>",
51
+ en: "<p><a href=\"/logout\">Sign out</a> and try again later.</p>",
52
+ tr: "<p><a href=\"/logout\">Giriş yapmadan</a> sonra tekrar deneyiniz.</p>"
53
+ },
54
+ errorPage403NaturalOnlyPolicyBody: {
55
+ el: "<p>Η πρόσβαση επιτρέπεται μόνο σε φυσικά πρόσωπα με επιβεβαιωμένο προφίλ. <a href=\"/logout\">Αποσυνδεθείτε</a> και δοκιμάστε ξανά αργότερα.</p>",
56
+ en: "<p>Access is only allowed to individuals with a verified profile.<a href=\"/logout\">Sign out</a> and try again later.</p>",
57
+ tr: "<p>Access is only allowed to individuals with a confirmed profile.<a href=\"/logout\">Giriş yapmadan</a> sonra tekrar deneyiniz.</p>"
58
+ },
59
+ errorPage500Title: {
60
+ el: "Λυπούμαστε, υπάρχει πρόβλημα με την υπηρεσία",
61
+ en: "Sorry, there is a problem with the service",
62
+ tr: "Üzgünüz, serviste bir sorun var"
63
+ },
64
+ errorPage500Body: {
65
+ el: "<p>Αποσυνδεθείτε και δοκιμάστε ξανά αργότερα.</p>",
66
+ en: "<p>Sign out and try again later.</p>",
67
+ tr: "<p>Giriş yapmadan sonra tekrar deneyiniz.</p>"
68
+ },
69
+ checkYourAnswersTitle : {
70
+ en: "Check your answers",
71
+ el: "Ελέγξτε τις απαντήσεις σας",
72
+ tr: "Cevaplarınızı kontrol edin"
73
+ },
74
+ valueNotOnList : {
75
+ en: "Select one of the available options",
76
+ el: "Επιλέξτε μία από τις διαθέσιμες επιλογές",
77
+ tr: "Mevcut seçeneklerden birini seçin"
78
+ },
79
+ submissionSuccessTitle : {
80
+ en: "We have received your request",
81
+ el: "Έχουμε λάβει την αίτησή σας",
82
+ tr: "We have received your request"
83
+ },
84
+ yourSubmissionId : {
85
+ en: "Your reference number: ",
86
+ el: "Ο αριθμός αναφοράς: ",
87
+ tr: "Your reference number: "
88
+ },
89
+ weHaveSendYouAnEmail : {
90
+ en: "We have sent you a confirmation email.",
91
+ el: "Έχουμε εσταλει email επιβεβαιωσης.",
92
+ tr: "We have sent you a confirmation email."
93
+ },
94
+ theDataFromYourRequest : {
95
+ en: "The data from your request: ",
96
+ el: "Τα δεδομένα της αίτησής σας: ",
97
+ tr: "The data from your request: "
98
+ },
99
+ emailSubmissionPreHeader : {
100
+ en: "We have received your request. ",
101
+ el: "Έχουμε λάβει την αίτησή σας. ",
102
+ tr: "We have received your request. "
103
+ }
104
+ },
105
+ //remderer sections
106
+ sections: {
107
+ beforeMain : {name: "beforeMain", elements: []},
108
+ main : {name: "main", elements: []}
109
+ },
110
+ //renderer elements
111
+ elements: {
112
+ govcyFormsJs: {
113
+ element: "htmlElement",
114
+ params: {
115
+ text: {
116
+ en: `<script src="/js/govcyForms.js"></script>`,
117
+ el: `<script src="/js/govcyForms.js"></script>`,
118
+ tr: `<script src="/js/govcyForms.js"></script>`
119
+ }
120
+ }
121
+ },
122
+ backLink: { element: "backLink", params: {} }
123
+ },
124
+ //renderer page data template
125
+ rendererPageData :
126
+ {
127
+ site: {
128
+ lang: "el",
129
+ title: {
130
+ en: "govcy Express Services",
131
+ el: "govcy Express Services",
132
+ tr: "govcy Express Services"
133
+ },
134
+ headerTitle: {
135
+ en: "",
136
+ el: "",
137
+ tr: ""
138
+ },
139
+ description: {
140
+ en: "govcy Express Services",
141
+ el: "govcy Express Services",
142
+ tr: "govcy Express Services"
143
+ },
144
+ copyrightText :{
145
+ en:"Republic of Cyprus, 2025",
146
+ el:"Κυπριακή Δημοκρατία, 2025",
147
+ tr:"Kıbrıs Cumhuriyeti, 2025"
148
+ },
149
+ url: "https://gov.cy",
150
+ cdn: {
151
+ dist: "https://cdn.jsdelivr.net/gh/gov-cy/govcy-design-system@3.2.0/dist"
152
+ }
153
+ },
154
+ pageData: {
155
+ title: {
156
+ en: "Page title",
157
+ el: "Τιτλός σελιδας",
158
+ tr: "Sayfa başlığı"
159
+ },
160
+ layout: "layouts/govcyBase.njk",
161
+ mainLayout: "two-thirds"
162
+ }
163
+ },
164
+ //renderer page template
165
+ emptySections: {
166
+ sections : []
167
+ },
168
+ //all other
169
+ other : {
170
+ noPrintClass: "govcy-d-print-none"
171
+ }
172
+ };
173
+
174
+ /**
175
+ * Get the csrf token input element
176
+ * @param {string} csrfToken
177
+ * @returns {object} htmlElement with csrf token
178
+ */
179
+ export function csrfTokenInput(csrfToken) {
180
+ const csrfTokenInput = `<input type="hidden" name="_csrf" value="${csrfToken}">`;
181
+ return {
182
+ element: "htmlElement",
183
+ params: {
184
+ text: {
185
+ en: csrfTokenInput,
186
+ el: csrfTokenInput,
187
+ tr: csrfTokenInput
188
+ }
189
+ }
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Error page template
195
+ * @param {object} title the title text element
196
+ * @param {object} body the body html element
197
+ * @returns {object} error page template
198
+ */
199
+ export function simpleHtmlPageTemplate(title, body) {
200
+ return {
201
+ sections: [
202
+ {
203
+ name: "main",
204
+ elements: [
205
+ {
206
+ element: "textElement",
207
+ params: {
208
+ id: "title",
209
+ type: "h1",
210
+ text: title
211
+ }
212
+ },
213
+ {
214
+ element: "htmlElement",
215
+ params: {
216
+ id: "instructions",
217
+ text: body
218
+ }
219
+ }
220
+ ]
221
+ }
222
+ ]
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Generate a page url
228
+ *
229
+ * @param {string} siteId The site id
230
+ * @param {string} pageUrl The page url
231
+ * @param {string} route Whether it comes from the `review` route
232
+ * @returns The page url
233
+ */
234
+ export function constructPageUrl(siteId, pageUrl, route) {
235
+ return `/${siteId}${pageUrl ? `/${pageUrl}` : ""}${route ? `?route=${route}` : ""}`;
236
+ }
237
+
238
+ /**
239
+ * Create an error summary element
240
+ *
241
+ * @param {array} errors The array of errors
242
+ * @returns The error summary element
243
+ */
244
+ export function errorSummary(errors) {
245
+ return {
246
+ element: "errorSummary",
247
+ params: {
248
+ id: "errorSummary",
249
+ errors: errors
250
+ }
251
+ };
252
+ }
253
+
254
+ export function constructErrorSummaryUrl(url) {
255
+ return `${url}#errorSummary-title`;
256
+ }
257
+
258
+ /**
259
+ * Create the user name section
260
+ *
261
+ * @param {string} userName the user name
262
+ * @returns The user name section with the username and logout link
263
+ */
264
+ export function userNameSection(userName) {
265
+ return {
266
+ name: "userName",
267
+ elements: [
268
+ {
269
+ "element": "userName",
270
+ "params": {
271
+ "name":{"en":userName,"el":userName, "tr":userName}
272
+ ,"signOutLink":"/logout"
273
+ }
274
+ }
275
+ ]
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Get the localized content for a given language
281
+ *
282
+ * @param {object} content The contnent object. For example `{"en": "Hello", "el": "Γειά σας"}`
283
+ * @param {string} lang The desired language code. For example `en`, `el`, `tr`
284
+ * @returns {string|undefined} Localized string or empty string if nothing available.
285
+ */
286
+ export function getLocalizeContent(content,lang){
287
+ if (!content || typeof content !== 'object') return "";
288
+
289
+ return content[lang] || content["el"] || content["en"] || content["tr"] || "";
290
+ }
291
+
292
+ /**
293
+ * Get the html for the submission pdf link
294
+ *
295
+ * @param {string} siteId
296
+ * @returns The html for the submission pdf link
297
+ */
298
+ export function getSubmissionPDFLinkHtml (siteId = "") {
299
+ return getMultilingualObject(
300
+ `<p><a class="govcy-d-print-none govcy-d-flex govcy-align-items-center" href="/${siteId}/success/pdf">
301
+ <img alt="" aria-hidden="true" src="/img/Certificate_A4.svg" style="width:30px; margin-right:10px; margin-bottom:0px;aspect-ratio: auto !important;">
302
+ Λήψη αίτησης
303
+ </a></p>`,
304
+ `<p><a class="govcy-d-print-none govcy-d-flex govcy-align-items-center" href="/${siteId}/success/pdf">
305
+ <img alt="" aria-hidden="true" src="/img/Certificate_A4.svg" style="width:30px; margin-right:10px; margin-bottom:0px;aspect-ratio: auto !important;">
306
+ Download application
307
+ </a></p>`,
308
+ `<p><a class="govcy-d-print-none govcy-d-flex govcy-align-items-center" href="/${siteId}/success/pdf">
309
+ <img alt="" aria-hidden="true" src="/img/Certificate_A4.svg" style="width:30px; margin-right:10px; margin-bottom:0px;aspect-ratio: auto !important;">
310
+ Download application
311
+ </a></p>`
312
+ )
313
+ }
314
+
315
+ /**
316
+ * Generate a localized page template listing available services.
317
+ * @param {Array} listOfAvailableSites - Array of site objects with filename and title.
318
+ * @param {string} lang - Language code ('el', 'en', 'tr').
319
+ * @returns {object} Page template object.
320
+ */
321
+ export function availableServicesPageTemplate(listOfAvailableSites, lang = "el") {
322
+ // Supported languages
323
+ const supportedLangs = ["el", "en", "tr"];
324
+ const usedLang = supportedLangs.includes(lang) ? lang : "el";
325
+
326
+ // Localized titles
327
+ const titles = {
328
+ el: "Διαθέσιμες Υπηρεσίες",
329
+ en: "Available Services",
330
+ tr: "Available Services"
331
+ };
332
+
333
+ // Localized intro text
334
+ const intros = {
335
+ el: "<p>Από εδώ μπορείτε να επισκεφτείτε τις πιο κάτω υπηρεσίες:</p>",
336
+ en: "<p>From here you can visit the following services:</p>",
337
+ tr: "<p>From here you can visit the following services:</p>",
338
+ };
339
+
340
+ let siteLinks = "";
341
+ if (Array.isArray(listOfAvailableSites) && listOfAvailableSites.length > 0) {
342
+ siteLinks = `<ul>` + listOfAvailableSites.map(site =>
343
+ `<li><a href="/${site.filename}">${site.title?.[usedLang] || site.filename}</a></li>`
344
+ ).join('') + `</ul>`;
345
+ } else {
346
+ // No services available
347
+ siteLinks = {
348
+ el: `<div class="govcy-warning-text"><span class="govcy-warning-text-icon" aria-hidden="true">!</span><span class="govcy-warning-text-message">Δεν υπάρχουν διαθέσιμες υπηρεσίες αυτή τη στιγμή.</span></div>`,
349
+ en: `<div class="govcy-warning-text"><span class="govcy-warning-text-icon" aria-hidden="true">!</span><span class="govcy-warning-text-message"><p>No services are currently available.</span></div>`,
350
+ tr: `<div class="govcy-warning-text"><span class="govcy-warning-text-icon" aria-hidden="true">!</span><span class="govcy-warning-text-message"><p>Şu anda mevcut hizmet yok.</span></div>`
351
+ }[usedLang];
352
+ }
353
+
354
+ // Localized footer
355
+ const footers = {
356
+ el: `<p>Για περισσότερες υπηρεσίες επισκεφτείτε το <a href="https://gov.cy">gov.cy</a></p>`,
357
+ en: `<p>For more services visit <a href="https://gov.cy">gov.cy</a></p>`,
358
+ tr: `<p>For more services visit <a href="https://gov.cy">gov.cy</a></p>`
359
+ };
360
+
361
+ // Compose the body
362
+ const body = `${intros[lang] || intros.el}
363
+ ${siteLinks}
364
+ ${footers[lang] || footers.el}`;
365
+
366
+ // Use your existing simpleHtmlPageTemplate
367
+ return simpleHtmlPageTemplate(
368
+ { el: titles.el, en: titles.en, tr: titles.tr },
369
+ { el: body, en: body, tr: body }
370
+ );
371
+ }
372
+
373
+
374
+ /**
375
+ * Returns a multilingual object with the text in all languages
376
+ *
377
+ * @param {string} el The Greek text
378
+ * @param {string} en The English text
379
+ * @param {string} tr The Turkish text
380
+ * @returns {object} The multilingual object with the text in all languages
381
+ */
382
+ export function getMultilingualObject(el, en, tr) {
383
+ return {el: el || "", en: en || "", tr: tr || ""};
384
+ }
385
+
386
+ /**
387
+ * Returns a multilingual object with the same text in all languages
388
+ *
389
+ * @param {array} languages The site's language object
390
+ * @param {string} value The value to be set for all languages. If not provided, it will be set to an empty string.
391
+ * @returns {object} The multilingual object with the value set for all languages
392
+ */
393
+ export function getSameMultilingualObject(languages, value) {
394
+ const obj = {};
395
+ for (const lang of languages) {
396
+ obj[lang.code] = value || "";
397
+ }
398
+ return obj;
399
+ }
400
+
401
+ /**
402
+ * Get the email object with the subject, preHeader, header, username and footer in the desired language
403
+ *
404
+ * @param {object} subject The subject object. For example `{"en": "Hello", "el": "Γειά σας"}`
405
+ * @param {object} preHeader The preHeader object. For example `{"en": "Hello", "el": "Γειά σας"}`
406
+ * @param {object} header The header object. For example `{"en": "Hello", "el": "Γειά σας"}`
407
+ * @param {string} username The username. For example `"User1"`
408
+ * @param {array} body The body array.
409
+ * @param {object} footer The footer object. For example `{"en": "Hello", "el": "Γειά σας"}`
410
+ * @param {string} lang The desired language code. For example `en`, `el`, `tr`
411
+ * @returns {object} The email object with the subject, preHeader, header, username and footer in the desired language
412
+ */
413
+ export function getEmailObject( subject, preHeader, header, username, body, footer, lang) {
414
+
415
+ const usedLang = lang || "el";
416
+
417
+ return {
418
+ lang: usedLang,
419
+ subject: getLocalizeContent(subject, usedLang),
420
+ pre: getLocalizeContent(preHeader, usedLang),
421
+ header: {
422
+ serviceName: getLocalizeContent(header, usedLang),
423
+ name: username || ""
424
+ },
425
+ body: body || [],
426
+ footer: {
427
+ footerText: getLocalizeContent(footer, usedLang)
428
+ }
429
+ }
430
+ }
@@ -0,0 +1,7 @@
1
+ import initializeGovCyExpressService from './index.mjs';
2
+
3
+ // Initialize the service
4
+ const service = initializeGovCyExpressService();
5
+
6
+ // Start the server
7
+ service.startServer();
@@ -0,0 +1,114 @@
1
+ import axios from "axios";
2
+ import { logger } from "./govcyLogger.mjs";
3
+
4
+ /**
5
+ * Utility to handle API communication with retry logic
6
+ * @param {string} method - HTTP method (e.g., 'post', 'get', etc.)
7
+ * @param {string} url - API endpoint URL
8
+ * @param {object} inputData - Payload for the request (optional)
9
+ * @param {boolean} useAccessTokenAuth - Whether to use Authorization header with Bearer token
10
+ * @param {object} user - User object containing access_token (optional)
11
+ * @param {object} headers - Custom headers (optional)
12
+ * @param {number} retries - Number of retry attempts (default: 3)
13
+ * @returns {Promise<object>} - API response
14
+ */
15
+ export async function govcyApiRequest(
16
+ method,
17
+ url,
18
+ inputData = {},
19
+ useAccessTokenAuth = false,
20
+ user = null,
21
+ headers = {},
22
+ retries = 3
23
+ ) {
24
+ let attempt = 0;
25
+
26
+ // Clone headers to avoid mutation
27
+ let requestHeaders = { ...headers };
28
+
29
+ // Set authorization header if access token is provided
30
+ if (
31
+ useAccessTokenAuth &&
32
+ typeof user?.access_token === "string" &&
33
+ user.access_token.trim().length > 0
34
+ ) {
35
+ requestHeaders['Authorization'] = `Bearer ${user.access_token}`;
36
+ }
37
+
38
+ while (attempt < retries) {
39
+ try {
40
+ logger.debug(`📤 Sending API request (Attempt ${attempt + 1})`, { method, url, inputData, requestHeaders });
41
+
42
+ const response = await axios({
43
+ method,
44
+ url,
45
+ [method?.toLowerCase() === 'get' ? 'params' : 'data']: inputData,
46
+ headers: requestHeaders,
47
+ timeout: 10000, // 10 seconds timeout
48
+ });
49
+
50
+ logger.debug(`📥 Received API response`, { status: response.status, data: response.data });
51
+
52
+ if (response.status !== 200) {
53
+ throw new Error(`Unexpected HTTP status: ${response.status}`);
54
+ }
55
+
56
+ // const { Succeeded, ErrorCode, ErrorMessage } = response.data;
57
+ // Normalize to PascalCase regardless of input case
58
+ const {
59
+ succeeded,
60
+ errorCode,
61
+ errorMessage,
62
+ data,
63
+ informationMessage,
64
+ Succeeded,
65
+ ErrorCode,
66
+ ErrorMessage,
67
+ Data,
68
+ InformationMessage
69
+ } = response.data;
70
+
71
+ const normalized = {
72
+ Succeeded: Succeeded !== undefined ? Succeeded : succeeded,
73
+ ErrorCode: ErrorCode !== undefined ? ErrorCode : errorCode,
74
+ ErrorMessage: ErrorMessage !== undefined ? ErrorMessage : errorMessage,
75
+ Data: Data !== undefined ? Data : data,
76
+ InformationMessage: InformationMessage !== undefined ? InformationMessage : informationMessage
77
+ };
78
+
79
+ // Merge any extra fields (like ReceivedAuthorization, etc.)
80
+ for (const key of Object.keys(response.data)) {
81
+ if (!(key in normalized)) {
82
+ normalized[key] = response.data[key];
83
+ }
84
+ }
85
+
86
+ // Validate the normalized response structure
87
+ if (typeof normalized.Succeeded !== "boolean") {
88
+ throw new Error("Invalid API response structure: Succeeded must be a boolean");
89
+ }
90
+
91
+ // Check if ErrorCode is a number when Succeeded is false
92
+ if (!normalized.Succeeded && typeof normalized.ErrorCode !== "number") {
93
+ throw new Error("Invalid API response structure: ErrorCode must be a number when Succeeded is false");
94
+ }
95
+
96
+ logger.info(`✅ API call succeeded: ${url}`, response.data);
97
+ return normalized; // Return normalized to pascal case the successful response
98
+ } catch (error) {
99
+ attempt++;
100
+ logger.debug(`🚨 API call failed (Attempt ${attempt})`, {
101
+ message: error.message,
102
+ status: error.response?.status,
103
+ data: error.response?.data,
104
+ });
105
+
106
+ if (attempt >= retries) {
107
+ logger.error(`🚨 API call failed after ${retries} attempts: ${url}`, error.message);
108
+ throw new Error(error.response?.data?.ErrorMessage || "API call failed after retries");
109
+ }
110
+
111
+ logger.info(`🔄 Retrying API request (Attempt ${attempt + 1})...`);
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared constants for allowed form elements.
3
+ */
4
+ export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios", "checkboxes", "datePicker", "dateInput"];