@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
@@ -1,12 +1,14 @@
1
1
  import { hasComponentsEvenIfNoNext } from '@defra/forms-model';
2
2
  import Boom from '@hapi/boom';
3
- import { COMPONENT_STATE_ERROR } from "../../../constants.js";
3
+ import { COMPONENT_STATE_ERROR, PAYMENT_EXPIRED_NOTIFICATION } from "../../../constants.js";
4
4
  import { ComponentCollection } from "../components/ComponentCollection.js";
5
- import { getAnswer } from "../components/helpers/components.js";
5
+ import { PaymentField } from "../components/PaymentField.js";
6
6
  import { checkEmailAddressForLiveFormSubmission, checkFormStatus, createError, getCacheService } from "../helpers.js";
7
7
  import { SummaryViewModel } from "../models/index.js";
8
8
  import { QuestionPageController } from "./QuestionPageController.js";
9
- import { InvalidComponentStateError } from "./errors.js";
9
+ import { InvalidComponentStateError, PaymentErrorTypes, PaymentPreAuthError, PaymentSubmissionError } from "./errors.js";
10
+ import { buildMainRecords, buildRepeaterRecords } from "./helpers/submission.js";
11
+ import { DEFAULT_PAYMENT_HELP_URL, formatPaymentAmount, formatPaymentDate } from "../../payment/helper.js";
10
12
  import { FormAction } from "../../../routes/types.js";
