@defra/forms-engine-plugin 4.0.43 → 4.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.public/stylesheets/application.min.css +1 -1
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/_payment-field.scss +8 -0
  4. package/.server/client/stylesheets/application.scss +2 -0
  5. package/.server/index.js +3 -1
  6. package/.server/index.js.map +1 -1
  7. package/.server/server/constants.d.ts +1 -0
  8. package/.server/server/constants.js +1 -0
  9. package/.server/server/constants.js.map +1 -1
  10. package/.server/server/forms/payment-test.yaml +42 -0
  11. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
  12. package/.server/server/plugins/engine/components/FormComponent.d.ts +1 -0
  13. package/.server/server/plugins/engine/components/FormComponent.js +1 -0
  14. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  15. package/.server/server/plugins/engine/components/PaymentField.d.ts +135 -0
  16. package/.server/server/plugins/engine/components/PaymentField.js +228 -0
  17. package/.server/server/plugins/engine/components/PaymentField.js.map +1 -0
  18. package/.server/server/plugins/engine/components/PaymentField.types.d.ts +21 -0
  19. package/.server/server/plugins/engine/components/PaymentField.types.js +2 -0
  20. package/.server/server/plugins/engine/components/PaymentField.types.js.map +1 -0
  21. package/.server/server/plugins/engine/components/UkAddressField.d.ts +1 -1
  22. package/.server/server/plugins/engine/components/UkAddressField.js +3 -1
  23. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  24. package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
  25. package/.server/server/plugins/engine/components/helpers/components.js +3 -0
  26. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  27. package/.server/server/plugins/engine/components/index.d.ts +1 -0
  28. package/.server/server/plugins/engine/components/index.js +1 -0
  29. package/.server/server/plugins/engine/components/index.js.map +1 -1
  30. package/.server/server/plugins/engine/helpers.d.ts +1 -0
  31. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +3 -0
  32. package/.server/server/plugins/engine/models/SummaryViewModel.js +7 -0
  33. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  34. package/.server/server/plugins/engine/outputFormatters/human/v1.js +34 -1
  35. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  36. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +22 -0
  37. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +43 -1
  38. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  39. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +29 -8
  40. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  41. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -0
  42. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +17 -0
  43. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +173 -51
  44. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  45. package/.server/server/plugins/engine/pageControllers/errors.d.ts +31 -0
  46. package/.server/server/plugins/engine/pageControllers/errors.js +59 -2
  47. package/.server/server/plugins/engine/pageControllers/errors.js.map +1 -1
  48. package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +27 -0
  49. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +77 -0
  50. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -0
  51. package/.server/server/plugins/engine/plugin.js +4 -1
  52. package/.server/server/plugins/engine/plugin.js.map +1 -1
  53. package/.server/server/plugins/engine/routes/index.js +8 -4
  54. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  55. package/.server/server/plugins/engine/routes/payment-helper.d.ts +14 -0
  56. package/.server/server/plugins/engine/routes/payment-helper.js +41 -0
  57. package/.server/server/plugins/engine/routes/payment-helper.js.map +1 -0
  58. package/.server/server/plugins/engine/routes/payment-helper.test.js +81 -0
  59. package/.server/server/plugins/engine/routes/payment-helper.test.js.map +1 -0
  60. package/.server/server/plugins/engine/routes/payment.d.ts +8 -0
  61. package/.server/server/plugins/engine/routes/payment.js +140 -0
  62. package/.server/server/plugins/engine/routes/payment.js.map +1 -0
  63. package/.server/server/plugins/engine/routes/payment.test.js +187 -0
  64. package/.server/server/plugins/engine/routes/payment.test.js.map +1 -0
  65. package/.server/server/plugins/engine/services/localFormsService.js +6 -0
  66. package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
  67. package/.server/server/plugins/engine/types/schema.js +7 -0
  68. package/.server/server/plugins/engine/types/schema.js.map +1 -1
  69. package/.server/server/plugins/engine/types.d.ts +19 -1
  70. package/.server/server/plugins/engine/types.js +4 -0
  71. package/.server/server/plugins/engine/types.js.map +1 -1
  72. package/.server/server/plugins/engine/validationHelpers.d.ts +1 -1
  73. package/.server/server/plugins/engine/validationHelpers.js.map +1 -1
  74. package/.server/server/plugins/engine/views/components/paymentfield.html +42 -0
  75. package/.server/server/plugins/engine/views/index.html +9 -1
  76. package/.server/server/plugins/engine/views/partials/form.html +20 -5
  77. package/.server/server/plugins/engine/views/summary.html +17 -1
  78. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  79. package/.server/server/plugins/payment/helper.d.ts +30 -0
  80. package/.server/server/plugins/payment/helper.js +49 -0
  81. package/.server/server/plugins/payment/helper.js.map +1 -0
  82. package/.server/server/plugins/payment/helper.test.js +37 -0
  83. package/.server/server/plugins/payment/helper.test.js.map +1 -0
  84. package/.server/server/plugins/payment/service.d.ts +40 -0
  85. package/.server/server/plugins/payment/service.js +129 -0
  86. package/.server/server/plugins/payment/service.js.map +1 -0
  87. package/.server/server/plugins/payment/service.test.js +162 -0
  88. package/.server/server/plugins/payment/service.test.js.map +1 -0
  89. package/.server/server/plugins/payment/types.d.ts +172 -0
  90. package/.server/server/plugins/payment/types.js +78 -0
  91. package/.server/server/plugins/payment/types.js.map +1 -0
  92. package/.server/server/types.d.ts +2 -0
  93. package/.server/server/types.js.map +1 -1
  94. package/.server/typings/hapi/index.d.js.map +1 -1
  95. package/README.md +12 -9
  96. package/package.json +2 -2
  97. package/src/client/stylesheets/_payment-field.scss +8 -0
  98. package/src/client/stylesheets/application.scss +2 -0
  99. package/src/index.ts +5 -1
  100. package/src/server/constants.js +1 -0
  101. package/src/server/forms/payment-test.yaml +42 -0
  102. package/src/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
  103. package/src/server/plugins/engine/components/FormComponent.ts +1 -0
  104. package/src/server/plugins/engine/components/PaymentField.test.ts +611 -0
  105. package/src/server/plugins/engine/components/PaymentField.ts +367 -0
  106. package/src/server/plugins/engine/components/PaymentField.types.ts +21 -0
  107. package/src/server/plugins/engine/components/UkAddressField.ts +2 -1
  108. package/src/server/plugins/engine/components/helpers/components.ts +5 -0
  109. package/src/server/plugins/engine/components/index.ts +1 -0
  110. package/src/server/plugins/engine/models/SummaryViewModel.ts +8 -0
  111. package/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts +147 -0
  112. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +105 -103
  113. package/src/server/plugins/engine/outputFormatters/human/v1.ts +61 -2
  114. package/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts +115 -0
  115. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +60 -1
  116. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +32 -6
  117. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +247 -72
  118. package/src/server/plugins/engine/pageControllers/errors.test.ts +13 -1
  119. package/src/server/plugins/engine/pageControllers/errors.ts +79 -4
  120. package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +299 -0
  121. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +110 -0
  122. package/src/server/plugins/engine/plugin.ts +11 -6
  123. package/src/server/plugins/engine/routes/index.ts +17 -16
  124. package/src/server/plugins/engine/routes/payment-helper.js +39 -0
  125. package/src/server/plugins/engine/routes/payment-helper.test.js +90 -0
  126. package/src/server/plugins/engine/routes/payment.js +151 -0
  127. package/src/server/plugins/engine/routes/payment.test.js +180 -0
  128. package/src/server/plugins/engine/services/localFormsService.js +7 -0
  129. package/src/server/plugins/engine/types/schema.ts +9 -0
  130. package/src/server/plugins/engine/types.ts +24 -1
  131. package/src/server/plugins/engine/validationHelpers.ts +1 -1
  132. package/src/server/plugins/engine/views/components/paymentfield.html +42 -0
  133. package/src/server/plugins/engine/views/index.html +9 -1
  134. package/src/server/plugins/engine/views/partials/form.html +20 -5
  135. package/src/server/plugins/engine/views/summary.html +17 -1
  136. package/src/server/plugins/payment/helper.js +56 -0
  137. package/src/server/plugins/payment/helper.test.js +52 -0
  138. package/src/server/plugins/payment/service.js +171 -0
  139. package/src/server/plugins/payment/service.test.js +205 -0
  140. package/src/server/plugins/payment/types.js +77 -0
  141. package/src/server/types.ts +2 -0
  142. package/src/typings/hapi/index.d.ts +1 -0
@@ -0,0 +1,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
+ */
@@ -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 {
@@ -40,6 +40,7 @@ declare module '@hapi/hapi' {
40
40
  request: AnyFormRequest | null
41
41
  ) => Record<string, unknown> | Promise<Record<string, unknown>>
42
42
  saveAndExit?: PluginOptions['saveAndExit']
43
+ baseUrl: string
43
44
  }
44
45
  }
45
46