@defra/fcp-sfd-frontend-engine 0.2.12 → 0.4.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.
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
32
  mappers: () => mappers,
33
+ presenters: () => presenters,
33
34
  schemas: () => schemas,
34
35
  utils: () => utils
35
36
  });
@@ -101,15 +102,66 @@ var businessSbiSchema = import_joi.default.object({
101
102
  })
102
103
  });
103
104
 
105
+ // src/schemas/address-schema.js
106
+ var import_joi2 = __toESM(require("joi"), 1);
107
+
108
+ // src/constants/validation-fields.js
109
+ var FIRST_NAME_MAX = 100;
110
+ var LAST_NAME_MAX = 100;
111
+ var MIDDLE_NAMES_MAX = 100;
112
+ var EMAIL_MAX = 254;
113
+ var PHONE_NUMBER_MIN = 10;
114
+ var PHONE_NUMBER_MAX = 50;
115
+ var MAX_AGE_YEARS = 120;
116
+ var ADDRESS_LINE_MAX = 100;
117
+ var TOWN_CITY_MAX = 60;
118
+ var COUNTY_MAX = 60;
119
+ var COUNTRY_MAX = 60;
120
+ var POSTCODE_MAX = 8;
121
+
122
+ // src/schemas/address-schema.js
123
+ var addressSchema = import_joi2.default.object({
124
+ address1: import_joi2.default.string().required().max(ADDRESS_LINE_MAX).messages({
125
+ "string.empty": "Enter address line 1, typically the building and street",
126
+ "string.max": `Address line 1 must be ${ADDRESS_LINE_MAX} characters or less`,
127
+ "any.required": "Enter address line 1, typically the building and street"
128
+ }),
129
+ address2: import_joi2.default.string().allow("").max(ADDRESS_LINE_MAX).messages({
130
+ "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`
131
+ }),
132
+ address3: import_joi2.default.string().allow("").max(ADDRESS_LINE_MAX).messages({
133
+ "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`
134
+ }),
135
+ city: import_joi2.default.string().required().max(TOWN_CITY_MAX).messages({
136
+ "string.empty": "Enter town or city",
137
+ "string.max": `Town or city must be ${TOWN_CITY_MAX} characters or less`,
138
+ "any.required": "Enter town or city"
139
+ }),
140
+ county: import_joi2.default.string().allow("").max(COUNTY_MAX).messages({
141
+ "string.max": `County must be ${COUNTY_MAX} characters or less`
142
+ }),
143
+ postcode: import_joi2.default.string().required().max(POSTCODE_MAX).messages({
144
+ "any.required": "Enter a postal code or zip code",
145
+ "string.empty": "Enter a postal code or zip code",
146
+ "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`
147
+ }),
148
+ country: import_joi2.default.string().required().max(COUNTRY_MAX).messages({
149
+ "string.empty": "Enter a country",
150
+ "string.max": `Country must be ${COUNTRY_MAX} characters or less`,
151
+ "any.required": "Enter a country"
152
+ })
153
+ });
154
+
104
155
  // src/schemas/business/business-schemas.js
105
156
  var businessSchemas = {
106
- sbi: businessSbiSchema
157
+ sbi: businessSbiSchema,
158
+ address: addressSchema
107
159
  };
108
160
 
109
161
  // src/schemas/customer/customer-crn-schema.js
