@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
|
@@ -11,133 +11,135 @@ import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControl
|
|
|
11
11
|
import { FormStatus } from '~/src/server/routes/types.js'
|
|
12
12
|
import definition from '~/test/form/definitions/repeat-mixed.js'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
describe('v1 human formatter', () => {
|
|
15
|
+
const itemId1 = 'abc-123'
|
|
16
|
+
const itemId2 = 'xyz-987'
|
|
17
|
+
|
|
18
|
+
const submitResponse = {
|
|
19
|
+
message: 'Submit completed',
|
|
20
|
+
result: {
|
|
21
|
+
files: {
|
|
22
|
+
main: '00000000-0000-0000-0000-000000000000',
|
|
23
|
+
repeaters: {
|
|
24
|
+
pizza: '11111111-1111-1111-1111-111111111111'
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
}
|
|
28
29
|
|
|
29
|
-
const model = new FormModel(definition, {
|
|
30
|
-
|
|
31
|
-
})
|
|
30
|
+
const model = new FormModel(definition, {
|
|
31
|
+
basePath: 'test'
|
|
32
|
+
})
|
|
32
33
|
|
|
33
|
-
const state = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const pageDef = definition.pages[2]
|
|
51
|
-
const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
52
|
-
|
|
53
|
-
const controller = new SummaryPageController(model, pageDef)
|
|
54
|
-
|
|
55
|
-
const request = buildFormContextRequest({
|
|
56
|
-
method: 'get',
|
|
57
|
-
url: pageUrl,
|
|
58
|
-
path: pageUrl.pathname,
|
|
59
|
-
params: {
|
|
60
|
-
path: 'pizza-order',
|
|
61
|
-
slug: 'repeat'
|
|
62
|
-
},
|
|
63
|
-
query: {},
|
|
64
|
-
app: { model }
|
|
65
|
-
})
|
|
34
|
+
const state = {
|
|
35
|
+
$$__referenceNumber: 'foobar',
|
|
36
|
+
orderType: 'delivery',
|
|
37
|
+
pizza: [
|
|
38
|
+
{
|
|
39
|
+
toppings: 'Ham',
|
|
40
|
+
quantity: 2,
|
|
41
|
+
itemId: itemId1
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
toppings: 'Pepperoni',
|
|
45
|
+
quantity: 1,
|
|
46
|
+
itemId: itemId2
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
66
50
|
|
|
67
|
-
const
|
|
68
|
-
const
|
|
51
|
+
const pageDef = definition.pages[2]
|
|
52
|
+
const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
69
53
|
|
|
70
|
-
const
|
|
71
|
-
summaryViewModel.context,
|
|
72
|
-
summaryViewModel.details
|
|
73
|
-
)
|
|
54
|
+
const controller = new SummaryPageController(model, pageDef)
|
|
74
55
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
const request = buildFormContextRequest({
|
|
57
|
+
method: 'get',
|
|
58
|
+
url: pageUrl,
|
|
59
|
+
path: pageUrl.pathname,
|
|
60
|
+
params: {
|
|
61
|
+
path: 'pizza-order',
|
|
62
|
+
slug: 'repeat'
|
|
80
63
|
},
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
])('should personalise $state email', (formStatus) => {
|
|
86
|
-
const body = format(context, items, model, submitResponse, formStatus)
|
|
87
|
-
|
|
88
|
-
const dateNow = new Date()
|
|
89
|
-
const dateExpiry = addDays(dateNow, 90)
|
|
64
|
+
query: {},
|
|
65
|
+
app: { model }
|
|
66
|
+
})
|
|
90
67
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
68
|
+
const context = model.getFormContext(request, state)
|
|
69
|
+
const summaryViewModel = controller.getSummaryViewModel(request, context)
|
|
70
|
+
|
|
71
|
+
const items = getFormSubmissionData(
|
|
72
|
+
summaryViewModel.context,
|
|
73
|
+
summaryViewModel.details
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
describe('getPersonalisation', () => {
|
|
77
|
+
it.each([
|
|
78
|
+
{
|
|
79
|
+
state: FormStatus.Live,
|
|
80
|
+
isPreview: false
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
state: FormStatus.Draft,
|
|
84
|
+
isPreview: true
|
|
85
|
+
}
|
|
86
|
+
])('should personalise $state email', (formStatus) => {
|
|
87
|
+
const body = format(context, items, model, submitResponse, formStatus)
|
|
95
88
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
${definition.name} form received at ${dateFormat(dateNow, 'h:mmaaa')} on ${dateFormat(dateNow, 'd MMMM yyyy')}.
|
|
89
|
+
const dateNow = new Date()
|
|
90
|
+
const dateExpiry = addDays(dateNow, 90)
|
|
99
91
|
|
|
100
|
-
|
|
92
|
+
// Check for link expiry message
|
|
93
|
+
expect(body).toContain(
|
|
94
|
+
`^ For security reasons, the links in this email expire at ${dateFormat(dateExpiry, 'h:mmaaa')} on ${dateFormat(dateExpiry, 'eeee d MMMM yyyy')}`
|
|
95
|
+
)
|
|
101
96
|
|
|
102
|
-
|
|
97
|
+
expect(body).toContain(
|
|
98
|
+
outdent`
|
|
99
|
+
${definition.name} form received at ${dateFormat(dateNow, 'h:mmaaa')} on ${dateFormat(dateNow, 'd MMMM yyyy')}.
|
|
103
100
|
|
|
104
|
-
|
|
101
|
+
---
|
|
105
102
|
|
|
106
|
-
|
|
103
|
+
## How would you like to receive your pizza?
|
|
107
104
|
|
|
108
|
-
|
|
105
|
+
Delivery
|
|
109
106
|
|
|
110
|
-
|
|
107
|
+
---
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
## Pizza
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
`
|
|
116
|
-
)
|
|
117
|
-
})
|
|
111
|
+
[Download Pizza \\(CSV\\)](https://forms-designer/file-download/11111111-1111-1111-1111-111111111111)
|
|
118
112
|
|
|
119
|
-
|
|
120
|
-
const formStatus = {
|
|
121
|
-
state: FormStatus.Draft,
|
|
122
|
-
isPreview: true
|
|
123
|
-
}
|
|
113
|
+
---
|
|
124
114
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
[Download main form \\(CSV\\)](https://forms-designer/file-download/00000000-0000-0000-0000-000000000000)
|
|
116
|
+
`
|
|
117
|
+
)
|
|
128
118
|
})
|
|
129
119
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
120
|
+
it('should add test warnings to preview email only', () => {
|
|
121
|
+
const formStatus = {
|
|
122
|
+
state: FormStatus.Draft,
|
|
123
|
+
isPreview: true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const body1 = format(context, items, model, submitResponse, {
|
|
127
|
+
state: FormStatus.Live,
|
|
128
|
+
isPreview: false
|
|
129
|
+
})
|
|
134
130
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
const body2 = format(context, items, model, submitResponse, {
|
|
132
|
+
state: FormStatus.Draft,
|
|
133
|
+
isPreview: true
|
|
134
|
+
})
|
|
138
135
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
expect(body1).not.toContain(
|
|
137
|
+
`This is a test of the ${definition.name} ${formStatus.state} form`
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
expect(body2).toContain(
|
|
141
|
+
`This is a test of the ${definition.name} ${formStatus.state} form`
|
|
142
|
+
)
|
|
143
|
+
})
|
|
142
144
|
})
|
|
143
145
|
})
|
|
@@ -7,10 +7,18 @@ import { addDays, format as dateFormat } from 'date-fns'
|
|
|
7
7
|
import { config } from '~/src/config/index.js'
|
|
8
8
|
import { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'
|
|
9
9
|
import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'
|
|
10
|
+
import { PaymentField } from '~/src/server/plugins/engine/components/index.js'
|
|
10
11
|
import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
11
12
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type DetailItem,
|
|
15
|
+
type DetailItemField
|
|
16
|
+
} from '~/src/server/plugins/engine/models/types.js'
|
|
13
17
|
import { type FormContext } from '~/src/server/plugins/engine/types.js'
|
|
18
|
+
import {
|
|
19
|
+
formatPaymentAmount,
|
|
20
|
+
formatPaymentDate
|
|
21
|
+
} from '~/src/server/plugins/payment/helper.js'
|
|
14
22
|
|
|
15
23
|
const designerUrl = config.get('designerUrl')
|
|
16
24
|
|
|
@@ -49,7 +57,10 @@ export function format(
|
|
|
49
57
|
lines.push(`${formName} form received at ${escapeMarkdown(formattedNow)}.\n`)
|
|
50
58
|
lines.push('---\n')
|
|
51
59
|
|
|
52
|
-
items.
|
|
60
|
+
const regularItems = items.filter((item) => !isPaymentItem(item))
|
|
61
|
+
const paymentItems = items.filter((item) => isPaymentItem(item))
|
|
62
|
+
|
|
63
|
+
regularItems.forEach((item) => {
|
|
53
64
|
const label = escapeMarkdown(item.label)
|
|
54
65
|
|
|
55
66
|
lines.push(`## ${label}\n`)
|
|
@@ -73,5 +84,53 @@ export function format(
|
|
|
73
84
|
const filename = escapeMarkdown('Download main form (CSV)')
|
|
74
85
|
lines.push(`[${filename}](${designerUrl}/file-download/${files.main})\n`)
|
|
75
86
|
|
|
87
|
+
appendPaymentSection(paymentItems, lines)
|
|
88
|
+
|
|
76
89
|
return lines.join('\n')
|
|
77
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if an item is a PaymentField
|
|
94
|
+
*/
|
|
95
|
+
function isPaymentItem(item: DetailItem): boolean {
|
|
96
|
+
if ('subItems' in item) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
return item.field instanceof PaymentField
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Appends the payment details section to the email lines if payment exists
|
|
104
|
+
*/
|
|
105
|
+
function appendPaymentSection(paymentItems: DetailItem[], lines: string[]) {
|
|
106
|
+
if (paymentItems.length === 0) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const paymentItem = paymentItems[0] as DetailItemField
|
|
111
|
+
const paymentField = paymentItem.field as PaymentField
|
|
112
|
+
const paymentState = paymentField.getPaymentStateFromState(paymentItem.state)
|
|
113
|
+
|
|
114
|
+
if (!paymentState) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const formattedAmount = formatPaymentAmount(paymentState.amount)
|
|
119
|
+
const dateOfPayment = paymentState.preAuth?.createdAt
|
|
120
|
+
? formatPaymentDate(paymentState.preAuth.createdAt)
|
|
121
|
+
: ''
|
|
122
|
+
|
|
123
|
+
lines.push(
|
|
124
|
+
'---\n',
|
|
125
|
+
`# Your payment of ${formattedAmount} was successful\n`,
|
|
126
|
+
'## Payment for\n',
|
|
127
|
+
`${escapeMarkdown(paymentState.description)}\n`,
|
|
128
|
+
'---\n',
|
|
129
|
+
'## Total amount\n',
|
|
130
|
+
`${formattedAmount}\n`,
|
|
131
|
+
'---\n',
|
|
132
|
+
'## Date of payment\n',
|
|
133
|
+
`${escapeMarkdown(dateOfPayment)}\n`,
|
|
134
|
+
'---\n'
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
2
|
+
|
|
3
|
+
import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
|
|
4
|
+
import { FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
5
|
+
import { format } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
|
|
6
|
+
import {
|
|
7
|
+
SummaryPageController,
|
|
8
|
+
getFormSubmissionData
|
|
9
|
+
} from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
|
|
10
|
+
import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
11
|
+
import { type FormSubmissionState } from '~/src/server/plugins/engine/types.js'
|
|
12
|
+
import { FormStatus } from '~/src/server/routes/types.js'
|
|
13
|
+
import definition from '~/test/form/definitions/payment.js'
|
|
14
|
+
|
|
15
|
+
const submitResponse = {
|
|
16
|
+
message: 'Submit completed',
|
|
17
|
+
result: {
|
|
18
|
+
files: {
|
|
19
|
+
main: '00000000-0000-0000-0000-000000000000',
|
|
20
|
+
repeaters: {
|
|
21
|
+
pizza: '11111111-1111-1111-1111-111111111111'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const model = new FormModel(definition, {
|
|
28
|
+
basePath: 'test'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const formStatus = {
|
|
32
|
+
isPreview: false,
|
|
33
|
+
state: FormStatus.Live
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const state = {
|
|
37
|
+
$$__referenceNumber: 'foobar',
|
|
38
|
+
licenceLength: 365,
|
|
39
|
+
fullName: 'John Smith',
|
|
40
|
+
paymentField: {
|
|
41
|
+
paymentId: 'payment-id',
|
|
42
|
+
reference: 'payment-ref',
|
|
43
|
+
amount: 250,
|
|
44
|
+
description: 'Payment desc',
|
|
45
|
+
uuid: 'uuid',
|
|
46
|
+
formId: 'form-id',
|
|
47
|
+
isLivePayment: false,
|
|
48
|
+
preAuth: {
|
|
49
|
+
status: 'success',
|
|
50
|
+
createdAt: '2026-01-02T11:00:04+0000'
|
|
51
|
+
}
|
|
52
|
+
} as PaymentState
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
56
|
+
|
|
57
|
+
const request = buildFormContextRequest({
|
|
58
|
+
method: 'get',
|
|
59
|
+
url: pageUrl,
|
|
60
|
+
path: pageUrl.pathname,
|
|
61
|
+
params: {
|
|
62
|
+
path: 'summary',
|
|
63
|
+
slug: 'payment'
|
|
64
|
+
},
|
|
65
|
+
query: {},
|
|
66
|
+
app: { model }
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const context = model.getFormContext(
|
|
70
|
+
request,
|
|
71
|
+
state as unknown as FormSubmissionState
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const pageDef = definition.pages[2]
|
|
75
|
+
|
|
76
|
+
const controller = new SummaryPageController(model, pageDef)
|
|
77
|
+
|
|
78
|
+
const summaryViewModel = controller.getSummaryViewModel(request, context)
|
|
79
|
+
|
|
80
|
+
const items = getFormSubmissionData(
|
|
81
|
+
summaryViewModel.context,
|
|
82
|
+
summaryViewModel.details
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
describe('getPersonalisation', () => {
|
|
86
|
+
it('should return the machine output', () => {
|
|
87
|
+
model.def = definition
|
|
88
|
+
|
|
89
|
+
const body = format(context, items, model, submitResponse, formStatus)
|
|
90
|
+
|
|
91
|
+
const parsedBody = JSON.parse(body)
|
|
92
|
+
|
|
93
|
+
const expectedData = {
|
|
94
|
+
main: {
|
|
95
|
+
licenceLength: 365,
|
|
96
|
+
fullName: 'John Smith'
|
|
97
|
+
},
|
|
98
|
+
payment: {
|
|
99
|
+
amount: 250,
|
|
100
|
+
createdAt: '2026-01-02T11:00:04+0000',
|
|
101
|
+
description: 'Payment desc',
|
|
102
|
+
paymentId: 'payment-id',
|
|
103
|
+
reference: 'payment-ref'
|
|
104
|
+
},
|
|
105
|
+
repeaters: {},
|
|
106
|
+
files: {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
expect(parsedBody.meta.schemaVersion).toBe('2')
|
|
110
|
+
expect(parsedBody.meta.timestamp).toBeDateString()
|
|
111
|
+
expect(parsedBody.meta.definition).toEqual(definition)
|
|
112
|
+
expect(parsedBody.meta.referenceNumber).toBe('foobar')
|
|
113
|
+
expect(parsedBody.data).toEqual(expectedData)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type SubmitResponsePayload } from '@defra/forms-model'
|
|
2
2
|
|
|
3
3
|
import { config } from '~/src/config/index.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FileUploadField,
|
|
6
|
+
PaymentField
|
|
7
|
+
} from '~/src/server/plugins/engine/components/index.js'
|
|
5
8
|
import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
6
9
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
7
10
|
import {
|
|
@@ -16,6 +19,18 @@ import {
|
|
|
16
19
|
type RichFormValue
|
|
17
20
|
} from '~/src/server/plugins/engine/types.js'
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Payment data for the machine output format
|
|
24
|
+
* Defined locally to avoid circular dependency with types.ts
|
|
25
|
+
*/
|
|
26
|
+
interface PaymentOutput {
|
|
27
|
+
paymentId: string
|
|
28
|
+
reference: string
|
|
29
|
+
amount: number
|
|
30
|
+
description: string
|
|
31
|
+
createdAt: string
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
const designerUrl = config.get('designerUrl')
|
|
20
35
|
|
|
21
36
|
export function format(
|
|
@@ -71,6 +86,15 @@ export function format(
|
|
|
71
86
|
* userDownloadLink: 'https://forms-designer/file-download/123-456-789'
|
|
72
87
|
* }
|
|
73
88
|
* ]
|
|
89
|
+
* },
|
|
90
|
+
* payments: {
|
|
91
|
+
* paymentComponentName: {
|
|
92
|
+
* paymentId: 'abc123',
|
|
93
|
+
* reference: 'REF-123',
|
|
94
|
+
* amount: 10.00,
|
|
95
|
+
* description: 'Application fee',
|
|
96
|
+
* createdAt: '2025-01-23T10:30:00.000Z'
|
|
97
|
+
* }
|
|
74
98
|
* }
|
|
75
99
|
* }
|
|
76
100
|
*/
|
|
@@ -82,6 +106,7 @@ export function categoriseData(items: DetailItem[]) {
|
|
|
82
106
|
string,
|
|
83
107
|
{ fileId: string; fileName: string; userDownloadLink: string }[]
|
|
84
108
|
>
|
|
109
|
+
payment?: PaymentOutput
|
|
85
110
|
} = { main: {}, repeaters: {}, files: {} }
|
|
86
111
|
|
|
87
112
|
items.forEach((item) => {
|
|
@@ -91,6 +116,11 @@ export function categoriseData(items: DetailItem[]) {
|
|
|
91
116
|
output.repeaters[name] = extractRepeaters(item)
|
|
92
117
|
} else if (isFileUploadFieldItem(item)) {
|
|
93
118
|
output.files[name] = extractFileUploads(item)
|
|
119
|
+
} else if (isPaymentFieldItem(item)) {
|
|
120
|
+
const payment = extractPayment(item)
|
|
121
|
+
if (payment) {
|
|
122
|
+
output.payment = payment
|
|
123
|
+
}
|
|
94
124
|
} else {
|
|
95
125
|
output.main[name] = item.field.getFormValueFromState(state)
|
|
96
126
|
}
|
|
@@ -148,3 +178,32 @@ function isFileUploadFieldItem(
|
|
|
148
178
|
): item is FileUploadFieldDetailitem {
|
|
149
179
|
return item.field instanceof FileUploadField
|
|
150
180
|
}
|
|
181
|
+
|
|
182
|
+
function isPaymentFieldItem(item: DetailItemField): item is DetailItemField & {
|
|
183
|
+
field: PaymentField
|
|
184
|
+
} {
|
|
185
|
+
return item.field instanceof PaymentField
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns the "payments" section of the response body
|
|
190
|
+
* @param item - the payment item in the form
|
|
191
|
+
* @returns the payment data
|
|
192
|
+
*/
|
|
193
|
+
function extractPayment(
|
|
194
|
+
item: DetailItemField & { field: PaymentField }
|
|
195
|
+
): PaymentOutput | undefined {
|
|
196
|
+
const paymentState = item.field.getPaymentStateFromState(item.state)
|
|
197
|
+
|
|
198
|
+
if (!paymentState) {
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
paymentId: paymentState.paymentId,
|
|
204
|
+
reference: paymentState.reference,
|
|
205
|
+
amount: paymentState.amount,
|
|
206
|
+
description: paymentState.description,
|
|
207
|
+
createdAt: paymentState.preAuth?.createdAt ?? ''
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -15,12 +15,14 @@ import { type ValidationErrorItem } from 'joi'
|
|
|
15
15
|
import {
|
|
16
16
|
COMPONENT_STATE_ERROR,
|
|
17
17
|
EXTERNAL_STATE_APPENDAGE,
|
|
18
|
-
EXTERNAL_STATE_PAYLOAD
|
|
18
|
+
EXTERNAL_STATE_PAYLOAD,
|
|
19
|
+
PAYMENT_EXPIRED_NOTIFICATION
|
|
19
20
|
} from '~/src/server/constants.js'
|
|
20
21
|
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
21
22
|
import { optionalText } from '~/src/server/plugins/engine/components/constants.js'
|
|
22
23
|
import { type BackLink } from '~/src/server/plugins/engine/components/types.js'
|
|
23
24
|
import {
|
|
25
|
+
checkFormStatus,
|
|
24
26
|
getCacheService,
|
|
25
27
|
getErrors,
|
|
26
28
|
getSaveAndExitHelpers,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
import { getComponentsByType } from '~/src/server/plugins/engine/validationHelpers.js'
|
|
48
50
|
import {
|
|
49
51
|
FormAction,
|
|
52
|
+
FormStatus,
|
|
50
53
|
type FormRequest,
|
|
51
54
|
type FormRequestPayload,
|
|
52
55
|
type FormRequestPayloadRefs,
|
|
@@ -182,6 +185,16 @@ export class QuestionPageController extends PageController {
|
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
const hasIncompletePayment = components.some(({ model }) => {
|
|
189
|
+
if ('paymentState' in model) {
|
|
190
|
+
const paymentState = model.paymentState as
|
|
191
|
+
| { preAuth?: { status?: string } }
|
|
192
|
+
| undefined
|
|
193
|
+
return !paymentState?.preAuth?.status
|
|
194
|
+
}
|
|
195
|
+
return false
|
|
196
|
+
})
|
|
197
|
+
|
|
185
198
|
return {
|
|
186
199
|
...viewModel,
|
|
187
200
|
backLink: this.getBackLink(request, context),
|
|
@@ -189,7 +202,8 @@ export class QuestionPageController extends PageController {
|
|
|
189
202
|
showTitle,
|
|
190
203
|
components,
|
|
191
204
|
errors,
|
|
192
|
-
allowSaveAndExit: this.shouldShowSaveAndExit(request.server)
|
|
205
|
+
allowSaveAndExit: this.shouldShowSaveAndExit(request.server),
|
|
206
|
+
showSubmitButton: !hasIncompletePayment
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
|
|
@@ -423,6 +437,12 @@ export class QuestionPageController extends PageController {
|
|
|
423
437
|
|
|
424
438
|
viewModel.errors = (viewModel.errors ?? []).concat(flashedErrors)
|
|
425
439
|
|
|
440
|
+
const paymentExpiredFlash = request.yar.flash(
|
|
441
|
+
PAYMENT_EXPIRED_NOTIFICATION
|
|
442
|
+
)
|
|
443
|
+
viewModel.showPaymentExpiredNotification =
|
|
444
|
+
!Array.isArray(paymentExpiredFlash)
|
|
445
|
+
|
|
426
446
|
/**
|
|
427
447
|
* Content components can be hidden based on a condition. If the condition evaluates to true, it is safe to be kept, otherwise discard it
|
|
428
448
|
*/
|
|
@@ -517,7 +537,7 @@ export class QuestionPageController extends PageController {
|
|
|
517
537
|
const action = request.payload.action
|
|
518
538
|
|
|
519
539
|
if (action?.startsWith(FormAction.External)) {
|
|
520
|
-
return this.dispatchExternal(request, h, context)
|
|
540
|
+
return await this.dispatchExternal(request, h, context)
|
|
521
541
|
}
|
|
522
542
|
|
|
523
543
|
/**
|
|
@@ -551,7 +571,7 @@ export class QuestionPageController extends PageController {
|
|
|
551
571
|
}
|
|
552
572
|
}
|
|
553
573
|
|
|
554
|
-
private dispatchExternal(
|
|
574
|
+
private async dispatchExternal(
|
|
555
575
|
request: FormRequestPayload,
|
|
556
576
|
h: FormResponseToolkit,
|
|
557
577
|
context: FormContext
|
|
@@ -602,11 +622,17 @@ export class QuestionPageController extends PageController {
|
|
|
602
622
|
// Clear any previous state appendage
|
|
603
623
|
request.yar.clear(EXTERNAL_STATE_APPENDAGE)
|
|
604
624
|
|
|
605
|
-
|
|
625
|
+
// Determine if this is a live form (not preview/draft)
|
|
626
|
+
const { state, isPreview } = checkFormStatus(request.params)
|
|
627
|
+
const isLive = state === FormStatus.Live
|
|
628
|
+
|
|
629
|
+
return await selectedComponent.dispatcher(request, h, {
|
|
606
630
|
component,
|
|
607
631
|
controller: this,
|
|
608
632
|
sourceUrl: request.url.toString(),
|
|
609
|
-
actionArgs: args
|
|
633
|
+
actionArgs: args,
|
|
634
|
+
isLive,
|
|
635
|
+
isPreview
|
|
610
636
|
})
|
|
611
637
|
}
|
|
612
638
|
|