Skip to content

Commit 84c23fa

Browse files
committed
Merge branch 'feat/df-623-payment' of https://github.com/DEFRA/forms-engine-plugin into feat/df-623-payment
2 parents 6bbd766 + 6d49994 commit 84c23fa

15 files changed

Lines changed: 169 additions & 33 deletions

File tree

package-lock.json

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@defra/forms-engine-plugin",
3-
"version": "4.0.38",
3+
"version": "4.0.39",
44
"description": "Defra forms engine",
55
"type": "module",
66
"files": [
@@ -108,6 +108,7 @@
108108
"lodash": "^4.17.21",
109109
"marked": "^15.0.12",
110110
"nunjucks": "^3.2.4",
111+
"obscenity": "^0.4.5",
111112
"outdent": "^0.8.0",
112113
"pino": "^9.14.0",
113114
"pino-pretty": "^13.1.2",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@use "govuk-frontend" as *;
2+
3+
.app-payment-field {
4+
background-color: govuk-colour("light-grey");
5+
border-top: 5px solid govuk-colour("blue");
6+
padding: govuk-spacing(4);
7+
margin-bottom: govuk-spacing(6);
8+
}

src/client/stylesheets/application.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@use "code";
44
@use "tag-env";
55
@use "location-fields";
6+
@use "payment-field";
67

78
// An example of some user-supplied styling
89
// Not great practice but it illustrates the point

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ const ordnanceSurveyApiKey = config.get('ordnanceSurveyApiKey')
2323
* Main entrypoint to the application.
2424
*/
2525
async function startServer() {
26-
const server = await createServer({ ordnanceSurveyApiKey })
26+
const server = await createServer({
27+
ordnanceSurveyApiKey,
28+
// Enable save and exit for devserver
29+
saveAndExit: (_request, h) => h.redirect('/')
30+
})
2731
await server.start()
2832

2933
process.send?.('online')

src/server/plugins/engine/components/PaymentField.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ export class PaymentField extends FormComponent {
6161
? (payload[this.name] as unknown as PaymentState)
6262
: undefined
6363

64+
const amount = this.options.amount ?? 0
65+
const formattedAmount = amount.toFixed(2)
66+
6467
return {
6568
...viewModel,
66-
amount: this.options.amount,
69+
amount: formattedAmount,
6770
description: this.options.description,
6871
paymentState
6972
}

src/server/plugins/engine/components/PaymentField.types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,18 @@ export interface PaymentStatus {
3939
}
4040
createdDate: string
4141
}
42+
43+
/**
44+
* Service interface for GOV.UK Pay integration
45+
*/
46+
export interface PaymentService {
47+
createPayment(
48+
amount: number,
49+
description: string,
50+
metadata: { formId: string; slug: string }
51+
): Promise<{ paymentId: string; paymentUrl: string }>
52+
53+
getPaymentStatus(paymentId: string): Promise<PaymentStatus>
54+
55+
capturePayment(paymentId: string): Promise<boolean>
56+
}

src/server/plugins/engine/pageControllers/QuestionPageController.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,28 @@ export class QuestionPageController extends PageController {
182182
}
183183
}
184184

185+
// Check if any PaymentField component needs payment to be added
186+
// If so, hide the submit button until payment is ready
187+
const hasIncompletePayment = components.some(({ model }) => {
188+
// Check if this is a PaymentField by looking for paymentState in model
189+
if ('paymentState' in model && 'amount' 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+
185198
return {
186199
...viewModel,
187200
backLink: this.getBackLink(request, context),
188201
context,
189202
showTitle,
190203
components,
191204
errors,
192-
allowSaveAndExit: this.shouldShowSaveAndExit(request.server)
205+
allowSaveAndExit: this.shouldShowSaveAndExit(request.server),
206+
showSubmitButton: !hasIncompletePayment
193207
}
194208
}
195209

src/server/plugins/engine/pageControllers/StatusPageController.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {
1212
export class StatusPageController extends QuestionPageController {
1313
declare pageDef: PageStatus
1414
allowSaveAndExit = false
15+
showReferenceNumber = false
1516

1617
constructor(model: FormModel, pageDef: PageStatus) {
1718
super(model, pageDef)
1819
this.viewName = 'confirmation'
20+
this.showReferenceNumber = model.def.options?.showReferenceNumber ?? false
1921
}
2022

2123
getRelevantPath() {
@@ -54,7 +56,9 @@ export class StatusPageController extends QuestionPageController {
5456
return h.view(viewName, {
5557
...viewModel,
5658
submissionGuidance,
57-
formName
59+
formName,
60+
showReferenceNumber: this.showReferenceNumber,
61+
referenceNumber: context.referenceNumber
5862
})
5963
}
6064
}

src/server/plugins/engine/referenceNumbers.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'
1+
import {
2+
convertToDecAlpha,
3+
generateUniqueReference
4+
} from '~/src/server/plugins/engine/referenceNumbers.js'
25

36
describe('generateUniqueReference', () => {
47
it('should generate a reference number with 3 segments when no prefix is provided', () => {
@@ -30,4 +33,42 @@ describe('generateUniqueReference', () => {
3033
const referenceNumber2 = generateUniqueReference()
3134
expect(referenceNumber1).not.toBe(referenceNumber2)
3235
})
36+
37+
describe('convertToDecAlpha', () => {
38+
it('should generate correct characters in string', () => {
39+
const allValuesHexPairs = Array.from(Array(256).keys())
40+
expect(convertToDecAlpha(allValuesHexPairs)).toBe(
41+
'AAAAAAAAA' +
42+
'BBBBBBBBB' +
43+
'CCCCCCCC' +
44+
'DDDDDDDDD' +
45+
'EEEEEEEE' +
46+
'FFFFFFFFF' +
47+
'HHHHHHHH' +
48+
'JJJJJJJJJ' +
49+
'KKKKKKKK' +
50+
'LLLLLLLLL' +
51+
'MMMMMMMM' +
52+
'NNNNNNNNN' +
53+
'PPPPPPPP' +
54+
'RRRRRRRRR' +
55+
'SSSSSSSS' +
56+
'TTTTTTTTT' +
57+
'UUUUUUUUU' +
58+
'VVVVVVVV' +
59+
'WWWWWWWWW' +
60+
'XXXXXXXX' +
61+
'YYYYYYYYY' +
62+
'ZZZZZZZZ' +
63+
'222222222' +
64+
'33333333' +
65+
'444444444' +
66+
'55555555' +
67+
'666666666' +
68+
'77777777' +
69+
'888888888' +
70+
'99999999'
71+
)
72+
})
73+
})
3374
})

0 commit comments

Comments
 (0)