@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.
- package/.public/stylesheets/application.min.css +1 -1
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/client/stylesheets/_payment-field.scss +8 -0
- package/.server/client/stylesheets/application.scss +2 -0
- package/.server/index.js +3 -1
- package/.server/index.js.map +1 -1
- package/.server/server/constants.d.ts +1 -0
- package/.server/server/constants.js +1 -0
- package/.server/server/constants.js.map +1 -1
- package/.server/server/forms/payment-test.yaml +42 -0
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
- package/.server/server/plugins/engine/components/FormComponent.d.ts +1 -0
- package/.server/server/plugins/engine/components/FormComponent.js +1 -0
- package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
- package/.server/server/plugins/engine/components/PaymentField.d.ts +135 -0
- package/.server/server/plugins/engine/components/PaymentField.js +228 -0
- package/.server/server/plugins/engine/components/PaymentField.js.map +1 -0
- package/.server/server/plugins/engine/components/PaymentField.types.d.ts +21 -0
- package/.server/server/plugins/engine/components/PaymentField.types.js +2 -0
- package/.server/server/plugins/engine/components/PaymentField.types.js.map +1 -0
- package/.server/server/plugins/engine/components/UkAddressField.d.ts +1 -1
- package/.server/server/plugins/engine/components/UkAddressField.js +3 -1
- package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
- package/.server/server/plugins/engine/components/helpers/components.js +3 -0
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/components/index.d.ts +1 -0
- package/.server/server/plugins/engine/components/index.js +1 -0
- package/.server/server/plugins/engine/components/index.js.map +1 -1
- package/.server/server/plugins/engine/helpers.d.ts +1 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +3 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js +7 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js +34 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +22 -0
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js +43 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +29 -8
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +17 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +173 -51
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/errors.d.ts +31 -0
- package/.server/server/plugins/engine/pageControllers/errors.js +59 -2
- package/.server/server/plugins/engine/pageControllers/errors.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +27 -0
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js +77 -0
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -0
- package/.server/server/plugins/engine/plugin.js +4 -1
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/index.js +8 -4
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/routes/payment-helper.d.ts +14 -0
- package/.server/server/plugins/engine/routes/payment-helper.js +41 -0
- package/.server/server/plugins/engine/routes/payment-helper.js.map +1 -0
- package/.server/server/plugins/engine/routes/payment-helper.test.js +81 -0
- package/.server/server/plugins/engine/routes/payment-helper.test.js.map +1 -0
- package/.server/server/plugins/engine/routes/payment.d.ts +8 -0
- package/.server/server/plugins/engine/routes/payment.js +140 -0
- package/.server/server/plugins/engine/routes/payment.js.map +1 -0
- package/.server/server/plugins/engine/routes/payment.test.js +187 -0
- package/.server/server/plugins/engine/routes/payment.test.js.map +1 -0
- package/.server/server/plugins/engine/services/localFormsService.js +6 -0
- package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
- package/.server/server/plugins/engine/types/schema.js +7 -0
- package/.server/server/plugins/engine/types/schema.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +19 -1
- package/.server/server/plugins/engine/types.js +4 -0
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/validationHelpers.d.ts +1 -1
- package/.server/server/plugins/engine/validationHelpers.js.map +1 -1
- package/.server/server/plugins/engine/views/components/paymentfield.html +42 -0
- package/.server/server/plugins/engine/views/index.html +9 -1
- package/.server/server/plugins/engine/views/partials/form.html +20 -5
- package/.server/server/plugins/engine/views/summary.html +17 -1
- package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
- package/.server/server/plugins/payment/helper.d.ts +30 -0
- package/.server/server/plugins/payment/helper.js +49 -0
- package/.server/server/plugins/payment/helper.js.map +1 -0
- package/.server/server/plugins/payment/helper.test.js +37 -0
- package/.server/server/plugins/payment/helper.test.js.map +1 -0
- package/.server/server/plugins/payment/service.d.ts +40 -0
- package/.server/server/plugins/payment/service.js +129 -0
- package/.server/server/plugins/payment/service.js.map +1 -0
- package/.server/server/plugins/payment/service.test.js +162 -0
- package/.server/server/plugins/payment/service.test.js.map +1 -0
- package/.server/server/plugins/payment/types.d.ts +172 -0
- package/.server/server/plugins/payment/types.js +78 -0
- package/.server/server/plugins/payment/types.js.map +1 -0
- package/.server/server/types.d.ts +2 -0
- package/.server/server/types.js.map +1 -1
- package/.server/typings/hapi/index.d.js.map +1 -1
- package/README.md +12 -9
- package/package.json +2 -2
- package/src/client/stylesheets/_payment-field.scss +8 -0
- package/src/client/stylesheets/application.scss +2 -0
- package/src/index.ts +5 -1
- package/src/server/constants.js +1 -0
- package/src/server/forms/payment-test.yaml +42 -0
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
- package/src/server/plugins/engine/components/FormComponent.ts +1 -0
- package/src/server/plugins/engine/components/PaymentField.test.ts +611 -0
- package/src/server/plugins/engine/components/PaymentField.ts +367 -0
- package/src/server/plugins/engine/components/PaymentField.types.ts +21 -0
- package/src/server/plugins/engine/components/UkAddressField.ts +2 -1
- package/src/server/plugins/engine/components/helpers/components.ts +5 -0
- package/src/server/plugins/engine/components/index.ts +1 -0
- package/src/server/plugins/engine/models/SummaryViewModel.ts +8 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts +147 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +105 -103
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +61 -2
- package/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts +115 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +60 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +32 -6
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +247 -72
- package/src/server/plugins/engine/pageControllers/errors.test.ts +13 -1
- package/src/server/plugins/engine/pageControllers/errors.ts +79 -4
- package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +299 -0
- package/src/server/plugins/engine/pageControllers/helpers/submission.ts +110 -0
- package/src/server/plugins/engine/plugin.ts +11 -6
- package/src/server/plugins/engine/routes/index.ts +17 -16
- package/src/server/plugins/engine/routes/payment-helper.js +39 -0
- package/src/server/plugins/engine/routes/payment-helper.test.js +90 -0
- package/src/server/plugins/engine/routes/payment.js +151 -0
- package/src/server/plugins/engine/routes/payment.test.js +180 -0
- package/src/server/plugins/engine/services/localFormsService.js +7 -0
- package/src/server/plugins/engine/types/schema.ts +9 -0
- package/src/server/plugins/engine/types.ts +24 -1
- package/src/server/plugins/engine/validationHelpers.ts +1 -1
- package/src/server/plugins/engine/views/components/paymentfield.html +42 -0
- package/src/server/plugins/engine/views/index.html +9 -1
- package/src/server/plugins/engine/views/partials/form.html +20 -5
- package/src/server/plugins/engine/views/summary.html +17 -1
- package/src/server/plugins/payment/helper.js +56 -0
- package/src/server/plugins/payment/helper.test.js +52 -0
- package/src/server/plugins/payment/service.js +171 -0
- package/src/server/plugins/payment/service.test.js +205 -0
- package/src/server/plugins/payment/types.js +77 -0
- package/src/server/types.ts +2 -0
- 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
|
-
...
|
|
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
|
-
|
|
116
|
-
...
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
const savedState = await page.mergeState(request, state,
|
|
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
|
+
*/
|