110
- var import_joi2 = __toESM(require("joi"), 1);
111
- var customerCrnSchema = import_joi2.default.object({
112
- crn: import_joi2.default.string().pattern(/^\d{10}$/).allow("").optional().messages({
162
+ var import_joi3 = __toESM(require("joi"), 1);
163
+ var customerCrnSchema = import_joi3.default.object({
164
+ crn: import_joi3.default.string().pattern(/^\d{10}$/).allow("").optional().messages({
113
165
  "string.pattern.base": "Enter the full CRN"
114
166
  })
115
167
  });
@@ -119,10 +171,219 @@ var customerSchemas = {
119
171
  crn: customerCrnSchema
120
172
  };
121
173
 
174
+ // src/schemas/personal/personal-name-schema.js
175
+ var import_joi4 = __toESM(require("joi"), 1);
176
+ var personalNameSchema = import_joi4.default.object({
177
+ first: import_joi4.default.string().required().max(FIRST_NAME_MAX).messages({
178
+ "string.empty": "Enter first name",
179
+ "string.max": `First name must be ${FIRST_NAME_MAX} characters or less`,
180
+ "any.required": "Enter first name"
181
+ }),
182
+ last: import_joi4.default.string().required().max(LAST_NAME_MAX).messages({
183
+ "string.empty": "Enter last name",
184
+ "string.max": `Last name must be ${LAST_NAME_MAX} characters or less`,
185
+ "any.required": "Enter last name"
186
+ }),
187
+ middle: import_joi4.default.string().allow("").max(MIDDLE_NAMES_MAX).messages({
188
+ "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`
189
+ })
190
+ });
191
+
192
+ // src/schemas/personal/personal-dob-schema.js
193
+ var import_joi5 = __toESM(require("joi"), 1);
194
+
195
+ // src/constants/patterns.js
196
+ var PHONE_NUMBER_PATTERN = /^\+?[0-9 ()]+$/;
197
+
198
+ // src/constants/month-map.js
199
+ var MONTH_MAP = {
200
+ january: 1,
201
+ jan: 1,
202
+ february: 2,
203
+ feb: 2,
204
+ march: 3,
205
+ mar: 3,
206
+ april: 4,
207
+ apr: 4,
208
+ may: 5,
209
+ june: 6,
210
+ jun: 6,
211
+ july: 7,
212
+ jul: 7,
213
+ august: 8,
214
+ aug: 8,
215
+ september: 9,
216
+ sep: 9,
217
+ sept: 9,
218
+ october: 10,
219
+ oct: 10,
220
+ november: 11,
221
+ nov: 11,
222
+ december: 12,
223
+ dec: 12
224
+ };
225
+
226
+ // src/schemas/personal/personal-dob-schema.js
227
+ var personalDobSchema = import_joi5.default.object({
228
+ day: import_joi5.default.string().allow(""),
229
+ month: import_joi5.default.string().allow(""),
230
+ year: import_joi5.default.string().allow("")
231
+ }).custom((value, helpers) => {
232
+ const { day, month, year } = value;
233
+ const missingFieldsError = checkMissingFields(day, month, year, helpers);
234
+ if (missingFieldsError) {
235
+ return missingFieldsError;
236
+ }
237
+ if (year && year.length !== 4) {
238
+ return makeError(helpers, "dob.yearLength", ["year"]);
239
+ }
240
+ const monthValue = getMonthNumber(month, helpers);
241
+ if (monthValue.isJoiError) {
242
+ return monthValue;
243
+ }
244
+ const fullDate = getFullDate(day, monthValue, year, helpers);
245
+ if (fullDate.isJoiError) {
246
+ return fullDate;
247
+ }
248
+ if (fullDate > /* @__PURE__ */ new Date()) {
249
+ return makeError(helpers, "dob.future", ["day", "month", "year"]);
250
+ }
251
+ const tooOldError = checkNotTooOld(fullDate, helpers);
252
+ if (tooOldError) {
253
+ return tooOldError;
254
+ }
255
+ return value;
256
+ }).messages({
257
+ "dob.missingAll": "Enter your date of birth",
258
+ "dob.missingDay": "Date of birth must include a day",
259
+ "dob.missingMonth": "Date of birth must include a month",
260
+ "dob.missingYear": "Date of birth must include a year",
261
+ "dob.missingDayMonth": "Date of birth must include a day and month",
262
+ "dob.missingDayYear": "Date of birth must include a day and year",
263
+ "dob.missingMonthYear": "Date of birth must include a month and year",
264
+ "dob.yearLength": "Enter a year with 4 numbers, like 1975",
265
+ "dob.invalid": "Date of birth must be a real date",
266
+ "dob.future": "Date of birth must be in the past",
267
+ "dob.tooOld": "Date of birth must be on or after {{#oldest}}"
268
+ });
269
+ var checkNotTooOld = (fullDate, helpers) => {
270
+ const oldestDateAllowed = getOldestAllowedDate();
271
+ if (fullDate < oldestDateAllowed) {
272
+ const oldestDateString = oldestDateAllowed.toLocaleDateString("en-GB", {
273
+ day: "numeric",
274
+ month: "long",
275
+ year: "numeric"
276
+ });
277
+ const error = helpers.error("dob.tooOld", { oldest: oldestDateString });
278
+ error.path = ["day", "month", "year"];
279
+ return error;
280
+ }
281
+ return null;
282
+ };
283
+ var getOldestAllowedDate = () => {
284
+ const date = /* @__PURE__ */ new Date();
285
+ date.setUTCHours(0, 0, 0, 0);
286
+ date.setUTCFullYear(date.getUTCFullYear() - MAX_AGE_YEARS);
287
+ return date;
288
+ };
289
+ var getFullDate = (day, monthValue, year, helpers) => {
290
+ const dayValue = Number.parseInt(day, 10);
291
+ const yearValue = Number.parseInt(year, 10);
292
+ const date = new Date(Date.UTC(yearValue, monthValue - 1, dayValue));
293
+ if (date.getUTCFullYear() !== yearValue || date.getUTCMonth() + 1 !== monthValue || date.getUTCDate() !== dayValue) {
294
+ return makeError(helpers, "dob.invalid", ["day", "month", "year"]);
295
+ }
296
+ return date;
297
+ };
298
+ var getMonthNumber = (month, helpers) => {
299
+ if (Number.isNaN(Number(month))) {
300
+ const lower = month.toLowerCase();
301
+ const mapped = MONTH_MAP[lower];
302
+ if (!mapped) {
303
+ return makeError(helpers, "dob.invalid", ["month"]);
304
+ }
305
+ return mapped;
306
+ }
307
+ return Number.parseInt(month, 10);
308
+ };
309
+ var checkMissingFields = (day, month, year, helpers) => {
310
+ if (!day && !month && !year) {
311
+ return makeError(helpers, "dob.missingAll", ["day", "month", "year"]);
312
+ }
313
+ if (!day && month && year) {
314
+ return makeError(helpers, "dob.missingDay", ["day"]);
315
+ }
316
+ if (day && !month && year) {
317
+ return makeError(helpers, "dob.missingMonth", ["month"]);
318
+ }
319
+ if (day && month && !year) {
320
+ return makeError(helpers, "dob.missingYear", ["year"]);
321
+ }
322
+ if (!day && !month && year) {
323
+ return makeError(helpers, "dob.missingDayMonth", ["day", "month"]);
324
+ }
325
+ if (!day && month && !year) {
326
+ return makeError(helpers, "dob.missingDayYear", ["day", "year"]);
327
+ }
328
+ if (day && !month && !year) {
329
+ return makeError(helpers, "dob.missingMonthYear", ["month", "year"]);
330
+ }
331
+ return null;
332
+ };
333
+ var makeError = (helpers, code, fields) => {
334
+ const error = helpers.error(code);
335
+ error.path = fields;
336
+ error.isJoiError = true;
337
+ return error;
338
+ };
339
+
340
+ // src/schemas/personal/personal-email-schema.js
341
+ var import_joi6 = __toESM(require("joi"), 1);
342
+ var personalEmailSchema = import_joi6.default.object({
343
+ personalEmail: import_joi6.default.string().required().max(EMAIL_MAX).email({
344
+ minDomainSegments: 2,
345
+ tlds: {
346
+ allow: true,
347
+ min: 2
348
+ }
349
+ }).messages({
350
+ "string.max": `Email address must be ${EMAIL_MAX} characters or less`,
351
+ "string.empty": "Enter a personal email address",
352
+ "string.email": "Enter an email address, like name@example.com"
353
+ })
354
+ });
355
+
356
+ // src/schemas/personal/personal-phone-schema.js
357
+ var import_joi7 = __toESM(require("joi"), 1);
358
+ var personalPhoneSchema = import_joi7.default.object({
359
+ personalTelephone: import_joi7.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
360
+ "string.min": `Personal telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
361
+ "string.max": `Personal telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
362
+ "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
363
+ }),
364
+ personalMobile: import_joi7.default.string().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
365
+ "string.min": `Personal mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
366
+ "string.max": `Personal mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
367
+ "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +"
368
+ })
369
+ }).or("personalTelephone", "personalMobile").messages({
370
+ "object.missing": "Enter at least one phone number"
371
+ });
372
+
373
+ // src/schemas/personal/personal-schemas.js
374
+ var personalSchemas = {
375
+ name: personalNameSchema,
376
+ dob: personalDobSchema,
377
+ address: addressSchema,
378
+ phone: personalPhoneSchema,
379
+ email: personalEmailSchema
380
+ };
381
+
122
382
  // src/schemas/schemas.js
