@defra/forms-engine-plugin 4.0.38 → 4.0.39

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.
@@ -6,6 +6,7 @@ import { type FormRequest, type FormResponseToolkit } from '~/src/server/routes/
6
6
  export declare class StatusPageController extends QuestionPageController {
7
7
  pageDef: PageStatus;
8
8
  allowSaveAndExit: boolean;
9
+ showReferenceNumber: boolean;
9
10
  constructor(model: FormModel, pageDef: PageStatus);
10
11
  getRelevantPath(): string;
11
12
  makeGetRouteHandler(): (request: FormRequest, context: FormContext, h: FormResponseToolkit) => Promise<import("@hapi/hapi").ResponseObject>;
@@ -2,9 +2,11 @@ import { getCacheService } from "../helpers.js";
2
2
  import { QuestionPageController } from "./QuestionPageController.js";
3
3
  export class StatusPageController extends QuestionPageController {
4
4
  allowSaveAndExit = false;
5
+ showReferenceNumber = false;
5
6
  constructor(model, pageDef) {
6
7
  super(model, pageDef);
7
8
  this.viewName = 'confirmation';
9
+ this.showReferenceNumber = model.def.options?.showReferenceNumber ?? false;
8
10
  }
9
11
  getRelevantPath() {
10
12
  return this.getStatusPath();
@@ -41,7 +43,9 @@ export class StatusPageController extends QuestionPageController {
41
43
  return h.view(viewName, {
42
44
  ...viewModel,
43
45
  submissionGuidance,
44
- formName
46
+ formName,
47
+ showReferenceNumber: this.showReferenceNumber,
48
+ referenceNumber: context.referenceNumber
45
49
  });
46
50
  };
47
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"StatusPageController.js","names":["getCacheService","QuestionPageController","StatusPageController","allowSaveAndExit","constructor","model","pageDef","viewName","getRelevantPath","getStatusPath","makeGetRouteHandler","request","context","h","viewModel","cacheService","server","confirmationState","getConfirmationState","confirmed","proceed","getStartPath","slug","params","formsService","services","getFormMetadata","getFormMetadataById","submissionGuidance","storedFormId","formId","formName","title","undefined","view"],"sources":["../../../../../src/server/plugins/engine/pageControllers/StatusPageController.ts"],"sourcesContent":["import { type PageStatus } from '@defra/forms-model'\n\nimport { getCacheService } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class StatusPageController extends QuestionPageController {\n declare pageDef: PageStatus\n allowSaveAndExit = false\n\n constructor(model: FormModel, pageDef: PageStatus) {\n super(model, pageDef)\n this.viewName = 'confirmation'\n }\n\n getRelevantPath() {\n return this.getStatusPath()\n }\n\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel, viewName } = this\n\n const cacheService = getCacheService(request.server)\n const confirmationState = await cacheService.getConfirmationState(request)\n\n // If there's no confirmation state, then\n // redirect the user back to the start of the form\n if (!confirmationState.confirmed) {\n return this.proceed(request, h, this.getStartPath())\n }\n\n const slug = request.params.slug\n const { formsService } = this.model.services\n const { getFormMetadata, getFormMetadataById } = formsService\n\n const { submissionGuidance } = await getFormMetadata(slug)\n\n // Re-read form name if overriding display (for example, in a feedback form)\n const storedFormId = confirmationState.formId\n const formName = storedFormId\n ? (await getFormMetadataById(storedFormId)).title\n : undefined\n\n return h.view(viewName, {\n ...viewModel,\n submissionGuidance,\n formName\n })\n }\n }\n}\n"],"mappings":"AAEA,SAASA,eAAe;AAExB,SAASC,sBAAsB;AAO/B,OAAO,MAAMC,oBAAoB,SAASD,sBAAsB,CAAC;EAE/DE,gBAAgB,GAAG,KAAK;EAExBC,WAAWA,CAACC,KAAgB,EAAEC,OAAmB,EAAE;IACjD,KAAK,CAACD,KAAK,EAAEC,OAAO,CAAC;IACrB,IAAI,CAACC,QAAQ,GAAG,cAAc;EAChC;EAEAC,eAAeA,CAAA,EAAG;IAChB,OAAO,IAAI,CAACC,aAAa,CAAC,CAAC;EAC7B;EAEAC,mBAAmBA,CAAA,EAAG;IACpB,OAAO,OACLC,OAAoB,EACpBC,OAAoB,EACpBC,CAAsB,KACnB;MACH,MAAM;QAAEC,SAAS;QAAEP;MAAS,CAAC,GAAG,IAAI;MAEpC,MAAMQ,YAAY,GAAGf,eAAe,CAACW,OAAO,CAACK,MAAM,CAAC;MACpD,MAAMC,iBAAiB,GAAG,MAAMF,YAAY,CAACG,oBAAoB,CAACP,OAAO,CAAC;;MAE1E;MACA;MACA,IAAI,CAACM,iBAAiB,CAACE,SAAS,EAAE;QAChC,OAAO,IAAI,CAACC,OAAO,CAACT,OAAO,EAAEE,CAAC,EAAE,IAAI,CAACQ,YAAY,CAAC,CAAC,CAAC;MACtD;MAEA,MAAMC,IAAI,GAAGX,OAAO,CAACY,MAAM,CAACD,IAAI;MAChC,MAAM;QAAEE;MAAa,CAAC,GAAG,IAAI,CAACnB,KAAK,CAACoB,QAAQ;MAC5C,MAAM;QAAEC,eAAe;QAAEC;MAAoB,CAAC,GAAGH,YAAY;MAE7D,MAAM;QAAEI;MAAmB,CAAC,GAAG,MAAMF,eAAe,CAACJ,IAAI,CAAC;;MAE1D;MACA,MAAMO,YAAY,GAAGZ,iBAAiB,CAACa,MAAM;MAC7C,MAAMC,QAAQ,GAAGF,YAAY,GACzB,CAAC,MAAMF,mBAAmB,CAACE,YAAY,CAAC,EAAEG,KAAK,GAC/CC,SAAS;MAEb,OAAOpB,CAAC,CAACqB,IAAI,CAAC3B,QAAQ,EAAE;QACtB,GAAGO,SAAS;QACZc,kBAAkB;QAClBG;MACF,CAAC,CAAC;IACJ,CAAC;EACH;AACF","ignoreList":[]}
1
+ {"version":3,"file":"StatusPageController.js","names":["getCacheService","QuestionPageController","StatusPageController","allowSaveAndExit","showReferenceNumber","constructor","model","pageDef","viewName","def","options","getRelevantPath","getStatusPath","makeGetRouteHandler","request","context","h","viewModel","cacheService","server","confirmationState","getConfirmationState","confirmed","proceed","getStartPath","slug","params","formsService","services","getFormMetadata","getFormMetadataById","submissionGuidance","storedFormId","formId","formName","title","undefined","view","referenceNumber"],"sources":["../../../../../src/server/plugins/engine/pageControllers/StatusPageController.ts"],"sourcesContent":["import { type PageStatus } from '@defra/forms-model'\n\nimport { getCacheService } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class StatusPageController extends QuestionPageController {\n declare pageDef: PageStatus\n allowSaveAndExit = false\n showReferenceNumber = false\n\n constructor(model: FormModel, pageDef: PageStatus) {\n super(model, pageDef)\n this.viewName = 'confirmation'\n this.showReferenceNumber = model.def.options?.showReferenceNumber ?? false\n }\n\n getRelevantPath() {\n return this.getStatusPath()\n }\n\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel, viewName } = this\n\n const cacheService = getCacheService(request.server)\n const confirmationState = await cacheService.getConfirmationState(request)\n\n // If there's no confirmation state, then\n // redirect the user back to the start of the form\n if (!confirmationState.confirmed) {\n return this.proceed(request, h, this.getStartPath())\n }\n\n const slug = request.params.slug\n const { formsService } = this.model.services\n const { getFormMetadata, getFormMetadataById } = formsService\n\n const { submissionGuidance } = await getFormMetadata(slug)\n\n // Re-read form name if overriding display (for example, in a feedback form)\n const storedFormId = confirmationState.formId\n const formName = storedFormId\n ? (await getFormMetadataById(storedFormId)).title\n : undefined\n\n return h.view(viewName, {\n ...viewModel,\n submissionGuidance,\n formName,\n showReferenceNumber: this.showReferenceNumber,\n referenceNumber: context.referenceNumber\n })\n }\n }\n}\n"],"mappings":"AAEA,SAASA,eAAe;AAExB,SAASC,sBAAsB;AAO/B,OAAO,MAAMC,oBAAoB,SAASD,sBAAsB,CAAC;EAE/DE,gBAAgB,GAAG,KAAK;EACxBC,mBAAmB,GAAG,KAAK;EAE3BC,WAAWA,CAACC,KAAgB,EAAEC,OAAmB,EAAE;IACjD,KAAK,CAACD,KAAK,EAAEC,OAAO,CAAC;IACrB,IAAI,CAACC,QAAQ,GAAG,cAAc;IAC9B,IAAI,CAACJ,mBAAmB,GAAGE,KAAK,CAACG,GAAG,CAACC,OAAO,EAAEN,mBAAmB,IAAI,KAAK;EAC5E;EAEAO,eAAeA,CAAA,EAAG;IAChB,OAAO,IAAI,CAACC,aAAa,CAAC,CAAC;EAC7B;EAEAC,mBAAmBA,CAAA,EAAG;IACpB,OAAO,OACLC,OAAoB,EACpBC,OAAoB,EACpBC,CAAsB,KACnB;MACH,MAAM;QAAEC,SAAS;QAAET;MAAS,CAAC,GAAG,IAAI;MAEpC,MAAMU,YAAY,GAAGlB,eAAe,CAACc,OAAO,CAACK,MAAM,CAAC;MACpD,MAAMC,iBAAiB,GAAG,MAAMF,YAAY,CAACG,oBAAoB,CAACP,OAAO,CAAC;;MAE1E;MACA;MACA,IAAI,CAACM,iBAAiB,CAACE,SAAS,EAAE;QAChC,OAAO,IAAI,CAACC,OAAO,CAACT,OAAO,EAAEE,CAAC,EAAE,IAAI,CAACQ,YAAY,CAAC,CAAC,CAAC;MACtD;MAEA,MAAMC,IAAI,GAAGX,OAAO,CAACY,MAAM,CAACD,IAAI;MAChC,MAAM;QAAEE;MAAa,CAAC,GAAG,IAAI,CAACrB,KAAK,CAACsB,QAAQ;MAC5C,MAAM;QAAEC,eAAe;QAAEC;MAAoB,CAAC,GAAGH,YAAY;MAE7D,MAAM;QAAEI;MAAmB,CAAC,GAAG,MAAMF,eAAe,CAACJ,IAAI,CAAC;;MAE1D;MACA,MAAMO,YAAY,GAAGZ,iBAAiB,CAACa,MAAM;MAC7C,MAAMC,QAAQ,GAAGF,YAAY,GACzB,CAAC,MAAMF,mBAAmB,CAACE,YAAY,CAAC,EAAEG,KAAK,GAC/CC,SAAS;MAEb,OAAOpB,CAAC,CAACqB,IAAI,CAAC7B,QAAQ,EAAE;QACtB,GAAGS,SAAS;QACZc,kBAAkB;QAClBG,QAAQ;QACR9B,mBAAmB,EAAE,IAAI,CAACA,mBAAmB;QAC7CkC,eAAe,EAAEvB,OAAO,CAACuB;MAC3B,CAAC,CAAC;IACJ,CAAC;EACH;AACF","ignoreList":[]}
@@ -1,5 +1,12 @@
1
+ /**
2
+ * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.
3
+ * @param strCodes - array of binary input values
4
+ */
5
+ export declare function convertToDecAlpha(strCodes: number[]): string;
1
6
  /**
2
7
  * Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.
3
8
  * Provides no guarantee on uniqueness.
9
+ * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed
10
+ * (see https://gunkies.org/wiki/DEC_alphabet )
4
11
  */
5
12
  export declare function generateUniqueReference(prefix?: string): string;
@@ -1,17 +1,42 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { RegExpMatcher, englishDataset, englishRecommendedTransformers } from 'obscenity';
3
+
4
+ /**
5
+ * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.
6
+ * @param strCodes - array of binary input values
7
+ */
8
+ export function convertToDecAlpha(strCodes) {
9
+ const validChars = 'ABCDEFHJKLMNPRSTUVWXYZ23456789';
10
+ const strLen = validChars.length;
11
+ const outArray = [];
12
+ strCodes.forEach(code => {
13
+ const pos = code / 256 * strLen;
14
+ outArray.push(validChars.charAt(pos));
15
+ });
16
+ return outArray.join('');
17
+ }
2
18
 
3
19
  /**
4
20
  * Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.
5
21
  * Provides no guarantee on uniqueness.
22
+ * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed
23
+ * (see https://gunkies.org/wiki/DEC_alphabet )
6
24
  */
7
25
  export function generateUniqueReference(prefix) {
8
26
  const segmentLength = 3;
9
27
  const segmentCount = prefix ? 2 : 3;
10
28
  prefix = prefix ? `${prefix}-` : '';
11
- const segments = Array.from({
12
- length: segmentCount
13
- }, () => randomBytes(segmentLength).toString('hex').slice(0, segmentLength) // 0-9a-f, might be good enough?
14
- );
15
- return `${prefix}${segments.join('-')}`.toUpperCase();
29
+ const profanityMatcher = new RegExpMatcher({
30
+ ...englishDataset.build(),
31
+ ...englishRecommendedTransformers
32
+ });
33
+ let referenceNumber;
34
+ do {
35
+ const segments = Array.from({
36
+ length: segmentCount
37
+ }, () => convertToDecAlpha([...randomBytes(segmentLength)]).slice(0, segmentLength * 2));
38
+ referenceNumber = `${prefix}${segments.join('-')}`.toUpperCase();
39
+ } while (profanityMatcher.hasMatch(referenceNumber.replaceAll('-', '')));
40
+ return referenceNumber;
16
41
  }
17
42
  //# sourceMappingURL=referenceNumbers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"referenceNumbers.js","names":["randomBytes","generateUniqueReference","prefix","segmentLength","segmentCount","segments","Array","from","length","toString","slice","join","toUpperCase"],"sources":["../../../../src/server/plugins/engine/referenceNumbers.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\n\n/**\n * Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.\n * Provides no guarantee on uniqueness.\n */\nexport function generateUniqueReference(prefix?: string) {\n const segmentLength = 3\n const segmentCount = prefix ? 2 : 3\n prefix = prefix ? `${prefix}-` : ''\n\n const segments = Array.from(\n { length: segmentCount },\n () => randomBytes(segmentLength).toString('hex').slice(0, segmentLength) // 0-9a-f, might be good enough?\n )\n\n return `${prefix}${segments.join('-')}`.toUpperCase()\n}\n"],"mappings":"AAAA,SAASA,WAAW,QAAQ,aAAa;;AAEzC;AACA;AACA;AACA;AACA,OAAO,SAASC,uBAAuBA,CAACC,MAAe,EAAE;EACvD,MAAMC,aAAa,GAAG,CAAC;EACvB,MAAMC,YAAY,GAAGF,MAAM,GAAG,CAAC,GAAG,CAAC;EACnCA,MAAM,GAAGA,MAAM,GAAG,GAAGA,MAAM,GAAG,GAAG,EAAE;EAEnC,MAAMG,QAAQ,GAAGC,KAAK,CAACC,IAAI,CACzB;IAAEC,MAAM,EAAEJ;EAAa,CAAC,EACxB,MAAMJ,WAAW,CAACG,aAAa,CAAC,CAACM,QAAQ,CAAC,KAAK,CAAC,CAACC,KAAK,CAAC,CAAC,EAAEP,aAAa,CAAC,CAAC;EAC3E,CAAC;EAED,OAAO,GAAGD,MAAM,GAAGG,QAAQ,CAACM,IAAI,CAAC,GAAG,CAAC,EAAE,CAACC,WAAW,CAAC,CAAC;AACvD","ignoreList":[]}
1
+ {"version":3,"file":"referenceNumbers.js","names":["randomBytes","RegExpMatcher","englishDataset","englishRecommendedTransformers","convertToDecAlpha","strCodes","validChars","strLen","length","outArray","forEach","code","pos","push","charAt","join","generateUniqueReference","prefix","segmentLength","segmentCount","profanityMatcher","build","referenceNumber","segments","Array","from","slice","toUpperCase","hasMatch","replaceAll"],"sources":["../../../../src/server/plugins/engine/referenceNumbers.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\n\nimport {\n RegExpMatcher,\n englishDataset,\n englishRecommendedTransformers\n} from 'obscenity'\n\n/**\n * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.\n * @param strCodes - array of binary input values\n */\nexport function convertToDecAlpha(strCodes: number[]) {\n const validChars = 'ABCDEFHJKLMNPRSTUVWXYZ23456789'\n const strLen = validChars.length\n const outArray = [] as string[]\n\n strCodes.forEach((code) => {\n const pos = (code / 256) * strLen\n outArray.push(validChars.charAt(pos))\n })\n\n return outArray.join('')\n}\n\n/**\n * Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.\n * Provides no guarantee on uniqueness.\n * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed\n * (see https://gunkies.org/wiki/DEC_alphabet )\n */\nexport function generateUniqueReference(prefix?: string) {\n const segmentLength = 3\n const segmentCount = prefix ? 2 : 3\n prefix = prefix ? `${prefix}-` : ''\n\n const profanityMatcher = new RegExpMatcher({\n ...englishDataset.build(),\n ...englishRecommendedTransformers\n })\n\n let referenceNumber\n\n do {\n const segments = Array.from({ length: segmentCount }, () =>\n convertToDecAlpha([...randomBytes(segmentLength)]).slice(\n 0,\n segmentLength * 2\n )\n )\n\n referenceNumber = `${prefix}${segments.join('-')}`.toUpperCase()\n } while (profanityMatcher.hasMatch(referenceNumber.replaceAll('-', '')))\n\n return referenceNumber\n}\n"],"mappings":"AAAA,SAASA,WAAW,QAAQ,aAAa;AAEzC,SACEC,aAAa,EACbC,cAAc,EACdC,8BAA8B,QACzB,WAAW;;AAElB;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAACC,QAAkB,EAAE;EACpD,MAAMC,UAAU,GAAG,gCAAgC;EACnD,MAAMC,MAAM,GAAGD,UAAU,CAACE,MAAM;EAChC,MAAMC,QAAQ,GAAG,EAAc;EAE/BJ,QAAQ,CAACK,OAAO,CAAEC,IAAI,IAAK;IACzB,MAAMC,GAAG,GAAID,IAAI,GAAG,GAAG,GAAIJ,MAAM;IACjCE,QAAQ,CAACI,IAAI,CAACP,UAAU,CAACQ,MAAM,CAACF,GAAG,CAAC,CAAC;EACvC,CAAC,CAAC;EAEF,OAAOH,QAAQ,CAACM,IAAI,CAAC,EAAE,CAAC;AAC1B;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,uBAAuBA,CAACC,MAAe,EAAE;EACvD,MAAMC,aAAa,GAAG,CAAC;EACvB,MAAMC,YAAY,GAAGF,MAAM,GAAG,CAAC,GAAG,CAAC;EACnCA,MAAM,GAAGA,MAAM,GAAG,GAAGA,MAAM,GAAG,GAAG,EAAE;EAEnC,MAAMG,gBAAgB,GAAG,IAAInB,aAAa,CAAC;IACzC,GAAGC,cAAc,CAACmB,KAAK,CAAC,CAAC;IACzB,GAAGlB;EACL,CAAC,CAAC;EAEF,IAAImB,eAAe;EAEnB,GAAG;IACD,MAAMC,QAAQ,GAAGC,KAAK,CAACC,IAAI,CAAC;MAAEjB,MAAM,EAAEW;IAAa,CAAC,EAAE,MACpDf,iBAAiB,CAAC,CAAC,GAAGJ,WAAW,CAACkB,aAAa,CAAC,CAAC,CAAC,CAACQ,KAAK,CACtD,CAAC,EACDR,aAAa,GAAG,CAClB,CACF,CAAC;IAEDI,eAAe,GAAG,GAAGL,MAAM,GAAGM,QAAQ,CAACR,IAAI,CAAC,GAAG,CAAC,EAAE,CAACY,WAAW,CAAC,CAAC;EAClE,CAAC,QAAQP,gBAAgB,CAACQ,QAAQ,CAACN,eAAe,CAACO,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;EAEvE,OAAOP,eAAe;AACxB","ignoreList":[]}
@@ -8,7 +8,8 @@
8
8
  <div class="govuk-grid-row">
9
9
  <div class="govuk-grid-column-two-thirds">
10
10
  {{ govukPanel({
11
- titleText: pageTitle
11
+ titleText: pageTitle,
12
+ html: "Your reference number<br><strong>" + referenceNumber + "</strong>" if showReferenceNumber
12
13
  }) }}
13
14
  <h2 class="govuk-heading-m">What happens next</h2>
14
15
  <div class="app-prose-scope">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.0.38",
3
+ "version": "4.0.39",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "license": "SEE LICENSE IN LICENSE",
72
72
  "dependencies": {
73
- "@defra/forms-model": "^3.0.597",
73
+ "@defra/forms-model": "^3.0.603",
74
74
  "@defra/hapi-tracing": "^1.29.0",
75
75
  "@elastic/ecs-pino-format": "^1.5.0",
76
76
  "@hapi/boom": "^10.0.1",
@@ -108,6 +108,7 @@
108
108
  "lodash": "^4.17.21",
109
109
  "marked": "^15.0.12",
110
110
  "nunjucks": "^3.2.4",
111
+ "obscenity": "^0.4.5",
111
112
  "outdent": "^0.8.0",
112
113
  "pino": "^9.14.0",
113
114
  "pino-pretty": "^13.1.2",
@@ -12,10 +12,12 @@ import {
12
12
  export class StatusPageController extends QuestionPageController {
13
13
  declare pageDef: PageStatus
14
14
  allowSaveAndExit = false
15
+ showReferenceNumber = false
15
16
 
16
17
  constructor(model: FormModel, pageDef: PageStatus) {
17
18
  super(model, pageDef)
18
19
  this.viewName = 'confirmation'
20
+ this.showReferenceNumber = model.def.options?.showReferenceNumber ?? false
19
21
  }
20
22
 
21
23
  getRelevantPath() {
@@ -54,7 +56,9 @@ export class StatusPageController extends QuestionPageController {
54
56
  return h.view(viewName, {
55
57
  ...viewModel,
56
58
  submissionGuidance,
57
- formName
59
+ formName,
60
+ showReferenceNumber: this.showReferenceNumber,
61
+ referenceNumber: context.referenceNumber
58
62
  })
59
63
  }
60
64
  }
@@ -1,4 +1,7 @@
1
- import { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'
1
+ import {
2
+ convertToDecAlpha,
3
+ generateUniqueReference
4
+ } from '~/src/server/plugins/engine/referenceNumbers.js'
2
5
 
3
6
  describe('generateUniqueReference', () => {
4
7
  it('should generate a reference number with 3 segments when no prefix is provided', () => {
@@ -30,4 +33,42 @@ describe('generateUniqueReference', () => {
30
33
  const referenceNumber2 = generateUniqueReference()
31
34
  expect(referenceNumber1).not.toBe(referenceNumber2)
32
35
  })
36
+
37
+ describe('convertToDecAlpha', () => {
38
+ it('should generate correct characters in string', () => {
39
+ const allValuesHexPairs = Array.from(Array(256).keys())
40
+ expect(convertToDecAlpha(allValuesHexPairs)).toBe(
41
+ 'AAAAAAAAA' +
42
+ 'BBBBBBBBB' +
43
+ 'CCCCCCCC' +
44
+ 'DDDDDDDDD' +
45
+ 'EEEEEEEE' +
46
+ 'FFFFFFFFF' +
47
+ 'HHHHHHHH' +
48
+ 'JJJJJJJJJ' +
49
+ 'KKKKKKKK' +
50
+ 'LLLLLLLLL' +
51
+ 'MMMMMMMM' +
52
+ 'NNNNNNNNN' +
53
+ 'PPPPPPPP' +
54
+ 'RRRRRRRRR' +
55
+ 'SSSSSSSS' +
56
+ 'TTTTTTTTT' +
57
+ 'UUUUUUUUU' +
58
+ 'VVVVVVVV' +
59
+ 'WWWWWWWWW' +
60
+ 'XXXXXXXX' +
61
+ 'YYYYYYYYY' +
62
+ 'ZZZZZZZZ' +
63
+ '222222222' +
64
+ '33333333' +
65
+ '444444444' +
66
+ '55555555' +
67
+ '666666666' +
68
+ '77777777' +
69
+ '888888888' +
70
+ '99999999'
71
+ )
72
+ })
73
+ })
33
74
  })
@@ -1,18 +1,56 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
 
3
+ import {
4
+ RegExpMatcher,
5
+ englishDataset,
6
+ englishRecommendedTransformers
7
+ } from 'obscenity'
8
+
9
+ /**
10
+ * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.
11
+ * @param strCodes - array of binary input values
12
+ */
13
+ export function convertToDecAlpha(strCodes: number[]) {
14
+ const validChars = 'ABCDEFHJKLMNPRSTUVWXYZ23456789'
15
+ const strLen = validChars.length
16
+ const outArray = [] as string[]
17
+
18
+ strCodes.forEach((code) => {
19
+ const pos = (code / 256) * strLen
20
+ outArray.push(validChars.charAt(pos))
21
+ })
22
+
23
+ return outArray.join('')
24
+ }
25
+
3
26
  /**
4
27
  * Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.
5
28
  * Provides no guarantee on uniqueness.
29
+ * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed
30
+ * (see https://gunkies.org/wiki/DEC_alphabet )
6
31
  */
7
32
  export function generateUniqueReference(prefix?: string) {
8
33
  const segmentLength = 3
9
34
  const segmentCount = prefix ? 2 : 3
10
35
  prefix = prefix ? `${prefix}-` : ''
11
36
 
12
- const segments = Array.from(
13
- { length: segmentCount },
14
- () => randomBytes(segmentLength).toString('hex').slice(0, segmentLength) // 0-9a-f, might be good enough?
15
- )
37
+ const profanityMatcher = new RegExpMatcher({
38
+ ...englishDataset.build(),
39
+ ...englishRecommendedTransformers
40
+ })
41
+
42
+ let referenceNumber
43
+
44
+ do {
45
+ const segments = Array.from({ length: segmentCount }, () =>
46
+ convertToDecAlpha([...randomBytes(segmentLength)]).slice(
47
+ 0,
48
+ segmentLength * 2
49
+ )
50
+ )
51
+
52
+ referenceNumber = `${prefix}${segments.join('-')}`.toUpperCase()
53
+ } while (profanityMatcher.hasMatch(referenceNumber.replaceAll('-', '')))
16
54
 
17
- return `${prefix}${segments.join('-')}`.toUpperCase()
55
+ return referenceNumber
18
56
  }
@@ -8,7 +8,8 @@
8
8
  <div class="govuk-grid-row">
9
9
  <div class="govuk-grid-column-two-thirds">
10
10
  {{ govukPanel({
11
- titleText: pageTitle
11
+ titleText: pageTitle,
12
+ html: "Your reference number<br><strong>" + referenceNumber + "</strong>" if showReferenceNumber
12
13
  }) }}
13
14
  <h2 class="govuk-heading-m">What happens next</h2>
14
15
  <div class="app-prose-scope">