@defra/fcp-sfd-frontend-engine 0.17.0 → 0.19.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
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  mutations: () => mutations,
35
35
  presenters: () => presenters,
36
36
  schemas: () => schemas,
37
+ services: () => services,
37
38
  utils: () => utils
38
39
  });
39
40
  module.exports = __toCommonJS(index_exports);
@@ -224,706 +225,805 @@ var mapBusinessDetails = (value) => {
224
225
  };
225
226
  };
226
227
 
227
- // src/mappers/mappers.js
228
- var mappers = {
229
- personalBusinessDetails: mapPersonalBusinessDetails,
230
- address: mapAddress,
231
- customerName: mapCustomerName,
232
- businessDetails: mapBusinessDetails
233
- };
234
-
235
- // src/mutations/business/update-business-email.js
236
- var updateBusinessEmailMutation = `
237
- mutation Mutation($input: UpdateBusinessEmailInput!) {
238
- updateBusinessEmail(input: $input) {
239
- business {
240
- info {
241
- email {
242
- address
243
- }
244
- }
245
- }
246
- success
228
+ // src/utils/joi.js
229
+ var import_joi = __toESM(require("joi"), 1);
230
+ var Joi = import_joi.default.extend((joi) => ({
231
+ type: "string",
232
+ base: joi.string(),
233
+ messages: {
234
+ "string.noControlChars": "Field must not contain invalid characters"
235
+ },
236
+ validate(value, helpers) {
237
+ if (typeof value === "string" && !NO_CONTROL_CHARS_PATTERN.test(value)) {
238
+ return { value, errors: helpers.error("string.noControlChars") };
247
239
  }
240
+ return { value };
248
241
  }
249
- `;
242
+ }));
250
243
 
251
- // src/mutations/business/update-business-name.js
252
- var updateBusinessNameMutation = `
253
- mutation UpdateBusinessName($input: UpdateBusinessNameInput!) {
254
- updateBusinessName(input: $input) {
255
- business {
256
- info {
257
- name
258
- }
259
- }
260
- success
261
- }
262
- }
263
- `;
244
+ // src/schemas/business/business-sbi-schema.js
245
+ var businessSbiSchema = Joi.object({
246
+ sbi: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
247
+ "string.pattern.base": "Enter the full SBI",
248
+ "string.noControlChars": "SBI must not contain invalid characters"
249
+ })
250
+ });
264
251
 
265
- // src/mutations/personal/update-customer-name.js
266
- var updateCustomerNameMutation = `
267
- mutation UpdateCustomerName($input: UpdateCustomerNameInput!) {
268
- updateCustomerName(input: $input) {
269
- customer {
270
- info {
271
- name {
272
- first
273
- last
274
- middle
275
- }
276
- }
277
- }
278
- }
279
- }
280
- `;
252
+ // src/schemas/shared/address-schema.js
253
+ var addressSchema = Joi.object({
254
+ address1: Joi.string().trim().required().max(ADDRESS_LINE_MAX).messages({
255
+ "string.empty": "Enter address line 1, typically the building and street",
256
+ "string.max": `Address line 1 must be ${ADDRESS_LINE_MAX} characters or less`,
257
+ "any.required": "Enter address line 1, typically the building and street",
258
+ "string.noControlChars": "Address line 1 must not contain invalid characters"
259
+ }),
260
+ address2: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
261
+ "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`,
262
+ "string.noControlChars": "Address line 2 must not contain invalid characters"
263
+ }),
264
+ address3: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
265
+ "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`,
266
+ "string.noControlChars": "Address line 3 must not contain invalid characters"
267
+ }),
268
+ city: Joi.string().trim().required().max(TOWN_CITY_MAX).messages({
269
+ "string.empty": "Enter town or city",
270
+ "string.max": `Town or city must be ${TOWN_CITY_MAX} characters or less`,
271
+ "any.required": "Enter town or city",
272
+ "string.noControlChars": "Town or city must not contain invalid characters"
273
+ }),
274
+ county: Joi.string().trim().allow("").max(COUNTY_MAX).messages({
275
+ "string.max": `County must be ${COUNTY_MAX} characters or less`,
276
+ "string.noControlChars": "County must not contain invalid characters"
277
+ }),
278
+ postcode: Joi.string().trim().required().max(POSTCODE_MAX).messages({
279
+ "any.required": "Enter a postal code or zip code",
280
+ "string.empty": "Enter a postal code or zip code",
281
+ "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`,
282
+ "string.noControlChars": "Postal code or zip code must not contain invalid characters"
283
+ }),
284
+ country: Joi.string().trim().required().max(COUNTRY_MAX).messages({
285
+ "string.empty": "Enter a country",
286
+ "string.max": `Country must be ${COUNTRY_MAX} characters or less`,
287
+ "any.required": "Enter a country",
288
+ "string.noControlChars": "Country must not contain invalid characters"
289
+ })
290
+ });
281
291
 
282
- // src/mutations/personal/update-customer-email.js
283
- var updateCustomerEmailMutation = `
284
- mutation UpdateCustomerEmail($input: UpdateCustomerEmailInput!) {
285
- updateCustomerEmail(input: $input) {
286
- customer {
287
- info {
288
- email {
289
- address
290
- }
291
- }
292
- }
293
- }
294
- }
295
- `;
292
+ // src/schemas/business/business-name-schema.js
293
+ var businessNameSchema = Joi.object({
294
+ businessName: Joi.string().trim().required().max(BUSINESS_NAME_MAX).messages({
295
+ "string.empty": "Enter business name",
296
+ "string.max": `Business name must be ${BUSINESS_NAME_MAX} characters or less`,
297
+ "any.required": "Enter business name",
298
+ "string.noControlChars": "Business name must not contain invalid characters"
299
+ })
300
+ });
296
301
 
297
- // src/mutations/personal/update-customer-phone.js
298
- var updateCustomerPhoneMutation = `
299
- mutation UpdateCustomerPhone($input: UpdateCustomerPhoneInput!) {
300
- updateCustomerPhone(input: $input) {
301
- customer {
302
- info {
303
- phone {
304
- landline
305
- mobile
306
- }
307
- }
308
- }
302
+ // src/schemas/business/business-email-schema.js
303
+ var businessEmailSchema = Joi.object({
304
+ businessEmail: Joi.string().required().max(EMAIL_MAX).email({
305
+ minDomainSegments: 2,
306
+ tlds: {
307
+ allow: true
309
308
  }
309
+ }).messages({
310
+ "string.max": `Business email address must be ${EMAIL_MAX} characters or less`,
311
+ "string.empty": "Enter business email address",
312
+ "string.email": "Enter an email address, like name@example.com",
313
+ "string.noControlChars": "Business email address must not contain invalid characters"
314
+ })
315
+ });
316
+
317
+ // src/schemas/business/business-phone-schema.js
318
+ var businessPhoneSchema = Joi.object({
319
+ businessTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
320
+ "string.min": `Business telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
321
+ "string.max": `Business telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
322
+ "string.pattern.base": "Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
323
+ "string.noControlChars": "Business telephone number must not contain invalid characters"
324
+ }),
325
+ businessMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
326
+ "string.min": `Business mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
327
+ "string.max": `Business mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
328
+ "string.pattern.base": "Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
329
+ "string.noControlChars": "Business mobile phone number must not contain invalid characters"
330
+ })
331
+ }).or("businessTelephone", "businessMobile").messages({
332
+ "object.missing": "Enter at least one phone number"
333
+ });
334
+
335
+ // src/schemas/business/business-vat-schema.js
336
+ var businessVatSchema = Joi.object({
337
+ vatNumber: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
338
+ "string.pattern.base": "Enter a VAT registration number, like 123456789",
339
+ "string.noControlChars": "VAT registration number must not contain invalid characters"
340
+ })
341
+ });
342
+
343
+ // src/schemas/business/business-vat-change-schema.js
344
+ var businessVatChangeSchema = Joi.object({
345
+ vatNumber: Joi.string().pattern(/^\d{9}$/).required().messages({
346
+ "string.pattern.base": "Enter a VAT registration number, like 123456789",
347
+ "string.empty": "Enter a VAT registration number",
348
+ "any.required": "Enter a VAT registration number",
349
+ "string.noControlChars": "VAT registration number must not contain invalid characters"
350
+ })
351
+ });
352
+
353
+ // src/schemas/business/business-vat-remove-schema.js
354
+ var businessVatRemoveSchema = Joi.object({
355
+ confirmRemove: Joi.string().valid("yes", "no").required().messages({
356
+ "any.required": "Select yes if you want to remove your VAT registration number",
357
+ "any.only": "Select yes if you want to remove your VAT registration number"
358
+ })
359
+ });
360
+
361
+ // src/schemas/business/business-schemas.js
362
+ var businessSchemas = {
363
+ sbi: businessSbiSchema,
364
+ details: {
365
+ name: businessNameSchema,
366
+ address: addressSchema,
367
+ phone: businessPhoneSchema,
368
+ email: businessEmailSchema,
369
+ vat: businessVatSchema
370
+ },
371
+ vat: {
372
+ change: businessVatChangeSchema,
373
+ remove: businessVatRemoveSchema
310
374
  }
311
- `;
375
+ };
312
376
 
