@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,52 @@
|
|
|
1
|
+
import { config } from '~/src/config/index.js'
|
|
2
|
+
import {
|
|
3
|
+
formatPaymentAmount,
|
|
4
|
+
formatPaymentDate,
|
|
5
|
+
getPaymentApiKey
|
|
6
|
+
} from '~/src/server/plugins/payment/helper.js'
|
|
7
|
+
|
|
8
|
+
describe('getPaymentApiKey', () => {
|
|
9
|
+
config.set('paymentProviderApiKeyTest', 'TEST-API-KEY')
|
|
10
|
+
const formId = 'form-id'
|
|
11
|
+
process.env['PAYMENT_PROVIDER_API_KEY_LIVE_form-id'] = 'LIVE-API-KEY'
|
|
12
|
+
process.env['PAYMENT_PROVIDER_API_KEY_TEST_form-id'] = 'TEST-API-KEY'
|
|
13
|
+
|
|
14
|
+
it('should read test key when non-live form', () => {
|
|
15
|
+
const apiKey = getPaymentApiKey(false, formId)
|
|
16
|
+
expect(apiKey).toBe('TEST-API-KEY')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should read live key when live form', () => {
|
|
20
|
+
const apiKey = getPaymentApiKey(true, formId)
|
|
21
|
+
expect(apiKey).toBe('LIVE-API-KEY')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should throw if TEST key is missing', () => {
|
|
25
|
+
expect(() => getPaymentApiKey(false, 'form-id-missing')).toThrow(
|
|
26
|
+
'Missing payment api key for test form id form-id-missing'
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should throw if LIVE key is missing', () => {
|
|
31
|
+
expect(() => getPaymentApiKey(true, 'form-id-missing')).toThrow(
|
|
32
|
+
'Missing payment api key for live form id form-id-missing'
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('formatPaymentDate', () => {
|
|
38
|
+
it('should format ISO date string to en-GB format', () => {
|
|
39
|
+
const result = formatPaymentDate('2025-11-10T17:01:29.000Z')
|
|
40
|
+
expect(result).toBe('10 November 2025 5:01pm')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('formatPaymentAmount', () => {
|
|
45
|
+
it('should format whole number with two decimal places', () => {
|
|
46
|
+
expect(formatPaymentAmount(10)).toBe('£10.00')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should format decimal amount', () => {
|
|
50
|
+
expect(formatPaymentAmount(99.5)).toBe('£99.50')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { StatusCodes } from 'http-status-codes'
|
|
2
|
+
|
|
3
|
+
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
|
|
4
|
+
import { get, post, postJson } from '~/src/server/services/httpService.js'
|
|
5
|
+
|
|
6
|
+
const PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'
|
|
7
|
+
const PAYMENT_ENDPOINT = '/v1/payments'
|
|
8
|
+
|
|
9
|
+
const logger = createLogger()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} apiKey
|
|
13
|
+
* @returns {{ Authorization: string }}
|
|
14
|
+
*/
|
|
15
|
+
function getAuthHeaders(apiKey) {
|
|
16
|
+
return {
|
|
17
|
+
Authorization: `Bearer ${apiKey}`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class PaymentService {
|
|
22
|
+
/** @type {string} */
|
|
23
|
+
#apiKey
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} apiKey - API key to use (global config for test value, per-form config for live value)
|
|
27
|
+
*/
|
|
28
|
+
constructor(apiKey) {
|
|
29
|
+
this.#apiKey = apiKey
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a payment with delayed capture (pre-authorisation)
|
|
34
|
+
* @param {number} amount - in pence
|
|
35
|
+
* @param {string} description
|
|
36
|
+
* @param {string} returnUrl
|
|
37
|
+
* @param {string} reference
|
|
38
|
+
* @param {{ formId: string, slug: string }} metadata
|
|
39
|
+
*/
|
|
40
|
+
async createPayment(amount, description, returnUrl, reference, metadata) {
|
|
41
|
+
const response = await this.postToPayProvider({
|
|
42
|
+
amount,
|
|
43
|
+
description,
|
|
44
|
+
reference,
|
|
45
|
+
metadata,
|
|
46
|
+
return_url: returnUrl,
|
|
47
|
+
delayed_capture: true
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
paymentId: response.payment_id,
|
|
52
|
+
paymentUrl: response._links.next_url.href
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} paymentId
|
|
58
|
+
* @returns {Promise<GetPaymentResponse>}
|
|
59
|
+
*/
|
|
60
|
+
async getPaymentStatus(paymentId) {
|
|
61
|
+
const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const response = await getByType(
|
|
65
|
+
`${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,
|
|
66
|
+
{
|
|
67
|
+
headers: getAuthHeaders(this.#apiKey),
|
|
68
|
+
json: true
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if (response.error) {
|
|
73
|
+
const errorMessage =
|
|
74
|
+
response.error instanceof Error
|
|
75
|
+
? response.error.message
|
|
76
|
+
: JSON.stringify(response.error)
|
|
77
|
+
throw new Error(`Failed to get payment status: ${errorMessage}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
state: response.payload.state,
|
|
82
|
+
_links: response.payload._links,
|
|
83
|
+
email: response.payload.email,
|
|
84
|
+
paymentId: response.payload.payment_id,
|
|
85
|
+
amount: response.payload.amount
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const error = /** @type {Error} */ (err)
|
|
89
|
+
logger.error(
|
|
90
|
+
error,
|
|
91
|
+
`[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`
|
|
92
|
+
)
|
|
93
|
+
throw err
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Captures a payment that is in 'capturable' status
|
|
99
|
+
* @param {string} paymentId
|
|
100
|
+
* @returns {Promise<boolean>}
|
|
101
|
+
*/
|
|
102
|
+
async capturePayment(paymentId) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await post(
|
|
105
|
+
`${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,
|
|
106
|
+
{
|
|
107
|
+
headers: getAuthHeaders(this.#apiKey)
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const statusCode = response.res.statusCode
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
statusCode === StatusCodes.OK ||
|
|
115
|
+
statusCode === StatusCodes.NO_CONTENT
|
|
116
|
+
) {
|
|
117
|
+
logger.info(`[payment] Successfully captured payment ${paymentId}`)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
logger.error(
|
|
122
|
+
`[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`
|
|
123
|
+
)
|
|
124
|
+
return false
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const error = /** @type {Error} */ (err)
|
|
127
|
+
logger.error(
|
|
128
|
+
error,
|
|
129
|
+
`[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`
|
|
130
|
+
)
|
|
131
|
+
throw err
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {CreatePaymentRequest} payload
|
|
137
|
+
*/
|
|
138
|
+
async postToPayProvider(payload) {
|
|
139
|
+
const postJsonByType =
|
|
140
|
+
/** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const response = await postJsonByType(
|
|
144
|
+
`${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,
|
|
145
|
+
{
|
|
146
|
+
payload,
|
|
147
|
+
headers: getAuthHeaders(this.#apiKey)
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (response.payload?.state.status !== 'created') {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Failed to create payment for reference=${payload.reference}`
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return response.payload
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const error = /** @type {Error} */ (err)
|
|
160
|
+
logger.error(
|
|
161
|
+
error,
|
|
162
|
+
`[payment] Error creating payment for reference=${payload.reference}: ${error.message}`
|
|
163
|
+
)
|
|
164
|
+
throw err
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @import { CreatePaymentRequest, CreatePaymentResponse, GetPaymentApiResponse, GetPaymentResponse } from '~/src/server/plugins/payment/types.js'
|
|
171
|
+
*/
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { PaymentService } from '~/src/server/plugins/payment/service.js'
|
|
2
|
+
import { get, post, postJson } from '~/src/server/services/httpService.js'
|
|
3
|
+
|
|
4
|
+
jest.mock('~/src/server/services/httpService.ts')
|
|
5
|
+
|
|
6
|
+
describe('payment service', () => {
|
|
7
|
+
const service = new PaymentService('my-api-key')
|
|
8
|
+
describe('constructor', () => {
|
|
9
|
+
it('should create instance', () => {
|
|
10
|
+
expect(service).toBeDefined()
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('createPayment', () => {
|
|
15
|
+
it('should create a payment', async () => {
|
|
16
|
+
const createPaymentResult = {
|
|
17
|
+
payment_id: 'payment-id-12345',
|
|
18
|
+
_links: {
|
|
19
|
+
next_url: {
|
|
20
|
+
href: 'http://next-url-href/payment'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
state: {
|
|
24
|
+
status: 'created'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
jest.mocked(postJson).mockResolvedValueOnce({
|
|
28
|
+
res: /** @type {IncomingMessage} */ ({
|
|
29
|
+
statusCode: 200,
|
|
30
|
+
headers: {}
|
|
31
|
+
}),
|
|
32
|
+
payload: createPaymentResult,
|
|
33
|
+
error: undefined
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const referenceNumber = 'ABC-DEF-123'
|
|
37
|
+
const returnUrl = 'http://localhost:3009/payment-callback-handler'
|
|
38
|
+
const metadata = { formId: 'form-id', slug: 'my-form-slug' }
|
|
39
|
+
const payment = await service.createPayment(
|
|
40
|
+
100,
|
|
41
|
+
'Payment description',
|
|
42
|
+
returnUrl,
|
|
43
|
+
referenceNumber,
|
|
44
|
+
metadata
|
|
45
|
+
)
|
|
46
|
+
expect(payment.paymentId).toBe('payment-id-12345')
|
|
47
|
+
expect(payment.paymentUrl).toBe('http://next-url-href/payment')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should throw if fails to create a payment - failed API call', async () => {
|
|
51
|
+
jest
|
|
52
|
+
.mocked(postJson)
|
|
53
|
+
.mockRejectedValueOnce(new Error('internal creation error'))
|
|
54
|
+
|
|
55
|
+
const referenceNumber = 'ABC-DEF-123'
|
|
56
|
+
const returnUrl = 'http://localhost:3009/payment-callback-handler'
|
|
57
|
+
const metadata = { formId: 'form-id', slug: 'my-form-slug' }
|
|
58
|
+
await expect(() =>
|
|
59
|
+
service.createPayment(
|
|
60
|
+
100,
|
|
61
|
+
'Payment description',
|
|
62
|
+
returnUrl,
|
|
63
|
+
referenceNumber,
|
|
64
|
+
metadata
|
|
65
|
+
)
|
|
66
|
+
).rejects.toThrow('internal creation error')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should throw if fails to create a payment - bad result from API call', async () => {
|
|
70
|
+
const createPaymentResult = {
|
|
71
|
+
state: {
|
|
72
|
+
status: 'failed'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
jest.mocked(postJson).mockResolvedValueOnce({
|
|
76
|
+
res: /** @type {IncomingMessage} */ ({
|
|
77
|
+
statusCode: 200,
|
|
78
|
+
headers: {}
|
|
79
|
+
}),
|
|
80
|
+
payload: createPaymentResult,
|
|
81
|
+
error: undefined
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const referenceNumber = 'ABC-DEF-123'
|
|
85
|
+
const returnUrl = 'http://localhost:3009/payment-callback-handler'
|
|
86
|
+
const metadata = { formId: 'form-id', slug: 'my-form-slug' }
|
|
87
|
+
await expect(() =>
|
|
88
|
+
service.createPayment(
|
|
89
|
+
100,
|
|
90
|
+
'Payment description',
|
|
91
|
+
returnUrl,
|
|
92
|
+
referenceNumber,
|
|
93
|
+
metadata
|
|
94
|
+
)
|
|
95
|
+
).rejects.toThrow('Failed to create payment')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('getPaymentStatus', () => {
|
|
100
|
+
it('should get payment status if exists', async () => {
|
|
101
|
+
const getPaymentStatusResult = {
|
|
102
|
+
payment_id: 'payment-id-12345',
|
|
103
|
+
_links: {
|
|
104
|
+
next_url: {
|
|
105
|
+
href: 'http://next-url-href/payment'
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
state: {
|
|
109
|
+
status: 'created'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
jest.mocked(get).mockResolvedValueOnce({
|
|
114
|
+
res: /** @type {IncomingMessage} */ ({
|
|
115
|
+
statusCode: 200,
|
|
116
|
+
headers: {}
|
|
117
|
+
}),
|
|
118
|
+
payload: getPaymentStatusResult,
|
|
119
|
+
error: undefined
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const paymentStatus = await service.getPaymentStatus('payment-id-12345')
|
|
123
|
+
expect(paymentStatus.paymentId).toBe('payment-id-12345')
|
|
124
|
+
expect(paymentStatus._links.next_url?.href).toBe(
|
|
125
|
+
'http://next-url-href/payment'
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should handle payment status error', async () => {
|
|
130
|
+
jest.mocked(get).mockResolvedValueOnce({
|
|
131
|
+
res: /** @type {IncomingMessage} */ ({
|
|
132
|
+
statusCode: 200,
|
|
133
|
+
headers: {}
|
|
134
|
+
}),
|
|
135
|
+
payload: undefined,
|
|
136
|
+
error: new Error('some-error')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await expect(() =>
|
|
140
|
+
service.getPaymentStatus('payment-id-12345')
|
|
141
|
+
).rejects.toThrow('Failed to get payment status: some-error')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('capturePayment', () => {
|
|
146
|
+
it('should return true when successful capture with statusCode 200', async () => {
|
|
147
|
+
const capturePaymentResult = {}
|
|
148
|
+
jest.mocked(post).mockResolvedValueOnce({
|
|
149
|
+
res: /** @type {IncomingMessage} */ ({
|
|
150
|
+
statusCode: 200,
|
|
151
|
+
headers: {}
|
|
152
|
+
}),
|
|
153
|
+
payload: capturePaymentResult,
|
|
154
|
+
error: undefined
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const captureResult = await service.capturePayment('payment-id-12345')
|
|
158
|
+
expect(captureResult).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should return true when successful capture with statusCode 204', async () => {
|
|
162
|
+
const capturePaymentResult = {}
|
|
163
|
+
jest.mocked(post).mockResolvedValueOnce({
|
|
164
|
+
res: /** @type {IncomingMessage} */ ({
|
|
165
|
+
statusCode: 204,
|
|
166
|
+
headers: {}
|
|
167
|
+
}),
|
|
168
|
+
payload: capturePaymentResult,
|
|
169
|
+
error: undefined
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const captureResult = await service.capturePayment('payment-id-12345')
|
|
173
|
+
expect(captureResult).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should return false when status code not 200 or 204', async () => {
|
|
177
|
+
const capturePaymentResult = {}
|
|
178
|
+
jest.mocked(post).mockResolvedValueOnce({
|
|
179
|
+
res: /** @type {IncomingMessage} */ ({
|
|
180
|
+
statusCode: 500,
|
|
181
|
+
headers: {}
|
|
182
|
+
}),
|
|
183
|
+
payload: capturePaymentResult,
|
|
184
|
+
error: undefined
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const captureResult = await service.capturePayment('payment-id-12345')
|
|
188
|
+
expect(captureResult).toBe(false)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should throw when internal error', async () => {
|
|
192
|
+
jest
|
|
193
|
+
.mocked(post)
|
|
194
|
+
.mockRejectedValueOnce(new Error('internal capture error'))
|
|
195
|
+
|
|
196
|
+
await expect(() =>
|
|
197
|
+
service.capturePayment('payment-id-12345')
|
|
198
|
+
).rejects.toThrow('internal capture error')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @import { IncomingMessage } from 'node:http'
|
|
205
|
+
*/
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} PaymentResponseState
|
|
3
|
+
* @property {'created' | 'started' | 'submitted' | 'capturable' | 'success' | 'failed' | 'cancelled' | 'error'} status - Current status of the payment
|
|
4
|
+
* @property {boolean} finished - Whether the payment process has completed
|
|
5
|
+
* @property {string} [message] - Human-readable message about the payment state
|
|
6
|
+
* @property {string} [code] - Error or status code for the payment state
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} PaymentLink
|
|
11
|
+
* @property {string} href - URL of the linked resource
|
|
12
|
+
* @property {string} method - HTTP method to use for the link
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} CreatePaymentRequest
|
|
17
|
+
* @property {number} amount - Payment amount in pence
|
|
18
|
+
* @property {string} reference - Unique reference for the payment
|
|
19
|
+
* @property {string} description - Human-readable description of the payment
|
|
20
|
+
* @property {string} return_url - URL to redirect the user to after payment
|
|
21
|
+
* @property {boolean} [delayed_capture] - Whether to delay capturing the payment
|
|
22
|
+
* @property {{ formId: string, slug: string }} [metadata] - Additional metadata for the payment
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} CreatePaymentResponse
|
|
27
|
+
* @property {string} payment_id - Unique identifier for the created payment
|
|
28
|
+
* @property {PaymentResponseState} state - Current state of the payment
|
|
29
|
+
* @property {{ next_url: PaymentLink }} _links - HATEOAS links for the payment
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Base response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint
|
|
34
|
+
* @typedef {object} GetPaymentResponseBase
|
|
35
|
+
* @property {PaymentResponseState} state - Current state of the payment
|
|
36
|
+
* @property {{ self: PaymentLink, next_url?: PaymentLink }} _links - HATEOAS links for the payment
|
|
37
|
+
* @property {string} [email] - The paying user's email address
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint - not underscore in property name
|
|
42
|
+
* @typedef {object} GetPaymentApiResponsePaymentProp
|
|
43
|
+
* @property {string} payment_id - Unique identifier for the payment
|
|
44
|
+
* @property {number} amount - amount of the payment
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint
|
|
49
|
+
* @typedef {GetPaymentResponseBase & GetPaymentApiResponsePaymentProp} GetPaymentApiResponse
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Response returned from getPaymentStatus - subtley different from GetPaymentApiResponse
|
|
54
|
+
* @typedef {object} GetPaymentResponsePaymentProp
|
|
55
|
+
* @property {string} paymentId - Unique identifier for the payment - note no underscore in property name
|
|
56
|
+
* @property {number} amount - amount of the payment
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Response returned from getPaymentStatus - subtley different from GetPaymentApiResponse
|
|
61
|
+
* @typedef {GetPaymentResponseBase & GetPaymentResponsePaymentProp} GetPaymentResponse
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Payment session data stored when dispatching to GOV.UK Pay
|
|
66
|
+
* @typedef {object} PaymentSessionData
|
|
67
|
+
* @property {string} uuid - unique identifier for this payment attempt
|
|
68
|
+
* @property {string} formId - id of the form
|
|
69
|
+
* @property {string} reference - form reference number
|
|
70
|
+
* @property {number} amount - amount in pounds
|
|
71
|
+
* @property {string} description - payment description
|
|
72
|
+
* @property {string} paymentId - GOV.UK Pay payment ID
|
|
73
|
+
* @property {string} componentName - name of the PaymentField component
|
|
74
|
+
* @property {string} returnUrl - URL to redirect to after successful payment
|
|
75
|
+
* @property {string} failureUrl - URL to redirect to after failed/cancelled payment
|
|
76
|
+
* @property {boolean} isLivePayment - whether the payment is using live API key
|
|
77
|
+
*/
|
package/src/server/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type PluginOptions,
|
|
16
16
|
type PreparePageEventRequestOptions
|
|
17
17
|
} from '~/src/server/plugins/engine/types.js'
|
|
18
|
+
import { type PaymentService } from '~/src/server/plugins/payment/service.js'
|
|
18
19
|
import {
|
|
19
20
|
type FormRequestPayload,
|
|
20
21
|
type FormStatus
|
|
@@ -42,6 +43,7 @@ export interface Services {
|
|
|
42
43
|
formsService: FormsService
|
|
43
44
|
formSubmissionService: FormSubmissionService
|
|
44
45
|
outputService: OutputService
|
|
46
|
+
paymentService?: PaymentService
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface RouteConfig {
|