@defra/forms-engine-plugin 4.0.43 → 4.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.public/stylesheets/application.min.css +1 -1
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/_payment-field.scss +8 -0
  4. package/.server/client/stylesheets/application.scss +2 -0
  5. package/.server/index.js +3 -1
  6. package/.server/index.js.map +1 -1
  7. package/.server/server/constants.d.ts +1 -0
  8. package/.server/server/constants.js +1 -0
  9. package/.server/server/constants.js.map +1 -1
  10. package/.server/server/forms/payment-test.yaml +42 -0
  11. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
  12. package/.server/server/plugins/engine/components/FormComponent.d.ts +1 -0
  13. package/.server/server/plugins/engine/components/FormComponent.js +1 -0
  14. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  15. package/.server/server/plugins/engine/components/PaymentField.d.ts +135 -0
  16. package/.server/server/plugins/engine/components/PaymentField.js +228 -0
  17. package/.server/server/plugins/engine/components/PaymentField.js.map +1 -0
  18. package/.server/server/plugins/engine/components/PaymentField.types.d.ts +21 -0
  19. package/.server/server/plugins/engine/components/PaymentField.types.js +2 -0
  20. package/.server/server/plugins/engine/components/PaymentField.types.js.map +1 -0
  21. package/.server/server/plugins/engine/components/UkAddressField.d.ts +1 -1
  22. package/.server/server/plugins/engine/components/UkAddressField.js +3 -1
  23. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  24. package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
  25. package/.server/server/plugins/engine/components/helpers/components.js +3 -0
  26. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  27. package/.server/server/plugins/engine/components/index.d.ts +1 -0
  28. package/.server/server/plugins/engine/components/index.js +1 -0
  29. package/.server/server/plugins/engine/components/index.js.map +1 -1
  30. package/.server/server/plugins/engine/helpers.d.ts +1 -0
  31. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +3 -0
  32. package/.server/server/plugins/engine/models/SummaryViewModel.js +7 -0
  33. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  34. package/.server/server/plugins/engine/outputFormatters/human/v1.js +34 -1
  35. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  36. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +22 -0
  37. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +43 -1
  38. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  39. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +29 -8
  40. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  41. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -0
  42. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +17 -0
  43. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +173 -51
  44. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  45. package/.server/server/plugins/engine/pageControllers/errors.d.ts +31 -0
  46. package/.server/server/plugins/engine/pageControllers/errors.js +59 -2
  47. package/.server/server/plugins/engine/pageControllers/errors.js.map +1 -1
  48. package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +27 -0
  49. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +77 -0
  50. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -0
  51. package/.server/server/plugins/engine/plugin.js +4 -1
  52. package/.server/server/plugins/engine/plugin.js.map +1 -1
  53. package/.server/server/plugins/engine/routes/index.js +8 -4
  54. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  55. package/.server/server/plugins/engine/routes/payment-helper.d.ts +14 -0
  56. package/.server/server/plugins/engine/routes/payment-helper.js +41 -0
  57. package/.server/server/plugins/engine/routes/payment-helper.js.map +1 -0
  58. package/.server/server/plugins/engine/routes/payment-helper.test.js +81 -0
  59. package/.server/server/plugins/engine/routes/payment-helper.test.js.map +1 -0
  60. package/.server/server/plugins/engine/routes/payment.d.ts +8 -0
  61. package/.server/server/plugins/engine/routes/payment.js +140 -0
  62. package/.server/server/plugins/engine/routes/payment.js.map +1 -0
  63. package/.server/server/plugins/engine/routes/payment.test.js +187 -0
  64. package/.server/server/plugins/engine/routes/payment.test.js.map +1 -0
  65. package/.server/server/plugins/engine/services/localFormsService.js +6 -0
  66. package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
  67. package/.server/server/plugins/engine/types/schema.js +7 -0
  68. package/.server/server/plugins/engine/types/schema.js.map +1 -1
  69. package/.server/server/plugins/engine/types.d.ts +19 -1
  70. package/.server/server/plugins/engine/types.js +4 -0
  71. package/.server/server/plugins/engine/types.js.map +1 -1
  72. package/.server/server/plugins/engine/validationHelpers.d.ts +1 -1
  73. package/.server/server/plugins/engine/validationHelpers.js.map +1 -1
  74. package/.server/server/plugins/engine/views/components/paymentfield.html +42 -0
  75. package/.server/server/plugins/engine/views/index.html +9 -1
  76. package/.server/server/plugins/engine/views/partials/form.html +20 -5
  77. package/.server/server/plugins/engine/views/summary.html +17 -1
  78. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  79. package/.server/server/plugins/payment/helper.d.ts +30 -0
  80. package/.server/server/plugins/payment/helper.js +49 -0
  81. package/.server/server/plugins/payment/helper.js.map +1 -0
  82. package/.server/server/plugins/payment/helper.test.js +37 -0
  83. package/.server/server/plugins/payment/helper.test.js.map +1 -0
  84. package/.server/server/plugins/payment/service.d.ts +40 -0
  85. package/.server/server/plugins/payment/service.js +129 -0
  86. package/.server/server/plugins/payment/service.js.map +1 -0
  87. package/.server/server/plugins/payment/service.test.js +162 -0
  88. package/.server/server/plugins/payment/service.test.js.map +1 -0
  89. package/.server/server/plugins/payment/types.d.ts +172 -0
  90. package/.server/server/plugins/payment/types.js +78 -0
  91. package/.server/server/plugins/payment/types.js.map +1 -0
  92. package/.server/server/types.d.ts +2 -0
  93. package/.server/server/types.js.map +1 -1
  94. package/.server/typings/hapi/index.d.js.map +1 -1
  95. package/README.md +12 -9
  96. package/package.json +2 -2
  97. package/src/client/stylesheets/_payment-field.scss +8 -0
  98. package/src/client/stylesheets/application.scss +2 -0
  99. package/src/index.ts +5 -1
  100. package/src/server/constants.js +1 -0
  101. package/src/server/forms/payment-test.yaml +42 -0
  102. package/src/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
  103. package/src/server/plugins/engine/components/FormComponent.ts +1 -0
  104. package/src/server/plugins/engine/components/PaymentField.test.ts +611 -0
  105. package/src/server/plugins/engine/components/PaymentField.ts +367 -0
  106. package/src/server/plugins/engine/components/PaymentField.types.ts +21 -0
  107. package/src/server/plugins/engine/components/UkAddressField.ts +2 -1
  108. package/src/server/plugins/engine/components/helpers/components.ts +5 -0
  109. package/src/server/plugins/engine/components/index.ts +1 -0
  110. package/src/server/plugins/engine/models/SummaryViewModel.ts +8 -0
  111. package/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts +147 -0
  112. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +105 -103
  113. package/src/server/plugins/engine/outputFormatters/human/v1.ts +61 -2
  114. package/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts +115 -0
  115. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +60 -1
  116. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +32 -6
  117. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +247 -72
  118. package/src/server/plugins/engine/pageControllers/errors.test.ts +13 -1
  119. package/src/server/plugins/engine/pageControllers/errors.ts +79 -4
  120. package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +299 -0
  121. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +110 -0
  122. package/src/server/plugins/engine/plugin.ts +11 -6
  123. package/src/server/plugins/engine/routes/index.ts +17 -16
  124. package/src/server/plugins/engine/routes/payment-helper.js +39 -0
  125. package/src/server/plugins/engine/routes/payment-helper.test.js +90 -0
  126. package/src/server/plugins/engine/routes/payment.js +151 -0
  127. package/src/server/plugins/engine/routes/payment.test.js +180 -0
  128. package/src/server/plugins/engine/services/localFormsService.js +7 -0
  129. package/src/server/plugins/engine/types/schema.ts +9 -0
  130. package/src/server/plugins/engine/types.ts +24 -1
  131. package/src/server/plugins/engine/validationHelpers.ts +1 -1
  132. package/src/server/plugins/engine/views/components/paymentfield.html +42 -0
  133. package/src/server/plugins/engine/views/index.html +9 -1
  134. package/src/server/plugins/engine/views/partials/form.html +20 -5
  135. package/src/server/plugins/engine/views/summary.html +17 -1
  136. package/src/server/plugins/payment/helper.js +56 -0
  137. package/src/server/plugins/payment/helper.test.js +52 -0
  138. package/src/server/plugins/payment/service.js +171 -0
  139. package/src/server/plugins/payment/service.test.js +205 -0
  140. package/src/server/plugins/payment/types.js +77 -0
  141. package/src/server/types.ts +2 -0
  142. package/src/typings/hapi/index.d.ts +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission.js","names":["PaymentField","getAnswer","formatPaymentAmount","formatPaymentDate","buildMainRecords","items","fieldItems","filter","item","records","field","push","buildPaymentRecords","name","title","label","value","state","format","paymentState","getPaymentStateFromState","description","amount","reference","preAuth","createdAt","buildRepeaterRecords","map","subItems","detailItems","subItem"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/helpers/submission.ts"],"sourcesContent":["import { type SubmitPayload } from '@defra/forms-model'\n\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n formatPaymentAmount,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nexport interface SubmitRecord {\n name: string\n title: string\n value: string\n}\n\n/**\n * Builds the main submission records from field items.\n * Regular fields are converted to single records, while PaymentField\n * components are expanded into four separate records.\n */\nexport function buildMainRecords(items: DetailItem[]): SubmitRecord[] {\n const fieldItems = items.filter(\n (item): item is DetailItemField => 'field' in item\n )\n\n const records: SubmitRecord[] = []\n\n for (const item of fieldItems) {\n if (item.field instanceof PaymentField) {\n records.push(...buildPaymentRecords(item))\n } else {\n records.push({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })\n }\n }\n\n return records\n}\n\n/**\n * Expands a PaymentField into four submission records:\n * - Payment description\n * - Payment amount (formatted with currency symbol)\n * - Payment reference\n * - Payment date (formatted date/time)\n *\n * Returns an empty array if no payment state exists.\n */\nexport function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {\n const paymentState = (item.field as PaymentField).getPaymentStateFromState(\n item.state\n )\n\n if (!paymentState) {\n return []\n }\n\n return [\n {\n name: `${item.name}_paymentDescription`,\n title: 'Payment description',\n value: paymentState.description\n },\n {\n name: `${item.name}_paymentAmount`,\n title: 'Payment amount',\n value: formatPaymentAmount(paymentState.amount)\n },\n {\n name: `${item.name}_paymentReference`,\n title: 'Payment reference',\n value: paymentState.reference\n },\n {\n name: `${item.name}_paymentDate`,\n title: 'Payment date',\n value: paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt)\n : ''\n }\n ]\n}\n\n/**\n * Builds the repeater submission records from repeater items.\n */\nexport function buildRepeaterRecords(\n items: DetailItem[]\n): SubmitPayload['repeaters'] {\n return items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => ({\n name: subItem.name,\n title: subItem.label,\n value: getAnswer(subItem.field, subItem.state, { format: 'data' })\n }))\n )\n }))\n}\n"],"mappings":"AAEA,SAASA,YAAY;AACrB,SAASC,SAAS;AAKlB,SACEC,mBAAmB,EACnBC,iBAAiB;AASnB;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,KAAmB,EAAkB;EACpE,MAAMC,UAAU,GAAGD,KAAK,CAACE,MAAM,CAC5BC,IAAI,IAA8B,OAAO,IAAIA,IAChD,CAAC;EAED,MAAMC,OAAuB,GAAG,EAAE;EAElC,KAAK,MAAMD,IAAI,IAAIF,UAAU,EAAE;IAC7B,IAAIE,IAAI,CAACE,KAAK,YAAYV,YAAY,EAAE;MACtCS,OAAO,CAACE,IAAI,CAAC,GAAGC,mBAAmB,CAACJ,IAAI,CAAC,CAAC;IAC5C,CAAC,MAAM;MACLC,OAAO,CAACE,IAAI,CAAC;QACXE,IAAI,EAAEL,IAAI,CAACK,IAAI;QACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;QACjBC,KAAK,EAAEf,SAAS,CAACO,IAAI,CAACE,KAAK,EAAEF,IAAI,CAACS,KAAK,EAAE;UAAEC,MAAM,EAAE;QAAO,CAAC;MAC7D,CAAC,CAAC;IACJ;EACF;EAEA,OAAOT,OAAO;AAChB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,mBAAmBA,CAACJ,IAAqB,EAAkB;EACzE,MAAMW,YAAY,GAAIX,IAAI,CAACE,KAAK,CAAkBU,wBAAwB,CACxEZ,IAAI,CAACS,KACP,CAAC;EAED,IAAI,CAACE,YAAY,EAAE;IACjB,OAAO,EAAE;EACX;EAEA,OAAO,CACL;IACEN,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,qBAAqB;IACvCC,KAAK,EAAE,qBAAqB;IAC5BE,KAAK,EAAEG,YAAY,CAACE;EACtB,CAAC,EACD;IACER,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,gBAAgB;IAClCC,KAAK,EAAE,gBAAgB;IACvBE,KAAK,EAAEd,mBAAmB,CAACiB,YAAY,CAACG,MAAM;EAChD,CAAC,EACD;IACET,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,mBAAmB;IACrCC,KAAK,EAAE,mBAAmB;IAC1BE,KAAK,EAAEG,YAAY,CAACI;EACtB,CAAC,EACD;IACEV,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,cAAc;IAChCC,KAAK,EAAE,cAAc;IACrBE,KAAK,EAAEG,YAAY,CAACK,OAAO,EAAEC,SAAS,GAClCtB,iBAAiB,CAACgB,YAAY,CAACK,OAAO,CAACC,SAAS,CAAC,GACjD;EACN,CAAC,CACF;AACH;;AAEA;AACA;AACA;AACA,OAAO,SAASC,oBAAoBA,CAClCrB,KAAmB,EACS;EAC5B,OAAOA,KAAK,CACTE,MAAM,CAAEC,IAAI,IAAK,UAAU,IAAIA,IAAI,CAAC,CACpCmB,GAAG,CAAEnB,IAAI,KAAM;IACdK,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;IACjBC,KAAK,EAAER,IAAI,CAACoB,QAAQ,CAACD,GAAG,CAAEE,WAAW,IACnCA,WAAW,CAACF,GAAG,CAAEG,OAAO,KAAM;MAC5BjB,IAAI,EAAEiB,OAAO,CAACjB,IAAI;MAClBC,KAAK,EAAEgB,OAAO,CAACf,KAAK;MACpBC,KAAK,EAAEf,SAAS,CAAC6B,OAAO,CAACpB,KAAK,EAAEoB,OAAO,CAACb,KAAK,EAAE;QAAEC,MAAM,EAAE;MAAO,CAAC;IACnE,CAAC,CAAC,CACJ;EACF,CAAC,CAAC,CAAC;AACP","ignoreList":[]}
