diff --git a/jest.setup.cjs b/jest.setup.cjs index fad6f3680..2686c4c7f 100644 --- a/jest.setup.cjs +++ b/jest.setup.cjs @@ -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' diff --git a/src/server/plugins/engine/beta/form-context.test.ts b/src/server/plugins/engine/beta/form-context.test.ts index 609441d88..a3152b503 100644 --- a/src/server/plugins/engine/beta/form-context.test.ts +++ b/src/server/plugins/engine/beta/form-context.test.ts @@ -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(), diff --git a/src/server/plugins/engine/components/PaymentField.test.ts b/src/server/plugins/engine/components/PaymentField.test.ts index 14462def2..93df6cb3a 100644 --- a/src/server/plugins/engine/components/PaymentField.test.ts +++ b/src/server/plugins/engine/components/PaymentField.test.ts @@ -3,6 +3,7 @@ import { type FormMetadata, type PaymentFieldComponent } from '@defra/forms-model' +import { StatusCodes } from 'http-status-codes' import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js' @@ -14,18 +15,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 @@ -250,6 +259,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 () => { @@ -277,7 +287,8 @@ describe('PaymentField', () => { model: { formId: 'formid', basePath: 'base-path', - name: 'PaymentModel' + name: 'PaymentModel', + services: mockServices }, getState: jest .fn() @@ -287,7 +298,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: { @@ -342,7 +353,8 @@ describe('PaymentField', () => { model: { formId: 'formid', basePath: 'base-path', - name: 'PaymentModel' + name: 'PaymentModel', + services: mockServices }, getState: jest.fn().mockResolvedValueOnce({ $$__referenceNumber: 'pay-ref-123', @@ -361,7 +373,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) @@ -372,6 +384,128 @@ describe('PaymentField', () => { expect(mockRedirectCode).toHaveBeenCalledWith(303) expect(postJson).not.toHaveBeenCalled() }) + + it('should display error if create payment fails (e.g. network or bad api key) - test payment', async () => { + const mockYarSet = jest.fn() + const mockYarFlash = jest.fn() + const mockRequest = { + server: { + plugins: { + // eslint-disable-next-line no-useless-computed-key + ['forms-engine-plugin']: { + baseUrl: 'base-url' + } + } + }, + yar: { + set: mockYarSet, + flash: mockYarFlash + }, + url: { + href: '/here' + } + } as unknown as FormRequestPayload + const mockH = { + redirect: jest + .fn() + .mockReturnValueOnce({ code: jest.fn().mockReturnValueOnce('ok') }) + } as unknown as FormResponseToolkit + const args = { + controller: { + model: { + formId: 'formid', + basePath: 'base-path', + name: 'PaymentModel', + services: mockServices + }, + getState: jest + .fn() + .mockResolvedValueOnce({ $$__referenceNumber: 'pay-ref-123' }) + }, + component: paymentField, + sourceUrl: 'http://localhost:3009/test-payment', + isLive: false, + isPreview: true + } as unknown as PaymentExternalArgs + jest.mocked(postJson).mockImplementationOnce(() => { + // eslint-disable-next-line @typescript-eslint/only-throw-error + throw { output: { statusCode: StatusCodes.UNAUTHORIZED } } + }) + + const res = await PaymentField.dispatcher(mockRequest, mockH, args) + expect(res).toBe('ok') + expect(mockYarSet).not.toHaveBeenCalled() + expect(mockYarFlash).toHaveBeenCalledWith( + 'COMPONENT_STATE_ERROR', + { + href: '#myComponent', + name: 'myComponent', + text: 'Add a valid test API key before you can preview the payment journey.' + }, + true + ) + }) + + it('should display error if create payment fails (e.g. network or bad api key) - live payment', async () => { + const mockYarSet = jest.fn() + const mockYarFlash = jest.fn() + const mockRequest = { + server: { + plugins: { + // eslint-disable-next-line no-useless-computed-key + ['forms-engine-plugin']: { + baseUrl: 'base-url' + } + } + }, + yar: { + set: mockYarSet, + flash: mockYarFlash + }, + url: { + href: '/here' + } + } as unknown as FormRequestPayload + const mockH = { + redirect: jest + .fn() + .mockReturnValueOnce({ code: jest.fn().mockReturnValueOnce('ok') }) + } as unknown as FormResponseToolkit + const args = { + controller: { + model: { + formId: 'formid', + basePath: 'base-path', + name: 'PaymentModel', + services: mockServices + }, + getState: jest + .fn() + .mockResolvedValueOnce({ $$__referenceNumber: 'pay-ref-123' }) + }, + component: paymentField, + sourceUrl: 'http://localhost:3009/test-payment', + isLive: true, + isPreview: false + } as unknown as PaymentExternalArgs + jest.mocked(postJson).mockImplementationOnce(() => { + // eslint-disable-next-line @typescript-eslint/only-throw-error + throw { output: { statusCode: StatusCodes.UNAUTHORIZED } } + }) + + const res = await PaymentField.dispatcher(mockRequest, mockH, args) + expect(res).toBe('ok') + expect(mockYarSet).not.toHaveBeenCalled() + expect(mockYarFlash).toHaveBeenCalledWith( + 'COMPONENT_STATE_ERROR', + { + href: '#myComponent', + name: 'myComponent', + text: 'There is a problem and we cannot take a payment. Contact us (details in the footer of this form) or save your progress and return to the form later.' + }, + true + ) + }) }) describe('onSubmit', () => { diff --git a/src/server/plugins/engine/components/PaymentField.ts b/src/server/plugins/engine/components/PaymentField.ts index 4be399263..ddac28bab 100644 --- a/src/server/plugins/engine/components/PaymentField.ts +++ b/src/server/plugins/engine/components/PaymentField.ts @@ -7,16 +7,19 @@ import { import { StatusCodes } from 'http-status-codes' import joi, { type ObjectSchema } from 'joi' +import { COMPONENT_STATE_ERROR } from '~/src/server/constants.js' import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js' import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js' -import { getPluginOptions } from '~/src/server/plugins/engine/helpers.js' +import { + createError, + getPluginOptions +} from '~/src/server/plugins/engine/helpers.js' import { PaymentErrorTypes, PaymentPreAuthError, PaymentSubmissionError } from '~/src/server/plugins/engine/pageControllers/errors.js' import { - type AnyFormRequest, type FormContext, type FormRequestPayload, type FormResponseToolkit @@ -27,7 +30,8 @@ import { type FormState, type FormStateValue, type FormSubmissionError, - type FormSubmissionState + type FormSubmissionState, + type PaymentExternalArgs } from '~/src/server/plugins/engine/types.js' import { createPaymentService, @@ -186,7 +190,7 @@ export class PaymentField extends FormComponent { static async dispatcher( request: FormRequestPayload, h: FormResponseToolkit, - args: PaymentDispatcherArgs + args: PaymentExternalArgs ): Promise { const { options, name: componentName } = args.component const { model } = args.controller @@ -205,7 +209,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() @@ -229,6 +238,15 @@ export class PaymentField extends FormComponent { { formId, slug } ) + if (!payment) { + const message = isLivePayment + ? 'There is a problem and we cannot take a payment. Contact us (details in the footer of this form) or save your progress and return to the form later.' + : 'Add a valid test API key before you can preview the payment journey.' + const govukError = createError(componentName, message) + request.yar.flash(COMPONENT_STATE_ERROR, govukError, true) + return h.redirect(request.url.href).code(StatusCodes.SEE_OTHER) + } + const sessionData: PaymentSessionData = { uuid, formId, @@ -272,7 +290,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 @@ -343,21 +366,6 @@ export class PaymentField extends FormComponent { } } -export interface PaymentDispatcherArgs { - controller: { - model: { - formId: string - basePath: string - name: string - } - getState: (request: AnyFormRequest) => Promise - } - component: PaymentField - sourceUrl: string - isLive: boolean - isPreview: boolean -} - /** * Session data stored when dispatching to GOV.UK Pay */ diff --git a/src/server/plugins/engine/plugin.ts b/src/server/plugins/engine/plugin.ts index 1856a038e..2fec65366 100644 --- a/src/server/plugins/engine/plugin.ts +++ b/src/server/plugins/engine/plugin.ts @@ -41,7 +41,8 @@ export const plugin = { onRequest, ordnanceSurveyApiKey, baseUrl, - ordnanceSurveyApiSecret + ordnanceSurveyApiSecret, + services } = options const cacheService = @@ -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 diff --git a/src/server/plugins/engine/routes/payment-helper.js b/src/server/plugins/engine/routes/payment-helper.js index bfa0ab0d9..1bb1e07c3 100644 --- a/src/server/plugins/engine/routes/payment-helper.js +++ b/src/server/plugins/engine/routes/payment-helper.js @@ -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) @@ -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 @@ -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' */ diff --git a/src/server/plugins/engine/routes/payment-helper.test.js b/src/server/plugins/engine/routes/payment-helper.test.js index d96821539..38134ba01 100644 --- a/src/server/plugins/engine/routes/payment-helper.test.js +++ b/src/server/plugins/engine/routes/payment-helper.test.js @@ -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', diff --git a/src/server/plugins/engine/routes/payment.js b/src/server/plugins/engine/routes/payment.js index 50ba8becc..0d12969c5 100644 --- a/src/server/plugins/engine/routes/payment.js +++ b/src/server/plugins/engine/routes/payment.js @@ -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, @@ -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) ) /** @@ -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' */ diff --git a/src/server/plugins/engine/services/formsService.js b/src/server/plugins/engine/services/formsService.js index 79a538d2e..80de5b423 100644 --- a/src/server/plugins/engine/services/formsService.js +++ b/src/server/plugins/engine/services/formsService.js @@ -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} + */ +export function getFormSecret(_id, _secretName) { + throw error +} + /** * @import { FormStatus, FormDefinition, FormMetadata } from '@defra/forms-model' */ diff --git a/src/server/plugins/engine/services/formsService.test.js b/src/server/plugins/engine/services/formsService.test.js index e7ae06ccc..f9fc4cdec 100644 --- a/src/server/plugins/engine/services/formsService.test.js +++ b/src/server/plugins/engine/services/formsService.test.js @@ -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', () => { @@ -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() + }) }) diff --git a/src/server/plugins/engine/types.ts b/src/server/plugins/engine/types.ts index 901950e2e..cfcae855b 100644 --- a/src/server/plugins/engine/types.ts +++ b/src/server/plugins/engine/types.ts @@ -5,6 +5,7 @@ import { type Item, type List, type Page, + type PaymentFieldComponent, type UkAddressFieldComponent } from '@defra/forms-model' import { @@ -397,7 +398,7 @@ export interface ExternalArgs { component: ComponentDef controller: QuestionPageController sourceUrl: string - actionArgs: Record + actionArgs?: Record isLive: boolean isPreview: boolean } @@ -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 diff --git a/src/server/plugins/payment/helper.js b/src/server/plugins/payment/helper.js index b5d1bcdfd..3374228c7 100644 --- a/src/server/plugins/payment/helper.js +++ b/src/server/plugins/payment/helper.js @@ -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} */ -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) } @@ -61,3 +49,7 @@ export function formatCurrency(amount, locale = 'en-GB', currency = 'GBP') { return formatter.format(amount) } + +/** + * @import { FormsService } from '~/src/server/types.js' + */ diff --git a/src/server/plugins/payment/helper.test.js b/src/server/plugins/payment/helper.test.js index c48a850ef..498fba81c 100644 --- a/src/server/plugins/payment/helper.test.js +++ b/src/server/plugins/payment/helper.test.js @@ -1,39 +1,8 @@ -import { config } from '~/src/config/index.js' import { formatCurrency, - formatPaymentDate, - getPaymentApiKey + formatPaymentDate } from '~/src/server/plugins/payment/helper.js' -describe('getPaymentApiKey', () => { - config.set('paymentProviderApiKeyTest', 'TEST-API-KEY') - const formId = 'form-id' - process.env['PAYMENT_PROVIDER_API_KEY_LIVE_form-id'] = 'LIVE-API-KEY' - process.env['PAYMENT_PROVIDER_API_KEY_TEST_form-id'] = 'TEST-API-KEY' - - it('should read test key when non-live form', () => { - const apiKey = getPaymentApiKey(false, formId) - expect(apiKey).toBe('TEST-API-KEY') - }) - - it('should read live key when live form', () => { - const apiKey = getPaymentApiKey(true, formId) - expect(apiKey).toBe('LIVE-API-KEY') - }) - - it('should throw if TEST key is missing', () => { - expect(() => getPaymentApiKey(false, 'form-id-missing')).toThrow( - 'Missing payment api key for test form id form-id-missing' - ) - }) - - it('should throw if LIVE key is missing', () => { - expect(() => getPaymentApiKey(true, 'form-id-missing')).toThrow( - 'Missing payment api key for live form id form-id-missing' - ) - }) -}) - describe('formatPaymentDate', () => { it('should format ISO date string to en-GB format', () => { const result = formatPaymentDate('2025-11-10T17:01:29.000Z') diff --git a/src/server/plugins/payment/service.js b/src/server/plugins/payment/service.js index b643fe6a4..ed9bee809 100644 --- a/src/server/plugins/payment/service.js +++ b/src/server/plugins/payment/service.js @@ -40,7 +40,7 @@ export class PaymentService { * @param {string} returnUrl * @param {string} reference * @param {boolean} isLivePayment - * @param {{ formId: string, slug: string }} metadata + * @param {{ formId: string, slug: string } | undefined } metadata */ async createPayment( amount, @@ -50,30 +50,42 @@ export class PaymentService { isLivePayment, metadata ) { - const response = await this.postToPayProvider({ - amount, - description, - reference, - metadata, - return_url: returnUrl, - delayed_capture: true - }) - - logger.info( - buildPaymentInfo( - 'create-payment', - 'success', - `amount=${convertPenceToPounds(amount)}`, - isLivePayment, - response.payment_id - ), - `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}` - ) - - return { - paymentId: response.payment_id, - paymentUrl: response._links.next_url.href + try { + const response = await this.postToPayProvider({ + amount, + description, + reference, + metadata, + return_url: returnUrl, + delayed_capture: true + }) + + logger.info( + buildPaymentInfo( + 'create-payment', + 'success', + `amount=${convertPenceToPounds(amount)}`, + isLivePayment, + response.payment_id + ), + `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}` + ) + + return { + paymentId: response.payment_id, + paymentUrl: response._links.next_url.href + } + } catch (err) { + const error = + /** @type {{ output?: { payload?: any }, message?: any }} */ (err) + if (isLivePayment) { + logger.error( + error.output?.payload ?? error.message, + `[payment] Failed to create payment session for reference ${reference}` + ) + } } + return undefined } /** @@ -207,10 +219,12 @@ export class PaymentService { return response.payload } catch (err) { const error = /** @type {Error} */ (err) - logger.error( - error, - `[payment] Error creating payment for reference=${payload.reference}: ${error.message}` - ) + if (!error.message.includes('401 Unauthorized')) { + logger.error( + error, + `[payment] Error creating payment for reference=${payload.reference}: ${error.message}` + ) + } throw err } } diff --git a/src/server/plugins/payment/service.test.js b/src/server/plugins/payment/service.test.js index ccd4db80b..b5a389b9a 100644 --- a/src/server/plugins/payment/service.test.js +++ b/src/server/plugins/payment/service.test.js @@ -44,11 +44,11 @@ describe('payment service', () => { false, metadata ) - expect(payment.paymentId).toBe('payment-id-12345') - expect(payment.paymentUrl).toBe('http://next-url-href/payment') + expect(payment?.paymentId).toBe('payment-id-12345') + expect(payment?.paymentUrl).toBe('http://next-url-href/payment') }) - it('should throw if fails to create a payment - failed API call', async () => { + it('should return undefined if fails to create a payment - failed API call', async () => { jest .mocked(postJson) .mockRejectedValueOnce(new Error('internal creation error')) @@ -56,19 +56,18 @@ describe('payment service', () => { const referenceNumber = 'ABC-DEF-123' const returnUrl = 'http://localhost:3009/payment-callback-handler' const metadata = { formId: 'form-id', slug: 'my-form-slug' } - await expect(() => - service.createPayment( - 100, - 'Payment description', - returnUrl, - referenceNumber, - false, - metadata - ) - ).rejects.toThrow('internal creation error') + const res = await service.createPayment( + 100, + 'Payment description', + returnUrl, + referenceNumber, + false, + metadata + ) + expect(res).toBeUndefined() }) - it('should throw if fails to create a payment - bad result from API call', async () => { + it('should return undefined if fails to create a payment - bad result from API call', async () => { const createPaymentResult = { state: { status: 'failed' @@ -86,16 +85,15 @@ describe('payment service', () => { const referenceNumber = 'ABC-DEF-123' const returnUrl = 'http://localhost:3009/payment-callback-handler' const metadata = { formId: 'form-id', slug: 'my-form-slug' } - await expect(() => - service.createPayment( - 100, - 'Payment description', - returnUrl, - referenceNumber, - false, - metadata - ) - ).rejects.toThrow('Failed to create payment') + const res = await service.createPayment( + 100, + 'Payment description', + returnUrl, + referenceNumber, + false, + metadata + ) + expect(res).toBeUndefined() }) }) diff --git a/src/server/types.ts b/src/server/types.ts index ab90d4cce..c51ba9390 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -29,6 +29,7 @@ export interface FormsService { id: string, state: FormStatus ) => Promise + getFormSecret: (formId: string, secretName: string) => Promise } export interface FormSubmissionService { diff --git a/src/server/utils/file-form-service.js b/src/server/utils/file-form-service.js index 6e5401f9b..bb694f4eb 100644 --- a/src/server/utils/file-form-service.js +++ b/src/server/utils/file-form-service.js @@ -160,6 +160,17 @@ export class FileFormService { */ getFormDefinition: (id) => { return Promise.resolve(this.getFormDefinition(id)) + }, + + /** + * Get a form secret + * @param {string} _formId + * @param {string} _secretName + * @returns {Promise} + */ + getFormSecret: (_formId, _secretName) => { + // For local env only + return Promise.resolve(process.env.PAYMENT_PROVIDER_API_KEY_TEST ?? '') } } } diff --git a/src/server/utils/file-form-service.test.js b/src/server/utils/file-form-service.test.js index 266a00288..6b69ff470 100644 --- a/src/server/utils/file-form-service.test.js +++ b/src/server/utils/file-form-service.test.js @@ -101,6 +101,19 @@ describe('File-form-service', () => { ) expect(res3?.name).toBe('All components') expect(res3?.startPage).toBe('/all-components') + + const res4 = await interfaceImpl.getFormSecret( + '95e92559-968d-44ae-8666-2b1ad3dffd31', + 'my-secret-name' + ) + expect(res4).toBe('test-api-key') + + delete process.env.PAYMENT_PROVIDER_API_KEY_TEST + const res5 = await interfaceImpl.getFormSecret( + '95e92559-968d-44ae-8666-2b1ad3dffd31', + 'my-secret-name' + ) + expect(res5).toBe('') }) }) diff --git a/src/typings/hapi/index.d.ts b/src/typings/hapi/index.d.ts index 12da3dc11..fa7577016 100644 --- a/src/typings/hapi/index.d.ts +++ b/src/typings/hapi/index.d.ts @@ -39,6 +39,7 @@ declare module '@hapi/hapi' { ) => Record | Promise> saveAndExit?: PluginOptions['saveAndExit'] baseUrl: string + services: PluginOptions['services'] } }