@koloseum/utils 0.2.28 → 0.3.0

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.
@@ -0,0 +1,764 @@
1
+ import { Transform } from "./formatting.js";
2
+ import { v4 as uuidv4 } from "uuid";
3
+ import validator from "validator";
4
+ /* HELPERS */
5
+ const { trim, escape } = validator;
6
+ const { KenyaAdministrativeDivisions } = (await import("kenya-administrative-divisions")).default;
7
+ /* CONFIG HELPERS */
8
+ export const Config = {
9
+ /**
10
+ * A reference of microservices available on the platform, represented as an object with each user group (i.e. `public`, `players`, `lounges`, and `backroom`) having its own array of microservices. Each microservice in a list is an object with the following properties:
11
+ * - `name`: The name of the microservice.
12
+ * - `description`: A description of the microservice.
13
+ * - `slug`: The slug of the microservice.
14
+ * - `features`: An array of features that the microservice has.
15
+ * - `roles`: An array of roles attached to the microservice.
16
+ */
17
+ microservices: {
18
+ backroom: [
19
+ {
20
+ name: "Compliance",
21
+ description: "Management of compliance and regulatory adherence for Players and Lounges.",
22
+ slug: "compliance",
23
+ features: [
24
+ {
25
+ name: "Players",
26
+ description: "Search, review, and manage Player data.",
27
+ slug: "players",
28
+ tabs: [
29
+ {
30
+ name: "Profile",
31
+ description: "Review basic Player data.",
32
+ root: true
33
+ },
34
+ {
35
+ name: "Documents",
36
+ description: "Review Player documents.",
37
+ slug: "documents"
38
+ },
39
+ {
40
+ name: "Data updates",
41
+ description: "Review Player data updates.",
42
+ slug: "data-updates",
43
+ tabs: [
44
+ {
45
+ name: "Requests",
46
+ description: "Review Player data update requests.",
47
+ root: true
48
+ },
49
+ {
50
+ name: "History",
51
+ description: "Review Player data update history.",
52
+ slug: "history"
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ name: "Credits",
58
+ description: "Review Koloseum Credits data for the Player.",
59
+ slug: "credits",
60
+ tabs: [
61
+ {
62
+ name: "Transactions",
63
+ description: "Review Player transactions with Koloseum Credits.",
64
+ root: true
65
+ },
66
+ {
67
+ name: "Transfers",
68
+ description: "Review Player transfers with Koloseum Credits.",
69
+ slug: "transfers"
70
+ },
71
+ {
72
+ name: "Subscriptions",
73
+ description: "Review Player subscriptions with Koloseum Credits.",
74
+ slug: "subscriptions"
75
+ }
76
+ ]
77
+ },
78
+ {
79
+ name: "Misconduct",
80
+ description: "Review Player reports and sanctions.",
81
+ slug: "misconduct",
82
+ tabs: [
83
+ {
84
+ name: "Reports",
85
+ description: "Review Player reports.",
86
+ root: true
87
+ },
88
+ {
89
+ name: "Sanctions",
90
+ description: "Review Player sanctions.",
91
+ slug: "sanctions"
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ name: "Points / KXP",
97
+ description: "Review Koloseum Experience Points (KXP) transactions for the Player.",
98
+ slug: "points"
99
+ }
100
+ ]
101
+ },
102
+ {
103
+ name: "Lounges",
104
+ description: "Search, review, and manage Lounge data.",
105
+ slug: "lounges",
106
+ tabs: [
107
+ {
108
+ name: "Profile",
109
+ description: "Review basic Lounge data.",
110
+ root: true
111
+ },
112
+ {
113
+ name: "Documents",
114
+ description: "Review Lounge documents.",
115
+ slug: "documents"
116
+ },
117
+ {
118
+ name: "Staff",
119
+ description: "Search, review, and manage Lounge staff data.",
120
+ slug: "staff"
121
+ },
122
+ {
123
+ name: "Branches",
124
+ description: "Review Lounge branches.",
125
+ slug: "branches",
126
+ tabs: [
127
+ {
128
+ name: "Profile",
129
+ description: "Review basic Lounge branch data.",
130
+ root: true
131
+ },
132
+ {
133
+ name: "Amenities",
134
+ description: "Review Lounge branch amenities.",
135
+ slug: "amenities"
136
+ },
137
+ {
138
+ name: "Staff",
139
+ description: "Search, review, and manage Lounge branchstaff data.",
140
+ slug: "staff"
141
+ }
142
+ ]
143
+ },
144
+ {
145
+ name: "Data updates",
146
+ description: "Review Lounge data updates.",
147
+ slug: "data-updates",
148
+ tabs: [
149
+ {
150
+ name: "Requests",
151
+ description: "Review Lounge data update requests.",
152
+ root: true
153
+ },
154
+ {
155
+ name: "History",
156
+ description: "Review Lounge data update history.",
157
+ slug: "history"
158
+ }
159
+ ]
160
+ },
161
+ {
162
+ name: "Credits",
163
+ description: "Review Koloseum Credits data for the Lounge.",
164
+ slug: "credits",
165
+ tabs: [
166
+ {
167
+ name: "Transactions",
168
+ description: "Review Lounge transactions with Koloseum Credits.",
169
+ root: true
170
+ },
171
+ {
172
+ name: "Transfers",
173
+ description: "Review Lounge transfers with Koloseum Credits.",
174
+ slug: "transfers"
175
+ },
176
+ {
177
+ name: "Subscriptions",
178
+ description: "Review Lounge subscriptions with Koloseum Credits.",
179
+ slug: "subscriptions"
180
+ }
181
+ ]
182
+ },
183
+ {
184
+ name: "Misconduct",
185
+ description: "Review Lounge reports and sanctions.",
186
+ slug: "misconduct",
187
+ tabs: [
188
+ {
189
+ name: "Reports",
190
+ description: "Review Lounge reports.",
191
+ root: true
192
+ },
193
+ {
194
+ name: "Sanctions",
195
+ description: "Review Lounge sanctions.",
196
+ slug: "sanctions"
197
+ }
198
+ ]
199
+ },
200
+ {
201
+ name: "Points / KXP",
202
+ description: "Review Koloseum Experience Points (KXP) transactions for the Lounge.",
203
+ slug: "points"
204
+ }
205
+ ]
206
+ }
207
+ ],
208
+ roles: [
209
+ {
210
+ name: "Compliance",
211
+ slug: "compliance",
212
+ root: true
213
+ },
214
+ {
215
+ name: "Players",
216
+ slug: "players",
217
+ featureSlugs: ["players"]
218
+ },
219
+ {
220
+ name: "Lounges",
221
+ slug: "lounges",
222
+ featureSlugs: ["lounges"]
223
+ }
224
+ ]
225
+ },
226
+ {
227
+ name: "Competitions",
228
+ description: "Management of competitions data and live events across Markets.",
229
+ slug: "competitions",
230
+ features: [
231
+ {
232
+ name: "Ma Esto",
233
+ description: "Manage football esports data for Ma Esto.",
234
+ slug: "fbl"
235
+ },
236
+ {
237
+ name: "Savanna FGC",
238
+ description: "Manage fighting games esports data for Savanna FGC.",
239
+ slug: "fgc",
240
+ tabs: [
241
+ {
242
+ name: "Savanna Circuit",
243
+ description: "Manage competition data for the Savanna Circuit.",
244
+ slug: "league",
245
+ tabs: [
246
+ {
247
+ name: "Rankings",
248
+ description: "Manage rankings for any Savanna Circuit season.",
249
+ root: true
250
+ },
251
+ {
252
+ name: "Tournaments",
253
+ description: "Manage tournaments for any Savanna Circuit season.",
254
+ slug: "tournaments",
255
+ tabs: [
256
+ {
257
+ name: "Events",
258
+ description: "Manage events for any Savanna Circuit tournament.",
259
+ root: true
260
+ },
261
+ {
262
+ name: "Tickets",
263
+ description: "Manage tickets for any Savanna Circuit tournament.",
264
+ slug: "tickets"
265
+ },
266
+ {
267
+ name: "Settings",
268
+ description: "Manage settings for any Savanna Circuit tournament.",
269
+ slug: "settings"
270
+ }
271
+ ]
272
+ },
273
+ {
274
+ name: "Settings",
275
+ description: "Manage settings for any Savanna Circuit season.",
276
+ slug: "settings"
277
+ }
278
+ ]
279
+ },
280
+ {
281
+ name: "Savanna Fight Night",
282
+ description: "Manage competition data for Savanna Fight Night.",
283
+ slug: "challenges"
284
+ }
285
+ ]
286
+ },
287
+ {
288
+ name: "Hit List",
289
+ description: "Manage battle royale esports data for Hit List.",
290
+ slug: "bryl"
291
+ }
292
+ ],
293
+ roles: [
294
+ {
295
+ name: "Competitions",
296
+ slug: "competitions",
297
+ root: true
298
+ },
299
+ {
300
+ name: "Ma Esto",
301
+ slug: "fbl",
302
+ featureSlugs: ["fbl"]
303
+ },
304
+ {
305
+ name: "Savanna FGC",
306
+ slug: "fgc",
307
+ featureSlugs: ["fgc"]
308
+ },
309
+ {
310
+ name: "Hit List",
311
+ slug: "bryl",
312
+ featureSlugs: ["bryl"]
313
+ }
314
+ ]
315
+ },
316
+ {
317
+ name: "KLSM",
318
+ description: "Management of game and console trades between users.",
319
+ slug: "commerce",
320
+ features: [],
321
+ roles: [
322
+ {
323
+ name: "Commerce",
324
+ slug: "commerce",
325
+ root: true
326
+ }
327
+ ]
328
+ },
329
+ {
330
+ name: "Staff",
331
+ description: "Management of Backroom staff authorisation.",
332
+ slug: "staff",
333
+ features: [
334
+ {
335
+ name: "Roles",
336
+ description: "Review and manage Backroom roles.",
337
+ slug: "roles"
338
+ },
339
+ {
340
+ name: "Users",
341
+ description: "Review and manage Backroom users.",
342
+ slug: "users"
343
+ }
344
+ ],
345
+ roles: [
346
+ {
347
+ name: "Human Resources",
348
+ slug: "hr",
349
+ root: true
350
+ }
351
+ ]
352
+ }
353
+ ],
354
+ players: [
355
+ {
356
+ name: "Sessions",
357
+ description: "Track your gaming sessions at registered lounges.",
358
+ slug: "sessions",
359
+ features: [],
360
+ roles: null
361
+ },
362
+ {
363
+ name: "KLSM",
364
+ description: "Trade used games and consoles with other users at affordable prices.",
365
+ slug: "commerce",
366
+ features: [],
367
+ roles: null
368
+ },
369
+ {
370
+ name: "Savanna FGC",
371
+ description: "Check out the latest news and competitions from Savanna FGC.",
372
+ slug: "fgc",
373
+ features: [
374
+ {
375
+ name: "Home",
376
+ description: "Learn more about the Savanna Circuit.",
377
+ root: true
378
+ },
379
+ {
380
+ name: "Rules",
381
+ description: "Review the rules and regulations of the Savanna Circuit.",
382
+ slug: "rules"
383
+ },
384
+ {
385
+ name: "Tournaments",
386
+ description: "Review and join upcoming tournaments on the Savanna Circuit.",
387
+ slug: "tournaments"
388
+ }
389
+ ],
390
+ roles: null
391
+ },
392
+ {
393
+ name: "Account",
394
+ description: "Review Player account settings and preferences.",
395
+ slug: "account",
396
+ features: [
397
+ {
398
+ name: "General",
399
+ description: "Review and manage your general settings.",
400
+ slug: "general"
401
+ },
402
+ {
403
+ name: "Documents",
404
+ description: "Review and manage your ID documents.",
405
+ slug: "documents"
406
+ },
407
+ {
408
+ name: "Personal",
409
+ description: "Review and manage your personal information.",
410
+ slug: "personal"
411
+ },
412
+ {
413
+ name: "Gaming & Socials",
414
+ description: "Review and manage your gaming and social media information.",
415
+ slug: "gaming-socials"
416
+ },
417
+ {
418
+ name: "Notifications",
419
+ description: "Review and manage your notification preferences.",
420
+ slug: "notifications"
421
+ },
422
+ {
423
+ name: "Security",
424
+ description: "Review and manage your security preferences.",
425
+ slug: "security"
426
+ },
427
+ {
428
+ name: "Lounges",
429
+ description: "Review your access to Lounges microservices.",
430
+ slug: "lounges"
431
+ },
432
+ {
433
+ name: "Backroom",
434
+ description: "Review your access to Backroom microservices.",
435
+ slug: "backroom"
436
+ }
437
+ ],
438
+ roles: null
439
+ }
440
+ ]
441
+ }
442
+ };
443
+ /* DUMMY DATA */
444
+ export const Mock = {
445
+ /**
446
+ * A generic authenticated user.
447
+ * @param {string} id - The user ID; defaults to a random UUID
448
+ * @param {Date} date - The date and time; defaults to the current date and time
449
+ * @param {string} phone - The phone number; defaults to a generic Kenyan phone number
450
+ * @param {string} identityId - A default identity ID; defaults to a random UUID
451
+ * @returns A generic authenticated user.
452
+ */
453
+ authenticatedUser: (id = uuidv4(), date = new Date(), phone = "254111222333", identityId = uuidv4()) => {
454
+ // Convert date to ISO string
455
+ const time = date.toISOString();
456
+ // User data
457
+ return {
458
+ id,
459
+ aud: "authenticated",
460
+ role: "authenticated",
461
+ email: "",
462
+ phone,
463
+ phone_confirmed_at: time,
464
+ confirmation_sent_at: time,
465
+ confirmed_at: time,
466
+ last_sign_in_at: time,
467
+ app_metadata: {
468
+ person_data: { player_id: "KP1234567", first_name: "John", last_name: "Test", pseudonym: "JDtest" },
469
+ provider: "phone",
470
+ providers: ["phone"],
471
+ roles: ["player", "backroom_superuser"]
472
+ },
473
+ user_metadata: {
474
+ email_verified: false,
475
+ phone_verified: false,
476
+ sub: id,
477
+ backroom: {
478
+ welcome_notification_sent: true
479
+ },
480
+ players: {
481
+ welcome_notification_sent: true
482
+ }
483
+ },
484
+ identities: [
485
+ {
486
+ identity_id: identityId,
487
+ id,
488
+ user_id: id,
489
+ identity_data: {
490
+ email_verified: false,
491
+ phone_verified: false,
492
+ sub: id
493
+ },
494
+ provider: "phone",
495
+ last_sign_in_at: time,
496
+ created_at: time,
497
+ updated_at: time
498
+ }
499
+ ],
500
+ created_at: time,
501
+ updated_at: time,
502
+ is_anonymous: false
503
+ };
504
+ }
505
+ };
506
+ /* RESOURCE HELPERS */
507
+ export const Resource = {
508
+ /**
509
+ * Returns a list of all counties in Kenya.
510
+ * @param {"name" | "code"} sortBy - The field to sort the counties by, i.e. `name` or `code`; defaults to `name`
511
+ * @returns {Promise<County[]>} A list of objects with the county `name`, `code`, and a list of `subCounties`
512
+ */
513
+ getKenyaCounties: async (sortBy = "name") => {
514
+ // Create Kenya administrative divisions instance
515
+ const kenyaAdmin = new KenyaAdministrativeDivisions();
516
+ // Get all counties
517
+ const countiesData = await kenyaAdmin.getAll();
518
+ // Format counties
519
+ const counties = countiesData.map((county) => {
520
+ // Get list of sub-counties
521
+ const subCounties = [];
522
+ for (const subCounty of county.constituencies)
523
+ subCounties.push(subCounty.constituency_name);
524
+ subCounties.sort();
525
+ // Format county name and add to list
526
+ const countyName = county.county_name.split(" ");
527
+ let name = county.county_name;
528
+ if (countyName.length > 1)
529
+ name = countyName.map((word) => Transform.capitalise(word)).join(" ");
530
+ // Return county
531
+ return { name, code: county.county_code, subCounties };
532
+ });
533
+ // Return sorted counties
534
+ return counties.sort((a, b) => (sortBy === "name" ? a.name.localeCompare(b.name) : a.code - b.code));
535
+ },
536
+ /**
537
+ * Returns the parent URL for a given base URL.
538
+ * @param {string} base - The base URL
539
+ * @returns {string} The parent URL
540
+ */
541
+ getParentUrl: (base) => {
542
+ // Validate input
543
+ if (typeof base !== "string")
544
+ return "";
545
+ // Return parent URL
546
+ return base.replace(/\/$/, "").split("/").slice(0, -1).join("/") || "/";
547
+ },
548
+ /**
549
+ * Returns the redirect URL for a given URI.
550
+ * @param {string} uri - The URI to get the redirect URL for, i.e. `microserviceGroup:path` (e.g. `players:fgc/tournaments`)
551
+ * @param {"development" | "production"} env - The environment to use for the redirect URL; defaults to `production`
552
+ * @returns {string} An object with the redirect `url`, or an `error` if any occurs
553
+ */
554
+ getRedirectUrl: (uri, env = "production") => {
555
+ // Get microservice groups
556
+ const microserviceGroups = ["public", "players", "lounges", "backroom"];
557
+ // Extract microservice group and path from URI
558
+ let [microserviceGroup, path] = uri.split(":");
559
+ if (!microserviceGroup || !path)
560
+ return { error: { code: 400, message: "URI is invalid." } };
561
+ if (!microserviceGroups.includes(microserviceGroup))
562
+ return { error: { code: 400, message: "Microservice group is invalid." } };
563
+ // Initialise port number for local development
564
+ let port;
565
+ // Return redirect URL for Public microservices
566
+ if (microserviceGroup === "public") {
567
+ // Initialise subdomain for production
568
+ let subdomain = "";
569
+ // Handle Authentication microservice
570
+ if (path.startsWith("auth")) {
571
+ subdomain = "auth.";
572
+ port = 5173;
573
+ path = path.replace("auth", "");
574
+ }
575
+ // Handle Legal microservice
576
+ else if (path.startsWith("legal")) {
577
+ subdomain = "legal.";
578
+ port = 5174;
579
+ path = path.replace("legal", "");
580
+ }
581
+ // Handle Landing microservice
582
+ else if (path.startsWith("landing")) {
583
+ port = 5184;
584
+ path = path.replace("landing", "");
585
+ }
586
+ // Return redirect URL
587
+ return {
588
+ url: env === "production"
589
+ ? `https://${subdomain}koloseum.ke${path || ""}`
590
+ : `http://127.0.0.1:${port}${path || ""}`
591
+ };
592
+ }
593
+ // Handle Players microservices
594
+ if (microserviceGroup === "players") {
595
+ if (path.startsWith("account")) {
596
+ port = 5175;
597
+ if (env === "development")
598
+ path = path.replace("account", "");
599
+ }
600
+ if (path.startsWith("fgc")) {
601
+ port = 5178;
602
+ if (env === "development")
603
+ path = path.replace("fgc", "");
604
+ }
605
+ if (path.startsWith("commerce")) {
606
+ port = 5179;
607
+ if (env === "development")
608
+ path = path.replace("commerce", "");
609
+ }
610
+ }
611
+ // Handle Lounges microservices
612
+ if (microserviceGroup === "lounges") {
613
+ if (path.startsWith("branches")) {
614
+ port = 5181;
615
+ if (env === "development")
616
+ path = path.replace("branches", "");
617
+ }
618
+ if (path.startsWith("staff")) {
619
+ port = 5182;
620
+ if (env === "development")
621
+ path = path.replace("staff", "");
622
+ }
623
+ }
624
+ // Handle Backroom microservices
625
+ if (microserviceGroup === "backroom") {
626
+ if (path.startsWith("compliance")) {
627
+ port = 5176;
628
+ if (env === "development")
629
+ path = path.replace("compliance", "");
630
+ }
631
+ if (path.startsWith("competitions")) {
632
+ port = 5177;
633
+ if (env === "development")
634
+ path = path.replace("competitions", "");
635
+ }
636
+ if (path.startsWith("commerce")) {
637
+ port = 5180;
638
+ if (env === "development")
639
+ path = path.replace("commerce", "");
640
+ }
641
+ if (path.startsWith("staff")) {
642
+ port = 5183;
643
+ if (env === "development")
644
+ path = path.replace("staff", "");
645
+ }
646
+ }
647
+ // Return redirect URL
648
+ return {
649
+ url: env === "production"
650
+ ? `https://${microserviceGroup}.koloseum.ke/${path || ""}`
651
+ : `http://127.0.0.1:${port}${path || ""}`
652
+ };
653
+ },
654
+ /**
655
+ * Parses a resource request and returns the URL and path.
656
+ * @param request - The request to parse.
657
+ * @returns An object with the `url` and `path` of the request.
658
+ */
659
+ parseResourceRequest: (request) => {
660
+ // Get the request URL and path
661
+ const url = new URL(request.url);
662
+ const path = url.pathname.split("/").pop();
663
+ // Return the request URL and path
664
+ return { url, path };
665
+ },
666
+ /**
667
+ * Validates address data submitted in a form and returns the validated data.
668
+ * @param {FormData} formData - The submitted form data
669
+ * @returns An object with the validated `address`, or an `error` if any has occurred
670
+ */
671
+ validateAddress: async (formData) => {
672
+ // Create Kenya administrative divisions instance
673
+ const kenyaAdmin = new KenyaAdministrativeDivisions();
674
+ // Building name
675
+ let buildingName = formData.get("building-name");
676
+ if (!buildingName)
677
+ return { error: { code: 400, message: "Building name is required." } };
678
+ buildingName = Transform.sanitiseHtml(trim(escape(buildingName)));
679
+ // Unit number
680
+ let unitNumber = formData.get("unit-number") || undefined;
681
+ if (unitNumber) {
682
+ unitNumber = Transform.sanitiseHtml(trim(escape(unitNumber)));
683
+ if (unitNumber === "")
684
+ unitNumber = undefined;
685
+ }
686
+ // Street name
687
+ let streetName = formData.get("street-name");
688
+ if (!streetName)
689
+ return { error: { code: 400, message: "Street name is required." } };
690
+ streetName = Transform.sanitiseHtml(trim(escape(streetName)));
691
+ // P.O. box and postal code
692
+ let boxNumber = formData.get("box-number") || undefined;
693
+ let postalCode = formData.get("postal-code") || undefined;
694
+ if (boxNumber) {
695
+ boxNumber = Transform.sanitiseHtml(trim(escape(boxNumber)));
696
+ if (boxNumber === "")
697
+ boxNumber = undefined;
698
+ if (boxNumber && !postalCode)
699
+ return { error: { code: 400, message: "Postal code is required." } };
700
+ }
701
+ if (postalCode) {
702
+ postalCode = Transform.sanitiseHtml(trim(escape(postalCode)));
703
+ if (postalCode === "")
704
+ postalCode = undefined;
705
+ if (postalCode && !boxNumber)
706
+ return { error: { code: 400, message: "P.O. box is required." } };
707
+ }
708
+ // Town
709
+ let town = formData.get("town");
710
+ if (!town)
711
+ return { error: { code: 400, message: "City/town is required." } };
712
+ town = Transform.sanitiseHtml(trim(escape(town)));
713
+ // County
714
+ let county = undefined;
715
+ const countyCode = Number(formData.get("county-code"));
716
+ if (isNaN(countyCode))
717
+ return { error: { code: 400, message: "County is invalid." } };
718
+ if (!countyCode)
719
+ return { error: { code: 400, message: "County is required." } };
720
+ const counties = await kenyaAdmin.getAll();
721
+ if (!counties || counties.length === 0)
722
+ return { error: { code: 400, message: "County is invalid." } };
723
+ const countyData = counties.find(({ county_code }) => county_code === countyCode);
724
+ if (!countyData)
725
+ return { error: { code: 400, message: "County is invalid." } };
726
+ const { county_name, constituencies } = countyData;
727
+ county = county_name;
728
+ // Sub-county
729
+ let subCounty = formData.get("sub-county") || undefined;
730
+ if (subCounty) {
731
+ subCounty = Transform.sanitiseHtml(trim(escape(subCounty)));
732
+ if (subCounty === "")
733
+ subCounty = undefined;
734
+ else {
735
+ // Check if constituencies exists and is an array before mapping
736
+ if (!constituencies || !Array.isArray(constituencies))
737
+ return { error: { code: 400, message: "Sub-county data is unavailable." } };
738
+ // Get list of sub-counties
739
+ const subCounties = constituencies.map(({ constituency_name: name }) => name);
740
+ if (!subCounties.includes(subCounty))
741
+ return { error: { code: 400, message: "Sub-county is invalid." } };
742
+ }
743
+ }
744
+ // Additional information
745
+ let additionalInfo = formData.get("address-additional-info") || undefined;
746
+ if (additionalInfo) {
747
+ additionalInfo = Transform.sanitiseHtml(trim(escape(additionalInfo)));
748
+ if (additionalInfo === "")
749
+ additionalInfo = undefined;
750
+ }
751
+ // Create address object
752
+ const address = {
753
+ buildingName,
754
+ unitNumber,
755
+ streetName,
756
+ town,
757
+ subCounty,
758
+ county,
759
+ additionalInfo
760
+ };
761
+ // Return data
762
+ return { address };
763
+ }
764
+ };