@@ -1,6 +1,7 @@
1
1
  import { validatePluginOptions } from "./options.js";
2
2
  import { getRoutes as getFileUploadStatusRoutes } from "./routes/file-upload.js";
3
3
  import { makeLoadFormPreHandler } from "./routes/index.js";
4
+ import { getRoutes as getPaymentRoutes } from "./routes/payment.js";
4
5
  import { getRoutes as getQuestionRoutes } from "./routes/questions.js";
5
6
  import { getRoutes as getRepeaterItemDeleteRoutes } from "./routes/repeaters/item-delete.js";
6
7
  import { getRoutes as getRepeaterSummaryRoutes } from "./routes/repeaters/summary.js";
@@ -23,6 +24,7 @@ export const plugin = {
23
24
  preparePageEventRequestOptions,
24
25
  onRequest,
25
26
  ordnanceSurveyApiKey,
27
+ baseUrl,
26
28
  ordnanceSurveyApiSecret
27
29
  } = options;
28
30
  const cacheService = typeof cache === 'string' ? new CacheService({
@@ -55,6 +57,7 @@ export const plugin = {
55
57
  server.expose('viewContext', viewContext);
56
58
  server.expose('cacheService', cacheService);
57
59
  server.expose('saveAndExit', saveAndExit);
60
+ server.expose('baseUrl', baseUrl);
58
61
  server.app.model = model;
59
62
 
60
63
  // In-memory cache of FormModel items, exposed
@@ -75,7 +78,7 @@ export const plugin = {
75
78
  method: loadFormPreHandler
76
79
  }]
77
80
  };
78
- const routes = [...getQuestionRoutes(getRouteOptions, postRouteOptions, preparePageEventRequestOptions, onRequest), ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest), ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions, onRequest), ...getFileUploadStatusRoutes()];
81
+ const routes = [...getPaymentRoutes(), ...getFileUploadStatusRoutes(), ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest), ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions, onRequest), ...getQuestionRoutes(getRouteOptions, postRouteOptions, preparePageEventRequestOptions, onRequest)];
79
82
  server.route(routes); // TODO