11
13
  export class SummaryPageController extends QuestionPageController {
12
14
  allowSaveAndExit = true;
@@ -32,12 +34,18 @@ export class SummaryPageController extends QuestionPageController {
32
34
  } = request;
33
35
  const {
34
36
  payload,
35
- errors
37
+ errors,
38
+ state
36
39
  } = context;
40
+ const paymentField = context.relevantPages.flatMap(page => page.collection.fields).find(field => field instanceof PaymentField);
41
+ if (paymentField) {
42
+ const paymentState = paymentField.getPaymentStateFromState(state);
43
+ if (paymentState) {
44
+ viewModel.paymentState = paymentState;
45
+ viewModel.paymentDetails = this.buildPaymentDetails(paymentField, paymentState);
46
+ }
47
+ }
37
48
  const components = this.collection.getViewModel(payload, errors, query);
38
-
39
- // We already figure these out in the base page controller. Take them and apply them to our page-specific model.
40
- // This is a stop-gap until we can add proper inheritance in place.
41
49
  viewModel.backLink = this.getBackLink(request, context);
42
50
  viewModel.feedbackLink = this.feedbackLink;
43
51
  viewModel.phaseTag = this.phaseTag;
@@ -46,6 +54,48 @@ export class SummaryPageController extends QuestionPageController {
46
54
  viewModel.errors = errors;
47
55
  return viewModel;
48
56
  }
57
+ buildPaymentDetails(paymentField, paymentState) {
58
+ const rows = [{
59
+ key: {
60
+ text: 'Payment for'
61
+ },
62
+ value: {
63
+ text: paymentState.description
64
+ }
65
+ }, {
66
+ key: {
67
+ text: 'Total amount'
68
+ },
69
+ value: {
70
+ text: formatPaymentAmount(paymentState.amount)
71
+ }
72
+ }, {
73
+ key: {
74
+ text: 'Reference'
75
+ },
76
+ value: {
77
+ text: paymentState.reference
78
+ }
79
+ }];
80
+ if (paymentState.preAuth?.createdAt) {
81
+ rows.push({
82
+ key: {
83
+ text: 'Date of payment'
84
+ },
85
+ value: {
86
+ text: formatPaymentDate(paymentState.preAuth.createdAt)
87
+ }
88
+ });
89
+ }
90
+ return {
91
+ title: {
92
+ text: 'Payment details'
93
+ },
94
+ summaryList: {
95
+ rows
96
+ }
97
+ };
98
+ }
49
99
 
50
100
  /**
51
101
  * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,
@@ -67,7 +117,6 @@ export class SummaryPageController extends QuestionPageController {
67
117
  */
68
118
  makePostRouteHandler() {
69
119
  return async (request, context, h) => {
70
- // Check if this is a save-and-exit action
71
120
  const {
72
121
  action
73
122
  } = request.payload;
@@ -91,8 +140,6 @@ export class SummaryPageController extends QuestionPageController {
91
140
  const {
92
141
  getFormMetadata
93
142
  } = formsService;
94
-
95
- // Get the form metadata using the `slug` param
96
143
  const formMetadata = await getFormMetadata(params.slug);
97
144
  const {
98
145
  notificationEmail
@@ -101,20 +148,12 @@ export class SummaryPageController extends QuestionPageController {
101
148
  isPreview
102
149
  } = checkFormStatus(request.params);
103
150
  checkEmailAddressForLiveFormSubmission(notificationEmail, isPreview);
104
-
105
- // Send submission email
106
151
  if (notificationEmail) {
107
152
  const viewModel = this.getSummaryViewModel(request, context);
108
153
  try {
109
154
  await submitForm(context, formMetadata, request, viewModel, model, notificationEmail, formMetadata);
110
155
  } catch (error) {
111
- if (error instanceof InvalidComponentStateError) {
112
- const govukError = createError(error.component.name, error.userMessage);
113
- request.yar.flash(COMPONENT_STATE_ERROR, govukError, true);
114
- await cacheService.resetComponentStates(request, error.getStateKeys());
115
- return this.proceed(request, h, error.component.page?.path);
116
- }
117
- throw error;
156
+ return this.handleSubmissionError(error, request, h);
118
157
  }
119
158
  }
120
159
  await cacheService.setConfirmationState(request, {
@@ -122,11 +161,65 @@ export class SummaryPageController extends QuestionPageController {
122
161
  formId: context.state.formId,
123
162
  referenceNumber: context.referenceNumber
124
163
  });
125
-
126
- // Clear all form data
127
164
  await cacheService.clearState(request);
128
165
  return this.proceed(request, h, this.getStatusPath());
129
166
  }
167
+
168
+ /**
169
+ * Handles errors during form submission
170
+ */
171
+ async handleSubmissionError(error, request, h) {
172
+ if (error instanceof InvalidComponentStateError) {
173
+ return this.handleInvalidComponentStateError(error, request, h);
174
+ }
175
+ if (error instanceof PaymentPreAuthError) {
176
+ return this.handlePaymentPreAuthError(error, request, h);
177
+ }
178
+ if (error instanceof PaymentSubmissionError) {
179
+ return this.handlePaymentSubmissionError(error, request, h);
180
+ }
181
+ throw error;
182
+ }
183
+
184
+ /**
185
+ * Handles InvalidComponentStateError during submission
186
+ */
187
+ async handleInvalidComponentStateError(error, request, h) {
188
+ const cacheService = getCacheService(request.server);
189
+ const govukError = createError(error.component.name, error.userMessage);
190
+ request.yar.flash(COMPONENT_STATE_ERROR, govukError, true);
191
+ await cacheService.resetComponentStates(request, error.getStateKeys());
192
+ return this.proceed(request, h, error.component.page?.path);
193
+ }
194
+
195
+ /**
196
+ * Handles PaymentPreAuthError during submission
197
+ */
198
+ async handlePaymentPreAuthError(error, request, h) {
199
+ const cacheService = getCacheService(request.server);
200
+ if (error.shouldResetState) {
201
+ await cacheService.resetComponentStates(request, error.getStateKeys());
202
+ if (error.errorType === PaymentErrorTypes.PaymentExpired) {
203
+ request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true);
204
+ return this.proceed(request, h, error.component.page?.path);
205
+ }
206
+ }
207
+ const govukError = createError(error.component.name, error.userMessage);
208
+ request.yar.flash(COMPONENT_STATE_ERROR, govukError, true);
209
+ const redirectPath = error.shouldResetState ? error.component.page?.path : undefined;
210
+ return this.proceed(request, h, redirectPath);
211
+ }
212
+
213
+ /**
214
+ * Handles PaymentSubmissionError during submission
215
+ */
216
+ handlePaymentSubmissionError(error, request, h) {
217
+ const helpUrl = error.helpLink ?? DEFAULT_PAYMENT_HELP_URL;
218
+ const helpLinkHtml = ` or you can <a href="${helpUrl}" target="_blank" rel="noopener noreferrer" class="govuk-link">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`;
219
+ const govukError = createError('submission', `There was a problem and your form was not submitted. Try submitting the form again${helpLinkHtml}.`);
220
+ request.yar.flash(COMPONENT_STATE_ERROR, govukError, true);
221
+ return this.proceed(request, h);
222
+ }
130
223
  get postRouteOptions() {
131
224
  return {
132
225
  ext: {
@@ -141,20 +234,41 @@ export class SummaryPageController extends QuestionPageController {
141
234
  }
142
235
  export async function submitForm(context, metadata, request, summaryViewModel, model, emailAddress, formMetadata) {
143
236
  await finaliseComponents(request, metadata, context);
237
+ const paymentWasCaptured = hasPaymentBeenCaptured(context);
144
238
  const formStatus = checkFormStatus(request.params);
145
239
  const logTags = ['submit', 'submissionApi'];
146
240
  request.logger.info(logTags, 'Preparing email', formStatus);
147
-
148
- // Get detail items
149
241
  const items = getFormSubmissionData(summaryViewModel.context, summaryViewModel.details);
242
+ try {
243
+ request.logger.info(logTags, 'Submitting data');
244
+ const submitResponse = await submitData(model, items, emailAddress, request.yar.id);
245
+ if (submitResponse === undefined) {
246
+ throw Boom.badRequest('Unexpected empty response from submit api');
247
+ }
248
+ await model.services.outputService.submit(context, request, model, emailAddress, items, submitResponse, formMetadata);
249
+ } catch (err) {
250
+ if (paymentWasCaptured) {
251
+ throw new PaymentSubmissionError(context.referenceNumber, formMetadata.contact?.online?.url);
252
+ }
253
+ throw err;
254
+ }
255
+ }
150
256
 
151
- // Submit data
152
- request.logger.info(logTags, 'Submitting data');
153
- const submitResponse = await submitData(model, items, emailAddress, request.yar.id);
154
- if (submitResponse === undefined) {
155
- throw Boom.badRequest('Unexpected empty response from submit api');
257
+ /**
258
+ * Checks if any payment component has been captured
259
+ */
260
+ function hasPaymentBeenCaptured(context) {
261
+ for (const page of context.relevantPages) {
262
+ for (const field of page.collection.fields) {
263
+ if (field instanceof PaymentField) {
264
+ const paymentState = field.getPaymentStateFromState(context.state);
265
+ if (paymentState?.capture?.status === 'success') {
266
+ return true;
267
+ }
268
+ }
269
+ }
156
270
  }
157
- return model.services.outputService.submit(context, request, model, emailAddress, items, submitResponse, formMetadata);
271
+ return false;
158
272
  }
159
273
 
160
274
  /**
@@ -184,37 +298,45 @@ function submitData(model, items, retrievalKey, sessionId) {
184
298
  const payload = {
185
299
  sessionId,
186
300
  retrievalKey,
187
- // Main form answers
188
- main: items.filter(item => 'field' in item).map(item => ({
189
- name: item.name,
190
- title: item.label,
191
- value: getAnswer(item.field, item.state, {
192
- format: 'data'
193
- })
194
- })),
195
- // Repeater form answers
196
- repeaters: items.filter(item => 'subItems' in item).map(item => ({
197
- name: item.name,
198
- title: item.label,
199
- // Repeater item values
200
- value: item.subItems.map(detailItems => detailItems.map(subItem => ({
201
- name: subItem.name,
202
- title: subItem.label,
203
- value: getAnswer(subItem.field, subItem.state, {
204
- format: 'data'
205
- })
206
- })))
207
- }))
301
+ main: buildMainRecords(items),
302
+ repeaters: buildRepeaterRecords(items)
208
303
  };
209
304
  return submit(payload);
210
305
  }
211
306
  export function getFormSubmissionData(context, details) {
212
- return context.relevantPages.map(({
307
+ const items = context.relevantPages.map(({
213
308
  href
214
309
  }) => details.flatMap(({
215
310
  items
216
311
  }) => items.filter(({
217
312
  page
218
313
  }) => page.href === href))).flat();
314
+ const paymentItems = getPaymentFieldItems(context);
315
+ return [...items, ...paymentItems];
316
+ }
317
+
318
+ /**
319
+ * Gets DetailItems for PaymentField components
320
+ * PaymentField is excluded from summaryDetails for UI but needs to be in submission data
321
+ */
322
+ function getPaymentFieldItems(context) {
323
+ const items = [];
324
+ for (const page of context.relevantPages) {
325
+ for (const field of page.collection.fields) {
326
+ if (field instanceof PaymentField) {
327
+ items.push({
328
+ name: field.name,
329
+ page,
330
+ title: field.title,
331
+ label: field.label,
332
+ field,
333
+ state: context.state,
334
+ href: page.href,
335
+ value: field.getDisplayStringFromState(context.state)
336
+ });
337
+ }
338
+ }
339
+ }
340
+ return items;
219
341
  }
220
342
  //# sourceMappingURL=SummaryPageController.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SummaryPageController.js","names":["hasComponentsEvenIfNoNext","Boom","COMPONENT_STATE_ERROR","ComponentCollection","getAnswer","checkEmailAddressForLiveFormSubmission","checkFormStatus","createError","getCacheService","SummaryViewModel","QuestionPageController","InvalidComponentStateError","FormAction","SummaryPageController","allowSaveAndExit","constructor","model","pageDef","viewName","collection","components","page","getSummaryViewModel","request","context","viewModel","query","payload","errors","getViewModel","backLink","getBackLink","feedbackLink","phaseTag","shouldShowSaveAndExit","server","makeGetRouteHandler","h","hasMissingNotificationEmail","view","makePostRouteHandler","action","SaveAndExit","handleSaveAndExit","handleFormSubmit","params","cacheService","formsService","services","getFormMetadata","formMetadata","slug","notificationEmail","isPreview","submitForm","error","govukError","component","name","userMessage","yar","flash","resetComponentStates","getStateKeys","proceed","path","setConfirmationState","confirmed","formId","state","referenceNumber","clearState","getStatusPath","postRouteOptions","ext","onPreHandler","method","continue","metadata","summaryViewModel","emailAddress","finaliseComponents","formStatus","logTags","logger","info","items","getFormSubmissionData","details","submitResponse","submitData","id","undefined","badRequest","outputService","submit","relevantFields","relevantPages","flatMap","fields","onSubmit","retrievalKey","sessionId","formSubmissionService","main","filter","item","map","title","label","value","field","format","repeaters","subItems","detailItems","subItem","href","flat"],"sources":["../../../../../src/server/plugins/engine/pageControllers/SummaryPageController.ts"],"sourcesContent":["import {\n hasComponentsEvenIfNoNext,\n type FormMetadata,\n type Page,\n type SubmitPayload\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type RouteOptions } from '@hapi/hapi'\n\nimport { COMPONENT_STATE_ERROR } from '~/src/server/constants.js'\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n createError,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport {\n type Detail,\n type DetailItem\n} from '~/src/server/plugins/engine/models/types.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { InvalidComponentStateError } from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n type FormConfirmationState,\n type FormContext,\n type FormContextRequest\n} from '~/src/server/plugins/engine/types.js'\nimport {\n FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class SummaryPageController extends QuestionPageController {\n declare pageDef: Page\n allowSaveAndExit = true\n\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/summary.js\"\n */\n\n constructor(model: FormModel, pageDef: Page) {\n super(model, pageDef)\n this.viewName = 'summary'\n\n // Components collection\n this.collection = new ComponentCollection(\n hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [],\n { model, page: this }\n )\n }\n\n getSummaryViewModel(\n request: FormContextRequest,\n context: FormContext\n ): SummaryViewModel {\n const viewModel = new SummaryViewModel(request, this, context)\n\n const { query } = request\n const { payload, errors } = context\n const components = this.collection.getViewModel(payload, errors, query)\n\n // We already figure these out in the base page controller. Take them and apply them to our page-specific model.\n // This is a stop-gap until we can add proper inheritance in place.\n viewModel.backLink = this.getBackLink(request, context)\n viewModel.feedbackLink = this.feedbackLink\n viewModel.phaseTag = this.phaseTag\n viewModel.components = components\n viewModel.allowSaveAndExit = this.shouldShowSaveAndExit(request.server)\n viewModel.errors = errors\n\n return viewModel\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,\n */\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewName } = this\n\n const viewModel = this.getSummaryViewModel(request, context)\n\n viewModel.hasMissingNotificationEmail =\n await this.hasMissingNotificationEmail(request, context)\n\n return h.view(viewName, viewModel)\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`.\n * If a form is incomplete, a user will be redirected to the start page.\n */\n makePostRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n // Check if this is a save-and-exit action\n const { action } = request.payload\n if (action === FormAction.SaveAndExit) {\n return this.handleSaveAndExit(request, context, h)\n }\n\n return this.handleFormSubmit(request, context, h)\n }\n }\n\n async handleFormSubmit(\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) {\n const { model } = this\n const { params } = request\n\n const cacheService = getCacheService(request.server)\n\n const { formsService } = this.model.services\n const { getFormMetadata } = formsService\n\n // Get the form metadata using the `slug` param\n const formMetadata = await getFormMetadata(params.slug)\n const { notificationEmail } = formMetadata\n const { isPreview } = checkFormStatus(request.params)\n\n checkEmailAddressForLiveFormSubmission(notificationEmail, isPreview)\n\n // Send submission email\n if (notificationEmail) {\n const viewModel = this.getSummaryViewModel(request, context)\n\n try {\n await submitForm(\n context,\n formMetadata,\n request,\n viewModel,\n model,\n notificationEmail,\n formMetadata\n )\n } catch (error) {\n if (error instanceof InvalidComponentStateError) {\n const govukError = createError(\n error.component.name,\n error.userMessage\n )\n\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n await cacheService.resetComponentStates(request, error.getStateKeys())\n\n return this.proceed(request, h, error.component.page?.path)\n }\n\n throw error\n }\n }\n\n await cacheService.setConfirmationState(request, {\n confirmed: true,\n formId: context.state.formId,\n referenceNumber: context.referenceNumber\n } as FormConfirmationState)\n\n // Clear all form data\n await cacheService.clearState(request)\n\n return this.proceed(request, h, this.getStatusPath())\n }\n\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {\n ext: {\n onPreHandler: {\n method(request, h) {\n return h.continue\n }\n }\n }\n }\n }\n}\n\nexport async function submitForm(\n context: FormContext,\n metadata: FormMetadata,\n request: FormRequestPayload,\n summaryViewModel: SummaryViewModel,\n model: FormModel,\n emailAddress: string,\n formMetadata: FormMetadata\n) {\n await finaliseComponents(request, metadata, context)\n\n const formStatus = checkFormStatus(request.params)\n const logTags = ['submit', 'submissionApi']\n\n request.logger.info(logTags, 'Preparing email', formStatus)\n\n // Get detail items\n const items = getFormSubmissionData(\n summaryViewModel.context,\n summaryViewModel.details\n )\n\n // Submit data\n request.logger.info(logTags, 'Submitting data')\n const submitResponse = await submitData(\n model,\n items,\n emailAddress,\n request.yar.id\n )\n\n if (submitResponse === undefined) {\n throw Boom.badRequest('Unexpected empty response from submit api')\n }\n\n return model.services.outputService.submit(\n context,\n request,\n model,\n emailAddress,\n items,\n submitResponse,\n formMetadata\n )\n}\n\n/**\n * Finalises any components that need post-processing before form submission. Candidates usually involve\n * those that have external state.\n * Examples include:\n * - file uploads which are 'persisted' before submission\n * - payments which are 'captured' before submission\n */\nasync function finaliseComponents(\n request: FormRequestPayload,\n metadata: FormMetadata,\n context: FormContext\n) {\n const relevantFields = context.relevantPages.flatMap(\n (page) => page.collection.fields\n )\n\n for (const component of relevantFields) {\n /*\n Each component will throw InvalidComponent if its state is invalid, which is handled\n by handleFormSubmit\n */\n await component.onSubmit(request, metadata, context)\n }\n}\n\nfunction submitData(\n model: FormModel,\n items: DetailItem[],\n retrievalKey: string,\n sessionId: string\n) {\n const { formSubmissionService } = model.services\n const { submit } = formSubmissionService\n\n const payload: SubmitPayload = {\n sessionId,\n retrievalKey,\n\n // Main form answers\n main: items\n .filter((item) => 'field' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })),\n\n // Repeater form answers\n repeaters: items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n\n // Repeater item values\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\n return submit(payload)\n}\n\nexport function getFormSubmissionData(context: FormContext, details: Detail[]) {\n return context.relevantPages\n .map(({ href }) =>\n details.flatMap(({ items }) =>\n items.filter(({ page }) => page.href === href)\n )\n )\n .flat()\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"SummaryPageController.js","names":["hasComponentsEvenIfNoNext","Boom","COMPONENT_STATE_ERROR","PAYMENT_EXPIRED_NOTIFICATION","ComponentCollection","PaymentField","checkEmailAddressForLiveFormSubmission","checkFormStatus","createError","getCacheService","SummaryViewModel","QuestionPageController","InvalidComponentStateError","PaymentErrorTypes","PaymentPreAuthError","PaymentSubmissionError","buildMainRecords","buildRepeaterRecords","DEFAULT_PAYMENT_HELP_URL","formatPaymentAmount","formatPaymentDate","FormAction","SummaryPageController","allowSaveAndExit","constructor","model","pageDef","viewName","collection","components","page","getSummaryViewModel","request","context","viewModel","query","payload","errors","state","paymentField","relevantPages","flatMap","fields","find","field","paymentState","getPaymentStateFromState","paymentDetails","buildPaymentDetails","getViewModel","backLink","getBackLink","feedbackLink","phaseTag","shouldShowSaveAndExit","server","rows","key","text","value","description","amount","reference","preAuth","createdAt","push","title","summaryList","makeGetRouteHandler","h","hasMissingNotificationEmail","view","makePostRouteHandler","action","SaveAndExit","handleSaveAndExit","handleFormSubmit","params","cacheService","formsService","services","getFormMetadata","formMetadata","slug","notificationEmail","isPreview","submitForm","error","handleSubmissionError","setConfirmationState","confirmed","formId","referenceNumber","clearState","proceed","getStatusPath","handleInvalidComponentStateError","handlePaymentPreAuthError","handlePaymentSubmissionError","govukError","component","name","userMessage","yar","flash","resetComponentStates","getStateKeys","path","shouldResetState","errorType","PaymentExpired","redirectPath","undefined","helpUrl","helpLink","helpLinkHtml","postRouteOptions","ext","onPreHandler","method","continue","metadata","summaryViewModel","emailAddress","finaliseComponents","paymentWasCaptured","hasPaymentBeenCaptured","formStatus","logTags","logger","info","items","getFormSubmissionData","details","submitResponse","submitData","id","badRequest","outputService","submit","err","contact","online","url","capture","status","relevantFields","onSubmit","retrievalKey","sessionId","formSubmissionService","main","repeaters","map","href","filter","flat","paymentItems","getPaymentFieldItems","label","getDisplayStringFromState"],"sources":["../../../../../src/server/plugins/engine/pageControllers/SummaryPageController.ts"],"sourcesContent":["import {\n hasComponentsEvenIfNoNext,\n type FormMetadata,\n type Page,\n type SubmitPayload\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type RouteOptions } from '@hapi/hapi'\n\nimport {\n COMPONENT_STATE_ERROR,\n PAYMENT_EXPIRED_NOTIFICATION\n} from '~/src/server/constants.js'\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n createError,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport {\n type Detail,\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n InvalidComponentStateError,\n PaymentErrorTypes,\n PaymentPreAuthError,\n PaymentSubmissionError\n} from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n buildMainRecords,\n buildRepeaterRecords\n} from '~/src/server/plugins/engine/pageControllers/helpers/submission.js'\nimport {\n type FormConfirmationState,\n type FormContext,\n type FormContextRequest\n} from '~/src/server/plugins/engine/types.js'\nimport {\n DEFAULT_PAYMENT_HELP_URL,\n formatPaymentAmount,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\nimport {\n FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class SummaryPageController extends QuestionPageController {\n declare pageDef: Page\n allowSaveAndExit = true\n\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/summary.js\"\n */\n\n constructor(model: FormModel, pageDef: Page) {\n super(model, pageDef)\n this.viewName = 'summary'\n\n // Components collection\n this.collection = new ComponentCollection(\n hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [],\n { model, page: this }\n )\n }\n\n getSummaryViewModel(\n request: FormContextRequest,\n context: FormContext\n ): SummaryViewModel {\n const viewModel = new SummaryViewModel(request, this, context)\n\n const { query } = request\n const { payload, errors, state } = context\n\n const paymentField = context.relevantPages\n .flatMap((page) => page.collection.fields)\n .find((field): field is PaymentField => field instanceof PaymentField)\n\n if (paymentField) {\n const paymentState = paymentField.getPaymentStateFromState(state)\n if (paymentState) {\n viewModel.paymentState = paymentState\n viewModel.paymentDetails = this.buildPaymentDetails(\n paymentField,\n paymentState\n )\n }\n }\n\n const components = this.collection.getViewModel(payload, errors, query)\n\n viewModel.backLink = this.getBackLink(request, context)\n viewModel.feedbackLink = this.feedbackLink\n viewModel.phaseTag = this.phaseTag\n viewModel.components = components\n viewModel.allowSaveAndExit = this.shouldShowSaveAndExit(request.server)\n viewModel.errors = errors\n\n return viewModel\n }\n\n private buildPaymentDetails(\n paymentField: PaymentField,\n paymentState: NonNullable<\n ReturnType<PaymentField['getPaymentStateFromState']>\n >\n ) {\n const rows = [\n {\n key: { text: 'Payment for' },\n value: { text: paymentState.description }\n },\n {\n key: { text: 'Total amount' },\n value: { text: formatPaymentAmount(paymentState.amount) }\n },\n {\n key: { text: 'Reference' },\n value: { text: paymentState.reference }\n }\n ]\n\n if (paymentState.preAuth?.createdAt) {\n rows.push({\n key: { text: 'Date of payment' },\n value: { text: formatPaymentDate(paymentState.preAuth.createdAt) }\n })\n }\n\n return {\n title: { text: 'Payment details' },\n summaryList: { rows }\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,\n */\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewName } = this\n\n const viewModel = this.getSummaryViewModel(request, context)\n\n viewModel.hasMissingNotificationEmail =\n await this.hasMissingNotificationEmail(request, context)\n\n return h.view(viewName, viewModel)\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`.\n * If a form is incomplete, a user will be redirected to the start page.\n */\n makePostRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { action } = request.payload\n if (action === FormAction.SaveAndExit) {\n return this.handleSaveAndExit(request, context, h)\n }\n\n return this.handleFormSubmit(request, context, h)\n }\n }\n\n async handleFormSubmit(\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) {\n const { model } = this\n const { params } = request\n\n const cacheService = getCacheService(request.server)\n\n const { formsService } = this.model.services\n const { getFormMetadata } = formsService\n\n const formMetadata = await getFormMetadata(params.slug)\n const { notificationEmail } = formMetadata\n const { isPreview } = checkFormStatus(request.params)\n\n checkEmailAddressForLiveFormSubmission(notificationEmail, isPreview)\n\n if (notificationEmail) {\n const viewModel = this.getSummaryViewModel(request, context)\n\n try {\n await submitForm(\n context,\n formMetadata,\n request,\n viewModel,\n model,\n notificationEmail,\n formMetadata\n )\n } catch (error) {\n return this.handleSubmissionError(error, request, h)\n }\n }\n\n await cacheService.setConfirmationState(request, {\n confirmed: true,\n formId: context.state.formId,\n referenceNumber: context.referenceNumber\n } as FormConfirmationState)\n\n await cacheService.clearState(request)\n\n return this.proceed(request, h, this.getStatusPath())\n }\n\n /**\n * Handles errors during form submission\n */\n private async handleSubmissionError(\n error: unknown,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n if (error instanceof InvalidComponentStateError) {\n return this.handleInvalidComponentStateError(error, request, h)\n }\n\n if (error instanceof PaymentPreAuthError) {\n return this.handlePaymentPreAuthError(error, request, h)\n }\n\n if (error instanceof PaymentSubmissionError) {\n return this.handlePaymentSubmissionError(error, request, h)\n }\n\n throw error\n }\n\n /**\n * Handles InvalidComponentStateError during submission\n */\n private async handleInvalidComponentStateError(\n error: InvalidComponentStateError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const cacheService = getCacheService(request.server)\n\n const govukError = createError(error.component.name, error.userMessage)\n\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n await cacheService.resetComponentStates(request, error.getStateKeys())\n\n return this.proceed(request, h, error.component.page?.path)\n }\n\n /**\n * Handles PaymentPreAuthError during submission\n */\n private async handlePaymentPreAuthError(\n error: PaymentPreAuthError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const cacheService = getCacheService(request.server)\n\n if (error.shouldResetState) {\n await cacheService.resetComponentStates(request, error.getStateKeys())\n\n if (error.errorType === PaymentErrorTypes.PaymentExpired) {\n request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true)\n return this.proceed(request, h, error.component.page?.path)\n }\n }\n\n const govukError = createError(error.component.name, error.userMessage)\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n const redirectPath = error.shouldResetState\n ? error.component.page?.path\n : undefined\n\n return this.proceed(request, h, redirectPath)\n }\n\n /**\n * Handles PaymentSubmissionError during submission\n */\n private handlePaymentSubmissionError(\n error: PaymentSubmissionError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const helpUrl = error.helpLink ?? DEFAULT_PAYMENT_HELP_URL\n const helpLinkHtml = ` or you can <a href=\"${helpUrl}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"govuk-link\">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`\n\n const govukError = createError(\n 'submission',\n `There was a problem and your form was not submitted. Try submitting the form again${helpLinkHtml}.`\n )\n\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n return this.proceed(request, h)\n }\n\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {\n ext: {\n onPreHandler: {\n method(request, h) {\n return h.continue\n }\n }\n }\n }\n }\n}\n\nexport async function submitForm(\n context: FormContext,\n metadata: FormMetadata,\n request: FormRequestPayload,\n summaryViewModel: SummaryViewModel,\n model: FormModel,\n emailAddress: string,\n formMetadata: FormMetadata\n) {\n await finaliseComponents(request, metadata, context)\n\n const paymentWasCaptured = hasPaymentBeenCaptured(context)\n\n const formStatus = checkFormStatus(request.params)\n const logTags = ['submit', 'submissionApi']\n\n request.logger.info(logTags, 'Preparing email', formStatus)\n\n const items = getFormSubmissionData(\n summaryViewModel.context,\n summaryViewModel.details\n )\n\n try {\n request.logger.info(logTags, 'Submitting data')\n const submitResponse = await submitData(\n model,\n items,\n emailAddress,\n request.yar.id\n )\n\n if (submitResponse === undefined) {\n throw Boom.badRequest('Unexpected empty response from submit api')\n }\n\n await model.services.outputService.submit(\n context,\n request,\n model,\n emailAddress,\n items,\n submitResponse,\n formMetadata\n )\n } catch (err) {\n if (paymentWasCaptured) {\n throw new PaymentSubmissionError(\n context.referenceNumber,\n formMetadata.contact?.online?.url\n )\n }\n throw err\n }\n}\n\n/**\n * Checks if any payment component has been captured\n */\nfunction hasPaymentBeenCaptured(context: FormContext): boolean {\n for (const page of context.relevantPages) {\n for (const field of page.collection.fields) {\n if (field instanceof PaymentField) {\n const paymentState = field.getPaymentStateFromState(context.state)\n if (paymentState?.capture?.status === 'success') {\n return true\n }\n }\n }\n }\n return false\n}\n\n/**\n * Finalises any components that need post-processing before form submission. Candidates usually involve\n * those that have external state.\n * Examples include:\n * - file uploads which are 'persisted' before submission\n * - payments which are 'captured' before submission\n */\nasync function finaliseComponents(\n request: FormRequestPayload,\n metadata: FormMetadata,\n context: FormContext\n) {\n const relevantFields = context.relevantPages.flatMap(\n (page) => page.collection.fields\n )\n\n for (const component of relevantFields) {\n /*\n Each component will throw InvalidComponent if its state is invalid, which is handled\n by handleFormSubmit\n */\n await component.onSubmit(request, metadata, context)\n }\n}\n\nfunction submitData(\n model: FormModel,\n items: DetailItem[],\n retrievalKey: string,\n sessionId: string\n) {\n const { formSubmissionService } = model.services\n const { submit } = formSubmissionService\n\n const payload: SubmitPayload = {\n sessionId,\n retrievalKey,\n main: buildMainRecords(items),\n repeaters: buildRepeaterRecords(items)\n }\n\n return submit(payload)\n}\n\nexport function getFormSubmissionData(context: FormContext, details: Detail[]) {\n const items = context.relevantPages\n .map(({ href }) =>\n details.flatMap(({ items }) =>\n items.filter(({ page }) => page.href === href)\n )\n )\n .flat()\n\n const paymentItems = getPaymentFieldItems(context)\n\n return [...items, ...paymentItems]\n}\n\n/**\n * Gets DetailItems for PaymentField components\n * PaymentField is excluded from summaryDetails for UI but needs to be in submission data\n */\nfunction getPaymentFieldItems(context: FormContext): DetailItemField[] {\n const items: DetailItemField[] = []\n\n for (const page of context.relevantPages) {\n for (const field of page.collection.fields) {\n if (field instanceof PaymentField) {\n items.push({\n name: field.name,\n page,\n title: field.title,\n label: field.label,\n field,\n state: context.state,\n href: page.href,\n value: field.getDisplayStringFromState(context.state)\n })\n }\n }\n }\n\n return items\n}\n"],"mappings":"AAAA,SACEA,yBAAyB,QAIpB,oBAAoB;AAC3B,OAAOC,IAAI,MAAM,YAAY;AAG7B,SACEC,qBAAqB,EACrBC,4BAA4B;AAE9B,SAASC,mBAAmB;AAC5B,SAASC,YAAY;AACrB,SACEC,sCAAsC,EACtCC,eAAe,EACfC,WAAW,EACXC,eAAe;AAEjB,SACEC,gBAAgB;AAQlB,SAASC,sBAAsB;AAC/B,SACEC,0BAA0B,EAC1BC,iBAAiB,EACjBC,mBAAmB,EACnBC,sBAAsB;AAExB,SACEC,gBAAgB,EAChBC,oBAAoB;AAOtB,SACEC,wBAAwB,EACxBC,mBAAmB,EACnBC,iBAAiB;AAEnB,SACEC,UAAU;AAOZ,OAAO,MAAMC,qBAAqB,SAASX,sBAAsB,CAAC;EAEhEY,gBAAgB,GAAG,IAAI;;EAEvB;AACF;AACA;;EAEEC,WAAWA,CAACC,KAAgB,EAAEC,OAAa,EAAE;IAC3C,KAAK,CAACD,KAAK,EAAEC,OAAO,CAAC;IACrB,IAAI,CAACC,QAAQ,GAAG,SAAS;;IAEzB;IACA,IAAI,CAACC,UAAU,GAAG,IAAIxB,mBAAmB,CACvCJ,yBAAyB,CAAC0B,OAAO,CAAC,GAAGA,OAAO,CAACG,UAAU,GAAG,EAAE,EAC5D;MAAEJ,KAAK;MAAEK,IAAI,EAAE;IAAK,CACtB,CAAC;EACH;EAEAC,mBAAmBA,CACjBC,OAA2B,EAC3BC,OAAoB,EACF;IAClB,MAAMC,SAAS,GAAG,IAAIxB,gBAAgB,CAACsB,OAAO,EAAE,IAAI,EAAEC,OAAO,CAAC;IAE9D,MAAM;MAAEE;IAAM,CAAC,GAAGH,OAAO;IACzB,MAAM;MAAEI,OAAO;MAAEC,MAAM;MAAEC;IAAM,CAAC,GAAGL,OAAO;IAE1C,MAAMM,YAAY,GAAGN,OAAO,CAACO,aAAa,CACvCC,OAAO,CAAEX,IAAI,IAAKA,IAAI,CAACF,UAAU,CAACc,MAAM,CAAC,CACzCC,IAAI,CAAEC,KAAK,IAA4BA,KAAK,YAAYvC,YAAY,CAAC;IAExE,IAAIkC,YAAY,EAAE;MAChB,MAAMM,YAAY,GAAGN,YAAY,CAACO,wBAAwB,CAACR,KAAK,CAAC;MACjE,IAAIO,YAAY,EAAE;QAChBX,SAAS,CAACW,YAAY,GAAGA,YAAY;QACrCX,SAAS,CAACa,cAAc,GAAG,IAAI,CAACC,mBAAmB,CACjDT,YAAY,EACZM,YACF,CAAC;MACH;IACF;IAEA,MAAMhB,UAAU,GAAG,IAAI,CAACD,UAAU,CAACqB,YAAY,CAACb,OAAO,EAAEC,MAAM,EAAEF,KAAK,CAAC;IAEvED,SAAS,CAACgB,QAAQ,GAAG,IAAI,CAACC,WAAW,CAACnB,OAAO,EAAEC,OAAO,CAAC;IACvDC,SAAS,CAACkB,YAAY,GAAG,IAAI,CAACA,YAAY;IAC1ClB,SAAS,CAACmB,QAAQ,GAAG,IAAI,CAACA,QAAQ;IAClCnB,SAAS,CAACL,UAAU,GAAGA,UAAU;IACjCK,SAAS,CAACX,gBAAgB,GAAG,IAAI,CAAC+B,qBAAqB,CAACtB,OAAO,CAACuB,MAAM,CAAC;IACvErB,SAAS,CAACG,MAAM,GAAGA,MAAM;IAEzB,OAAOH,SAAS;EAClB;EAEQc,mBAAmBA,CACzBT,YAA0B,EAC1BM,YAEC,EACD;IACA,MAAMW,IAAI,GAAG,CACX;MACEC,GAAG,EAAE;QAAEC,IAAI,EAAE;MAAc,CAAC;MAC5BC,KAAK,EAAE;QAAED,IAAI,EAAEb,YAAY,CAACe;MAAY;IAC1C,CAAC,EACD;MACEH,GAAG,EAAE;QAAEC,IAAI,EAAE;MAAe,CAAC;MAC7BC,KAAK,EAAE;QAAED,IAAI,EAAEvC,mBAAmB,CAAC0B,YAAY,CAACgB,MAAM;MAAE;IAC1D,CAAC,EACD;MACEJ,GAAG,EAAE;QAAEC,IAAI,EAAE;MAAY,CAAC;MAC1BC,KAAK,EAAE;QAAED,IAAI,EAAEb,YAAY,CAACiB;MAAU;IACxC,CAAC,CACF;IAED,IAAIjB,YAAY,CAACkB,OAAO,EAAEC,SAAS,EAAE;MACnCR,IAAI,CAACS,IAAI,CAAC;QACRR,GAAG,EAAE;UAAEC,IAAI,EAAE;QAAkB,CAAC;QAChCC,KAAK,EAAE;UAAED,IAAI,EAAEtC,iBAAiB,CAACyB,YAAY,CAACkB,OAAO,CAACC,SAAS;QAAE;MACnE,CAAC,CAAC;IACJ;IAEA,OAAO;MACLE,KAAK,EAAE;QAAER,IAAI,EAAE;MAAkB,CAAC;MAClCS,WAAW,EAAE;QAAEX;MAAK;IACtB,CAAC;EACH;;EAEA;AACF;AACA;EACEY,mBAAmBA,CAAA,EAAG;IACpB,OAAO,OACLpC,OAAoB,EACpBC,OAAoB,EACpBoC,CAAsB,KACnB;MACH,MAAM;QAAE1C;MAAS,CAAC,GAAG,IAAI;MAEzB,MAAMO,SAAS,GAAG,IAAI,CAACH,mBAAmB,CAACC,OAAO,EAAEC,OAAO,CAAC;MAE5DC,SAAS,CAACoC,2BAA2B,GACnC,MAAM,IAAI,CAACA,2BAA2B,CAACtC,OAAO,EAAEC,OAAO,CAAC;MAE1D,OAAOoC,CAAC,CAACE,IAAI,CAAC5C,QAAQ,EAAEO,SAAS,CAAC;IACpC,CAAC;EACH;;EAEA;AACF;AACA;AACA;EACEsC,oBAAoBA,CAAA,EAAG;IACrB,OAAO,OACLxC,OAA2B,EAC3BC,OAAoB,EACpBoC,CAAsB,KACnB;MACH,MAAM;QAAEI;MAAO,CAAC,GAAGzC,OAAO,CAACI,OAAO;MAClC,IAAIqC,MAAM,KAAKpD,UAAU,CAACqD,WAAW,EAAE;QACrC,OAAO,IAAI,CAACC,iBAAiB,CAAC3C,OAAO,EAAEC,OAAO,EAAEoC,CAAC,CAAC;MACpD;MAEA,OAAO,IAAI,CAACO,gBAAgB,CAAC5C,OAAO,EAAEC,OAAO,EAAEoC,CAAC,CAAC;IACnD,CAAC;EACH;EAEA,MAAMO,gBAAgBA,CACpB5C,OAA2B,EAC3BC,OAAoB,EACpBoC,CAAsB,EACtB;IACA,MAAM;MAAE5C;IAAM,CAAC,GAAG,IAAI;IACtB,MAAM;MAAEoD;IAAO,CAAC,GAAG7C,OAAO;IAE1B,MAAM8C,YAAY,GAAGrE,eAAe,CAACuB,OAAO,CAACuB,MAAM,CAAC;IAEpD,MAAM;MAAEwB;IAAa,CAAC,GAAG,IAAI,CAACtD,KAAK,CAACuD,QAAQ;IAC5C,MAAM;MAAEC;IAAgB,CAAC,GAAGF,YAAY;IAExC,MAAMG,YAAY,GAAG,MAAMD,eAAe,CAACJ,MAAM,CAACM,IAAI,CAAC;IACvD,MAAM;MAAEC;IAAkB,CAAC,GAAGF,YAAY;IAC1C,MAAM;MAAEG;IAAU,CAAC,GAAG9E,eAAe,CAACyB,OAAO,CAAC6C,MAAM,CAAC;IAErDvE,sCAAsC,CAAC8E,iBAAiB,EAAEC,SAAS,CAAC;IAEpE,IAAID,iBAAiB,EAAE;MACrB,MAAMlD,SAAS,GAAG,IAAI,CAACH,mBAAmB,CAACC,OAAO,EAAEC,OAAO,CAAC;MAE5D,IAAI;QACF,MAAMqD,UAAU,CACdrD,OAAO,EACPiD,YAAY,EACZlD,OAAO,EACPE,SAAS,EACTT,KAAK,EACL2D,iBAAiB,EACjBF,YACF,CAAC;MACH,CAAC,CAAC,OAAOK,KAAK,EAAE;QACd,OAAO,IAAI,CAACC,qBAAqB,CAACD,KAAK,EAAEvD,OAAO,EAAEqC,CAAC,CAAC;MACtD;IACF;IAEA,MAAMS,YAAY,CAACW,oBAAoB,CAACzD,OAAO,EAAE;MAC/C0D,SAAS,EAAE,IAAI;MACfC,MAAM,EAAE1D,OAAO,CAACK,KAAK,CAACqD,MAAM;MAC5BC,eAAe,EAAE3D,OAAO,CAAC2D;IAC3B,CAA0B,CAAC;IAE3B,MAAMd,YAAY,CAACe,UAAU,CAAC7D,OAAO,CAAC;IAEtC,OAAO,IAAI,CAAC8D,OAAO,CAAC9D,OAAO,EAAEqC,CAAC,EAAE,IAAI,CAAC0B,aAAa,CAAC,CAAC,CAAC;EACvD;;EAEA;AACF;AACA;EACE,MAAcP,qBAAqBA,CACjCD,KAAc,EACdvD,OAA2B,EAC3BqC,CAAsB,EACtB;IACA,IAAIkB,KAAK,YAAY3E,0BAA0B,EAAE;MAC/C,OAAO,IAAI,CAACoF,gCAAgC,CAACT,KAAK,EAAEvD,OAAO,EAAEqC,CAAC,CAAC;IACjE;IAEA,IAAIkB,KAAK,YAAYzE,mBAAmB,EAAE;MACxC,OAAO,IAAI,CAACmF,yBAAyB,CAACV,KAAK,EAAEvD,OAAO,EAAEqC,CAAC,CAAC;IAC1D;IAEA,IAAIkB,KAAK,YAAYxE,sBAAsB,EAAE;MAC3C,OAAO,IAAI,CAACmF,4BAA4B,CAACX,KAAK,EAAEvD,OAAO,EAAEqC,CAAC,CAAC;IAC7D;IAEA,MAAMkB,KAAK;EACb;;EAEA;AACF;AACA;EACE,MAAcS,gCAAgCA,CAC5CT,KAAiC,EACjCvD,OAA2B,EAC3BqC,CAAsB,EACtB;IACA,MAAMS,YAAY,GAAGrE,eAAe,CAACuB,OAAO,CAACuB,MAAM,CAAC;IAEpD,MAAM4C,UAAU,GAAG3F,WAAW,CAAC+E,KAAK,CAACa,SAAS,CAACC,IAAI,EAAEd,KAAK,CAACe,WAAW,CAAC;IAEvEtE,OAAO,CAACuE,GAAG,CAACC,KAAK,CAACtG,qBAAqB,EAAEiG,UAAU,EAAE,IAAI,CAAC;IAE1D,MAAMrB,YAAY,CAAC2B,oBAAoB,CAACzE,OAAO,EAAEuD,KAAK,CAACmB,YAAY,CAAC,CAAC,CAAC;IAEtE,OAAO,IAAI,CAACZ,OAAO,CAAC9D,OAAO,EAAEqC,CAAC,EAAEkB,KAAK,CAACa,SAAS,CAACtE,IAAI,EAAE6E,IAAI,CAAC;EAC7D;;EAEA;AACF;AACA;EACE,MAAcV,yBAAyBA,CACrCV,KAA0B,EAC1BvD,OAA2B,EAC3BqC,CAAsB,EACtB;IACA,MAAMS,YAAY,GAAGrE,eAAe,CAACuB,OAAO,CAACuB,MAAM,CAAC;IAEpD,IAAIgC,KAAK,CAACqB,gBAAgB,EAAE;MAC1B,MAAM9B,YAAY,CAAC2B,oBAAoB,CAACzE,OAAO,EAAEuD,KAAK,CAACmB,YAAY,CAAC,CAAC,CAAC;MAEtE,IAAInB,KAAK,CAACsB,SAAS,KAAKhG,iBAAiB,CAACiG,cAAc,EAAE;QACxD9E,OAAO,CAACuE,GAAG,CAACC,KAAK,CAACrG,4BAA4B,EAAE,IAAI,EAAE,IAAI,CAAC;QAC3D,OAAO,IAAI,CAAC2F,OAAO,CAAC9D,OAAO,EAAEqC,CAAC,EAAEkB,KAAK,CAACa,SAAS,CAACtE,IAAI,EAAE6E,IAAI,CAAC;MAC7D;IACF;IAEA,MAAMR,UAAU,GAAG3F,WAAW,CAAC+E,KAAK,CAACa,SAAS,CAACC,IAAI,EAAEd,KAAK,CAACe,WAAW,CAAC;IACvEtE,OAAO,CAACuE,GAAG,CAACC,KAAK,CAACtG,qBAAqB,EAAEiG,UAAU,EAAE,IAAI,CAAC;IAE1D,MAAMY,YAAY,GAAGxB,KAAK,CAACqB,gBAAgB,GACvCrB,KAAK,CAACa,SAAS,CAACtE,IAAI,EAAE6E,IAAI,GAC1BK,SAAS;IAEb,OAAO,IAAI,CAAClB,OAAO,CAAC9D,OAAO,EAAEqC,CAAC,EAAE0C,YAAY,CAAC;EAC/C;;EAEA;AACF;AACA;EACUb,4BAA4BA,CAClCX,KAA6B,EAC7BvD,OAA2B,EAC3BqC,CAAsB,EACtB;IACA,MAAM4C,OAAO,GAAG1B,KAAK,CAAC2B,QAAQ,IAAIhG,wBAAwB;IAC1D,MAAMiG,YAAY,GAAG,wBAAwBF,OAAO,sJAAsJ;IAE1M,MAAMd,UAAU,GAAG3F,WAAW,CAC5B,YAAY,EACZ,qFAAqF2G,YAAY,GACnG,CAAC;IAEDnF,OAAO,CAACuE,GAAG,CAACC,KAAK,CAACtG,qBAAqB,EAAEiG,UAAU,EAAE,IAAI,CAAC;IAE1D,OAAO,IAAI,CAACL,OAAO,CAAC9D,OAAO,EAAEqC,CAAC,CAAC;EACjC;EAEA,IAAI+C,gBAAgBA,CAAA,EAAyC;IAC3D,OAAO;MACLC,GAAG,EAAE;QACHC,YAAY,EAAE;UACZC,MAAMA,CAACvF,OAAO,EAAEqC,CAAC,EAAE;YACjB,OAAOA,CAAC,CAACmD,QAAQ;UACnB;QACF;MACF;IACF,CAAC;EACH;AACF;AAEA,OAAO,eAAelC,UAAUA,CAC9BrD,OAAoB,EACpBwF,QAAsB,EACtBzF,OAA2B,EAC3B0F,gBAAkC,EAClCjG,KAAgB,EAChBkG,YAAoB,EACpBzC,YAA0B,EAC1B;EACA,MAAM0C,kBAAkB,CAAC5F,OAAO,EAAEyF,QAAQ,EAAExF,OAAO,CAAC;EAEpD,MAAM4F,kBAAkB,GAAGC,sBAAsB,CAAC7F,OAAO,CAAC;EAE1D,MAAM8F,UAAU,GAAGxH,eAAe,CAACyB,OAAO,CAAC6C,MAAM,CAAC;EAClD,MAAMmD,OAAO,GAAG,CAAC,QAAQ,EAAE,eAAe,CAAC;EAE3ChG,OAAO,CAACiG,MAAM,CAACC,IAAI,CAACF,OAAO,EAAE,iBAAiB,EAAED,UAAU,CAAC;EAE3D,MAAMI,KAAK,GAAGC,qBAAqB,CACjCV,gBAAgB,CAACzF,OAAO,EACxByF,gBAAgB,CAACW,OACnB,CAAC;EAED,IAAI;IACFrG,OAAO,CAACiG,MAAM,CAACC,IAAI,CAACF,OAAO,EAAE,iBAAiB,CAAC;IAC/C,MAAMM,cAAc,GAAG,MAAMC,UAAU,CACrC9G,KAAK,EACL0G,KAAK,EACLR,YAAY,EACZ3F,OAAO,CAACuE,GAAG,CAACiC,EACd,CAAC;IAED,IAAIF,cAAc,KAAKtB,SAAS,EAAE;MAChC,MAAM/G,IAAI,CAACwI,UAAU,CAAC,2CAA2C,CAAC;IACpE;IAEA,MAAMhH,KAAK,CAACuD,QAAQ,CAAC0D,aAAa,CAACC,MAAM,CACvC1G,OAAO,EACPD,OAAO,EACPP,KAAK,EACLkG,YAAY,EACZQ,KAAK,EACLG,cAAc,EACdpD,YACF,CAAC;EACH,CAAC,CAAC,OAAO0D,GAAG,EAAE;IACZ,IAAIf,kBAAkB,EAAE;MACtB,MAAM,IAAI9G,sBAAsB,CAC9BkB,OAAO,CAAC2D,eAAe,EACvBV,YAAY,CAAC2D,OAAO,EAAEC,MAAM,EAAEC,GAChC,CAAC;IACH;IACA,MAAMH,GAAG;EACX;AACF;;AAEA;AACA;AACA;AACA,SAASd,sBAAsBA,CAAC7F,OAAoB,EAAW;EAC7D,KAAK,MAAMH,IAAI,IAAIG,OAAO,CAACO,aAAa,EAAE;IACxC,KAAK,MAAMI,KAAK,IAAId,IAAI,CAACF,UAAU,CAACc,MAAM,EAAE;MAC1C,IAAIE,KAAK,YAAYvC,YAAY,EAAE;QACjC,MAAMwC,YAAY,GAAGD,KAAK,CAACE,wBAAwB,CAACb,OAAO,CAACK,KAAK,CAAC;QAClE,IAAIO,YAAY,EAAEmG,OAAO,EAAEC,MAAM,KAAK,SAAS,EAAE;UAC/C,OAAO,IAAI;QACb;MACF;IACF;EACF;EACA,OAAO,KAAK;AACd;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAerB,kBAAkBA,CAC/B5F,OAA2B,EAC3ByF,QAAsB,EACtBxF,OAAoB,EACpB;EACA,MAAMiH,cAAc,GAAGjH,OAAO,CAACO,aAAa,CAACC,OAAO,CACjDX,IAAI,IAAKA,IAAI,CAACF,UAAU,CAACc,MAC5B,CAAC;EAED,KAAK,MAAM0D,SAAS,IAAI8C,cAAc,EAAE;IACtC;AACJ;AACA;AACA;IACI,MAAM9C,SAAS,CAAC+C,QAAQ,CAACnH,OAAO,EAAEyF,QAAQ,EAAExF,OAAO,CAAC;EACtD;AACF;AAEA,SAASsG,UAAUA,CACjB9G,KAAgB,EAChB0G,KAAmB,EACnBiB,YAAoB,EACpBC,SAAiB,EACjB;EACA,MAAM;IAAEC;EAAsB,CAAC,GAAG7H,KAAK,CAACuD,QAAQ;EAChD,MAAM;IAAE2D;EAAO,CAAC,GAAGW,qBAAqB;EAExC,MAAMlH,OAAsB,GAAG;IAC7BiH,SAAS;IACTD,YAAY;IACZG,IAAI,EAAEvI,gBAAgB,CAACmH,KAAK,CAAC;IAC7BqB,SAAS,EAAEvI,oBAAoB,CAACkH,KAAK;EACvC,CAAC;EAED,OAAOQ,MAAM,CAACvG,OAAO,CAAC;AACxB;AAEA,OAAO,SAASgG,qBAAqBA,CAACnG,OAAoB,EAAEoG,OAAiB,EAAE;EAC7E,MAAMF,KAAK,GAAGlG,OAAO,CAACO,aAAa,CAChCiH,GAAG,CAAC,CAAC;IAAEC;EAAK,CAAC,KACZrB,OAAO,CAAC5F,OAAO,CAAC,CAAC;IAAE0F;EAAM,CAAC,KACxBA,KAAK,CAACwB,MAAM,CAAC,CAAC;IAAE7H;EAAK,CAAC,KAAKA,IAAI,CAAC4H,IAAI,KAAKA,IAAI,CAC/C,CACF,CAAC,CACAE,IAAI,CAAC,CAAC;EAET,MAAMC,YAAY,GAAGC,oBAAoB,CAAC7H,OAAO,CAAC;EAElD,OAAO,CAAC,GAAGkG,KAAK,EAAE,GAAG0B,YAAY,CAAC;AACpC;;AAEA;AACA;AACA;AACA;AACA,SAASC,oBAAoBA,CAAC7H,OAAoB,EAAqB;EACrE,MAAMkG,KAAwB,GAAG,EAAE;EAEnC,KAAK,MAAMrG,IAAI,IAAIG,OAAO,CAACO,aAAa,EAAE;IACxC,KAAK,MAAMI,KAAK,IAAId,IAAI,CAACF,UAAU,CAACc,MAAM,EAAE;MAC1C,IAAIE,KAAK,YAAYvC,YAAY,EAAE;QACjC8H,KAAK,CAAClE,IAAI,CAAC;UACToC,IAAI,EAAEzD,KAAK,CAACyD,IAAI;UAChBvE,IAAI;UACJoC,KAAK,EAAEtB,KAAK,CAACsB,KAAK;UAClB6F,KAAK,EAAEnH,KAAK,CAACmH,KAAK;UAClBnH,KAAK;UACLN,KAAK,EAAEL,OAAO,CAACK,KAAK;UACpBoH,IAAI,EAAE5H,IAAI,CAAC4H,IAAI;UACf/F,KAAK,EAAEf,KAAK,CAACoH,yBAAyB,CAAC/H,OAAO,CAACK,KAAK;QACtD,CAAC,CAAC;MACJ;IACF;EACF;EAEA,OAAO6F,KAAK;AACd","ignoreList":[]}
@@ -1,4 +1,35 @@
1
1
  import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js';
2
+ export declare enum PaymentErrorTypes {
3
+ PaymentExpired = "PaymentExpired",
4
+ PaymentIncomplete = "PaymentIncomplete",
5
+ PaymentAmountMismatch = "PaymentAmountMismatch"
6
+ }
7
+ export declare class PaymentPreAuthError extends Error {
8
+ readonly component: FormComponent;
9
+ readonly userMessage: string;
10
+ /**
11
+ * Whether to reset the component state and redirect to the component's page.
12
+ * - `true`: Reset state and redirect (e.g., payment expired - user must re-enter)
13
+ * - `false`: Keep state and stay on current page with error (e.g., capture failed - user can retry)
14
+ */
15
+ readonly shouldResetState: boolean;
16
+ /**
17
+ * When supplied, an "Important" notification banner will be shown based on the value.
18
+ */
19
+ readonly errorType: PaymentErrorTypes | undefined;
20
+ constructor(component: FormComponent, userMessage: string, shouldResetState: boolean, errorType?: PaymentErrorTypes);
21
+ getStateKeys(): string[];
22
+ }
23
+ /**
24
+ * Thrown when form submission fails after payment has been captured.
25
+ * User needs to retry or contact support for a refund.
26
+ */
27
+ export declare class PaymentSubmissionError extends Error {
28
+ readonly referenceNumber: string;
29
+ readonly helpLink?: string;
30
+ constructor(referenceNumber: string, helpLink?: string);
31
+ static checkPaymentAmount(stateAmount: number, definitionAmount: number | undefined, component: FormComponent): void;
32
+ }
2
33
  /**
3
34
  * Thrown when a component has an invalid state. This is typically only required where state needs
4
35
  * to be checked against an external source upon submission of a form. For example: file upload
@@ -1,3 +1,61 @@
1
+ export let PaymentErrorTypes = /*#__PURE__*/function (PaymentErrorTypes) {
2
+ PaymentErrorTypes["PaymentExpired"] = "PaymentExpired";
3
+ PaymentErrorTypes["PaymentIncomplete"] = "PaymentIncomplete";
4
+ PaymentErrorTypes["PaymentAmountMismatch"] = "PaymentAmountMismatch";
5
+ return PaymentErrorTypes;
6
+ }({});
7
+ function getStateKeys(component) {
8
+ const extraStateKeys = component.page?.getStateKeys(component) ?? [];
9
+ return [component.name].concat(extraStateKeys);
10
+ }
11
+ export class PaymentPreAuthError extends Error {
12
+ component;
13
+ userMessage;
14
+
15
+ /**
16
+ * Whether to reset the component state and redirect to the component's page.
17
+ * - `true`: Reset state and redirect (e.g., payment expired - user must re-enter)
18
+ * - `false`: Keep state and stay on current page with error (e.g., capture failed - user can retry)
19
+ */
20
+ shouldResetState;
21
+
22
+ /**
23
+ * When supplied, an "Important" notification banner will be shown based on the value.
24
+ */
25
+ errorType;
26
+ constructor(component, userMessage, shouldResetState, errorType) {
27
+ super('Payment capture failed');
28
+ this.name = 'PaymentPreAuthError';
29
+ this.component = component;
30
+ this.userMessage = userMessage;
31
+ this.shouldResetState = shouldResetState;
32
+ this.errorType = errorType;
33
+ }
34
+ getStateKeys() {
35
+ return getStateKeys(this.component);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Thrown when form submission fails after payment has been captured.
41
+ * User needs to retry or contact support for a refund.
42
+ */
43
+ export class PaymentSubmissionError extends Error {
44
+ referenceNumber;
45
+ helpLink;
46
+ constructor(referenceNumber, helpLink) {
47
+ super('Form submission failed after payment capture');
48
+ this.name = 'PaymentSubmissionError';
49
+ this.referenceNumber = referenceNumber;
50
+ this.helpLink = helpLink;
51
+ }
52
+ static checkPaymentAmount(stateAmount, definitionAmount, component) {
53
+ if (stateAmount / 100 !== definitionAmount) {
54
+ throw new PaymentPreAuthError(component, 'The pre-authorised payment amount is somehow different from that requested. Try adding payment details again.', true, PaymentErrorTypes.PaymentIncomplete);
55
+ }
56
+ }
57
+ }
58
+
1
59
  /**
2
60
  * Thrown when a component has an invalid state. This is typically only required where state needs
3
61
  * to be checked against an external source upon submission of a form. For example: file upload
@@ -17,8 +75,7 @@ export class InvalidComponentStateError extends Error {
17
75
  this.userMessage = userMessage;
18
76
  }
19
77
  getStateKeys() {
20
- const extraStateKeys = this.component.page?.getStateKeys(this.component) ?? [];
21
- return [this.component.name].concat(extraStateKeys);
78
+ return getStateKeys(this.component);
22
79
  }
23
80
  }
24
81
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":["InvalidComponentStateError","Error","component","userMessage","constructor","message","name","getStateKeys","extraStateKeys","page","concat"],"sources":["../../../../../src/server/plugins/engine/pageControllers/errors.ts"],"sourcesContent":["import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\n\n/**\n * Thrown when a component has an invalid state. This is typically only required where state needs\n * to be checked against an external source upon submission of a form. For example: file upload\n * has internal state (file upload IDs) but also external state (files in S3). The internal state\n * is always validated by the engine, but the external state needs validating too.\n *\n * This should be used within a formComponent.onSubmit(...).\n */\nexport class InvalidComponentStateError extends Error {\n public readonly component: FormComponent\n public readonly userMessage: string\n\n constructor(component: FormComponent, userMessage: string) {\n const message = `Invalid component state for: ${component.name}`\n super(message)\n this.name = 'InvalidComponentStateError'\n this.component = component\n this.userMessage = userMessage\n }\n\n getStateKeys() {\n const extraStateKeys =\n this.component.page?.getStateKeys(this.component) ?? []\n\n return [this.component.name].concat(extraStateKeys)\n }\n}\n"],"mappings":"AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,0BAA0B,SAASC,KAAK,CAAC;EACpCC,SAAS;EACTC,WAAW;EAE3BC,WAAWA,CAACF,SAAwB,EAAEC,WAAmB,EAAE;IACzD,MAAME,OAAO,GAAG,gCAAgCH,SAAS,CAACI,IAAI,EAAE;IAChE,KAAK,CAACD,OAAO,CAAC;IACd,IAAI,CAACC,IAAI,GAAG,4BAA4B;IACxC,IAAI,CAACJ,SAAS,GAAGA,SAAS;IAC1B,IAAI,CAACC,WAAW,GAAGA,WAAW;EAChC;EAEAI,YAAYA,CAAA,EAAG;IACb,MAAMC,cAAc,GAClB,IAAI,CAACN,SAAS,CAACO,IAAI,EAAEF,YAAY,CAAC,IAAI,CAACL,SAAS,CAAC,IAAI,EAAE;IAEzD,OAAO,CAAC,IAAI,CAACA,SAAS,CAACI,IAAI,CAAC,CAACI,MAAM,CAACF,cAAc,CAAC;EACrD;AACF","ignoreList":[]}
1
+ {"version":3,"file":"errors.js","names":["PaymentErrorTypes","getStateKeys","component","extraStateKeys","page","name","concat","PaymentPreAuthError","Error","userMessage","shouldResetState","errorType","constructor","PaymentSubmissionError","referenceNumber","helpLink","checkPaymentAmount","stateAmount","definitionAmount","PaymentIncomplete","InvalidComponentStateError","message"],"sources":["../../../../../src/server/plugins/engine/pageControllers/errors.ts"],"sourcesContent":["import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\n\nexport enum PaymentErrorTypes {\n PaymentExpired = 'PaymentExpired',\n PaymentIncomplete = 'PaymentIncomplete',\n PaymentAmountMismatch = 'PaymentAmountMismatch'\n}\n\nfunction getStateKeys(component: FormComponent) {\n const extraStateKeys = component.page?.getStateKeys(component) ?? []\n\n return [component.name].concat(extraStateKeys)\n}\n\nexport class PaymentPreAuthError extends Error {\n public readonly component: FormComponent\n public readonly userMessage: string\n\n /**\n * Whether to reset the component state and redirect to the component's page.\n * - `true`: Reset state and redirect (e.g., payment expired - user must re-enter)\n * - `false`: Keep state and stay on current page with error (e.g., capture failed - user can retry)\n */\n public readonly shouldResetState: boolean\n\n /**\n * When supplied, an \"Important\" notification banner will be shown based on the value.\n */\n public readonly errorType: PaymentErrorTypes | undefined\n\n constructor(\n component: FormComponent,\n userMessage: string,\n shouldResetState: boolean,\n errorType?: PaymentErrorTypes\n ) {\n super('Payment capture failed')\n this.name = 'PaymentPreAuthError'\n this.component = component\n this.userMessage = userMessage\n this.shouldResetState = shouldResetState\n this.errorType = errorType\n }\n\n getStateKeys() {\n return getStateKeys(this.component)\n }\n}\n\n/**\n * Thrown when form submission fails after payment has been captured.\n * User needs to retry or contact support for a refund.\n */\nexport class PaymentSubmissionError extends Error {\n public readonly referenceNumber: string\n public readonly helpLink?: string\n\n constructor(referenceNumber: string, helpLink?: string) {\n super('Form submission failed after payment capture')\n this.name = 'PaymentSubmissionError'\n this.referenceNumber = referenceNumber\n this.helpLink = helpLink\n }\n\n static checkPaymentAmount(\n stateAmount: number,\n definitionAmount: number | undefined,\n component: FormComponent\n ) {\n if (stateAmount / 100 !== definitionAmount) {\n throw new PaymentPreAuthError(\n component,\n 'The pre-authorised payment amount is somehow different from that requested. Try adding payment details again.',\n true,\n PaymentErrorTypes.PaymentIncomplete\n )\n }\n }\n}\n\n/**\n * Thrown when a component has an invalid state. This is typically only required where state needs\n * to be checked against an external source upon submission of a form. For example: file upload\n * has internal state (file upload IDs) but also external state (files in S3). The internal state\n * is always validated by the engine, but the external state needs validating too.\n *\n * This should be used within a formComponent.onSubmit(...).\n */\nexport class InvalidComponentStateError extends Error {\n public readonly component: FormComponent\n public readonly userMessage: string\n\n constructor(component: FormComponent, userMessage: string) {\n const message = `Invalid component state for: ${component.name}`\n super(message)\n this.name = 'InvalidComponentStateError'\n this.component = component\n this.userMessage = userMessage\n }\n\n getStateKeys() {\n return getStateKeys(this.component)\n }\n}\n"],"mappings":"AAEA,WAAYA,iBAAiB,0BAAjBA,iBAAiB;EAAjBA,iBAAiB;EAAjBA,iBAAiB;EAAjBA,iBAAiB;EAAA,OAAjBA,iBAAiB;AAAA;AAM7B,SAASC,YAAYA,CAACC,SAAwB,EAAE;EAC9C,MAAMC,cAAc,GAAGD,SAAS,CAACE,IAAI,EAAEH,YAAY,CAACC,SAAS,CAAC,IAAI,EAAE;EAEpE,OAAO,CAACA,SAAS,CAACG,IAAI,CAAC,CAACC,MAAM,CAACH,cAAc,CAAC;AAChD;AAEA,OAAO,MAAMI,mBAAmB,SAASC,KAAK,CAAC;EAC7BN,SAAS;EACTO,WAAW;;EAE3B;AACF;AACA;AACA;AACA;EACkBC,gBAAgB;;EAEhC;AACF;AACA;EACkBC,SAAS;EAEzBC,WAAWA,CACTV,SAAwB,EACxBO,WAAmB,EACnBC,gBAAyB,EACzBC,SAA6B,EAC7B;IACA,KAAK,CAAC,wBAAwB,CAAC;IAC/B,IAAI,CAACN,IAAI,GAAG,qBAAqB;IACjC,IAAI,CAACH,SAAS,GAAGA,SAAS;IAC1B,IAAI,CAACO,WAAW,GAAGA,WAAW;IAC9B,IAAI,CAACC,gBAAgB,GAAGA,gBAAgB;IACxC,IAAI,CAACC,SAAS,GAAGA,SAAS;EAC5B;EAEAV,YAAYA,CAAA,EAAG;IACb,OAAOA,YAAY,CAAC,IAAI,CAACC,SAAS,CAAC;EACrC;AACF;;AAEA;AACA;AACA;AACA;AACA,OAAO,MAAMW,sBAAsB,SAASL,KAAK,CAAC;EAChCM,eAAe;EACfC,QAAQ;EAExBH,WAAWA,CAACE,eAAuB,EAAEC,QAAiB,EAAE;IACtD,KAAK,CAAC,8CAA8C,CAAC;IACrD,IAAI,CAACV,IAAI,GAAG,wBAAwB;IACpC,IAAI,CAACS,eAAe,GAAGA,eAAe;IACtC,IAAI,CAACC,QAAQ,GAAGA,QAAQ;EAC1B;EAEA,OAAOC,kBAAkBA,CACvBC,WAAmB,EACnBC,gBAAoC,EACpChB,SAAwB,EACxB;IACA,IAAIe,WAAW,GAAG,GAAG,KAAKC,gBAAgB,EAAE;MAC1C,MAAM,IAAIX,mBAAmB,CAC3BL,SAAS,EACT,+GAA+G,EAC/G,IAAI,EACJF,iBAAiB,CAACmB,iBACpB,CAAC;IACH;EACF;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,0BAA0B,SAASZ,KAAK,CAAC;EACpCN,SAAS;EACTO,WAAW;EAE3BG,WAAWA,CAACV,SAAwB,EAAEO,WAAmB,EAAE;IACzD,MAAMY,OAAO,GAAG,gCAAgCnB,SAAS,CAACG,IAAI,EAAE;IAChE,KAAK,CAACgB,OAAO,CAAC;IACd,IAAI,CAAChB,IAAI,GAAG,4BAA4B;IACxC,IAAI,CAACH,SAAS,GAAGA,SAAS;IAC1B,IAAI,CAACO,WAAW,GAAGA,WAAW;EAChC;EAEAR,YAAYA,CAAA,EAAG;IACb,OAAOA,YAAY,CAAC,IAAI,CAACC,SAAS,CAAC;EACrC;AACF","ignoreList":[]}
@@ -0,0 +1,27 @@
1
+ import { type SubmitPayload } from '@defra/forms-model';
2
+ import { type DetailItem, type DetailItemField } from '~/src/server/plugins/engine/models/types.js';
3
+ export interface SubmitRecord {
4
+ name: string;
5
+ title: string;
6
+ value: string;
7
+ }
8
+ /**
9
+ * Builds the main submission records from field items.
10
+ * Regular fields are converted to single records, while PaymentField
11
+ * components are expanded into four separate records.
12
+ */
13
+ export declare function buildMainRecords(items: DetailItem[]): SubmitRecord[];
14
+ /**
15
+ * Expands a PaymentField into four submission records:
16
+ * - Payment description
17
+ * - Payment amount (formatted with currency symbol)
18
+ * - Payment reference
19
+ * - Payment date (formatted date/time)
20
+ *
21
+ * Returns an empty array if no payment state exists.
22
+ */
23
+ export declare function buildPaymentRecords(item: DetailItemField): SubmitRecord[];
24
+ /**
25
+ * Builds the repeater submission records from repeater items.
26
+ */
27
+ export declare function buildRepeaterRecords(items: DetailItem[]): SubmitPayload['repeaters'];
@@ -0,0 +1,77 @@
1
+ import { PaymentField } from "../../components/PaymentField.js";
2
+ import { getAnswer } from "../../components/helpers/components.js";
3
+ import { formatPaymentAmount, formatPaymentDate } from "../../../payment/helper.js";
4
+ /**
5
+ * Builds the main submission records from field items.
6
+ * Regular fields are converted to single records, while PaymentField
7
+ * components are expanded into four separate records.
8
+ */
9
+ export function buildMainRecords(items) {
10
+ const fieldItems = items.filter(item => 'field' in item);
11
+ const records = [];
12
+ for (const item of fieldItems) {
13
+ if (item.field instanceof PaymentField) {
14
+ records.push(...buildPaymentRecords(item));
15
+ } else {
16
+ records.push({
17
+ name: item.name,
18
+ title: item.label,
19
+ value: getAnswer(item.field, item.state, {
20
+ format: 'data'
21
+ })
22
+ });
23
+ }
24
+ }
25
+ return records;
26
+ }
27
+
28
+ /**
29
+ * Expands a PaymentField into four submission records:
30
+ * - Payment description
31
+ * - Payment amount (formatted with currency symbol)
32
+ * - Payment reference
33
+ * - Payment date (formatted date/time)
34
+ *
35
+ * Returns an empty array if no payment state exists.
36
+ */
37
+ export function buildPaymentRecords(item) {
38
+ const paymentState = item.field.getPaymentStateFromState(item.state);
39
+ if (!paymentState) {
40
+ return [];
41
+ }
42
+ return [{
43
+ name: `${item.name}_paymentDescription`,
44
+ title: 'Payment description',
45
+ value: paymentState.description
46
+ }, {
47
+ name: `${item.name}_paymentAmount`,
48
+ title: 'Payment amount',
49
+ value: formatPaymentAmount(paymentState.amount)
50
+ }, {
51
+ name: `${item.name}_paymentReference`,
52
+ title: 'Payment reference',
53
+ value: paymentState.reference
54
+ }, {
55
+ name: `${item.name}_paymentDate`,
56
+ title: 'Payment date',
57
+ value: paymentState.preAuth?.createdAt ? formatPaymentDate(paymentState.preAuth.createdAt) : ''
58
+ }];
59
+ }
60
+
61
+ /**
62
+ * Builds the repeater submission records from repeater items.
63
+ */
64
+ export function buildRepeaterRecords(items) {
65
+ return items.filter(item => 'subItems' in item).map(item => ({
66
+ name: item.name,
67
+ title: item.label,
68
+ value: item.subItems.map(detailItems => detailItems.map(subItem => ({
69
+ name: subItem.name,
70
+ title: subItem.label,
71
+ value: getAnswer(subItem.field, subItem.state, {
72
+ format: 'data'
73
+ })
74
+ })))
75
+ }));
76
+ }
77
+ //# sourceMappingURL=submission.js.map