123
383
  var schemas = {
124
384
  business: businessSchemas,
125
- customer: customerSchemas
385
+ customer: customerSchemas,
386
+ personal: personalSchemas
126
387
  };
127
388
 
128
389
  // src/utils/format-validation-errors.js
@@ -155,9 +416,159 @@ var formatValidationErrors = (errors) => {
155
416
  var utils = {
156
417
  formatValidationErrors
157
418
  };
419
+
420
+ // src/presenters/base-presenter.js
421
+ var BACK_LINK_DISPLAY_MAX = 50;
422
+ var formatBackLink = (businessName) => {
423
+ if (businessName.length > BACK_LINK_DISPLAY_MAX) {
424
+ return `Back to ${businessName.slice(0, BACK_LINK_DISPLAY_MAX)}\u2026`;
425
+ }
426
+ return `Back to ${businessName}`;
427
+ };
428
+ var formatNumber = (payloadNumber, changedNumber, originalNumber) => {
429
+ if (payloadNumber !== void 0) {
430
+ return payloadNumber;
431
+ }
432
+ if (changedNumber !== void 0) {
433
+ return changedNumber;
434
+ }
435
+ return originalNumber;
436
+ };
437
+ var formatDisplayAddress = (address) => {
438
+ const { lookup, manual, postcode, country, city } = address;
439
+ let addressLines = [];
440
+ if (lookup.uprn) {
441
+ const buildingAndStreet = [
442
+ lookup.buildingNumberRange,
443
+ lookup.street
444
+ ].filter(Boolean).join(" ");
445
+ addressLines = [
446
+ lookup.pafOrganisationName,
447
+ lookup.flatName,
448
+ lookup.buildingName,
449
+ buildingAndStreet,
450
+ lookup.doubleDependentLocality,
451
+ lookup.dependentLocality,
452
+ city,
453
+ lookup.county
454
+ ];
455
+ } else {
456
+ addressLines = [
457
+ manual.line1,
458
+ manual.line2,
459
+ manual.line3,
460
+ city,
461
+ manual.line4,
462
+ // County
463
+ manual.line5
464
+ ];
465
+ }
466
+ return [
467
+ ...addressLines.filter(Boolean),
468
+ postcode,
469
+ country
470
+ ];
471
+ };
472
+ var buildAddressLine = (parts) => {
473
+ return parts.filter(Boolean).join(", ") || null;
474
+ };
475
+ var buildStreetLine = (buildingRange, street) => {
476
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
477
+ };
478
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
479
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
480
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
481
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
482
+ county: lookup.county ?? null,
483
+ city: city ?? null,
484
+ country: country ?? null,
485
+ postcode: postcode ?? null
486
+ });
487
+ var formatManualAddress = (manual, city, country, postcode) => ({
488
+ address1: manual.line1 ?? null,
489
+ address2: manual.line2 ?? null,
490
+ address3: manual.line3 ?? null,
491
+ city: city ?? null,
492
+ county: manual.line4 ?? null,
493
+ country: country ?? null,
494
+ postcode: postcode ?? null
495
+ });
496
+ var formatOriginalAddress = (originalAddress) => {
497
+ const { lookup, manual, city, country, postcode } = originalAddress;
498
+ return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
499
+ };
500
+ var formatChangedAddress = (changeBusinessAddress) => {
501
+ if (!changeBusinessAddress.uprn) {
502
+ return changeBusinessAddress;
503
+ }
504
+ const {
505
+ pafOrganisationName,
506
+ flatName,
507
+ buildingName,
508
+ buildingNumberRange,
509
+ street,
510
+ doubleDependentLocality,
511
+ dependentLocality,
512
+ city,
513
+ county,
514
+ country,
515
+ postcode
516
+ } = changeBusinessAddress;
517
+ return {
518
+ address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
519
+ address2: buildStreetLine(buildingNumberRange, street),
520
+ address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
521
+ city: city ?? null,
522
+ county: county ?? null,
523
+ country: country ?? null,
524
+ postcode: postcode ?? null
525
+ };
526
+ };
527
+ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
528
+ const displayAddresses = addresses.map((address) => ({
529
+ value: `${address.uprn}${address.displayAddress}`,
530
+ text: address.displayAddress,
531
+ selected: (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.uprn) === address.uprn && (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.displayAddress) === address.displayAddress
532
+ }));
533
+ const hasSelectedAddress = displayAddresses.some((addr) => addr.selected);
534
+ const text = addresses.length === 1 ? "1 address found" : `${addresses.length} addresses found`;
535
+ displayAddresses.unshift({
536
+ value: "display",
537
+ text,
538
+ selected: !hasSelectedAddress
539
+ });
540
+ return displayAddresses;
541
+ };
542
+ var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
543
+ const sortedErrors = [];
544
+ for (const section of orderedSectionsToFix) {
545
+ const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
546
+ for (const field of fieldsInSection) {
547
+ if (errors[field]) {
548
+ sortedErrors.push({
549
+ field,
550
+ ...errors[field]
551
+ });
552
+ }
553
+ }
554
+ }
555
+ return sortedErrors;
556
+ };
557
+
558
+ // src/presenters/presenters.js
559
+ var presenters = {
560
+ formatBackLink,
561
+ formatNumber,
562
+ formatDisplayAddress,
563
+ formatOriginalAddress,
564
+ formatChangedAddress,
565
+ formatDisplayAddresses,
566
+ sortErrorsBySectionOrder
567
+ };
158
568
  // Annotate the CommonJS export names for ESM import in node:
159
569
  0 && (module.exports = {
160
570
  mappers,
571
+ presenters,
161
572
  schemas,
162
573
  utils
163
574
  });