Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.setup.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ process.env.UPLOADER_BUCKET_NAME = 'dummy-bucket'
process.env.GOOGLE_ANALYTICS_TRACKING_ID = 'G-123456789'
process.env.SUBMISSION_EMAIL_ADDRESS = 'dummy@defra.gov.uk'
process.env.ORDNANCE_SURVEY_API_KEY = 'dummy'
process.env.PAYMENT_PROVIDER_API_KEY_TEST_formid = 'test-api-key'
process.env.PAYMENT_PROVIDER_API_KEY_TEST = 'test-api-key'
3 changes: 2 additions & 1 deletion src/server/plugins/engine/beta/form-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ describe('getFormModel helper', () => {
formsService: {
getFormMetadata: jest.fn().mockResolvedValue(metadata),
getFormMetadataById: jest.fn(),
getFormDefinition: jest.fn().mockResolvedValue(definition)
getFormDefinition: jest.fn().mockResolvedValue(definition),
getFormSecret: jest.fn()
},
formSubmissionService: {
persistFiles: jest.fn(),
Expand Down
21 changes: 16 additions & 5 deletions src/server/plugins/engine/components/PaymentField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,26 @@ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
import { PaymentPreAuthError } from '~/src/server/plugins/engine/pageControllers/errors.js'
import {
type FormContext,
type FormValue
type FormValue,
type PaymentExternalArgs
} from '~/src/server/plugins/engine/types.js'
import {
type FormRequestPayload,
type FormResponseToolkit
} from '~/src/server/routes/types.js'
import { get, post, postJson } from '~/src/server/services/httpService.js'
import { type Services } from '~/src/server/types.js'
import definition from '~/test/form/definitions/blank.js'
import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'

jest.mock('~/src/server/services/httpService.ts')

const mockServices = {
formsService: {
getFormSecret: () => 'secret-value'
}
} as unknown as Services

describe('PaymentField', () => {
let model: FormModel

Expand Down Expand Up @@ -250,6 +258,7 @@ describe('PaymentField', () => {

const collection = new ComponentCollection([def], { model })
const paymentField = collection.fields[0] as PaymentField
paymentField.model = { services: mockServices } as unknown as FormModel

describe('dispatcher', () => {
it('should create payment and redirect to gov pay', async () => {
Expand Down Expand Up @@ -277,7 +286,8 @@ describe('PaymentField', () => {
model: {
formId: 'formid',
basePath: 'base-path',
name: 'PaymentModel'
name: 'PaymentModel',
services: mockServices
},
getState: jest
.fn()
Expand All @@ -287,7 +297,7 @@ describe('PaymentField', () => {
sourceUrl: 'http://localhost:3009/test-payment',
isLive: false,
isPreview: true
}
} as unknown as PaymentExternalArgs
// @ts-expect-error - partial mock
jest.mocked(postJson).mockResolvedValueOnce({
payload: {
Expand Down Expand Up @@ -342,7 +352,8 @@ describe('PaymentField', () => {
model: {
formId: 'formid',
basePath: 'base-path',
name: 'PaymentModel'
name: 'PaymentModel',
services: mockServices
},
getState: jest.fn().mockResolvedValueOnce({
$$__referenceNumber: 'pay-ref-123',
Expand All @@ -361,7 +372,7 @@ describe('PaymentField', () => {
sourceUrl: 'http://localhost:3009/test-payment',
isLive: false,
isPreview: true
}
} as unknown as PaymentExternalArgs

const res = await PaymentField.dispatcher(mockRequest, mockH, args)

Expand Down
35 changes: 15 additions & 20 deletions src/server/plugins/engine/components/PaymentField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
PaymentSubmissionError
} from '~/src/server/plugins/engine/pageControllers/errors.js'
import {
type AnyFormRequest,
type FormContext,
type FormRequestPayload,
type FormResponseToolkit
Expand All @@ -27,7 +26,8 @@ import {
type FormState,
type FormStateValue,
type FormSubmissionError,
type FormSubmissionState
type FormSubmissionState,
type PaymentExternalArgs
} from '~/src/server/plugins/engine/types.js'
import {
createPaymentService,
Expand Down Expand Up @@ -186,7 +186,7 @@ export class PaymentField extends FormComponent {
static async dispatcher(
request: FormRequestPayload,
h: FormResponseToolkit,
args: PaymentDispatcherArgs
args: PaymentExternalArgs
): Promise<unknown> {
const { options, name: componentName } = args.component
const { model } = args.controller
Expand All @@ -205,7 +205,12 @@ export class PaymentField extends FormComponent {

const isLivePayment = args.isLive && !args.isPreview
const formId = args.controller.model.formId
const paymentService = createPaymentService(isLivePayment, formId)
const formsService = model.services.formsService
const paymentService = await createPaymentService(
isLivePayment,
formId,
formsService
)

const uuid = randomUUID()

Expand Down Expand Up @@ -272,7 +277,12 @@ export class PaymentField extends FormComponent {
}

const { paymentId, isLivePayment, formId } = paymentState
const paymentService = createPaymentService(isLivePayment, formId)
const formsService = this.model.services.formsService
const paymentService = await createPaymentService(
isLivePayment,
formId,
formsService
)

/**
* @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
Expand Down Expand Up @@ -343,21 +353,6 @@ export class PaymentField extends FormComponent {
}
}

export interface PaymentDispatcherArgs {
controller: {
model: {
formId: string
basePath: string
name: string
}
getState: (request: AnyFormRequest) => Promise<FormSubmissionState>
}
component: PaymentField
sourceUrl: string
isLive: boolean
isPreview: boolean
}

/**
* Session data stored when dispatching to GOV.UK Pay
*/
Expand Down
4 changes: 3 additions & 1 deletion src/server/plugins/engine/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export const plugin = {
onRequest,
ordnanceSurveyApiKey,
baseUrl,
ordnanceSurveyApiSecret
ordnanceSurveyApiSecret,
services
} = options

const cacheService =
Expand Down Expand Up @@ -77,6 +78,7 @@ export const plugin = {
server.expose('cacheService', cacheService)
server.expose('saveAndExit', saveAndExit)
server.expose('baseUrl', baseUrl)
server.expose('services', services)

server.app.model = model

Expand Down
14 changes: 9 additions & 5 deletions src/server/plugins/engine/routes/payment-helper.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import Boom from '@hapi/boom'

import { PAYMENT_SESSION_PREFIX } from '~/src/server/plugins/engine/routes/payment.js'
import { getPaymentApiKey } from '~/src/server/plugins/payment/helper.js'
import { PaymentService } from '~/src/server/plugins/payment/service.js'
import { createPaymentService } from '~/src/server/plugins/payment/helper.js'

/**
* Validates session data and retrieves payment status
* @param {Request} request - the request
* @param {string} uuid - the payment UUID
* @param {FormsService} formsService - the forms service
* @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}
*/
export async function getPaymentContext(request, uuid) {
export async function getPaymentContext(request, uuid, formsService) {
const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`
const session = /** @type {PaymentSessionData | null} */ (
request.yar.get(sessionKey)
Expand All @@ -26,8 +26,11 @@ export async function getPaymentContext(request, uuid) {
throw Boom.badRequest('No paymentId in session')
}

const apiKey = getPaymentApiKey(isLivePayment, formId)
const paymentService = new PaymentService(apiKey)
const paymentService = await createPaymentService(
isLivePayment,
formId,
formsService
)
const paymentStatus = await paymentService.getPaymentStatus(
paymentId,
isLivePayment
Expand Down Expand Up @@ -73,4 +76,5 @@ export function convertPenceToPounds(amount) {
/**
* @import { Request } from '@hapi/hapi'
* @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
* @import { FormsService } from '~/src/server/types.js'
*/
5 changes: 4 additions & 1 deletion src/server/plugins/engine/routes/payment-helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ describe('payment helper', () => {
error: undefined
})

const mockFormsService = {
getFormSecret: () => 'secret-value'
}
// @ts-expect-error - partial request mock
const res = await getPaymentContext(mockRequest, uuid)
const res = await getPaymentContext(mockRequest, uuid, mockFormsService)
expect(res).toEqual({
paymentStatus: {
paymentId: 'payment-id-12345',
Expand Down
9 changes: 8 additions & 1 deletion src/server/plugins/engine/routes/payment.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Joi from 'joi'

import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
import { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'
import { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'
import {
buildPaymentInfo,
convertPenceToPounds,
Expand Down Expand Up @@ -128,9 +129,13 @@ function getReturnRoute() {
path: PAYMENT_RETURN_PATH,
async handler(request, h) {
const { uuid } = /** @type {{ uuid: string }} */ (request.query)

const { services } = getPluginOptions(request.server)

const { session, sessionKey, paymentStatus } = await getPaymentContext(
request,
uuid
uuid,
/** @type {FormsService} */ (services?.formsService)
)

/**
Expand Down Expand Up @@ -193,4 +198,6 @@ function getReturnRoute() {
* @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
* @import { PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
* @import { ExternalStateAppendage, FormState } from '~/src/server/plugins/engine/types.js'
* @import { PluginOptions } from '~/src/server/plugins/engine/types.js'
* @import { FormsService } from '~/src/server/types.js'
*/
11 changes: 11 additions & 0 deletions src/server/plugins/engine/services/formsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ export function getFormDefinition(_id, _state) {
throw error
}

// eslint-disable-next-line jsdoc/require-returns-check
/**
* Dummy function to get a form secret.
* @param {string} _id - the id of the form
* @param {string} _secretName - the name of the secret
* @returns {Promise<string>}
*/
export function getFormSecret(_id, _secretName) {
throw error
}

/**
* @import { FormStatus, FormDefinition, FormMetadata } from '@defra/forms-model'
*/
7 changes: 6 additions & 1 deletion src/server/plugins/engine/services/formsService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { FormStatus } from '@defra/forms-model'
import {
getFormDefinition,
getFormMetadata,
getFormMetadataById
getFormMetadataById,
getFormSecret
} from '~/src/server/plugins/engine/services/formsService.js'

describe('formsService', () => {
Expand All @@ -18,4 +19,8 @@ describe('formsService', () => {
it('getFormDefinition should throw error', () => {
expect(() => getFormDefinition('id', FormStatus.Draft)).toThrow()
})

it('getFormSecret should throw error', () => {
expect(() => getFormSecret('id', 'my-secret-name')).toThrow()
})
})
7 changes: 6 additions & 1 deletion src/server/plugins/engine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type Item,
type List,
type Page,
type PaymentFieldComponent,
type UkAddressFieldComponent
} from '@defra/forms-model'
import {
Expand Down Expand Up @@ -397,7 +398,7 @@ export interface ExternalArgs {
component: ComponentDef
controller: QuestionPageController
sourceUrl: string
actionArgs: Record<string, string>
actionArgs?: Record<string, string>
isLive: boolean
isPreview: boolean
}
Expand All @@ -407,6 +408,10 @@ export interface PostcodeLookupExternalArgs extends ExternalArgs {
actionArgs: { step: string }
}

export interface PaymentExternalArgs extends ExternalArgs {
component: PaymentFieldComponent
}

export interface ExternalStateAppendage {
component: string
data: FormStateValue | FormState
Expand Down
38 changes: 15 additions & 23 deletions src/server/plugins/payment/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,23 @@ import { PaymentService } from '~/src/server/plugins/payment/service.js'
export const DEFAULT_PAYMENT_HELP_URL =
'https://www.gov.uk/government/organisations/department-for-environment-food-rural-affairs'

/**
* Determine which payment API key value to use.
* If a draft preview form or a live preview form, read the TEST API key value specific to that form.
* If a live (non-preview) form, read the LIVE API key value specific to that form.
* @param {boolean} isLivePayment - true if this is a live payment (as opposed to a test one)
* @param {string} formId - id of the form
* @returns {string}
*/
export function getPaymentApiKey(isLivePayment, formId) {
const apiKeyValue = isLivePayment
? process.env[`PAYMENT_PROVIDER_API_KEY_LIVE_${formId}`]
: process.env[`PAYMENT_PROVIDER_API_KEY_TEST_${formId}`]

if (!apiKeyValue) {
throw new Error(
`[payment] Missing payment api key for ${isLivePayment ? 'live' : 'test'} form id ${formId}`
)
}
return apiKeyValue
}
const PAYMENT_TEST_API_KEY = 'payment-test-api-key'
const PAYMENT_LIVE_API_KEY = 'payment-live-api-key'

/**
* Creates a PaymentService instance with the appropriate API key
* @param {boolean} isLivePayment - true if this is a live payment
* @param {string} formId - id of the form
* @returns {PaymentService}
* @param {FormsService} formsService - service to handle form data operations
* @returns {Promise<PaymentService>}
*/
export function createPaymentService(isLivePayment, formId) {
const apiKey = getPaymentApiKey(isLivePayment, formId)
export async function createPaymentService(
isLivePayment,
formId,
formsService
) {
const secretName = isLivePayment ? PAYMENT_LIVE_API_KEY : PAYMENT_TEST_API_KEY
const apiKey = await formsService.getFormSecret(formId, secretName)
return new PaymentService(apiKey)
}

Expand Down Expand Up @@ -61,3 +49,7 @@ export function formatCurrency(amount, locale = 'en-GB', currency = 'GBP') {

return formatter.format(amount)
}

/**
* @import { FormsService } from '~/src/server/types.js'
*/
Loading
Loading