@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,299 @@
1
+ import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
2
+ import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
3
+ import { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'
4
+ import {
5
+ buildMainRecords,
6
+ buildPaymentRecords,
7
+ buildRepeaterRecords
8
+ } from '~/src/server/plugins/engine/pageControllers/helpers/submission.js'
9
+ import { type FormSubmissionState } from '~/src/server/plugins/engine/types.js'
10
+
11
+ describe('Submission helpers', () => {
12
+ describe('buildPaymentRecords', () => {
13
+ it('should return empty array when no payment state exists', () => {
14
+ const mockPaymentField = Object.create(PaymentField.prototype)
15
+ mockPaymentField.getPaymentStateFromState = jest
16
+ .fn()
17
+ .mockReturnValue(undefined)
18
+
19
+ const item = {
20
+ name: 'payment',
21
+ label: 'Payment',
22
+ field: mockPaymentField,
23
+ state: {} as FormSubmissionState
24
+ } as unknown as DetailItemField
25
+
26
+ const result = buildPaymentRecords(item)
27
+
28
+ expect(result).toEqual([])
29
+ expect(mockPaymentField.getPaymentStateFromState).toHaveBeenCalledWith(
30
+ item.state
31
+ )
32
+ })
33
+
34
+ it('should return four records when payment state exists', () => {
35
+ const mockPaymentState = {
36
+ paymentId: 'pay_123',
37
+ description: 'Application fee',
38
+ amount: 150,
39
+ reference: 'REF-ABC-123',
40
+ preAuth: {
41
+ status: 'success',
42
+ createdAt: '2026-01-26T14:30:00.000Z'
43
+ }
44
+ }
45
+
46
+ const mockPaymentField = Object.create(PaymentField.prototype)
47
+ mockPaymentField.getPaymentStateFromState = jest
48
+ .fn()
49
+ .mockReturnValue(mockPaymentState)
50
+
51
+ const item = {
52
+ name: 'payment',
53
+ label: 'Payment',
54
+ field: mockPaymentField,
55
+ state: {} as FormSubmissionState
56
+ } as unknown as DetailItemField
57
+
58
+ const result = buildPaymentRecords(item)
59
+
60
+ expect(result).toHaveLength(4)
61
+ expect(result[0]).toEqual({
62
+ name: 'payment_paymentDescription',
63
+ title: 'Payment description',
64
+ value: 'Application fee'
65
+ })
66
+ expect(result[1]).toEqual({
67
+ name: 'payment_paymentAmount',
68
+ title: 'Payment amount',
69
+ value: '£150.00'
70
+ })
71
+ expect(result[2]).toEqual({
72
+ name: 'payment_paymentReference',
73
+ title: 'Payment reference',
74
+ value: 'REF-ABC-123'
75
+ })
76
+ expect(result[3].name).toBe('payment_paymentDate')
77
+ expect(result[3].title).toBe('Payment date')
78
+ // Date will be formatted, just check it's not empty
79
+ expect(result[3].value).not.toBe('')
80
+ })
81
+
82
+ it('should return empty date when preAuth.createdAt is missing', () => {
83
+ const mockPaymentState = {
84
+ paymentId: 'pay_123',
85
+ description: 'Application fee',
86
+ amount: 150,
87
+ reference: 'REF-ABC-123',
88
+ preAuth: {
89
+ status: 'success'
90
+ // createdAt is missing
91
+ }
92
+ }
93
+
94
+ const mockPaymentField = Object.create(PaymentField.prototype)
95
+ mockPaymentField.getPaymentStateFromState = jest
96
+ .fn()
97
+ .mockReturnValue(mockPaymentState)
98
+
99
+ const item = {
100
+ name: 'payment',
101
+ label: 'Payment',
102
+ field: mockPaymentField,
103
+ state: {} as FormSubmissionState
104
+ } as unknown as DetailItemField
105
+
106
+ const result = buildPaymentRecords(item)
107
+
108
+ expect(result[3]).toEqual({
109
+ name: 'payment_paymentDate',
110
+ title: 'Payment date',
111
+ value: ''
112
+ })
113
+ })
114
+ })
115
+
116
+ describe('buildMainRecords', () => {
117
+ it('should return empty array for empty items', () => {
118
+ const result = buildMainRecords([])
119
+ expect(result).toEqual([])
120
+ })
121
+
122
+ it('should process regular fields correctly', () => {
123
+ const mockTextField = Object.create(TextField.prototype)
124
+ mockTextField.getDisplayStringFromState = jest
125
+ .fn()
126
+ .mockReturnValue('John Doe')
127
+ mockTextField.getContextValueFromState = jest
128
+ .fn()
129
+ .mockReturnValue('John Doe')
130
+
131
+ const items = [
132
+ {
133
+ name: 'fullName',
134
+ label: 'Full name',
135
+ field: mockTextField,
136
+ state: { fullName: 'John Doe' } as FormSubmissionState
137
+ }
138
+ ] as unknown as DetailItemField[]
139
+
140
+ const result = buildMainRecords(items)
141
+
142
+ expect(result).toHaveLength(1)
143
+ expect(result[0]).toEqual({
144
+ name: 'fullName',
145
+ title: 'Full name',
146
+ value: 'John Doe'
147
+ })
148
+ })
149
+
150
+ it('should expand PaymentField into four records', () => {
151
+ const mockPaymentState = {
152
+ paymentId: 'pay_123',
153
+ description: 'Licence fee',
154
+ amount: 75.5,
155
+ reference: 'LIC-999',
156
+ preAuth: {
157
+ status: 'success',
158
+ createdAt: '2026-01-26T10:00:00.000Z'
159
+ }
160
+ }
161
+
162
+ const mockPaymentField = Object.create(PaymentField.prototype)
163
+ mockPaymentField.getPaymentStateFromState = jest
164
+ .fn()
165
+ .mockReturnValue(mockPaymentState)
166
+
167
+ const items = [
168
+ {
169
+ name: 'licencePayment',
170
+ label: 'Licence Payment',
171
+ field: mockPaymentField,
172
+ state: {} as FormSubmissionState
173
+ }
174
+ ] as unknown as DetailItemField[]
175
+
176
+ const result = buildMainRecords(items)
177
+
178
+ expect(result).toHaveLength(4)
179
+ expect(result.map((r) => r.name)).toEqual([
180
+ 'licencePayment_paymentDescription',
181
+ 'licencePayment_paymentAmount',
182
+ 'licencePayment_paymentReference',
183
+ 'licencePayment_paymentDate'
184
+ ])
185
+ })
186
+
187
+ it('should handle mixed regular and payment fields', () => {
188
+ const mockTextField = Object.create(TextField.prototype)
189
+ mockTextField.getDisplayStringFromState = jest
190
+ .fn()
191
+ .mockReturnValue('test@example.com')
192
+ mockTextField.getContextValueFromState = jest
193
+ .fn()
194
+ .mockReturnValue('test@example.com')
195
+
196
+ const mockPaymentState = {
197
+ paymentId: 'pay_456',
198
+ description: 'Registration fee',
199
+ amount: 25,
200
+ reference: 'REG-001',
201
+ preAuth: { status: 'success', createdAt: '2026-01-26T12:00:00.000Z' }
202
+ }
203
+
204
+ const mockPaymentField = Object.create(PaymentField.prototype)
205
+ mockPaymentField.getPaymentStateFromState = jest
206
+ .fn()
207
+ .mockReturnValue(mockPaymentState)
208
+
209
+ const items = [
210
+ {
211
+ name: 'email',
212
+ label: 'Email address',
213
+ field: mockTextField,
214
+ state: { email: 'test@example.com' } as FormSubmissionState
215
+ },
216
+ {
217
+ name: 'payment',
218
+ label: 'Payment',
219
+ field: mockPaymentField,
220
+ state: {} as FormSubmissionState
221
+ }
222
+ ] as unknown as DetailItemField[]
223
+
224
+ const result = buildMainRecords(items)
225
+
226
+ // 1 regular field + 4 payment fields = 5 records
227
+ expect(result).toHaveLength(5)
228
+ expect(result[0].name).toBe('email')
229
+ expect(result[1].name).toBe('payment_paymentDescription')
230
+ })
231
+
232
+ it('should skip repeater items (items with subItems)', () => {
233
+ const repeaterItem = {
234
+ name: 'addresses',
235
+ label: 'Addresses',
236
+ subItems: [[]]
237
+ }
238
+
239
+ const result = buildMainRecords([
240
+ repeaterItem as unknown as DetailItemField
241
+ ])
242
+
243
+ expect(result).toEqual([])
244
+ })
245
+ })
246
+
247
+ describe('buildRepeaterRecords', () => {
248
+ it('should return empty array when no repeater items', () => {
249
+ const mockField = Object.create(TextField.prototype)
250
+
251
+ const items = [
252
+ {
253
+ name: 'textField',
254
+ label: 'Text',
255
+ field: mockField,
256
+ state: {} as FormSubmissionState
257
+ }
258
+ ]
259
+
260
+ const result = buildRepeaterRecords(items as unknown as DetailItemField[])
261
+
262
+ expect(result).toEqual([])
263
+ })
264
+
265
+ it('should process repeater items correctly', () => {
266
+ const mockSubField = Object.create(TextField.prototype)
267
+ mockSubField.getDisplayStringFromState = jest
268
+ .fn()
269
+ .mockReturnValue('123 Main St')
270
+ mockSubField.getContextValueFromState = jest
271
+ .fn()
272
+ .mockReturnValue('123 Main St')
273
+
274
+ const items = [
275
+ {
276
+ name: 'addresses',
277
+ label: 'Addresses',
278
+ subItems: [
279
+ [
280
+ {
281
+ name: 'street',
282
+ label: 'Street',
283
+ field: mockSubField,
284
+ state: { street: '123 Main St' } as FormSubmissionState
285
+ }
286
+ ]
287
+ ]
288
+ }
289
+ ]
290
+
291
+ const result = buildRepeaterRecords(items as unknown as DetailItemField[])
292
+
293
+ expect(result).toHaveLength(1)
294
+ expect(result[0].name).toBe('addresses')
295
+ expect(result[0].title).toBe('Addresses')
296
+ expect(result[0].value).toHaveLength(1)
297
+ })
298
+ })
299
+ })
@@ -0,0 +1,110 @@
1
+ import { type SubmitPayload } from '@defra/forms-model'
2
+
3
+ import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
4
+ import { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'
5
+ import {
6
+ type DetailItem,
7
+ type DetailItemField
8
+ } from '~/src/server/plugins/engine/models/types.js'
9
+ import {
10
+ formatPaymentAmount,
11
+ formatPaymentDate
12
+ } from '~/src/server/plugins/payment/helper.js'
13
+
14
+ export interface SubmitRecord {
15
+ name: string
16
+ title: string
17
+ value: string
18
+ }
19
+
20
+ /**
21
+ * Builds the main submission records from field items.
22
+ * Regular fields are converted to single records, while PaymentField
23
+ * components are expanded into four separate records.
24
+ */
25
+ export function buildMainRecords(items: DetailItem[]): SubmitRecord[] {
26
+ const fieldItems = items.filter(
27
+ (item): item is DetailItemField => 'field' in item
28
+ )
29
+
30
+ const records: SubmitRecord[] = []
31
+
32
+ for (const item of fieldItems) {
33
+ if (item.field instanceof PaymentField) {
34
+ records.push(...buildPaymentRecords(item))
35
+ } else {
36
+ records.push({
37
+ name: item.name,
38
+ title: item.label,
39
+ value: getAnswer(item.field, item.state, { format: 'data' })
40
+ })
41
+ }
42
+ }
43
+
44
+ return records
45
+ }
46
+
47
+ /**
48
+ * Expands a PaymentField into four submission records:
49
+ * - Payment description
50
+ * - Payment amount (formatted with currency symbol)
51
+ * - Payment reference
52
+ * - Payment date (formatted date/time)
53
+ *
54
+ * Returns an empty array if no payment state exists.
55
+ */
56
+ export function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {
57
+ const paymentState = (item.field as PaymentField).getPaymentStateFromState(
58
+ item.state
59
+ )
60
+
61
+ if (!paymentState) {
62
+ return []
63
+ }
64
+
65
+ return [
66
+ {
67
+ name: `${item.name}_paymentDescription`,
68
+ title: 'Payment description',
69
+ value: paymentState.description
70
+ },
71
+ {
72
+ name: `${item.name}_paymentAmount`,
73
+ title: 'Payment amount',
74
+ value: formatPaymentAmount(paymentState.amount)
75
+ },
76
+ {
77
+ name: `${item.name}_paymentReference`,
78
+ title: 'Payment reference',
79
+ value: paymentState.reference
80
+ },
81
+ {
82
+ name: `${item.name}_paymentDate`,
83
+ title: 'Payment date',
84
+ value: paymentState.preAuth?.createdAt
85
+ ? formatPaymentDate(paymentState.preAuth.createdAt)
86
+ : ''
87
+ }
88
+ ]
89
+ }
90
+
91
+ /**
92
+ * Builds the repeater submission records from repeater items.
93
+ */
94
+ export function buildRepeaterRecords(
95
+ items: DetailItem[]
96
+ ): SubmitPayload['repeaters'] {
97
+ return items
98
+ .filter((item) => 'subItems' in item)
99
+ .map((item) => ({
100
+ name: item.name,
101
+ title: item.label,
102
+ value: item.subItems.map((detailItems) =>
103
+ detailItems.map((subItem) => ({
104
+ name: subItem.name,
105
+ title: subItem.label,
106
+ value: getAnswer(subItem.field, subItem.state, { format: 'data' })
107
+ }))
108
+ )
109
+ }))
110
+ }
@@ -10,6 +10,7 @@ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
10
10
  import { validatePluginOptions } from '~/src/server/plugins/engine/options.js'
11
11
  import { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'
12
12
  import { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'
13
+ import { getRoutes as getPaymentRoutes } from '~/src/server/plugins/engine/routes/payment.js'
13
14
  import { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'
14
15
  import { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'
15
16
  import { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'
@@ -39,6 +40,7 @@ export const plugin = {
39
40
  preparePageEventRequestOptions,
40
41
  onRequest,
41
42
  ordnanceSurveyApiKey,
43
+ baseUrl,
42
44
  ordnanceSurveyApiSecret
43
45
  } = options
44
46
 
@@ -74,6 +76,7 @@ export const plugin = {
74
76
  server.expose('viewContext', viewContext)
75
77
  server.expose('cacheService', cacheService)
76
78
  server.expose('saveAndExit', saveAndExit)
79
+ server.expose('baseUrl', baseUrl)
77
80
 
78
81
  server.app.model = model
79
82
 
@@ -106,19 +109,21 @@ export const plugin = {
106
109
  }
107
110
 
108
111
  const routes = [
109
- ...getQuestionRoutes(
112
+ ...getPaymentRoutes(),
113
+ ...getFileUploadStatusRoutes(),
114
+ ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),
115
+ ...getRepeaterItemDeleteRoutes(
110
116
  getRouteOptions,
111
117
  postRouteOptions,
112
- preparePageEventRequestOptions,
113
118
  onRequest
114
119
  ),
115
- ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),
116
- ...getRepeaterItemDeleteRoutes(
120
+
121
+ ...getQuestionRoutes(
117
122
  getRouteOptions,
118
123
  postRouteOptions,
124
+ preparePageEventRequestOptions,
119
125
  onRequest
120
- ),
121
- ...getFileUploadStatusRoutes()
126
+ )
122
127
  ]
123
128
 
124
129
  server.route(routes as unknown as ServerRoute[]) // TODO
@@ -119,6 +119,7 @@ async function importExternalComponentState(
119
119
  const typedStateAppendage = externalComponentData as ExternalStateAppendage
120
120
  const componentName = typedStateAppendage.component
121
121
  const stateAppendage = typedStateAppendage.data
122
+
122
123
  const component = request.app.model?.componentMap.get(componentName)
123
124
 
124
125
  if (!component) {
@@ -137,22 +138,22 @@ async function importExternalComponentState(
137
138
  throw new Error(`State for component ${componentName} is invalid`)
138
139
  }
139
140
 
140
- const componentState = isFormState(stateAppendage)
141
- ? Object.fromEntries(
142
- Object.entries(stateAppendage).map(([key, value]) => [
143
- `${componentName}__${key}`,
144
- value
145
- ])
146
- )
147
- : { [componentName]: stateAppendage }
148
-
149
- // Save the external component state immediately
150
- const pageState = page.getStateFromValidForm(
151
- request,
152
- state,
153
- componentState as FormPayload
154
- )
155
- const savedState = await page.mergeState(request, state, pageState)
141
+ // Create state structure from appendage state
142
+ // Some components use a record structure with properties of the format of '<compName>__<fieldName>'
143
+ // e.g. UKAddressField
144
+ // Some components use a single object structure e.g. PaymentField
145
+ const componentState =
146
+ isFormState(stateAppendage) && !component.isAppendageStateSingleObject
147
+ ? Object.fromEntries(
148
+ Object.entries(stateAppendage).map(([key, value]) => [
149
+ `${componentName}__${key}`,
150
+ value
151
+ ])
152
+ )
153
+ : { [componentName]: stateAppendage }
154
+
155
+ // Save the external component state directly (already has correct key format)
156
+ const savedState = await page.mergeState(request, state, componentState)
156
157
 
157
158
  // Merge any stashed payload into the local state
158
159
  const payload = request.yar.flash(EXTERNAL_STATE_PAYLOAD)
@@ -0,0 +1,39 @@
1
+ import Boom from '@hapi/boom'
2
+
3
+ import { PAYMENT_SESSION_PREFIX } from '~/src/server/plugins/engine/routes/payment.js'
4
+ import { getPaymentApiKey } from '~/src/server/plugins/payment/helper.js'
5
+ import { PaymentService } from '~/src/server/plugins/payment/service.js'
6
+
7
+ /**
8
+ * Validates session data and retrieves payment status
9
+ * @param {Request} request - the request
10
+ * @param {string} uuid - the payment UUID
11
+ * @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}
12
+ */
13
+ export async function getPaymentContext(request, uuid) {
14
+ const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`
15
+ const session = /** @type {PaymentSessionData | null} */ (
16
+ request.yar.get(sessionKey)
17
+ )
18
+
19
+ if (!session) {
20
+ throw Boom.badRequest(`No payment session found for uuid=${uuid}`)
21
+ }
22
+
23
+ const { paymentId, isLivePayment, formId } = session
24
+
25
+ if (!paymentId) {
26
+ throw Boom.badRequest('No paymentId in session')
27
+ }
28
+
29
+ const apiKey = getPaymentApiKey(isLivePayment, formId)
30
+ const paymentService = new PaymentService(apiKey)
31
+ const paymentStatus = await paymentService.getPaymentStatus(paymentId)
32
+
33
+ return { session, sessionKey, paymentStatus }
34
+ }
35
+
36
+ /**
37
+ * @import { Request } from '@hapi/hapi'
38
+ * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
39
+ */
@@ -0,0 +1,90 @@
1
+ import { getPaymentContext } from '~/src/server/plugins/engine/routes/payment-helper.js'
2
+ import { get } from '~/src/server/services/httpService.js'
3
+
4
+ jest.mock('~/src/server/services/httpService.ts')
5
+
6
+ describe('payment helper', () => {
7
+ const uuid = '5a54c2fe-da49-4202-8cd3-2121eaca03c3'
8
+ it('should throw if no session', async () => {
9
+ const mockRequest = {
10
+ yar: {
11
+ get: jest.fn().mockReturnValueOnce(undefined)
12
+ }
13
+ }
14
+ // @ts-expect-error - partial request mock
15
+ await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow(
16
+ 'No payment session found for uuid=5a54c2fe-da49-4202-8cd3-2121eaca03c3'
17
+ )
18
+ })
19
+
20
+ it('should throw if no payment id', async () => {
21
+ const mockRequest = {
22
+ yar: {
23
+ get: jest.fn().mockReturnValueOnce({})
24
+ }
25
+ }
26
+ // @ts-expect-error - partial request mock
27
+ await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow(
28
+ 'No paymentId in session'
29
+ )
30
+ })
31
+
32
+ it('should get context successfully', async () => {
33
+ const mockRequest = {
34
+ yar: {
35
+ get: jest.fn().mockReturnValueOnce({
36
+ paymentId: 'payment-id',
37
+ isLivePayment: false,
38
+ formId: 'formid'
39
+ })
40
+ }
41
+ }
42
+
43
+ const getPaymentStatusApiResult = {
44
+ payment_id: 'payment-id-12345',
45
+ _links: {
46
+ next_url: {
47
+ href: 'http://next-url-href/payment'
48
+ }
49
+ },
50
+ state: {
51
+ status: 'created'
52
+ }
53
+ }
54
+
55
+ jest.mocked(get).mockResolvedValueOnce({
56
+ res: /** @type {IncomingMessage} */ ({
57
+ statusCode: 200,
58
+ headers: {}
59
+ }),
60
+ payload: getPaymentStatusApiResult,
61
+ error: undefined
62
+ })
63
+
64
+ // @ts-expect-error - partial request mock
65
+ const res = await getPaymentContext(mockRequest, uuid)
66
+ expect(res).toEqual({
67
+ paymentStatus: {
68
+ paymentId: 'payment-id-12345',
69
+ _links: {
70
+ next_url: {
71
+ href: 'http://next-url-href/payment'
72
+ }
73
+ },
74
+ state: {
75
+ status: 'created'
76
+ }
77
+ },
78
+ session: {
79
+ formId: 'formid',
80
+ isLivePayment: false,
81
+ paymentId: 'payment-id'
82
+ },
83
+ sessionKey: 'payment-5a54c2fe-da49-4202-8cd3-2121eaca03c3'
84
+ })
85
+ })
86
+ })
87
+
88
+ /**
89
+ * @import { IncomingMessage } from 'node:http'
90
+ */