From ec024a8043d7f5f26c4ad2f6499b8433f5fe98da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 11 Sep 2024 14:14:23 +0100 Subject: [PATCH] feat: Better handling of GovPay errors (#3654) --- api.planx.uk/modules/pay/controller.ts | 106 ++++++++++++++----------- api.planx.uk/modules/pay/helpers.ts | 11 +++ api.planx.uk/modules/pay/index.test.ts | 35 ++++++++ 3 files changed, 105 insertions(+), 47 deletions(-) diff --git a/api.planx.uk/modules/pay/controller.ts b/api.planx.uk/modules/pay/controller.ts index 57ae9a94b7..e8661aba8d 100644 --- a/api.planx.uk/modules/pay/controller.ts +++ b/api.planx.uk/modules/pay/controller.ts @@ -1,7 +1,7 @@ import assert from "assert"; import { Request } from "express"; import { responseInterceptor } from "http-proxy-middleware"; -import { logPaymentStatus } from "./helpers.js"; +import { handleGovPayErrors, logPaymentStatus } from "./helpers.js"; import { usePayProxy } from "./proxy.js"; import { $api } from "../../client/index.js"; import { ServerError } from "../../errors/index.js"; @@ -46,9 +46,12 @@ export const makePaymentViaProxy: PaymentProxyController = async ( pathRewrite: (path) => path.replace(/^\/pay.*$/, ""), selfHandleResponse: true, onProxyRes: responseInterceptor( - async (responseBuffer, _proxyRes, _req, _res) => { + async (responseBuffer, _proxyRes, _req, { statusCode }) => { const responseString = responseBuffer.toString("utf8"); const govUkResponse = JSON.parse(responseString); + + if (statusCode >= 400) return handleGovPayErrors(govUkResponse); + await logPaymentStatus({ sessionId, flowId, @@ -79,27 +82,32 @@ export const makeInviteToPayPaymentViaProxy: PaymentRequestProxyController = ( { pathRewrite: (path) => path.replace(/^\/pay.*$/, ""), selfHandleResponse: true, - onProxyRes: responseInterceptor(async (responseBuffer) => { - const responseString = responseBuffer.toString("utf8"); - const govUkResponse = JSON.parse(responseString); - await logPaymentStatus({ - sessionId, - flowId, - teamSlug, - govUkResponse, - }); - - try { - await addGovPayPaymentIdToPaymentRequest( - paymentRequestId, + onProxyRes: responseInterceptor( + async (responseBuffer, _proxyRes, _req, { statusCode }) => { + const responseString = responseBuffer.toString("utf8"); + const govUkResponse = JSON.parse(responseString); + + if (statusCode >= 400) return handleGovPayErrors(govUkResponse); + + await logPaymentStatus({ + sessionId, + flowId, + teamSlug, govUkResponse, - ); - } catch (error) { - throw Error(error as string); - } + }); - return responseBuffer; - }), + try { + await addGovPayPaymentIdToPaymentRequest( + paymentRequestId, + govUkResponse, + ); + } catch (error) { + throw Error(error as string); + } + + return responseBuffer; + }, + ), }, req, res, @@ -125,32 +133,36 @@ export function fetchPaymentViaProxyWithCallback( { pathRewrite: () => `/${req.params.paymentId}`, selfHandleResponse: true, - onProxyRes: responseInterceptor(async (responseBuffer) => { - const govUkResponse = JSON.parse(responseBuffer.toString("utf8")); - - await logPaymentStatus({ - sessionId, - flowId, - teamSlug, - govUkResponse, - }); - - try { - await callback(req, govUkResponse); - } catch (e) { - throw Error(e as string); - } - - // only return payment status, filter out PII - return JSON.stringify({ - payment_id: govUkResponse.payment_id, - amount: govUkResponse.amount, - state: govUkResponse.state, - _links: { - next_url: govUkResponse._links?.next_url, - }, - }); - }), + onProxyRes: responseInterceptor( + async (responseBuffer, _proxyRes, _req, { statusCode }) => { + const govUkResponse = JSON.parse(responseBuffer.toString("utf8")); + + if (statusCode >= 400) return handleGovPayErrors(govUkResponse); + + await logPaymentStatus({ + sessionId, + flowId, + teamSlug, + govUkResponse, + }); + + try { + await callback(req, govUkResponse); + } catch (e) { + throw Error(e as string); + } + + // only return payment status, filter out PII + return JSON.stringify({ + payment_id: govUkResponse.payment_id, + amount: govUkResponse.amount, + state: govUkResponse.state, + _links: { + next_url: govUkResponse._links?.next_url, + }, + }); + }, + ), }, req, res, diff --git a/api.planx.uk/modules/pay/helpers.ts b/api.planx.uk/modules/pay/helpers.ts index 76d8296c3c..f513339751 100644 --- a/api.planx.uk/modules/pay/helpers.ts +++ b/api.planx.uk/modules/pay/helpers.ts @@ -2,6 +2,17 @@ import { gql } from "graphql-request"; import airbrake from "../../airbrake.js"; import { $api } from "../../client/index.js"; +/** + * Gracefully handle GovPay errors + * Docs: https://docs.payments.service.gov.uk/api_reference/#responses + */ +export const handleGovPayErrors = (res: unknown) => + JSON.stringify({ + message: + "GovPay responded with an error when attempting to proxy to their API", + govPayResponse: res, + }); + export async function logPaymentStatus({ sessionId, flowId, diff --git a/api.planx.uk/modules/pay/index.test.ts b/api.planx.uk/modules/pay/index.test.ts index 38dff1e36c..b52f72d4fd 100644 --- a/api.planx.uk/modules/pay/index.test.ts +++ b/api.planx.uk/modules/pay/index.test.ts @@ -168,3 +168,38 @@ describe("fetching status of a GOV.UK payment", () => { }); }); }); + +test("handling GovPay error responses", async () => { + const govUKErrorResponse = { + code: "govUKErrorResponse", + description: + "Account is not fully configured. Please refer to documentation to setup your account or contact support with your error code - https://www.payments.service.gov.uk/support/ .", + }; + + nock("https://publicapi.payments.service.gov.uk/v1/payments") + .post("") + .reply(400, govUKErrorResponse); + + await supertest(app) + .post( + "/pay/southwark?flowId=7cd1c4b4-4229-424f-8d04-c9fdc958ef4e&sessionId=f2d8ca1d-a43b-43ec-b3d9-a9fec63ff19c", + ) + .send({ + amount: 100, + reference: "12343543", + description: "New application", + return_url: "https://editor.planx.uk", + metadata: { + source: "PlanX", + flow: "apply-for-a-lawful-development-certificate", + inviteToPay: false, + }, + }) + .expect(400) + .then((res) => { + expect(res.body.message).toMatch( + /GovPay responded with an error when attempting to proxy to their API/, + ); + expect(res.body.govPayResponse).toEqual(govUKErrorResponse); + }); +});