80
83
  }
81
84
  };
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerVision","mapPlugin","postcodeLookupPlugin","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cache","saveAndExit","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","onRequest","ordnanceSurveyApiKey","ordnanceSurveyApiSecret","cacheService","cacheName","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","getRouteOptions","pre","method","postRouteOptions","payload","parse","routes","route"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import {\n type Lifecycle,\n type Plugin,\n type RouteOptions,\n type Server,\n type ServerRoute\n} from '@hapi/hapi'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { validatePluginOptions } from '~/src/server/plugins/engine/options.js'\nimport { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'\nimport { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'\nimport { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'\nimport { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { registerVision } from '~/src/server/plugins/engine/vision.js'\nimport { mapPlugin } from '~/src/server/plugins/map/index.js'\nimport { postcodeLookupPlugin } from '~/src/server/plugins/postcode-lookup/index.js'\nimport {\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport { CacheService } from '~/src/server/services/index.js'\n\nexport const plugin = {\n name: '@defra/forms-engine-plugin',\n dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],\n multiple: true,\n async register(server: Server, options: PluginOptions) {\n options = validatePluginOptions(options)\n\n const {\n model,\n cache,\n saveAndExit,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions,\n onRequest,\n ordnanceSurveyApiKey,\n ordnanceSurveyApiSecret\n } = options\n\n const cacheService =\n typeof cache === 'string'\n ? new CacheService({ server, cacheName: cache })\n : cache\n\n await registerVision(server, options)\n\n // Register the postcode lookup plugin only if we have an OS api key\n if (ordnanceSurveyApiKey) {\n await server.register({\n plugin: postcodeLookupPlugin,\n options: {\n ordnanceSurveyApiKey\n }\n })\n }\n\n // Register the maps plugin only if we have an OS api key & secret\n if (ordnanceSurveyApiKey && ordnanceSurveyApiSecret) {\n await server.register({\n plugin: mapPlugin,\n options: {\n ordnanceSurveyApiKey,\n ordnanceSurveyApiSecret\n }\n })\n }\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n server.expose('saveAndExit', saveAndExit)\n\n server.app.model = model\n\n // In-memory cache of FormModel items, exposed\n // (for testing purposes) through `server.app.models`\n const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()\n server.app.models = itemCache\n\n const loadFormPreHandler = makeLoadFormPreHandler(server, options)\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestRefs>\n }\n ]\n }\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestPayloadRefs>\n }\n ]\n }\n\n const routes = [\n ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions,\n onRequest\n ),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),\n ...getRepeaterItemDeleteRoutes(\n getRouteOptions,\n postRouteOptions,\n onRequest\n ),\n ...getFileUploadStatusRoutes()\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"AASA,SAASA,qBAAqB;AAC9B,SAASC,SAAS,IAAIC,yBAAyB;AAC/C,SAASC,sBAAsB;AAC/B,SAASF,SAAS,IAAIG,iBAAiB;AACvC,SAASH,SAAS,IAAII,2BAA2B;AACjD,SAASJ,SAAS,IAAIK,wBAAwB;AAE9C,SAASC,cAAc;AACvB,SAASC,SAAS;AAClB,SAASC,oBAAoB;AAK7B,SAASC,YAAY;AAErB,OAAO,MAAMC,MAAM,GAAG;EACpBC,IAAI,EAAE,4BAA4B;EAClCC,YAAY,EAAE,CAAC,aAAa,EAAE,WAAW,EAAE,WAAW,CAAC;EACvDC,QAAQ,EAAE,IAAI;EACd,MAAMC,QAAQA,CAACC,MAAc,EAAEC,OAAsB,EAAE;IACrDA,OAAO,GAAGjB,qBAAqB,CAACiB,OAAO,CAAC;IAExC,MAAM;MACJC,KAAK;MACLC,KAAK;MACLC,WAAW;MACXC,QAAQ,EAAEC,eAAe;MACzBC,WAAW;MACXC,8BAA8B;MAC9BC,SAAS;MACTC,oBAAoB;MACpBC;IACF,CAAC,GAAGV,OAAO;IAEX,MAAMW,YAAY,GAChB,OAAOT,KAAK,KAAK,QAAQ,GACrB,IAAIT,YAAY,CAAC;MAAEM,MAAM;MAAEa,SAAS,EAAEV;IAAM,CAAC,CAAC,GAC9CA,KAAK;IAEX,MAAMZ,cAAc,CAACS,MAAM,EAAEC,OAAO,CAAC;;IAErC;IACA,IAAIS,oBAAoB,EAAE;MACxB,MAAMV,MAAM,CAACD,QAAQ,CAAC;QACpBJ,MAAM,EAAEF,oBAAoB;QAC5BQ,OAAO,EAAE;UACPS;QACF;MACF,CAAC,CAAC;IACJ;;IAEA;IACA,IAAIA,oBAAoB,IAAIC,uBAAuB,EAAE;MACnD,MAAMX,MAAM,CAACD,QAAQ,CAAC;QACpBJ,MAAM,EAAEH,SAAS;QACjBS,OAAO,EAAE;UACPS,oBAAoB;UACpBC;QACF;MACF,CAAC,CAAC;IACJ;IAEAX,MAAM,CAACc,MAAM,CAAC,gBAAgB,EAAER,eAAe,CAACS,cAAc,CAAC;IAC/Df,MAAM,CAACc,MAAM,CAAC,aAAa,EAAEP,WAAW,CAAC;IACzCP,MAAM,CAACc,MAAM,CAAC,cAAc,EAAEF,YAAY,CAAC;IAC3CZ,MAAM,CAACc,MAAM,CAAC,aAAa,EAAEV,WAAW,CAAC;IAEzCJ,MAAM,CAACgB,GAAG,CAACd,KAAK,GAAGA,KAAK;;IAExB;IACA;IACA,MAAMe,SAAS,GAAG,IAAIC,GAAG,CAAgD,CAAC;IAC1ElB,MAAM,CAACgB,GAAG,CAACG,MAAM,GAAGF,SAAS;IAE7B,MAAMG,kBAAkB,GAAGjC,sBAAsB,CAACa,MAAM,EAAEC,OAAO,CAAC;IAElE,MAAMoB,eAA8C,GAAG;MACrDC,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMI,gBAAsD,GAAG;MAC7DC,OAAO,EAAE;QACPC,KAAK,EAAE;MACT,CAAC;MACDJ,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMO,MAAM,GAAG,CACb,GAAGvC,iBAAiB,CAClBiC,eAAe,EACfG,gBAAgB,EAChBhB,8BAA8B,EAC9BC,SACF,CAAC,EACD,GAAGnB,wBAAwB,CAAC+B,eAAe,EAAEG,gBAAgB,EAAEf,SAAS,CAAC,EACzE,GAAGpB,2BAA2B,CAC5BgC,eAAe,EACfG,gBAAgB,EAChBf,SACF,CAAC,EACD,GAAGvB,yBAAyB,CAAC,CAAC,CAC/B;IAEDc,MAAM,CAAC4B,KAAK,CAACD,MAAkC,CAAC,EAAC;EACnD;AACF,CAAiC","ignoreList":[]}
1
+ {"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getPaymentRoutes","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerVision","mapPlugin","postcodeLookupPlugin","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cache","saveAndExit","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","onRequest","ordnanceSurveyApiKey","baseUrl","ordnanceSurveyApiSecret","cacheService","cacheName","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","getRouteOptions","pre","method","postRouteOptions","payload","parse","routes","route"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import {\n type Lifecycle,\n type Plugin,\n type RouteOptions,\n type Server,\n type ServerRoute\n} from '@hapi/hapi'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { validatePluginOptions } from '~/src/server/plugins/engine/options.js'\nimport { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'\nimport { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport { getRoutes as getPaymentRoutes } from '~/src/server/plugins/engine/routes/payment.js'\nimport { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'\nimport { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'\nimport { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { registerVision } from '~/src/server/plugins/engine/vision.js'\nimport { mapPlugin } from '~/src/server/plugins/map/index.js'\nimport { postcodeLookupPlugin } from '~/src/server/plugins/postcode-lookup/index.js'\nimport {\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport { CacheService } from '~/src/server/services/index.js'\n\nexport const plugin = {\n name: '@defra/forms-engine-plugin',\n dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],\n multiple: true,\n async register(server: Server, options: PluginOptions) {\n options = validatePluginOptions(options)\n\n const {\n model,\n cache,\n saveAndExit,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions,\n onRequest,\n ordnanceSurveyApiKey,\n baseUrl,\n ordnanceSurveyApiSecret\n } = options\n\n const cacheService =\n typeof cache === 'string'\n ? new CacheService({ server, cacheName: cache })\n : cache\n\n await registerVision(server, options)\n\n // Register the postcode lookup plugin only if we have an OS api key\n if (ordnanceSurveyApiKey) {\n await server.register({\n plugin: postcodeLookupPlugin,\n options: {\n ordnanceSurveyApiKey\n }\n })\n }\n\n // Register the maps plugin only if we have an OS api key & secret\n if (ordnanceSurveyApiKey && ordnanceSurveyApiSecret) {\n await server.register({\n plugin: mapPlugin,\n options: {\n ordnanceSurveyApiKey,\n ordnanceSurveyApiSecret\n }\n })\n }\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n server.expose('saveAndExit', saveAndExit)\n server.expose('baseUrl', baseUrl)\n\n server.app.model = model\n\n // In-memory cache of FormModel items, exposed\n // (for testing purposes) through `server.app.models`\n const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()\n server.app.models = itemCache\n\n const loadFormPreHandler = makeLoadFormPreHandler(server, options)\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestRefs>\n }\n ]\n }\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestPayloadRefs>\n }\n ]\n }\n\n const routes = [\n ...getPaymentRoutes(),\n ...getFileUploadStatusRoutes(),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),\n ...getRepeaterItemDeleteRoutes(\n getRouteOptions,\n postRouteOptions,\n onRequest\n ),\n\n ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions,\n onRequest\n )\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"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","ignoreList":[]}
@@ -76,13 +76,17 @@ async function importExternalComponentState(request, page, state) {
76
76
  if (!isStateValid) {
77
77
  throw new Error(`State for component ${componentName} is invalid`);
78
78
  }
79
- const componentState = isFormState(stateAppendage) ? Object.fromEntries(Object.entries(stateAppendage).map(([key, value]) => [`${componentName}__${key}`, value])) : {
79
+
80
+ // Create state structure from appendage state
81
+ // Some components use a record structure with properties of the format of '<compName>__<fieldName>'
82
+ // e.g. UKAddressField
83
+ // Some components use a single object structure e.g. PaymentField
84
+ const componentState = isFormState(stateAppendage) && !component.isAppendageStateSingleObject ? Object.fromEntries(Object.entries(stateAppendage).map(([key, value]) => [`${componentName}__${key}`, value])) : {
80
85
  [componentName]: stateAppendage
81
86
  };
82
87
 
83
- // Save the external component state immediately
84
- const pageState = page.getStateFromValidForm(request, state, componentState);
85
- const savedState = await page.mergeState(request, state, pageState);
88
+ // Save the external component state directly (already has correct key format)
89
+ const savedState = await page.mergeState(request, state, componentState);
86
90
 
87
91
  // Merge any stashed payload into the local state
88
92
  const payload = request.yar.flash(EXTERNAL_STATE_PAYLOAD);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Boom","EXTERNAL_STATE_APPENDAGE","EXTERNAL_STATE_PAYLOAD","resolveFormModel","FormComponent","isFormState","checkFormStatus","findPage","getCacheService","getPage","getStartPath","proceed","generateUniqueReference","defaultServices","redirectOrMakeHandler","request","h","onRequest","makeHandler","app","params","model","notFound","path","cacheService","server","page","state","getState","$$__referenceNumber","prefix","def","metadata","referenceNumberPrefix","badImplementation","referenceNumber","mergeState","importExternalComponentState","flash","getFlash","context","getFormContext","errors","relevantPath","getRelevantPath","summaryPath","getSummaryPath","result","continue","startsWith","isForceAccess","redirectTo","next","length","query","returnUrl","getHref","externalComponentData","yar","Array","isArray","typedStateAppendage","componentName","component","stateAppendage","data","componentMap","get","Error","TypeError","isStateValid","isState","componentState","Object","fromEntries","entries","map","key","value","pageState","getStateFromValidForm","savedState","payload","stashedPayload","localState","makeLoadFormPreHandler","options","realm","modifiers","route","services","controllers","ordnanceSurveyApiKey","handler","slug","isPreview","formState","routePrefix","dispatchHandler","servicePath","basePath"],"sources":["../../../../../src/server/plugins/engine/routes/index.ts"],"sourcesContent":["import Boom from '@hapi/boom'\nimport {\n type ResponseObject,\n type ResponseToolkit,\n type Server\n} from '@hapi/hapi'\n\nimport {\n EXTERNAL_STATE_APPENDAGE,\n EXTERNAL_STATE_PAYLOAD\n} from '~/src/server/constants.js'\nimport { resolveFormModel } from '~/src/server/plugins/engine/beta/form-context.js'\nimport {\n FormComponent,\n isFormState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n checkFormStatus,\n findPage,\n getCacheService,\n getPage,\n getStartPath,\n proceed\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport {\n type AnyFormRequest,\n type ExternalStateAppendage,\n type FormContext,\n type FormPayload,\n type FormSubmissionState,\n type OnRequestCallback,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport async function redirectOrMakeHandler(\n request: AnyFormRequest,\n h: FormResponseToolkit,\n onRequest: OnRequestCallback | undefined,\n makeHandler: (\n page: PageControllerClass,\n context: FormContext\n ) => ResponseObject | Promise<ResponseObject>\n) {\n const { app, params } = request\n const { model } = app\n\n if (!model) {\n throw Boom.notFound(`No model found for /${params.path}`)\n }\n\n const cacheService = getCacheService(request.server)\n const page = getPage(model, request)\n let state = await page.getState(request)\n\n if (!state.$$__referenceNumber) {\n const prefix = model.def.metadata?.referenceNumberPrefix ?? ''\n\n if (typeof prefix !== 'string') {\n throw Boom.badImplementation(\n 'Reference number prefix must be a string or undefined'\n )\n }\n\n const referenceNumber = generateUniqueReference(prefix)\n state = await page.mergeState(request, state, {\n $$__referenceNumber: referenceNumber\n })\n }\n\n state = await importExternalComponentState(request, page, state)\n\n const flash = cacheService.getFlash(request)\n const context = model.getFormContext(request, state, flash?.errors)\n const relevantPath = page.getRelevantPath(request, context)\n const summaryPath = page.getSummaryPath()\n\n // Call the onRequest callback if it has been supplied\n if (onRequest) {\n const result = await onRequest(request, h, context)\n if (result !== h.continue) {\n return result\n }\n }\n\n // Return handler for relevant pages or preview URL direct access\n if (relevantPath.startsWith(page.path) || context.isForceAccess) {\n return makeHandler(page, context)\n }\n\n // Redirect back to last relevant page\n const redirectTo = findPage(model, relevantPath)\n\n // Set the return URL unless an exit page\n if (redirectTo?.next.length) {\n request.query.returnUrl = page.getHref(summaryPath)\n }\n\n return proceed(request, h, page.getHref(relevantPath))\n}\n\nasync function importExternalComponentState(\n request: AnyFormRequest,\n page: PageControllerClass,\n state: FormSubmissionState\n): Promise<FormSubmissionState> {\n const externalComponentData = request.yar.flash(EXTERNAL_STATE_APPENDAGE)\n\n if (Array.isArray(externalComponentData)) {\n return state\n }\n\n const typedStateAppendage = externalComponentData as ExternalStateAppendage\n const componentName = typedStateAppendage.component\n const stateAppendage = typedStateAppendage.data\n const component = request.app.model?.componentMap.get(componentName)\n\n if (!component) {\n throw new Error(`Component ${componentName} not found in form`)\n }\n\n if (!(component instanceof FormComponent)) {\n throw new TypeError(\n `Component ${componentName} is not a FormComponent and does not support isState`\n )\n }\n\n const isStateValid = component.isState(stateAppendage)\n\n if (!isStateValid) {\n throw new Error(`State for component ${componentName} is invalid`)\n }\n\n const componentState = isFormState(stateAppendage)\n ? Object.fromEntries(\n Object.entries(stateAppendage).map(([key, value]) => [\n `${componentName}__${key}`,\n value\n ])\n )\n : { [componentName]: stateAppendage }\n\n // Save the external component state immediately\n const pageState = page.getStateFromValidForm(\n request,\n state,\n componentState as FormPayload\n )\n const savedState = await page.mergeState(request, state, pageState)\n\n // Merge any stashed payload into the local state\n const payload = request.yar.flash(EXTERNAL_STATE_PAYLOAD)\n const stashedPayload = Array.isArray(payload) ? {} : (payload as FormPayload)\n\n const localState = page.getStateFromValidForm(request, savedState, {\n ...stashedPayload,\n ...componentState\n } as FormPayload)\n\n return { ...savedState, ...localState }\n}\n\nexport function makeLoadFormPreHandler(server: Server, options: PluginOptions) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong\n const prefix = server.realm.modifiers.route.prefix ?? ''\n\n const {\n services = defaultServices,\n controllers,\n ordnanceSurveyApiKey\n } = options\n\n async function handler(request: AnyFormRequest, h: ResponseToolkit) {\n if (server.app.model) {\n request.app.model = server.app.model\n\n return h.continue\n }\n\n const { params } = request\n const { slug } = params\n const { isPreview, state: formState } = checkFormStatus(params)\n\n const model = await resolveFormModel(server, slug, formState, {\n services,\n controllers,\n ordnanceSurveyApiKey,\n routePrefix: prefix,\n isPreview\n })\n\n request.app.model = model\n\n return h.continue\n }\n\n return handler\n}\n\nexport function dispatchHandler(request: FormRequest, h: FormResponseToolkit) {\n const { model } = request.app\n\n const servicePath = model ? `/${model.basePath}` : ''\n return proceed(request, h, `${servicePath}${getStartPath(model)}`)\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["Boom","EXTERNAL_STATE_APPENDAGE","EXTERNAL_STATE_PAYLOAD","resolveFormModel","FormComponent","isFormState","checkFormStatus","findPage","getCacheService","getPage","getStartPath","proceed","generateUniqueReference","defaultServices","redirectOrMakeHandler","request","h","onRequest","makeHandler","app","params","model","notFound","path","cacheService","server","page","state","getState","$$__referenceNumber","prefix","def","metadata","referenceNumberPrefix","badImplementation","referenceNumber","mergeState","importExternalComponentState","flash","getFlash","context","getFormContext","errors","relevantPath","getRelevantPath","summaryPath","getSummaryPath","result","continue","startsWith","isForceAccess","redirectTo","next","length","query","returnUrl","getHref","externalComponentData","yar","Array","isArray","typedStateAppendage","componentName","component","stateAppendage","data","componentMap","get","Error","TypeError","isStateValid","isState","componentState","isAppendageStateSingleObject","Object","fromEntries","entries","map","key","value","savedState","payload","stashedPayload","localState","getStateFromValidForm","makeLoadFormPreHandler","options","realm","modifiers","route","services","controllers","ordnanceSurveyApiKey","handler","slug","isPreview","formState","routePrefix","dispatchHandler","servicePath","basePath"],"sources":["../../../../../src/server/plugins/engine/routes/index.ts"],"sourcesContent":["import Boom from '@hapi/boom'\nimport {\n type ResponseObject,\n type ResponseToolkit,\n type Server\n} from '@hapi/hapi'\n\nimport {\n EXTERNAL_STATE_APPENDAGE,\n EXTERNAL_STATE_PAYLOAD\n} from '~/src/server/constants.js'\nimport { resolveFormModel } from '~/src/server/plugins/engine/beta/form-context.js'\nimport {\n FormComponent,\n isFormState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n checkFormStatus,\n findPage,\n getCacheService,\n getPage,\n getStartPath,\n proceed\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport {\n type AnyFormRequest,\n type ExternalStateAppendage,\n type FormContext,\n type FormPayload,\n type FormSubmissionState,\n type OnRequestCallback,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport async function redirectOrMakeHandler(\n request: AnyFormRequest,\n h: FormResponseToolkit,\n onRequest: OnRequestCallback | undefined,\n makeHandler: (\n page: PageControllerClass,\n context: FormContext\n ) => ResponseObject | Promise<ResponseObject>\n) {\n const { app, params } = request\n const { model } = app\n\n if (!model) {\n throw Boom.notFound(`No model found for /${params.path}`)\n }\n\n const cacheService = getCacheService(request.server)\n const page = getPage(model, request)\n let state = await page.getState(request)\n\n if (!state.$$__referenceNumber) {\n const prefix = model.def.metadata?.referenceNumberPrefix ?? ''\n\n if (typeof prefix !== 'string') {\n throw Boom.badImplementation(\n 'Reference number prefix must be a string or undefined'\n )\n }\n\n const referenceNumber = generateUniqueReference(prefix)\n state = await page.mergeState(request, state, {\n $$__referenceNumber: referenceNumber\n })\n }\n\n state = await importExternalComponentState(request, page, state)\n\n const flash = cacheService.getFlash(request)\n const context = model.getFormContext(request, state, flash?.errors)\n const relevantPath = page.getRelevantPath(request, context)\n const summaryPath = page.getSummaryPath()\n\n // Call the onRequest callback if it has been supplied\n if (onRequest) {\n const result = await onRequest(request, h, context)\n if (result !== h.continue) {\n return result\n }\n }\n\n // Return handler for relevant pages or preview URL direct access\n if (relevantPath.startsWith(page.path) || context.isForceAccess) {\n return makeHandler(page, context)\n }\n\n // Redirect back to last relevant page\n const redirectTo = findPage(model, relevantPath)\n\n // Set the return URL unless an exit page\n if (redirectTo?.next.length) {\n request.query.returnUrl = page.getHref(summaryPath)\n }\n\n return proceed(request, h, page.getHref(relevantPath))\n}\n\nasync function importExternalComponentState(\n request: AnyFormRequest,\n page: PageControllerClass,\n state: FormSubmissionState\n): Promise<FormSubmissionState> {\n const externalComponentData = request.yar.flash(EXTERNAL_STATE_APPENDAGE)\n\n if (Array.isArray(externalComponentData)) {\n return state\n }\n\n const typedStateAppendage = externalComponentData as ExternalStateAppendage\n const componentName = typedStateAppendage.component\n const stateAppendage = typedStateAppendage.data\n\n const component = request.app.model?.componentMap.get(componentName)\n\n if (!component) {\n throw new Error(`Component ${componentName} not found in form`)\n }\n\n if (!(component instanceof FormComponent)) {\n throw new TypeError(\n `Component ${componentName} is not a FormComponent and does not support isState`\n )\n }\n\n const isStateValid = component.isState(stateAppendage)\n\n if (!isStateValid) {\n throw new Error(`State for component ${componentName} is invalid`)\n }\n\n // Create state structure from appendage state\n // Some components use a record structure with properties of the format of '<compName>__<fieldName>'\n // e.g. UKAddressField\n // Some components use a single object structure e.g. PaymentField\n const componentState =\n isFormState(stateAppendage) && !component.isAppendageStateSingleObject\n ? Object.fromEntries(\n Object.entries(stateAppendage).map(([key, value]) => [\n `${componentName}__${key}`,\n value\n ])\n )\n : { [componentName]: stateAppendage }\n\n // Save the external component state directly (already has correct key format)\n const savedState = await page.mergeState(request, state, componentState)\n\n // Merge any stashed payload into the local state\n const payload = request.yar.flash(EXTERNAL_STATE_PAYLOAD)\n const stashedPayload = Array.isArray(payload) ? {} : (payload as FormPayload)\n\n const localState = page.getStateFromValidForm(request, savedState, {\n ...stashedPayload,\n ...componentState\n } as FormPayload)\n\n return { ...savedState, ...localState }\n}\n\nexport function makeLoadFormPreHandler(server: Server, options: PluginOptions) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong\n const prefix = server.realm.modifiers.route.prefix ?? ''\n\n const {\n services = defaultServices,\n controllers,\n ordnanceSurveyApiKey\n } = options\n\n async function handler(request: AnyFormRequest, h: ResponseToolkit) {\n if (server.app.model) {\n request.app.model = server.app.model\n\n return h.continue\n }\n\n const { params } = request\n const { slug } = params\n const { isPreview, state: formState } = checkFormStatus(params)\n\n const model = await resolveFormModel(server, slug, formState, {\n services,\n controllers,\n ordnanceSurveyApiKey,\n routePrefix: prefix,\n isPreview\n })\n\n request.app.model = model\n\n return h.continue\n }\n\n return handler\n}\n\nexport function dispatchHandler(request: FormRequest, h: FormResponseToolkit) {\n const { model } = request.app\n\n const servicePath = model ? `/${model.basePath}` : ''\n return proceed(request, h, `${servicePath}${getStartPath(model)}`)\n}\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Validates session data and retrieves payment status
3
+ * @param {Request} request - the request
4
+ * @param {string} uuid - the payment UUID
5
+ * @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}
6
+ */
7
+ export function getPaymentContext(request: Request, uuid: string): Promise<{
8
+ session: PaymentSessionData;
9
+ sessionKey: string;
10
+ paymentStatus: GetPaymentResponse;
11
+ }>;
12
+ import type { Request } from '@hapi/hapi';
13
+ import type { PaymentSessionData } from '~/src/server/plugins/payment/types.js';
14
+ import type { GetPaymentResponse } from '~/src/server/plugins/payment/types.js';
@@ -0,0 +1,41 @@
1
+ import Boom from '@hapi/boom';
2
+ import { PAYMENT_SESSION_PREFIX } from "./payment.js";
3
+ import { getPaymentApiKey } from "../../payment/helper.js";
4
+ import { PaymentService } from "../../payment/service.js";
5
+
6
+ /**
7
+ * Validates session data and retrieves payment status
8
+ * @param {Request} request - the request
9
+ * @param {string} uuid - the payment UUID
10
+ * @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}
11
+ */
12
+ export async function getPaymentContext(request, uuid) {
13
+ const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`;
14
+ const session = /** @type {PaymentSessionData | null} */
15
+ request.yar.get(sessionKey);
16
+ if (!session) {
17
+ throw Boom.badRequest(`No payment session found for uuid=${uuid}`);
18
+ }
19
+ const {
20
+ paymentId,
21
+ isLivePayment,
22
+ formId
23
+ } = session;
24
+ if (!paymentId) {
25
+ throw Boom.badRequest('No paymentId in session');
26
+ }
27
+ const apiKey = getPaymentApiKey(isLivePayment, formId);
28
+ const paymentService = new PaymentService(apiKey);
29
+ const paymentStatus = await paymentService.getPaymentStatus(paymentId);
30
+ return {
31
+ session,
32
+ sessionKey,
33
+ paymentStatus
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @import { Request } from '@hapi/hapi'
39
+ * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
40
+ */
41
+ //# sourceMappingURL=payment-helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-helper.js","names":["Boom","PAYMENT_SESSION_PREFIX","getPaymentApiKey","PaymentService","getPaymentContext","request","uuid","sessionKey","session","yar","get","badRequest","paymentId","isLivePayment","formId","apiKey","paymentService","paymentStatus","getPaymentStatus"],"sources":["../../../../../src/server/plugins/engine/routes/payment-helper.js"],"sourcesContent":["import Boom from '@hapi/boom'\n\nimport { PAYMENT_SESSION_PREFIX } from '~/src/server/plugins/engine/routes/payment.js'\nimport { getPaymentApiKey } from '~/src/server/plugins/payment/helper.js'\nimport { PaymentService } from '~/src/server/plugins/payment/service.js'\n\n/**\n * Validates session data and retrieves payment status\n * @param {Request} request - the request\n * @param {string} uuid - the payment UUID\n * @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}\n */\nexport async function getPaymentContext(request, uuid) {\n const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`\n const session = /** @type {PaymentSessionData | null} */ (\n request.yar.get(sessionKey)\n )\n\n if (!session) {\n throw Boom.badRequest(`No payment session found for uuid=${uuid}`)\n }\n\n const { paymentId, isLivePayment, formId } = session\n\n if (!paymentId) {\n throw Boom.badRequest('No paymentId in session')\n }\n\n const apiKey = getPaymentApiKey(isLivePayment, formId)\n const paymentService = new PaymentService(apiKey)\n const paymentStatus = await paymentService.getPaymentStatus(paymentId)\n\n return { session, sessionKey, paymentStatus }\n}\n\n/**\n * @import { Request } from '@hapi/hapi'\n * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"AAAA,OAAOA,IAAI,MAAM,YAAY;AAE7B,SAASC,sBAAsB;AAC/B,SAASC,gBAAgB;AACzB,SAASC,cAAc;;AAEvB;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,iBAAiBA,CAACC,OAAO,EAAEC,IAAI,EAAE;EACrD,MAAMC,UAAU,GAAG,GAAGN,sBAAsB,GAAGK,IAAI,EAAE;EACrD,MAAME,OAAO,GAAG;EACdH,OAAO,CAACI,GAAG,CAACC,GAAG,CAACH,UAAU,CAC3B;EAED,IAAI,CAACC,OAAO,EAAE;IACZ,MAAMR,IAAI,CAACW,UAAU,CAAC,qCAAqCL,IAAI,EAAE,CAAC;EACpE;EAEA,MAAM;IAAEM,SAAS;IAAEC,aAAa;IAAEC;EAAO,CAAC,GAAGN,OAAO;EAEpD,IAAI,CAACI,SAAS,EAAE;IACd,MAAMZ,IAAI,CAACW,UAAU,CAAC,yBAAyB,CAAC;EAClD;EAEA,MAAMI,MAAM,GAAGb,gBAAgB,CAACW,aAAa,EAAEC,MAAM,CAAC;EACtD,MAAME,cAAc,GAAG,IAAIb,cAAc,CAACY,MAAM,CAAC;EACjD,MAAME,aAAa,GAAG,MAAMD,cAAc,CAACE,gBAAgB,CAACN,SAAS,CAAC;EAEtE,OAAO;IAAEJ,OAAO;IAAED,UAAU;IAAEU;EAAc,CAAC;AAC/C;;AAEA;AACA;AACA;AACA","ignoreList":[]}
@@ -0,0 +1,81 @@
1
+ import { getPaymentContext } from "./payment-helper.js";
2
+ import { get } from "../../../services/httpService.js";
3
+ jest.mock("../../../services/httpService.ts");
4
+ describe('payment helper', () => {
5
+ const uuid = '5a54c2fe-da49-4202-8cd3-2121eaca03c3';
6
+ it('should throw if no session', async () => {
7
+ const mockRequest = {
8
+ yar: {
9
+ get: jest.fn().mockReturnValueOnce(undefined)
10
+ }
11
+ };
12
+ // @ts-expect-error - partial request mock
13
+ await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow('No payment session found for uuid=5a54c2fe-da49-4202-8cd3-2121eaca03c3');
14
+ });
15
+ it('should throw if no payment id', async () => {
16
+ const mockRequest = {
17
+ yar: {
18
+ get: jest.fn().mockReturnValueOnce({})
19
+ }
20
+ };
21
+ // @ts-expect-error - partial request mock
22
+ await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow('No paymentId in session');
23
+ });
24
+ it('should get context successfully', async () => {
25
+ const mockRequest = {
26
+ yar: {
27
+ get: jest.fn().mockReturnValueOnce({
28
+ paymentId: 'payment-id',
29
+ isLivePayment: false,
30
+ formId: 'formid'
31
+ })
32
+ }
33
+ };
34
+ const getPaymentStatusApiResult = {
35
+ payment_id: 'payment-id-12345',
36
+ _links: {
37
+ next_url: {
38
+ href: 'http://next-url-href/payment'
39
+ }
40
+ },
41
+ state: {
42
+ status: 'created'
43
+ }
44
+ };
45
+ jest.mocked(get).mockResolvedValueOnce({
46
+ res: (/** @type {IncomingMessage} */{
47
+ statusCode: 200,
48
+ headers: {}
49
+ }),
50
+ payload: getPaymentStatusApiResult,
51
+ error: undefined
52
+ });
53
+
54
+ // @ts-expect-error - partial request mock
55
+ const res = await getPaymentContext(mockRequest, uuid);
56
+ expect(res).toEqual({
57
+ paymentStatus: {
58
+ paymentId: 'payment-id-12345',
59
+ _links: {
60
+ next_url: {
61
+ href: 'http://next-url-href/payment'
62
+ }
63
+ },
64
+ state: {
65
+ status: 'created'
66
+ }
67
+ },
68
+ session: {
69
+ formId: 'formid',
70
+ isLivePayment: false,
71
+ paymentId: 'payment-id'
72
+ },
73
+ sessionKey: 'payment-5a54c2fe-da49-4202-8cd3-2121eaca03c3'
74
+ });
75
+ });
76
+ });
77
+
78
+ /**
79
+ * @import { IncomingMessage } from 'node:http'
80
+ */
81
+ //# sourceMappingURL=payment-helper.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-helper.test.js","names":["getPaymentContext","get","jest","mock","describe","uuid","it","mockRequest","yar","fn","mockReturnValueOnce","undefined","expect","rejects","toThrow","paymentId","isLivePayment","formId","getPaymentStatusApiResult","payment_id","_links","next_url","href","state","status","mocked","mockResolvedValueOnce","res","statusCode","headers","payload","error","toEqual","paymentStatus","session","sessionKey"],"sources":["../../../../../src/server/plugins/engine/routes/payment-helper.test.js"],"sourcesContent":["import { getPaymentContext } from '~/src/server/plugins/engine/routes/payment-helper.js'\nimport { get } from '~/src/server/services/httpService.js'\n\njest.mock('~/src/server/services/httpService.ts')\n\ndescribe('payment helper', () => {\n const uuid = '5a54c2fe-da49-4202-8cd3-2121eaca03c3'\n it('should throw if no session', async () => {\n const mockRequest = {\n yar: {\n get: jest.fn().mockReturnValueOnce(undefined)\n }\n }\n // @ts-expect-error - partial request mock\n await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow(\n 'No payment session found for uuid=5a54c2fe-da49-4202-8cd3-2121eaca03c3'\n )\n })\n\n it('should throw if no payment id', async () => {\n const mockRequest = {\n yar: {\n get: jest.fn().mockReturnValueOnce({})\n }\n }\n // @ts-expect-error - partial request mock\n await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow(\n 'No paymentId in session'\n )\n })\n\n it('should get context successfully', async () => {\n const mockRequest = {\n yar: {\n get: jest.fn().mockReturnValueOnce({\n paymentId: 'payment-id',\n isLivePayment: false,\n formId: 'formid'\n })\n }\n }\n\n const getPaymentStatusApiResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: getPaymentStatusApiResult,\n error: undefined\n })\n\n // @ts-expect-error - partial request mock\n const res = await getPaymentContext(mockRequest, uuid)\n expect(res).toEqual({\n paymentStatus: {\n paymentId: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n },\n session: {\n formId: 'formid',\n isLivePayment: false,\n paymentId: 'payment-id'\n },\n sessionKey: 'payment-5a54c2fe-da49-4202-8cd3-2121eaca03c3'\n })\n })\n})\n\n/**\n * @import { IncomingMessage } from 'node:http'\n */\n"],"mappings":"AAAA,SAASA,iBAAiB;AAC1B,SAASC,GAAG;AAEZC,IAAI,CAACC,IAAI,mCAAuC,CAAC;AAEjDC,QAAQ,CAAC,gBAAgB,EAAE,MAAM;EAC/B,MAAMC,IAAI,GAAG,sCAAsC;EACnDC,EAAE,CAAC,4BAA4B,EAAE,YAAY;IAC3C,MAAMC,WAAW,GAAG;MAClBC,GAAG,EAAE;QACHP,GAAG,EAAEC,IAAI,CAACO,EAAE,CAAC,CAAC,CAACC,mBAAmB,CAACC,SAAS;MAC9C;IACF,CAAC;IACD;IACA,MAAMC,MAAM,CAAC,MAAMZ,iBAAiB,CAACO,WAAW,EAAEF,IAAI,CAAC,CAAC,CAACQ,OAAO,CAACC,OAAO,CACtE,wEACF,CAAC;EACH,CAAC,CAAC;EAEFR,EAAE,CAAC,+BAA+B,EAAE,YAAY;IAC9C,MAAMC,WAAW,GAAG;MAClBC,GAAG,EAAE;QACHP,GAAG,EAAEC,IAAI,CAACO,EAAE,CAAC,CAAC,CAACC,mBAAmB,CAAC,CAAC,CAAC;MACvC;IACF,CAAC;IACD;IACA,MAAME,MAAM,CAAC,MAAMZ,iBAAiB,CAACO,WAAW,EAAEF,IAAI,CAAC,CAAC,CAACQ,OAAO,CAACC,OAAO,CACtE,yBACF,CAAC;EACH,CAAC,CAAC;EAEFR,EAAE,CAAC,iCAAiC,EAAE,YAAY;IAChD,MAAMC,WAAW,GAAG;MAClBC,GAAG,EAAE;QACHP,GAAG,EAAEC,IAAI,CAACO,EAAE,CAAC,CAAC,CAACC,mBAAmB,CAAC;UACjCK,SAAS,EAAE,YAAY;UACvBC,aAAa,EAAE,KAAK;UACpBC,MAAM,EAAE;QACV,CAAC;MACH;IACF,CAAC;IAED,MAAMC,yBAAyB,GAAG;MAChCC,UAAU,EAAE,kBAAkB;MAC9BC,MAAM,EAAE;QACNC,QAAQ,EAAE;UACRC,IAAI,EAAE;QACR;MACF,CAAC;MACDC,KAAK,EAAE;QACLC,MAAM,EAAE;MACV;IACF,CAAC;IAEDtB,IAAI,CAACuB,MAAM,CAACxB,GAAG,CAAC,CAACyB,qBAAqB,CAAC;MACrCC,GAAG,GAAE,8BAAgC;QACnCC,UAAU,EAAE,GAAG;QACfC,OAAO,EAAE,CAAC;MACZ,CAAC,CAAC;MACFC,OAAO,EAAEZ,yBAAyB;MAClCa,KAAK,EAAEpB;IACT,CAAC,CAAC;;IAEF;IACA,MAAMgB,GAAG,GAAG,MAAM3B,iBAAiB,CAACO,WAAW,EAAEF,IAAI,CAAC;IACtDO,MAAM,CAACe,GAAG,CAAC,CAACK,OAAO,CAAC;MAClBC,aAAa,EAAE;QACblB,SAAS,EAAE,kBAAkB;QAC7BK,MAAM,EAAE;UACNC,QAAQ,EAAE;YACRC,IAAI,EAAE;UACR;QACF,CAAC;QACDC,KAAK,EAAE;UACLC,MAAM,EAAE;QACV;MACF,CAAC;MACDU,OAAO,EAAE;QACPjB,MAAM,EAAE,QAAQ;QAChBD,aAAa,EAAE,KAAK;QACpBD,SAAS,EAAE;MACb,CAAC;MACDoB,UAAU,EAAE;IACd,CAAC,CAAC;EACJ,CAAC,CAAC;AACJ,CAAC,CAAC;;AAEF;AACA;AACA","ignoreList":[]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Gets the payment routes for handling GOV.UK Pay callbacks
3
+ * @returns {ServerRoute[]}
4
+ */
5
+ export function getRoutes(): ServerRoute[];
6
+ export const PAYMENT_RETURN_PATH: "/payment-callback";
7
+ export const PAYMENT_SESSION_PREFIX: "payment-";
8
+ import type { ServerRoute } from '@hapi/hapi';
@@ -0,0 +1,140 @@
1
+ import Boom from '@hapi/boom';
2
+ import { StatusCodes } from 'http-status-codes';
3
+ import Joi from 'joi';
4
+ import { EXTERNAL_STATE_APPENDAGE } from "../../../constants.js";
5
+ import { getPaymentContext } from "./payment-helper.js";
6
+ export const PAYMENT_RETURN_PATH = '/payment-callback';
7
+ export const PAYMENT_SESSION_PREFIX = 'payment-';
8
+
9
+ /**
10
+ * Flash form component state after successful payment
11
+ * @param {Request} request - the request
12
+ * @param {PaymentSessionData} session - the session data containing payment state
13
+ * @param {GetPaymentResponse} paymentStatus - the payment status response from GOV.UK Pay
14
+ */
15
+ function flashComponentState(request, session, paymentStatus) {
16
+ /** @type {PaymentState} */
17
+ const paymentState = {
18
+ paymentId: paymentStatus.paymentId,
19
+ reference: session.reference,
20
+ amount: session.amount,
21
+ description: session.description,
22
+ uuid: session.uuid,
23
+ formId: session.formId,
24
+ isLivePayment: session.isLivePayment,
25
+ payerEmail: paymentStatus.email,
26
+ preAuth: {
27
+ status: 'success',
28
+ createdAt: new Date().toISOString()
29
+ }
30
+ };
31
+
32
+ /** @type {ExternalStateAppendage} */
33
+ const appendage = {
34
+ component: session.componentName,
35
+ data: (/** @type {FormState} */ /** @type {unknown} */paymentState)
36
+ };
37
+ request.yar.flash(EXTERNAL_STATE_APPENDAGE, appendage, true);
38
+ }
39
+
40
+ /**
41
+ * Gets the payment routes for handling GOV.UK Pay callbacks
42
+ * @returns {ServerRoute[]}
43
+ */
44
+ export function getRoutes() {
45
+ return [getReturnRoute()];
46
+ }
47
+
48
+ /**
49
+ * Handles successful payment states (capturable/success)
50
+ * @param {Request} request - the request
51
+ * @param {ResponseToolkit} h - the response toolkit
52
+ * @param {PaymentSessionData} session - the session data
53
+ * @param {string} sessionKey - the session key
54
+ * @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay
55
+ */
56
+ function handlePaymentSuccess(request, h, session, sessionKey, paymentStatus) {
57
+ flashComponentState(request, session, paymentStatus);
58
+ request.yar.clear(sessionKey);
59
+ return h.redirect(session.returnUrl).code(StatusCodes.SEE_OTHER);
60
+ }
61
+
62
+ /**
63
+ * Handles failed/cancelled/error payment states
64
+ * @param {Request} request - the request
65
+ * @param {ResponseToolkit} h - the response toolkit
66
+ * @param {PaymentSessionData} session - the session data
67
+ * @param {string} sessionKey - the session key
68
+ */
69
+ function handlePaymentFailure(request, h, session, sessionKey) {
70
+ request.yar.clear(sessionKey);
71
+ return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER);
72
+ }
73
+
74
+ /**
75
+ * Route handler for payment return URL
76
+ * This is called when GOV.UK Pay redirects the user back after payment
77
+ * @returns {ServerRoute}
78
+ */
79
+ function getReturnRoute() {
80
+ return {
81
+ method: 'GET',
82
+ path: PAYMENT_RETURN_PATH,
83
+ async handler(request, h) {
84
+ const {
85
+ uuid
86
+ } = /** @type {{ uuid: string }} */request.query;
87
+ const {
88
+ session,
89
+ sessionKey,
90
+ paymentStatus
91
+ } = await getPaymentContext(request, uuid);
92
+
93
+ /**
94
+ * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
95
+ */
96
+ const {
97
+ status
98
+ } = paymentStatus.state;
99
+ switch (status) {
100
+ case 'capturable':
101
+ case 'success':
102
+ return handlePaymentSuccess(request, h, session, sessionKey, paymentStatus);
103
+ case 'cancelled':
104
+ case 'failed':
105
+ case 'error':
106
+ return handlePaymentFailure(request, h, session, sessionKey);
107
+ case 'created':
108
+ case 'started':
109
+ case 'submitted':
110
+ {
111
+ const nextUrl = paymentStatus._links.next_url?.href;
112
+ if (!nextUrl) {
113
+ throw Boom.badRequest(`Payment in state '${status}' but no next_url available`);
114
+ }
115
+ return h.redirect(nextUrl).code(StatusCodes.SEE_OTHER);
116
+ }
117
+ default:
118
+ {
119
+ const unknownStatus = /** @type {string} */status;
120
+ throw Boom.internal(`Unknown payment status: ${unknownStatus}`);
121
+ }
122
+ }
123
+ },
124
+ options: {
125
+ validate: {
126
+ query: Joi.object().keys({
127
+ uuid: Joi.string().uuid().required()
128
+ }).required()
129
+ }
130
+ }
131
+ };
132
+ }
133
+
134
+ /**
135
+ * @import { Request, ResponseToolkit, ServerRoute } from '@hapi/hapi'
136
+ * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
137
+ * @import { PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
138
+ * @import { ExternalStateAppendage, FormState } from '~/src/server/plugins/engine/types.js'
139
+ */
140
+ //# sourceMappingURL=payment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment.js","names":["Boom","StatusCodes","Joi","EXTERNAL_STATE_APPENDAGE","getPaymentContext","PAYMENT_RETURN_PATH","PAYMENT_SESSION_PREFIX","flashComponentState","request","session","paymentStatus","paymentState","paymentId","reference","amount","description","uuid","formId","isLivePayment","payerEmail","email","preAuth","status","createdAt","Date","toISOString","appendage","component","componentName","data","yar","flash","getRoutes","getReturnRoute","handlePaymentSuccess","h","sessionKey","clear","redirect","returnUrl","code","SEE_OTHER","handlePaymentFailure","failureUrl","method","path","handler","query","state","nextUrl","_links","next_url","href","badRequest","unknownStatus","internal","options","validate","object","keys","string","required"],"sources":["../../../../../src/server/plugins/engine/routes/payment.js"],"sourcesContent":["import Boom from '@hapi/boom'\nimport { StatusCodes } from 'http-status-codes'\nimport Joi from 'joi'\n\nimport { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'\nimport { getPaymentContext } from '~/src/server/plugins/engine/routes/payment-helper.js'\n\nexport const PAYMENT_RETURN_PATH = '/payment-callback'\nexport const PAYMENT_SESSION_PREFIX = 'payment-'\n\n/**\n * Flash form component state after successful payment\n * @param {Request} request - the request\n * @param {PaymentSessionData} session - the session data containing payment state\n * @param {GetPaymentResponse} paymentStatus - the payment status response from GOV.UK Pay\n */\nfunction flashComponentState(request, session, paymentStatus) {\n /** @type {PaymentState} */\n const paymentState = {\n paymentId: paymentStatus.paymentId,\n reference: session.reference,\n amount: session.amount,\n description: session.description,\n uuid: session.uuid,\n formId: session.formId,\n isLivePayment: session.isLivePayment,\n payerEmail: paymentStatus.email,\n preAuth: {\n status: 'success',\n createdAt: new Date().toISOString()\n }\n }\n\n /** @type {ExternalStateAppendage} */\n const appendage = {\n component: session.componentName,\n data: /** @type {FormState} */ (/** @type {unknown} */ (paymentState))\n }\n\n request.yar.flash(EXTERNAL_STATE_APPENDAGE, appendage, true)\n}\n\n/**\n * Gets the payment routes for handling GOV.UK Pay callbacks\n * @returns {ServerRoute[]}\n */\nexport function getRoutes() {\n return [getReturnRoute()]\n}\n\n/**\n * Handles successful payment states (capturable/success)\n * @param {Request} request - the request\n * @param {ResponseToolkit} h - the response toolkit\n * @param {PaymentSessionData} session - the session data\n * @param {string} sessionKey - the session key\n * @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay\n */\nfunction handlePaymentSuccess(request, h, session, sessionKey, paymentStatus) {\n flashComponentState(request, session, paymentStatus)\n request.yar.clear(sessionKey)\n return h.redirect(session.returnUrl).code(StatusCodes.SEE_OTHER)\n}\n\n/**\n * Handles failed/cancelled/error payment states\n * @param {Request} request - the request\n * @param {ResponseToolkit} h - the response toolkit\n * @param {PaymentSessionData} session - the session data\n * @param {string} sessionKey - the session key\n */\nfunction handlePaymentFailure(request, h, session, sessionKey) {\n request.yar.clear(sessionKey)\n return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER)\n}\n\n/**\n * Route handler for payment return URL\n * This is called when GOV.UK Pay redirects the user back after payment\n * @returns {ServerRoute}\n */\nfunction getReturnRoute() {\n return {\n method: 'GET',\n path: PAYMENT_RETURN_PATH,\n async handler(request, h) {\n const { uuid } = /** @type {{ uuid: string }} */ (request.query)\n const { session, sessionKey, paymentStatus } = await getPaymentContext(\n request,\n uuid\n )\n\n /**\n * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle\n */\n const { status } = paymentStatus.state\n\n switch (status) {\n case 'capturable':\n case 'success':\n return handlePaymentSuccess(\n request,\n h,\n session,\n sessionKey,\n paymentStatus\n )\n\n case 'cancelled':\n case 'failed':\n case 'error':\n return handlePaymentFailure(request, h, session, sessionKey)\n\n case 'created':\n case 'started':\n case 'submitted': {\n const nextUrl = paymentStatus._links.next_url?.href\n\n if (!nextUrl) {\n throw Boom.badRequest(\n `Payment in state '${status}' but no next_url available`\n )\n }\n\n return h.redirect(nextUrl).code(StatusCodes.SEE_OTHER)\n }\n\n default: {\n const unknownStatus = /** @type {string} */ (status)\n throw Boom.internal(`Unknown payment status: ${unknownStatus}`)\n }\n }\n },\n options: {\n validate: {\n query: Joi.object()\n .keys({\n uuid: Joi.string().uuid().required()\n })\n .required()\n }\n }\n }\n}\n\n/**\n * @import { Request, ResponseToolkit, ServerRoute } from '@hapi/hapi'\n * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'\n * @import { PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\n * @import { ExternalStateAppendage, FormState } from '~/src/server/plugins/engine/types.js'\n */\n"],"mappings":"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","ignoreList":[]}