313
- // src/mutations/mutations.js
314
- var mutations = {
315
- updateBusinessEmail: updateBusinessEmailMutation,
316
- updateBusinessName: updateBusinessNameMutation,
317
- updateCustomerName: updateCustomerNameMutation,
318
- updateCustomerEmail: updateCustomerEmailMutation,
319
- updateCustomerPhone: updateCustomerPhoneMutation
377
+ // src/schemas/customer/customer-crn-schema.js
378
+ var customerCrnSchema = Joi.object({
379
+ crn: Joi.string().pattern(/^\d{10}$/).allow("").optional().messages({
380
+ "string.pattern.base": "Enter the full CRN",
381
+ "string.noControlChars": "CRN must not contain invalid characters"
382
+ })
383
+ });
384
+
385
+ // src/schemas/customer/customer-schemas.js
386
+ var customerSchemas = {
387
+ crn: customerCrnSchema
320
388
  };
321
389
 
322
- // src/presenters/base-presenter.js
323
- var BACK_LINK_DISPLAY_MAX = 50;
324
- var formatBackLink = (businessName) => {
325
- if (businessName.length > BACK_LINK_DISPLAY_MAX) {
326
- return `Back to ${businessName.slice(0, BACK_LINK_DISPLAY_MAX)}\u2026`;
390
+ // src/schemas/personal/personal-name-schema.js
391
+ var personalNameSchema = Joi.object({
392
+ first: Joi.string().trim().required().max(FIRST_NAME_MAX).messages({
393
+ "string.empty": "Enter first name",
394
+ "string.max": `First name must be ${FIRST_NAME_MAX} characters or less`,
395
+ "any.required": "Enter first name",
396
+ "string.noControlChars": "First name must not contain invalid characters"
397
+ }),
398
+ last: Joi.string().trim().required().max(LAST_NAME_MAX).messages({
399
+ "string.empty": "Enter last name",
400
+ "string.max": `Last name must be ${LAST_NAME_MAX} characters or less`,
401
+ "any.required": "Enter last name",
402
+ "string.noControlChars": "Last name must not contain invalid characters"
403
+ }),
404
+ middle: Joi.string().trim().allow("").max(MIDDLE_NAMES_MAX).messages({
405
+ "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`,
406
+ "string.noControlChars": "Middle names must not contain invalid characters"
407
+ })
408
+ });
409
+
410
+ // src/schemas/personal/personal-dob-schema.js
411
+ var personalDobSchema = Joi.object({
412
+ day: Joi.string().allow(""),
413
+ month: Joi.string().allow(""),
414
+ year: Joi.string().allow("")
415
+ }).custom((value, helpers) => {
416
+ const { day, month, year } = value;
417
+ const missingFieldsError = checkMissingFields(day, month, year, helpers);
418
+ if (missingFieldsError) {
419
+ return missingFieldsError;
420
+ }
421
+ if (year && year.length !== 4) {
422
+ return makeError(helpers, "dob.yearLength", ["year"]);
423
+ }
424
+ const monthValue = getMonthNumber(month, helpers);
425
+ if (monthValue.isJoiError) {
426
+ return monthValue;
427
+ }
428
+ const fullDate = getFullDate(day, monthValue, year, helpers);
429
+ if (fullDate.isJoiError) {
430
+ return fullDate;
431
+ }
432
+ const today = /* @__PURE__ */ new Date();
433
+ today.setUTCHours(0, 0, 0, 0);
434
+ if (fullDate >= today) {
435
+ return makeError(helpers, "dob.future", ["day", "month", "year"]);
436
+ }
437
+ const tooOldError = checkNotTooOld(fullDate, helpers);
438
+ if (tooOldError) {
439
+ return tooOldError;
440
+ }
441
+ return value;
442
+ }).messages({
443
+ "dob.missingAll": "Enter your date of birth",
444
+ "dob.missingDay": "Date of birth must include a day",
445
+ "dob.missingMonth": "Date of birth must include a month",
446
+ "dob.missingYear": "Date of birth must include a year",
447
+ "dob.missingDayMonth": "Date of birth must include a day and month",
448
+ "dob.missingDayYear": "Date of birth must include a day and year",
449
+ "dob.missingMonthYear": "Date of birth must include a month and year",
450
+ "dob.yearLength": "Enter a year with 4 numbers, like 1975",
451
+ "dob.invalid": "Date of birth must be a real date",
452
+ "dob.future": "Date of birth must be in the past",
453
+ "dob.tooOld": "Date of birth must be on or after {{#oldest}}",
454
+ "string.noControlChars": "Date of birth must not contain invalid characters"
455
+ });
456
+ var checkNotTooOld = (fullDate, helpers) => {
457
+ const oldestDateAllowed = getOldestAllowedDate();
458
+ if (fullDate < oldestDateAllowed) {
459
+ const oldestDateString = oldestDateAllowed.toLocaleDateString("en-GB", {
460
+ day: "numeric",
461
+ month: "long",
462
+ year: "numeric"
463
+ });
464
+ const error = helpers.error("dob.tooOld", { oldest: oldestDateString });
465
+ error.path = ["day", "month", "year"];
466
+ return error;
327
467
  }
328
- return `Back to ${businessName}`;
468
+ return null;
329
469
  };
330
- var formatNumber = (payloadNumber, changedNumber, originalNumber) => {
331
- if (payloadNumber !== void 0) {
332
- return payloadNumber;
333
- }
334
- if (changedNumber !== void 0) {
335
- return changedNumber;
336
- }
337
- return originalNumber;
470
+ var getOldestAllowedDate = () => {
471
+ const date = /* @__PURE__ */ new Date();
472
+ date.setUTCHours(0, 0, 0, 0);
473
+ date.setUTCFullYear(date.getUTCFullYear() - MAX_AGE_YEARS);
474
+ return date;
338
475
  };
339
- var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
340
- const sortedErrors = [];
341
- for (const section of orderedSectionsToFix) {
342
- const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
343
- for (const field of fieldsInSection) {
344
- if (errors[field]) {
345
- sortedErrors.push({
346
- field,
347
- ...errors[field]
348
- });
349
- }
350
- }
476
+ var getFullDate = (day, monthValue, year, helpers) => {
477
+ const dayValue = Number.parseInt(day, 10);
478
+ const yearValue = Number.parseInt(year, 10);
479
+ const date = new Date(Date.UTC(yearValue, monthValue - 1, dayValue));
480
+ if (date.getUTCFullYear() !== yearValue || date.getUTCMonth() + 1 !== monthValue || date.getUTCDate() !== dayValue) {
481
+ return makeError(helpers, "dob.invalid", ["day", "month", "year"]);
351
482
  }
352
- return sortedErrors;
483
+ return date;
353
484
  };
354
-
355
- // src/presenters/address-presenter.js
356
- var addressChangeLink = (postcodeLookup, context) => {
357
- if (context === "business") {
358
- if (postcodeLookup) {
359
- return "/business-address-change";
485
+ var getMonthNumber = (month, helpers) => {
486
+ if (Number.isNaN(Number(month))) {
487
+ const lower = month.toLowerCase();
488
+ const mapped = MONTH_MAP[lower];
489
+ if (!mapped) {
490
+ return makeError(helpers, "dob.invalid", ["month"]);
360
491
  }
361
- return "/business-address-enter";
492
+ return mapped;
362
493
  }
363
- if (context === "personal") {
364
- if (postcodeLookup) {
365
- return "/account-address-change";
366
- }
367
- return "/account-address-enter";
494
+ const parsedMonth = Number.parseInt(month, 10);
495
+ if (parsedMonth < 1 || parsedMonth > 12) {
496
+ return makeError(helpers, "dob.invalid", ["month"]);
368
497
  }
369
- return null;
498
+ return parsedMonth;
370
499
  };
371
- var addressBackLink = (postcodeLookup, context) => {
372
- if (context === "business") {
373
- if (postcodeLookup) {
374
- return { href: "/business-address-select" };
375
- }
376
- return { href: "/business-address-enter" };
500
+ var checkMissingFields = (day, month, year, helpers) => {
501
+ if (!day && !month && !year) {
502
+ return makeError(helpers, "dob.missingAll", ["day", "month", "year"]);
377
503
  }
378
- if (context === "personal") {
379
- if (postcodeLookup) {
380
- return { href: "/account-address-select" };
381
- }
382
- return { href: "/account-address-enter" };
504
+ if (!day && month && year) {
505
+ return makeError(helpers, "dob.missingDay", ["day"]);
383
506
  }
384
- return null;
385
- };
386
- var buildAddressLine = (parts) => {
387
- return parts.filter(Boolean).join(", ") || null;
388
- };
389
- var buildStreetLine = (buildingRange, street) => {
390
- return [buildingRange, street].filter(Boolean).join(" ") || null;
391
- };
392
- var formatLookupAddress = (lookup, city, country, postcode) => ({
393
- address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
394
- address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
395
- address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
396
- county: lookup.county ?? null,
397
- city: city ?? null,
398
- country: country ?? null,
399
- postcode: postcode ?? null
400
- });
401
- var formatManualAddress = (manual, city, country, postcode) => ({
402
- address1: manual.line1 ?? null,
403
- address2: manual.line2 ?? null,
404
- address3: manual.line3 ?? null,
405
- city: city ?? null,
406
- county: manual.line4 ?? null,
407
- country: country ?? null,
408
- postcode: postcode ?? null
409
- });
410
- var formatDisplayAddress = (address) => {
411
- const { lookup, manual, postcode, country, city } = address;
412
- let addressLines = [];
413
- if (lookup.uprn) {
414
- const buildingAndStreet = [
415
- lookup.buildingNumberRange,
416
- lookup.street
417
- ].filter(Boolean).join(" ");
418
- addressLines = [
419
- lookup.pafOrganisationName,
420
- lookup.flatName,
421
- lookup.buildingName,
422
- buildingAndStreet,
423
- lookup.doubleDependentLocality,
424
- lookup.dependentLocality,
425
- city,
426
- lookup.county
427
- ];
428
- } else {
429
- addressLines = [
430
- manual.line1,
431
- manual.line2,
432
- manual.line3,
433
- city,
434
- manual.line4,
435
- // County
436
- manual.line5
437
- ];
507
+ if (day && !month && year) {
508
+ return makeError(helpers, "dob.missingMonth", ["month"]);
438
509
  }
439
- return [
440
- ...addressLines.filter(Boolean),
441
- postcode,
442
- country
443
- ];
444
- };
445
- var formatOriginalAddress = (originalAddress) => {
446
- const { lookup, manual, city, country, postcode } = originalAddress;
447
- return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
448
- };
449
- var formatChangedAddress = (changeBusinessAddress) => {
450
- if (!changeBusinessAddress.uprn) {
451
- return changeBusinessAddress;
510
+ if (day && month && !year) {
511
+ return makeError(helpers, "dob.missingYear", ["year"]);
452
512
  }
453
- const {
454
- pafOrganisationName,
455
- flatName,
456
- buildingName,
457
- buildingNumberRange,
458
- street,
459
- doubleDependentLocality,
460
- dependentLocality,
461
- city,
462
- county,
463
- country,
464
- postcode
465
- } = changeBusinessAddress;
466
- return {
467
- address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
468
- address2: buildStreetLine(buildingNumberRange, street),
469
- address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
470
- city: city ?? null,
471
- county: county ?? null,
472
- country: country ?? null,
473
- postcode: postcode ?? null
474
- };
475
- };
476
- var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
477
- const displayAddresses = addresses.map((address) => ({
478
- value: `${address.uprn}${address.displayAddress}`,
479
- text: address.displayAddress,
480
- selected: (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.uprn) === address.uprn && (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.displayAddress) === address.displayAddress
481
- }));
482
- const hasSelectedAddress = displayAddresses.some((addr) => addr.selected);
483
- const text = addresses.length === 1 ? "1 address found" : `${addresses.length} addresses found`;
484
- displayAddresses.unshift({
485
- value: "display",
486
- text,
487
- selected: !hasSelectedAddress
488
- });
489
- return displayAddresses;
490
- };
491
-
492
- // src/presenters/business-details-presenter.js
493
- var getActionText = (value) => {
494
- return value ? "Change" : "Add";
495
- };
496
- var formatCph = (countyParishHoldings) => {
497
- return (countyParishHoldings || []).filter((cph) => cph == null ? void 0 : cph.cphNumber).map((cph) => cph.cphNumber);
498
- };
499
- var formatCphText = (count) => {
500
- return `County Parish Holding (CPH) number${count !== 1 ? "s" : ""}`;
501
- };
502
- var formatBusinessAddress = (businessAddress) => {
503
- var _a, _b;
504
- if (((_a = businessAddress == null ? void 0 : businessAddress.lookup) == null ? void 0 : _a.uprn) || ((_b = businessAddress == null ? void 0 : businessAddress.manual) == null ? void 0 : _b.line1)) {
505
- return formatDisplayAddress(businessAddress);
513
+ if (!day && !month && year) {
514
+ return makeError(helpers, "dob.missingDayMonth", ["day", "month"]);
506
515
  }
507
- return [];
508
- };
509
-
510
- // src/presenters/presenters.js
511
- var presenters = {
512
- formatBackLink,
513
- formatNumber,
514
- formatDisplayAddress,
515
- formatOriginalAddress,
516
- formatChangedAddress,
517
- formatDisplayAddresses,
518
- sortErrorsBySectionOrder,
519
- getActionText,
520
- formatCph,
521
- formatCphText,
522
- formatBusinessAddress,
523
- addressBackLink,
524
- addressChangeLink
525
- };
526
-
527
- // src/utils/joi.js
528
- var import_joi = __toESM(require("joi"), 1);
529
- var Joi = import_joi.default.extend((joi) => ({
530
- type: "string",
531
- base: joi.string(),
532
- messages: {
533
- "string.noControlChars": "Field must not contain invalid characters"
534
- },
535
- validate(value, helpers) {
536
- if (typeof value === "string" && !NO_CONTROL_CHARS_PATTERN.test(value)) {
537
- return { value, errors: helpers.error("string.noControlChars") };
538
- }
539
- return { value };
516
+ if (!day && month && !year) {
517
+ return makeError(helpers, "dob.missingDayYear", ["day", "year"]);
518
+ }
519
+ if (day && !month && !year) {
520
+ return makeError(helpers, "dob.missingMonthYear", ["month", "year"]);
540
521
  }
541
- }));
542
-
543
- // src/schemas/business/business-sbi-schema.js
544
- var businessSbiSchema = Joi.object({
545
- sbi: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
546
- "string.pattern.base": "Enter the full SBI",
547
- "string.noControlChars": "SBI must not contain invalid characters"
548
- })
549
- });
550
-
551
- // src/schemas/shared/address-schema.js
552
- var addressSchema = Joi.object({
553
- address1: Joi.string().trim().required().max(ADDRESS_LINE_MAX).messages({
554
- "string.empty": "Enter address line 1, typically the building and street",
555
- "string.max": `Address line 1 must be ${ADDRESS_LINE_MAX} characters or less`,
556
- "any.required": "Enter address line 1, typically the building and street",
557
- "string.noControlChars": "Address line 1 must not contain invalid characters"
558
- }),
559
- address2: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
560
- "string.max": `Address line 2 must be ${ADDRESS_LINE_MAX} characters or less`,
561
- "string.noControlChars": "Address line 2 must not contain invalid characters"
562
- }),
563
- address3: Joi.string().trim().allow("").max(ADDRESS_LINE_MAX).messages({
564
- "string.max": `Address line 3 must be ${ADDRESS_LINE_MAX} characters or less`,
565
- "string.noControlChars": "Address line 3 must not contain invalid characters"
566
- }),
567
- city: Joi.string().trim().required().max(TOWN_CITY_MAX).messages({
568
- "string.empty": "Enter town or city",
569
- "string.max": `Town or city must be ${TOWN_CITY_MAX} characters or less`,
570
- "any.required": "Enter town or city",
571
- "string.noControlChars": "Town or city must not contain invalid characters"
572
- }),
573
- county: Joi.string().trim().allow("").max(COUNTY_MAX).messages({
574
- "string.max": `County must be ${COUNTY_MAX} characters or less`,
575
- "string.noControlChars": "County must not contain invalid characters"
576
- }),
577
- postcode: Joi.string().trim().required().max(POSTCODE_MAX).messages({
578
- "any.required": "Enter a postal code or zip code",
579
- "string.empty": "Enter a postal code or zip code",
580
- "string.max": `Postal code or zip code must be ${POSTCODE_MAX} characters or less`,
581
- "string.noControlChars": "Postal code or zip code must not contain invalid characters"
582
- }),
583
- country: Joi.string().trim().required().max(COUNTRY_MAX).messages({
584
- "string.empty": "Enter a country",
585
- "string.max": `Country must be ${COUNTRY_MAX} characters or less`,
586
- "any.required": "Enter a country",
587
- "string.noControlChars": "Country must not contain invalid characters"
588
- })
589
- });
590
-
591
- // src/schemas/business/business-name-schema.js
592
- var businessNameSchema = Joi.object({
593
- businessName: Joi.string().trim().required().max(BUSINESS_NAME_MAX).messages({
594
- "string.empty": "Enter business name",
595
- "string.max": `Business name must be ${BUSINESS_NAME_MAX} characters or less`,
596
- "any.required": "Enter business name",
597
- "string.noControlChars": "Business name must not contain invalid characters"
598
- })
599
- });
522
+ return null;
523
+ };
524
+ var makeError = (helpers, code, fields) => {
525
+ const error = helpers.error(code);
526
+ error.path = fields;
527
+ error.isJoiError = true;
528
+ return error;
529
+ };
600
530
 
601
- // src/schemas/business/business-email-schema.js
602
- var businessEmailSchema = Joi.object({
603
- businessEmail: Joi.string().required().max(EMAIL_MAX).email({
531
+ // src/schemas/personal/personal-email-schema.js
532
+ var personalEmailSchema = Joi.object({
533
+ personalEmail: Joi.string().required().max(EMAIL_MAX).email({
604
534
  minDomainSegments: 2,
605
535
  tlds: {
606
- allow: true
536
+ allow: true,
537
+ min: 2
607
538
  }
608
539
  }).messages({
609
- "string.max": `Business email address must be ${EMAIL_MAX} characters or less`,
610
- "string.empty": "Enter business email address",
540
+ "string.max": `Email address must be ${EMAIL_MAX} characters or less`,
541
+ "string.empty": "Enter a personal email address",
611
542
  "string.email": "Enter an email address, like name@example.com",
612
- "string.noControlChars": "Business email address must not contain invalid characters"
543
+ "string.noControlChars": "Personal email address must not contain invalid characters"
613
544
  })
614
545
  });
615
546
 
616
- // src/schemas/business/business-phone-schema.js
617
- var businessPhoneSchema = Joi.object({
618
- businessTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
619
- "string.min": `Business telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
620
- "string.max": `Business telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
621
- "string.pattern.base": "Business telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
622
- "string.noControlChars": "Business telephone number must not contain invalid characters"
547
+ // src/schemas/personal/personal-phone-schema.js
548
+ var personalPhoneSchema = Joi.object({
549
+ personalTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
550
+ "string.min": `Personal telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
551
+ "string.max": `Personal telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
552
+ "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
553
+ "string.noControlChars": "Personal telephone number must not contain invalid characters"
623
554
  }),
624
- businessMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
625
- "string.min": `Business mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
626
- "string.max": `Business mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
627
- "string.pattern.base": "Business mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
628
- "string.noControlChars": "Business mobile phone number must not contain invalid characters"
555
+ personalMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
556
+ "string.min": `Personal mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
557
+ "string.max": `Personal mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
558
+ "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
559
+ "string.noControlChars": "Personal mobile phone number must not contain invalid characters"
629
560
  })
630
- }).or("businessTelephone", "businessMobile").messages({
561
+ }).or("personalTelephone", "personalMobile").messages({
631
562
  "object.missing": "Enter at least one phone number"
632
563
  });
633
564
 
634
- // src/schemas/business/business-vat-schema.js
635
- var businessVatSchema = Joi.object({
636
- vatNumber: Joi.string().pattern(/^\d{9}$/).allow("").optional().messages({
637
- "string.pattern.base": "Enter a VAT registration number, like 123456789",
638
- "string.noControlChars": "VAT registration number must not contain invalid characters"
639
- })
640
- });
565
+ // src/schemas/personal/personal-schemas.js
566
+ var personalSchemas = {
567
+ name: personalNameSchema,
568
+ dob: personalDobSchema,
569
+ address: addressSchema,
570
+ phone: personalPhoneSchema,
571
+ email: personalEmailSchema
572
+ };
641
573
 
642
- // src/schemas/business/business-vat-change-schema.js
643
- var businessVatChangeSchema = Joi.object({
644
- vatNumber: Joi.string().pattern(/^\d{9}$/).required().messages({
645
- "string.pattern.base": "Enter a VAT registration number, like 123456789",
646
- "string.empty": "Enter a VAT registration number",
647
- "any.required": "Enter a VAT registration number",
648
- "string.noControlChars": "VAT registration number must not contain invalid characters"
574
+ // src/schemas/os-places/address-lookup-schema.js
575
+ var addressLookupSchema = Joi.object({
576
+ properties: Joi.object({
577
+ UPRN: Joi.string().required(),
578
+ ADDRESS: Joi.string().required(),
579
+ ORGANISATION_NAME: Joi.string().allow(null),
580
+ DEPARTMENT_NAME: Joi.string().allow(null),
581
+ SUB_BUILDING_NAME: Joi.string().allow(null),
582
+ BUILDING_NAME: Joi.string().allow(null),
583
+ BUILDING_NUMBER: Joi.string().allow(null),
584
+ DEPENDENT_THOROUGHFARE_NAME: Joi.string().allow(null),
585
+ THOROUGHFARE_NAME: Joi.string().allow(null),
586
+ DOUBLE_DEPENDENT_LOCALITY: Joi.string().allow(null),
587
+ DEPENDENT_LOCALITY: Joi.string().allow(null),
588
+ POST_TOWN: Joi.string().required(),
589
+ POSTCODE: Joi.string().required(),
590
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: Joi.string().allow(null),
591
+ COUNTRY_CODE: Joi.string().required()
592
+ }).unknown(true).required()
593
+ }).unknown(true);
594
+
595
+ // src/schemas/os-places/addresses-schema.js
596
+ var CHOOSE_ADDRESS_ERROR = "Choose an address";
597
+ var addressesSchema = Joi.object({
598
+ addresses: Joi.string().invalid("display").required().messages({
599
+ "any.required": CHOOSE_ADDRESS_ERROR,
600
+ "string.empty": CHOOSE_ADDRESS_ERROR,
601
+ "any.invalid": CHOOSE_ADDRESS_ERROR
649
602
  })
650
603
  });
651
604
 
652
- // src/schemas/business/business-vat-remove-schema.js
653
- var businessVatRemoveSchema = Joi.object({
654
- confirmRemove: Joi.string().valid("yes", "no").required().messages({
655
- "any.required": "Select yes if you want to remove your VAT registration number",
656
- "any.only": "Select yes if you want to remove your VAT registration number"
605
+ // src/schemas/os-places/uk-postcode-schema.js
606
+ var ukPostcodeSchema = Joi.object({
607
+ postcode: Joi.string().trim().required().uppercase().max(POSTCODE_MAX).pattern(/^([A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}|GIR ?0A{2})$/).messages({
608
+ "any.required": "Enter a postcode",
609
+ "string.empty": "Enter a postcode",
610
+ "string.max": `Postal code must be ${POSTCODE_MAX} characters or less`,
611
+ "string.pattern.base": "Enter a full UK postcode, like AA3 1AB"
657
612
  })
658
613
  });
659
614
 
660
- // src/schemas/business/business-schemas.js
661
- var businessSchemas = {
662
- sbi: businessSbiSchema,
663
- details: {
664
- name: businessNameSchema,
665
- address: addressSchema,
666
- phone: businessPhoneSchema,
667
- email: businessEmailSchema,
668
- vat: businessVatSchema
669
- },
670
- vat: {
671
- change: businessVatChangeSchema,
672
- remove: businessVatRemoveSchema
615
+ // src/schemas/os-places/os-places-schemas.js
616
+ var osPlacesSchemas = {
617
+ addressLookup: addressLookupSchema,
618
+ addresses: addressesSchema,
619
+ ukPostcode: ukPostcodeSchema
620
+ };
621
+
622
+ // src/schemas/schemas.js
623
+ var schemas = {
624
+ business: businessSchemas,
625
+ customer: customerSchemas,
626
+ personal: personalSchemas,
627
+ osPlaces: osPlacesSchemas
628
+ };
629
+
630
+ // src/constants/country-names.js
631
+ var COUNTRY_NAMES = {
632
+ E: "ENGLAND",
633
+ W: "WALES",
634
+ S: "SCOTLAND",
635
+ N: "NORTHERN IRELAND",
636
+ L: "CHANNEL ISLANDS",
637
+ M: "ISLE OF MAN"
638
+ };
639
+
640
+ // src/mappers/address-lookup-mapper.js
641
+ var addressLookupMapper = (addresses) => {
642
+ if (!Array.isArray(addresses)) {
643
+ return [];
644
+ }
645
+ return addresses.map((address) => {
646
+ const { error } = schemas.osPlaces.addressLookup.validate(address);
647
+ if (error) {
648
+ return null;
649
+ }
650
+ const {
651
+ UPRN,
652
+ ADDRESS,
653
+ PO_BOX_NUMBER,
654
+ ORGANISATION_NAME,
655
+ DEPARTMENT_NAME,
656
+ SUB_BUILDING_NAME,
657
+ BUILDING_NAME,
658
+ BUILDING_NUMBER,
659
+ DEPENDENT_THOROUGHFARE_NAME,
660
+ THOROUGHFARE_NAME,
661
+ DOUBLE_DEPENDENT_LOCALITY,
662
+ DEPENDENT_LOCALITY,
663
+ POST_TOWN,
664
+ POSTCODE,
665
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION,
666
+ COUNTRY_CODE
667
+ } = address.properties;
668
+ const buildingName = PO_BOX_NUMBER ? `PO BOX ${PO_BOX_NUMBER}` : BUILDING_NAME || null;
669
+ return {
670
+ displayAddress: ADDRESS,
671
+ pafOrganisationName: filterAndJoin([ORGANISATION_NAME, DEPARTMENT_NAME]),
672
+ flatName: SUB_BUILDING_NAME ?? null,
673
+ buildingName,
674
+ buildingNumberRange: BUILDING_NUMBER ?? null,
675
+ street: filterAndJoin([DEPENDENT_THOROUGHFARE_NAME, THOROUGHFARE_NAME]),
676
+ dependentLocality: DEPENDENT_LOCALITY ?? null,
677
+ doubleDependentLocality: DOUBLE_DEPENDENT_LOCALITY ?? null,
678
+ city: POST_TOWN,
679
+ county: formatCounty(LOCAL_CUSTODIAN_CODE_DESCRIPTION, POST_TOWN),
680
+ postcode: POSTCODE,
681
+ country: COUNTRY_NAMES[COUNTRY_CODE] ?? null,
682
+ uprn: UPRN
683
+ };
684
+ }).filter(Boolean);
685
+ };
686
+ var formatCounty = (localCustodianCodeDescription, postTown) => {
687
+ if (localCustodianCodeDescription === "ORDNANCE SURVEY" || localCustodianCodeDescription === postTown) {
688
+ return null;
689
+ }
690
+ return localCustodianCodeDescription;
691
+ };
692
+ var filterAndJoin = (addressesProperties) => {
693
+ return addressesProperties.filter(Boolean).join(", ") || null;
694
+ };
695
+
696
+ // src/mappers/mappers.js
697
+ var mappers = {
698
+ personalBusinessDetails: mapPersonalBusinessDetails,
699
+ address: mapAddress,
700
+ customerName: mapCustomerName,
701
+ businessDetails: mapBusinessDetails,
702
+ addressLookup: addressLookupMapper
703
+ };
704
+
705
+ // src/mutations/business/update-business-email.js
706
+ var updateBusinessEmailMutation = `
707
+ mutation Mutation($input: UpdateBusinessEmailInput!) {
708
+ updateBusinessEmail(input: $input) {
709
+ business {
710
+ info {
711
+ email {
712
+ address
713
+ }
714
+ }
715
+ }
716
+ success
717
+ }
718
+ }
719
+ `;
720
+
721
+ // src/mutations/business/update-business-name.js
722
+ var updateBusinessNameMutation = `
723
+ mutation UpdateBusinessName($input: UpdateBusinessNameInput!) {
724
+ updateBusinessName(input: $input) {
725
+ business {
726
+ info {
727
+ name
728
+ }
729
+ }
730
+ success
731
+ }
732
+ }
733
+ `;
734
+
735
+ // src/mutations/personal/update-customer-name.js
736
+ var updateCustomerNameMutation = `
737
+ mutation UpdateCustomerName($input: UpdateCustomerNameInput!) {
738
+ updateCustomerName(input: $input) {
739
+ customer {
740
+ info {
741
+ name {
742
+ first
743
+ last
744
+ middle
745
+ }
746
+ }
747
+ }
748
+ }
749
+ }
750
+ `;
751
+
752
+ // src/mutations/personal/update-customer-dob.js
753
+ var updateCustomerDobMutation = `
754
+ mutation UpdateCustomerDateOfBirth($input: UpdateCustomerDateOfBirthInput!) {
755
+ updateCustomerDateOfBirth(input: $input) {
756
+ customer {
757
+ info {
758
+ dateOfBirth
759
+ }
760
+ }
761
+ }
762
+ }
763
+ `;
764
+
765
+ // src/mutations/personal/update-customer-phone.js
766
+ var updateCustomerPhoneMutation = `
767
+ mutation UpdateCustomerPhone($input: UpdateCustomerPhoneInput!) {
768
+ updateCustomerPhone(input: $input) {
769
+ customer {
770
+ info {
771
+ phone {
772
+ landline
773
+ mobile
774
+ }
775
+ }
776
+ }
777
+ }
673
778
  }
674
- };
779
+ `;
675
780
 
676
- // src/schemas/customer/customer-crn-schema.js
677
- var customerCrnSchema = Joi.object({
678
- crn: Joi.string().pattern(/^\d{10}$/).allow("").optional().messages({
679
- "string.pattern.base": "Enter the full CRN",
680
- "string.noControlChars": "CRN must not contain invalid characters"
681
- })
682
- });
781
+ // src/mutations/personal/update-customer-email.js
782
+ var updateCustomerEmailMutation = `
783
+ mutation UpdateCustomerEmail($input: UpdateCustomerEmailInput!) {
784
+ updateCustomerEmail(input: $input) {
785
+ customer {
786
+ info {
787
+ email {
788
+ address
789
+ }
790
+ }
791
+ }
792
+ }
793
+ }
794
+ `;
683
795
 
684
- // src/schemas/customer/customer-schemas.js
685
- var customerSchemas = {
686
- crn: customerCrnSchema
796
+ // src/mutations/mutations.js
797
+ var mutations = {
798
+ updateBusinessEmail: updateBusinessEmailMutation,
799
+ updateBusinessName: updateBusinessNameMutation,
800
+ updateCustomerName: updateCustomerNameMutation,
801
+ updateCustomerDob: updateCustomerDobMutation,
802
+ updateCustomerPhone: updateCustomerPhoneMutation,
803
+ updateCustomerEmail: updateCustomerEmailMutation
687
804
  };
688
805
 
689
- // src/schemas/personal/personal-name-schema.js
690
- var personalNameSchema = Joi.object({
691
- first: Joi.string().trim().required().max(FIRST_NAME_MAX).messages({
692
- "string.empty": "Enter first name",
693
- "string.max": `First name must be ${FIRST_NAME_MAX} characters or less`,
694
- "any.required": "Enter first name",
695
- "string.noControlChars": "First name must not contain invalid characters"
696
- }),
697
- last: Joi.string().trim().required().max(LAST_NAME_MAX).messages({
698
- "string.empty": "Enter last name",
699
- "string.max": `Last name must be ${LAST_NAME_MAX} characters or less`,
700
- "any.required": "Enter last name",
701
- "string.noControlChars": "Last name must not contain invalid characters"
702
- }),
703
- middle: Joi.string().trim().allow("").max(MIDDLE_NAMES_MAX).messages({
704
- "string.max": `Middle names must be ${MIDDLE_NAMES_MAX} characters or less`,
705
- "string.noControlChars": "Middle names must not contain invalid characters"
706
- })
707
- });
708
-
709
- // src/schemas/personal/personal-dob-schema.js
710
- var personalDobSchema = Joi.object({
711
- day: Joi.string().allow(""),
712
- month: Joi.string().allow(""),
713
- year: Joi.string().allow("")
714
- }).custom((value, helpers) => {
715
- const { day, month, year } = value;
716
- const missingFieldsError = checkMissingFields(day, month, year, helpers);
717
- if (missingFieldsError) {
718
- return missingFieldsError;
806
+ // src/presenters/base-presenter.js
807
+ var BACK_LINK_DISPLAY_MAX = 50;
808
+ var formatBackLink = (businessName) => {
809
+ if (businessName.length > BACK_LINK_DISPLAY_MAX) {
810
+ return `Back to ${businessName.slice(0, BACK_LINK_DISPLAY_MAX)}\u2026`;
719
811
  }
720
- if (year && year.length !== 4) {
721
- return makeError(helpers, "dob.yearLength", ["year"]);
812
+ return `Back to ${businessName}`;
813
+ };
814
+ var formatNumber = (payloadNumber, changedNumber, originalNumber) => {
815
+ if (payloadNumber !== void 0) {
816
+ return payloadNumber;
722
817
  }
723
- const monthValue = getMonthNumber(month, helpers);
724
- if (monthValue.isJoiError) {
725
- return monthValue;
818
+ if (changedNumber !== void 0) {
819
+ return changedNumber;
726
820
  }
727
- const fullDate = getFullDate(day, monthValue, year, helpers);
728
- if (fullDate.isJoiError) {
729
- return fullDate;
821
+ return originalNumber;
822
+ };
823
+ var formatDatePart = (changed, original) => {
824
+ return changed ?? (original == null ? void 0 : original.toString()) ?? "";
825
+ };
826
+ var formatDateInputValues = (payloadDob, changedDob, originalDob) => {
827
+ if (payloadDob) {
828
+ return {
829
+ day: payloadDob.day ?? "",
830
+ month: payloadDob.month ?? "",
831
+ year: payloadDob.year ?? ""
832
+ };
730
833
  }
731
- const today = /* @__PURE__ */ new Date();
732
- today.setUTCHours(0, 0, 0, 0);
733
- if (fullDate >= today) {
734
- return makeError(helpers, "dob.future", ["day", "month", "year"]);
834
+ return {
835
+ day: formatDatePart(changedDob == null ? void 0 : changedDob.day, originalDob == null ? void 0 : originalDob.day),
836
+ month: formatDatePart(changedDob == null ? void 0 : changedDob.month, originalDob == null ? void 0 : originalDob.month),
837
+ year: formatDatePart(changedDob == null ? void 0 : changedDob.year, originalDob == null ? void 0 : originalDob.year)
838
+ };
839
+ };
840
+ var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
841
+ const sortedErrors = [];
842
+ for (const section of orderedSectionsToFix) {
843
+ const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
844
+ for (const field of fieldsInSection) {
845
+ if (errors[field]) {
846
+ sortedErrors.push({
847
+ field,
848
+ ...errors[field]
849
+ });
850
+ }
851
+ }
735
852
  }
736
- const tooOldError = checkNotTooOld(fullDate, helpers);
737
- if (tooOldError) {
738
- return tooOldError;
853
+ return sortedErrors;
854
+ };
855
+
856
+ // src/presenters/address-presenter.js
857
+ var addressChangeLink = (postcodeLookup, context) => {
858
+ if (context === "business") {
859
+ if (postcodeLookup) {
860
+ return "/business-address-change";
861
+ }
862
+ return "/business-address-enter";
739
863
  }
740
- return value;
741
- }).messages({
742
- "dob.missingAll": "Enter your date of birth",
743
- "dob.missingDay": "Date of birth must include a day",
744
- "dob.missingMonth": "Date of birth must include a month",
745
- "dob.missingYear": "Date of birth must include a year",
746
- "dob.missingDayMonth": "Date of birth must include a day and month",
747
- "dob.missingDayYear": "Date of birth must include a day and year",
748
- "dob.missingMonthYear": "Date of birth must include a month and year",
749
- "dob.yearLength": "Enter a year with 4 numbers, like 1975",
750
- "dob.invalid": "Date of birth must be a real date",
751
- "dob.future": "Date of birth must be in the past",
752
- "dob.tooOld": "Date of birth must be on or after {{#oldest}}",
753
- "string.noControlChars": "Date of birth must not contain invalid characters"
754
- });
755
- var checkNotTooOld = (fullDate, helpers) => {
756
- const oldestDateAllowed = getOldestAllowedDate();
757
- if (fullDate < oldestDateAllowed) {
758
- const oldestDateString = oldestDateAllowed.toLocaleDateString("en-GB", {
759
- day: "numeric",
760
- month: "long",
761
- year: "numeric"
762
- });
763
- const error = helpers.error("dob.tooOld", { oldest: oldestDateString });
764
- error.path = ["day", "month", "year"];
765
- return error;
864
+ if (context === "personal") {
865
+ if (postcodeLookup) {
866
+ return "/account-address-change";
867
+ }
868
+ return "/account-address-enter";
766
869
  }
767
870
  return null;
768
871
  };
769
- var getOldestAllowedDate = () => {
770
- const date = /* @__PURE__ */ new Date();
771
- date.setUTCHours(0, 0, 0, 0);
772
- date.setUTCFullYear(date.getUTCFullYear() - MAX_AGE_YEARS);
773
- return date;
774
- };
775
- var getFullDate = (day, monthValue, year, helpers) => {
776
- const dayValue = Number.parseInt(day, 10);
777
- const yearValue = Number.parseInt(year, 10);
778
- const date = new Date(Date.UTC(yearValue, monthValue - 1, dayValue));
779
- if (date.getUTCFullYear() !== yearValue || date.getUTCMonth() + 1 !== monthValue || date.getUTCDate() !== dayValue) {
780
- return makeError(helpers, "dob.invalid", ["day", "month", "year"]);
872
+ var addressBackLink = (postcodeLookup, context) => {
873
+ if (context === "business") {
874
+ if (postcodeLookup) {
875
+ return { href: "/business-address-select" };
876
+ }
877
+ return { href: "/business-address-enter" };
781
878
  }
782
- return date;
783
- };
784
- var getMonthNumber = (month, helpers) => {
785
- if (Number.isNaN(Number(month))) {
786
- const lower = month.toLowerCase();
787
- const mapped = MONTH_MAP[lower];
788
- if (!mapped) {
789
- return makeError(helpers, "dob.invalid", ["month"]);
879
+ if (context === "personal") {
880
+ if (postcodeLookup) {
881
+ return { href: "/account-address-select" };
790
882
  }
791
- return mapped;
883
+ return { href: "/account-address-enter" };
792
884
  }
793
- const parsedMonth = Number.parseInt(month, 10);
794
- if (parsedMonth < 1 || parsedMonth > 12) {
795
- return makeError(helpers, "dob.invalid", ["month"]);
885
+ return null;
886
+ };
887
+ var buildAddressLine = (parts) => {
888
+ return parts.filter(Boolean).join(", ") || null;
889
+ };
890
+ var buildStreetLine = (buildingRange, street) => {
891
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
892
+ };
893
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
894
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
895
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
896
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
897
+ county: lookup.county ?? null,
898
+ city: city ?? null,
899
+ country: country ?? null,
900
+ postcode: postcode ?? null
901
+ });
902
+ var formatManualAddress = (manual, city, country, postcode) => ({
903
+ address1: manual.line1 ?? null,
904
+ address2: manual.line2 ?? null,
905
+ address3: manual.line3 ?? null,
906
+ city: city ?? null,
907
+ county: manual.line4 ?? null,
908
+ country: country ?? null,
909
+ postcode: postcode ?? null
910
+ });
911
+ var formatDisplayAddress = (address) => {
912
+ const { lookup, manual, postcode, country, city } = address;
913
+ let addressLines = [];
914
+ if (lookup.uprn) {
915
+ const buildingAndStreet = [
916
+ lookup.buildingNumberRange,
917
+ lookup.street
918
+ ].filter(Boolean).join(" ");
919
+ addressLines = [
920
+ lookup.pafOrganisationName,
921
+ lookup.flatName,
922
+ lookup.buildingName,
923
+ buildingAndStreet,
924
+ lookup.doubleDependentLocality,
925
+ lookup.dependentLocality,
926
+ city,
927
+ lookup.county
928
+ ];
929
+ } else {
930
+ addressLines = [
931
+ manual.line1,
932
+ manual.line2,
933
+ manual.line3,
934
+ city,
935
+ manual.line4,
936
+ // County
937
+ manual.line5
938
+ ];
796
939
  }
797
- return parsedMonth;
940
+ return [
941
+ ...addressLines.filter(Boolean),
942
+ postcode,
943
+ country
944
+ ];
798
945
  };
799
- var checkMissingFields = (day, month, year, helpers) => {
800
- if (!day && !month && !year) {
801
- return makeError(helpers, "dob.missingAll", ["day", "month", "year"]);
802
- }
803
- if (!day && month && year) {
804
- return makeError(helpers, "dob.missingDay", ["day"]);
805
- }
806
- if (day && !month && year) {
807
- return makeError(helpers, "dob.missingMonth", ["month"]);
808
- }
809
- if (day && month && !year) {
810
- return makeError(helpers, "dob.missingYear", ["year"]);
811
- }
812
- if (!day && !month && year) {
813
- return makeError(helpers, "dob.missingDayMonth", ["day", "month"]);
814
- }
815
- if (!day && month && !year) {
816
- return makeError(helpers, "dob.missingDayYear", ["day", "year"]);
817
- }
818
- if (day && !month && !year) {
819
- return makeError(helpers, "dob.missingMonthYear", ["month", "year"]);
946
+ var formatOriginalAddress = (originalAddress) => {
947
+ const { lookup, manual, city, country, postcode } = originalAddress;
948
+ return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
949
+ };
950
+ var formatChangedAddress = (changeBusinessAddress) => {
951
+ if (!changeBusinessAddress.uprn) {
952
+ return changeBusinessAddress;
820
953
  }
821
- return null;
954
+ const {
955
+ pafOrganisationName,
956
+ flatName,
957
+ buildingName,
958
+ buildingNumberRange,
959
+ street,
960
+ doubleDependentLocality,
961
+ dependentLocality,
962
+ city,
963
+ county,
964
+ country,
965
+ postcode
966
+ } = changeBusinessAddress;
967
+ return {
968
+ address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
969
+ address2: buildStreetLine(buildingNumberRange, street),
970
+ address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
971
+ city: city ?? null,
972
+ county: county ?? null,
973
+ country: country ?? null,
974
+ postcode: postcode ?? null
975
+ };
822
976
  };
823
- var makeError = (helpers, code, fields) => {
824
- const error = helpers.error(code);
825
- error.path = fields;
826
- error.isJoiError = true;
827
- return error;
977
+ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
978
+ const displayAddresses = addresses.map((address) => ({
979
+ value: `${address.uprn}${address.displayAddress}`,
980
+ text: address.displayAddress,
981
+ selected: (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.uprn) === address.uprn && (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.displayAddress) === address.displayAddress
982
+ }));
983
+ const hasSelectedAddress = displayAddresses.some((addr) => addr.selected);
984
+ const text = addresses.length === 1 ? "1 address found" : `${addresses.length} addresses found`;
985
+ displayAddresses.unshift({
986
+ value: "display",
987
+ text,
988
+ selected: !hasSelectedAddress
989
+ });
990
+ return displayAddresses;
828
991
  };
829
992
 
830
- // src/schemas/personal/personal-email-schema.js
831
- var personalEmailSchema = Joi.object({
832
- personalEmail: Joi.string().required().max(EMAIL_MAX).email({
833
- minDomainSegments: 2,
834
- tlds: {
835
- allow: true,
836
- min: 2
837
- }
838
- }).messages({
839
- "string.max": `Email address must be ${EMAIL_MAX} characters or less`,
840
- "string.empty": "Enter a personal email address",
841
- "string.email": "Enter an email address, like name@example.com",
842
- "string.noControlChars": "Personal email address must not contain invalid characters"
843
- })
844
- });
845
-
846
- // src/schemas/personal/personal-phone-schema.js
847
- var personalPhoneSchema = Joi.object({
848
- personalTelephone: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
849
- "string.min": `Personal telephone number must be ${PHONE_NUMBER_MIN} characters or more`,
850
- "string.max": `Personal telephone number must be ${PHONE_NUMBER_MAX} characters or less`,
851
- "string.pattern.base": "Personal telephone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
852
- "string.noControlChars": "Personal telephone number must not contain invalid characters"
853
- }),
854
- personalMobile: Joi.string().trim().empty("").min(PHONE_NUMBER_MIN).max(PHONE_NUMBER_MAX).pattern(PHONE_NUMBER_PATTERN).messages({
855
- "string.min": `Personal mobile phone number must be ${PHONE_NUMBER_MIN} characters or more`,
856
- "string.max": `Personal mobile phone number must be ${PHONE_NUMBER_MAX} characters or less`,
857
- "string.pattern.base": "Personal mobile phone number must only include numbers 0 to 9 and special characters such as spaces, brackets and +",
858
- "string.noControlChars": "Personal mobile phone number must not contain invalid characters"
859
- })
860
- }).or("personalTelephone", "personalMobile").messages({
861
- "object.missing": "Enter at least one phone number"
862
- });
863
-
864
- // src/schemas/personal/personal-schemas.js
865
- var personalSchemas = {
866
- name: personalNameSchema,
867
- dob: personalDobSchema,
868
- address: addressSchema,
869
- phone: personalPhoneSchema,
870
- email: personalEmailSchema
993
+ // src/presenters/business-details-presenter.js
994
+ var getActionText = (value) => {
995
+ return value ? "Change" : "Add";
871
996
  };
872
-
873
- // src/schemas/os-places/address-lookup-schema.js
874
- var addressLookupSchema = Joi.object({
875
- properties: Joi.object({
876
- UPRN: Joi.string().required(),
877
- ADDRESS: Joi.string().required(),
878
- ORGANISATION_NAME: Joi.string().allow(null),
879
- DEPARTMENT_NAME: Joi.string().allow(null),
880
- SUB_BUILDING_NAME: Joi.string().allow(null),
881
- BUILDING_NAME: Joi.string().allow(null),
882
- BUILDING_NUMBER: Joi.string().allow(null),
883
- DEPENDENT_THOROUGHFARE_NAME: Joi.string().allow(null),
884
- THOROUGHFARE_NAME: Joi.string().allow(null),
885
- DOUBLE_DEPENDENT_LOCALITY: Joi.string().allow(null),
886
- DEPENDENT_LOCALITY: Joi.string().allow(null),
887
- POST_TOWN: Joi.string().required(),
888
- POSTCODE: Joi.string().required(),
889
- LOCAL_CUSTODIAN_CODE_DESCRIPTION: Joi.string().allow(null),
890
- COUNTRY_CODE: Joi.string().required()
891
- }).unknown(true).required()
892
- }).unknown(true);
893
-
894
- // src/schemas/os-places/addresses-schema.js
895
- var CHOOSE_ADDRESS_ERROR = "Choose an address";
896
- var addressesSchema = Joi.object({
897
- addresses: Joi.string().invalid("display").required().messages({
898
- "any.required": CHOOSE_ADDRESS_ERROR,
899
- "string.empty": CHOOSE_ADDRESS_ERROR,
900
- "any.invalid": CHOOSE_ADDRESS_ERROR
901
- })
902
- });
903
-
904
- // src/schemas/os-places/uk-postcode-schema.js
905
- var ukPostcodeSchema = Joi.object({
906
- postcode: Joi.string().trim().required().uppercase().max(POSTCODE_MAX).pattern(/^([A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}|GIR ?0A{2})$/).messages({
907
- "any.required": "Enter a postcode",
908
- "string.empty": "Enter a postcode",
909
- "string.max": `Postal code must be ${POSTCODE_MAX} characters or less`,
910
- "string.pattern.base": "Enter a full UK postcode, like AA3 1AB"
911
- })
912
- });
913
-
914
- // src/schemas/os-places/os-places-schemas.js
915
- var osPlacesSchemas = {
916
- addressLookup: addressLookupSchema,
917
- addresses: addressesSchema,
918
- ukPostcode: ukPostcodeSchema
997
+ var formatCph = (countyParishHoldings) => {
998
+ return (countyParishHoldings || []).filter((cph) => cph == null ? void 0 : cph.cphNumber).map((cph) => cph.cphNumber);
999
+ };
1000
+ var formatCphText = (count) => {
1001
+ return `County Parish Holding (CPH) number${count !== 1 ? "s" : ""}`;
1002
+ };
1003
+ var formatBusinessAddress = (businessAddress) => {
1004
+ var _a, _b;
1005
+ if (((_a = businessAddress == null ? void 0 : businessAddress.lookup) == null ? void 0 : _a.uprn) || ((_b = businessAddress == null ? void 0 : businessAddress.manual) == null ? void 0 : _b.line1)) {
1006
+ return formatDisplayAddress(businessAddress);
1007
+ }
1008
+ return [];
919
1009
  };
920
1010
 
921
- // src/schemas/schemas.js
922
- var schemas = {
923
- business: businessSchemas,
924
- customer: customerSchemas,
925
- personal: personalSchemas,
926
- osPlaces: osPlacesSchemas
1011
+ // src/presenters/presenters.js
1012
+ var presenters = {
1013
+ formatBackLink,
1014
+ formatNumber,
1015
+ formatDateInputValues,
1016
+ formatDisplayAddress,
1017
+ formatOriginalAddress,
1018
+ formatChangedAddress,
1019
+ formatDisplayAddresses,
1020
+ sortErrorsBySectionOrder,
1021
+ getActionText,
1022
+ formatCph,
1023
+ formatCphText,
1024
+ formatBusinessAddress,
1025
+ addressBackLink,
1026
+ addressChangeLink
927
1027
  };
928
1028
 
929
1029
  // src/utils/format-full-name.js
@@ -978,6 +1078,253 @@ var utils = {
978
1078
  buildUpdateBusinessEmailVariables,
979
1079
  buildUpdateBusinessNameVariables
980
1080
  };
1081
+
1082
+ // src/services/os-places/address-lookup-service.js
1083
+ var import_osdatahub = require("osdatahub");
1084
+
1085
+ // src/mock-data/mock-os-places-addresses.js
1086
+ var mockAddresses = [
1087
+ {
1088
+ properties: {
1089
+ UPRN: "10000001",
1090
+ ADDRESS: "APARTMENT 1, REDBRIDGE HOUSE, 1 ROSE COURT, LONDON, SW1A 1AA",
1091
+ SUB_BUILDING_NAME: "APARTMENT 1",
1092
+ BUILDING_NAME: "REDBRIDGE HOUSE",
1093
+ BUILDING_NUMBER: "1",
1094
+ THOROUGHFARE_NAME: "ROSE COURT",
1095
+ POST_TOWN: "LONDON",
1096
+ POSTCODE: "SW1A 1AA",
1097
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "GREATER LONDON",
1098
+ COUNTRY_CODE: "E"
1099
+ }
1100
+ },
1101
+ {
1102
+ properties: {
1103
+ UPRN: "10000002",
1104
+ ADDRESS: "FLAT 2B, KINGSLEY APARTMENTS, 12 VICTORIA SQUARE, LONDON, SW1A 1AA",
1105
+ SUB_BUILDING_NAME: "FLAT 2B",
1106
+ BUILDING_NAME: "KINGSLEY APARTMENTS",
1107
+ BUILDING_NUMBER: "12",
1108
+ THOROUGHFARE_NAME: "VICTORIA SQUARE",
1109
+ DEPENDENT_LOCALITY: "",
1110
+ POST_TOWN: "LONDON",
1111
+ POSTCODE: "SW1A 1AA",
1112
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "GREATER LONDON",
1113
+ COUNTRY_CODE: "E"
1114
+ }
1115
+ },
1116
+ {
1117
+ properties: {
1118
+ UPRN: "10000003",
1119
+ ADDRESS: "THE GROUND FLOOR, THE OLD GRANARY, 5 CHURCH LANE, LONDON, SW1A 1AA",
1120
+ ORGANISATION_NAME: "OLD GRANARY STUDIOS",
1121
+ SUB_BUILDING_NAME: "GROUND FLOOR",
1122
+ BUILDING_NAME: "THE OLD GRANARY",
1123
+ BUILDING_NUMBER: "5",
1124
+ THOROUGHFARE_NAME: "CHURCH LANE",
1125
+ POST_TOWN: "LONDON",
1126
+ POSTCODE: "SW1A 1AA",
1127
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "GREATER LONDON",
1128
+ COUNTRY_CODE: "E"
1129
+ }
1130
+ },
1131
+ {
1132
+ properties: {
1133
+ UPRN: "10000004",
1134
+ ADDRESS: "SUITE 3, WELLINGTON HOUSE, 20 QUEEN STREET, LONDON, SW1A 1AA",
1135
+ ORGANISATION_NAME: "WELLINGTON CONSULTING LTD",
1136
+ DEPARTMENT_NAME: "CLIENT SERVICES",
1137
+ SUB_BUILDING_NAME: "SUITE 3",
1138
+ BUILDING_NAME: "WELLINGTON HOUSE",
1139
+ BUILDING_NUMBER: "20",
1140
+ THOROUGHFARE_NAME: "QUEEN STREET",
1141
+ POST_TOWN: "LONDON",
1142
+ POSTCODE: "SW1A 1AA",
1143
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "GREATER LONDON",
1144
+ COUNTRY_CODE: "E"
1145
+ }
1146
+ },
1147
+ {
1148
+ properties: {
1149
+ UPRN: "10000005",
1150
+ ADDRESS: "UNIT 6, MARKET ROW, 3 MARKET YARD, LONDON, SW1A 1AA",
1151
+ ORGANISATION_NAME: "MARKET ROW TRADERS",
1152
+ SUB_BUILDING_NAME: "UNIT 6",
1153
+ BUILDING_NAME: "MARKET ROW",
1154
+ BUILDING_NUMBER: "3",
1155
+ THOROUGHFARE_NAME: "MARKET YARD",
1156
+ POST_TOWN: "LONDON",
1157
+ POSTCODE: "SW1A 1AA",
1158
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "GREATER LONDON",
1159
+ COUNTRY_CODE: "E"
1160
+ }
1161
+ },
1162
+ {
1163
+ properties: {
1164
+ UPRN: "20000001",
1165
+ ADDRESS: "1 ORCHARD COTTAGES, WESTFIELD LANE, SHEPTON MALLET, BS14 8XX",
1166
+ BUILDING_NAME: "ORCHARD COTTAGES",
1167
+ BUILDING_NUMBER: "1",
1168
+ THOROUGHFARE_NAME: "WESTFIELD LANE",
1169
+ POST_TOWN: "SHEPTON MALLET",
1170
+ POSTCODE: "BS14 8XX",
1171
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "SOMERSET",
1172
+ COUNTRY_CODE: "E"
1173
+ }
1174
+ },
1175
+ {
1176
+ properties: {
1177
+ UPRN: "20000002",
1178
+ ADDRESS: "THE STABLES, MANOR FARM, HOLLOW ROAD, SHEPTON MALLET, BS14 8XX",
1179
+ ORGANISATION_NAME: "MANOR FARM",
1180
+ SUB_BUILDING_NAME: "THE STABLES",
1181
+ BUILDING_NAME: "MANOR FARM",
1182
+ THOROUGHFARE_NAME: "HOLLOW ROAD",
1183
+ POST_TOWN: "SHEPTON MALLET",
1184
+ POSTCODE: "BS14 8XX",
1185
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "SOMERSET",
1186
+ COUNTRY_CODE: "E"
1187
+ }
1188
+ },
1189
+ {
1190
+ properties: {
1191
+ UPRN: "30000001",
1192
+ ADDRESS: "FLAT 4, BISHOP COURT, 9 FLEET STREET, LONDON, EC1A 1BB",
1193
+ SUB_BUILDING_NAME: "FLAT 4",
1194
+ BUILDING_NAME: "BISHOP COURT",
1195
+ BUILDING_NUMBER: "9",
1196
+ THOROUGHFARE_NAME: "FLEET STREET",
1197
+ POST_TOWN: "LONDON",
1198
+ POSTCODE: "EC1A 1BB",
1199
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "CITY OF LONDON",
1200
+ COUNTRY_CODE: "E"
1201
+ }
1202
+ },
1203
+ {
1204
+ properties: {
1205
+ UPRN: "40000001",
1206
+ ADDRESS: "2A PICCADILLY ARCADE, MANCHESTER, M1 1AE",
1207
+ ORGANISATION_NAME: "PICCADILLY ARCADE LTD",
1208
+ SUB_BUILDING_NAME: "2A",
1209
+ BUILDING_NAME: "PICCADILLY ARCADE",
1210
+ BUILDING_NUMBER: "2A",
1211
+ THOROUGHFARE_NAME: "PICCADILLY",
1212
+ POST_TOWN: "MANCHESTER",
1213
+ POSTCODE: "M1 1AE",
1214
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "GREATER MANCHESTER",
1215
+ COUNTRY_CODE: "E"
1216
+ }
1217
+ },
1218
+ {
1219
+ properties: {
1220
+ UPRN: "50000001",
1221
+ ADDRESS: "3 WESTBOURNE TERRACE, LONDON, W1A 0AX",
1222
+ BUILDING_NAME: "WESTBOURNE TERRACE",
1223
+ BUILDING_NUMBER: "3",
1224
+ THOROUGHFARE_NAME: "WESTBOURNE TERRACE",
1225
+ POST_TOWN: "LONDON",
1226
+ POSTCODE: "W1A 0AX",
1227
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: "WESTMINSTER",
1228
+ COUNTRY_CODE: "E"
1229
+ }
1230
+ }
1231
+ ];
1232
+
1233
+ // src/services/os-places/os-places-stub.js
1234
+ var mockPostcode = (postcode) => {
1235
+ const formattedPostcode = postcode == null ? void 0 : postcode.toUpperCase().replaceAll(" ", "");
1236
+ const matchingAddresses = mockAddresses.filter((address) => {
1237
+ var _a;
1238
+ const addressPostcode = (_a = address.properties.POSTCODE) == null ? void 0 : _a.toUpperCase().replaceAll(" ", "");
1239
+ return addressPostcode === formattedPostcode;
1240
+ });
1241
+ return {
1242
+ features: matchingAddresses
1243
+ };
1244
+ };
1245
+
1246
+ // src/services/os-places/address-lookup-service.js
1247
+ var addressLookupService = async (postcode, osPlacesConfig) => {
1248
+ const addresses = await fetchAddressesFromPostcodeLookup(postcode, osPlacesConfig);
1249
+ if (addresses.error) {
1250
+ return addresses;
1251
+ }
1252
+ if (!(addresses == null ? void 0 : addresses.length)) {
1253
+ return {
1254
+ error: [
1255
+ {
1256
+ message: "No addresses found for this postcode",
1257
+ path: ["postcode"]
1258
+ }
1259
+ ]
1260
+ };
1261
+ }
1262
+ const mappedAddresses = addressLookupMapper(addresses);
1263
+ return mappedAddresses;
1264
+ };
1265
+ var fetchAddressesFromPostcodeLookup = async (postcode, osPlacesConfig) => {
1266
+ const MAX_RETRIES = 3;
1267
+ const INITIAL_BACKOFF_MS = 100;
1268
+ for (let attemptNumber = 1; attemptNumber <= MAX_RETRIES; attemptNumber++) {
1269
+ try {
1270
+ const { clientId, osPlacesStub } = osPlacesConfig;
1271
+ if (osPlacesStub) {
1272
+ const response2 = mockPostcode(postcode);
1273
+ return response2.features ?? [];
1274
+ }
1275
+ const response = await fetchFromPlacesAPI(clientId, postcode);
1276
+ return response.features ?? [];
1277
+ } catch (error) {
1278
+ const shouldRetry = isRetryable(error) && attemptNumber < MAX_RETRIES;
1279
+ if (!shouldRetry) {
1280
+ return buildErrorResponse(error.message);
1281
+ }
1282
+ const backoffMs = calculateExponentialBackoff(attemptNumber, INITIAL_BACKOFF_MS);
1283
+ await delayBeforeRetry(backoffMs);
1284
+ }
1285
+ }
1286
+ };
1287
+ var calculateExponentialBackoff = (attemptNumber, baseMs) => {
1288
+ const exponent = attemptNumber - 1;
1289
+ const powerOf2 = Math.pow(2, exponent);
1290
+ const backoffDuration = baseMs * powerOf2;
1291
+ return backoffDuration;
1292
+ };
1293
+ function delayBeforeRetry(ms) {
1294
+ const callback = (resolve) => {
1295
+ setTimeout(resolve, ms);
1296
+ };
1297
+ return new Promise(callback);
1298
+ }
1299
+ var buildErrorResponse = (message) => {
1300
+ return {
1301
+ error: [
1302
+ {
1303
+ message: message || "Failed to fetch addresses",
1304
+ path: ["postcode"]
1305
+ }
1306
+ ]
1307
+ };
1308
+ };
1309
+ var fetchFromPlacesAPI = async (clientId, postcode) => {
1310
+ const response = await import_osdatahub.placesAPI.postcode(clientId, postcode, { limit: 150 });
1311
+ return response;
1312
+ };
1313
+ var isRetryable = (error) => {
1314
+ const networkErrors = ["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT"];
1315
+ if (networkErrors.includes(error.code)) {
1316
+ return true;
1317
+ }
1318
+ if (error.status && error.status >= INTERNAL_SERVER_ERROR) {
1319
+ return true;
1320
+ }
1321
+ return false;
1322
+ };
1323
+
1324
+ // src/services/services.js
1325
+ var services = {
1326
+ addressLookup: addressLookupService
1327
+ };
981
1328
  // Annotate the CommonJS export names for ESM import in node:
982
1329
  0 && (module.exports = {
983
1330
  constants,
@@ -985,5 +1332,6 @@ var utils = {
985
1332
  mutations,
986
1333
  presenters,
987
1334
  schemas,
1335
+ services,
988
1336
  utils
989
1337
  });