From 1dd80e103e788f71b07260dde19bd951e8897f52 Mon Sep 17 00:00:00 2001 From: lailien3 <138867360+lailien3@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:53:04 +0100 Subject: [PATCH 1/3] RP job sends payment requests to GOV.UK Pay https://eaflood.atlassian.net/browse/IWTF-3657 We want the Recurring Payments Job to take payment for the recurring payments that have been retrieved, so that anglers permissions are automatically renewed. Requests should be sent to GOV.UK Pay for payment to be taken for all recurring payments --- .../connectors-lib/src/sales-api-connector.js | 10 ++ .../recurring-payments-processor.spec.js | 21 +++- .../src/recurring-payments-processor.js | 14 ++- .../__tests__/recurring-payments.spec.js | 95 +++++++++++++++++-- .../src/server/routes/recurring-payments.js | 27 +++++- 5 files changed, 153 insertions(+), 14 deletions(-) diff --git a/packages/connectors-lib/src/sales-api-connector.js b/packages/connectors-lib/src/sales-api-connector.js index 23fd7b719d..23a4d4524c 100644 --- a/packages/connectors-lib/src/sales-api-connector.js +++ b/packages/connectors-lib/src/sales-api-connector.js @@ -295,3 +295,13 @@ export const getDueRecurringPayments = async date => exec2xxOrThrow(call(new URL */ export const preparePermissionDataForRenewal = async referenceNumber => exec2xxOrThrow(call(new URL(`/permissionRenewalData/${referenceNumber}`, urlBase), 'get')) + +/** + * Send payment details for processing + * + * @param transaction + * @returns {Promise<*>} + * @throws on a non-2xx response + */ +export const sendPayment = async transaction => + exec2xxOrThrow(call(new URL('/sendPayment', urlBase), 'post', { body: JSON.stringify(transaction) })) diff --git a/packages/recurring-payments-job/src/__tests__/recurring-payments-processor.spec.js b/packages/recurring-payments-job/src/__tests__/recurring-payments-processor.spec.js index 4f36a78b5a..c47ff9d081 100644 --- a/packages/recurring-payments-job/src/__tests__/recurring-payments-processor.spec.js +++ b/packages/recurring-payments-job/src/__tests__/recurring-payments-processor.spec.js @@ -1,5 +1,5 @@ import { salesApi } from '@defra-fish/connectors-lib' -import { processRecurringPayments } from '../recurring-payments-processor.js' +import { processRecurringPayments, processPayment } from '../recurring-payments-processor.js' jest.mock('@defra-fish/business-rules-lib') jest.mock('@defra-fish/connectors-lib', () => ({ @@ -8,7 +8,9 @@ jest.mock('@defra-fish/connectors-lib', () => ({ preparePermissionDataForRenewal: jest.fn(() => ({ licensee: { countryCode: 'GB-ENG' } })), - createTransaction: jest.fn() + createTransaction: jest.fn(), + sendPayment: jest.fn(), + processPayment: jest.fn() } })) @@ -241,5 +243,20 @@ describe('recurring-payments-processor', () => { expect(salesApi.createTransaction.mock.calls).toEqual(expectedData) }) + + it('logs an error and throws it when sendPayment fails', async () => { + const transaction = { id: 'transaction-id' } + const error = new Error('Payment failed') + + salesApi.sendPayment.mockImplementationOnce(() => { + throw error + }) + + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn()) + + await expect(processPayment(transaction)).rejects.toThrow(error) + + expect(consoleLogSpy).toHaveBeenCalledWith('Error sending payment', JSON.stringify(transaction)) + }) }) }) diff --git a/packages/recurring-payments-job/src/recurring-payments-processor.js b/packages/recurring-payments-job/src/recurring-payments-processor.js index f4003ee135..cec72cf290 100644 --- a/packages/recurring-payments-job/src/recurring-payments-processor.js +++ b/packages/recurring-payments-job/src/recurring-payments-processor.js @@ -19,7 +19,8 @@ const processRecurringPayment = async record => { const transactionData = await processPermissionData(referenceNumber) console.log('Creating new transaction based on', referenceNumber) try { - const response = await salesApi.createTransaction(transactionData) + const transaction = await salesApi.createTransaction(transactionData) + const response = await processPayment(transaction) console.log('New transaction created:', response) } catch (e) { console.log('Error creating transaction', JSON.stringify(transactionData)) @@ -56,3 +57,14 @@ const prepareStartDate = permission => { .utc() .toISOString() } + +export const processPayment = async transaction => { + try { + const response = await salesApi.sendPayment(transaction) + console.log('Payment sent successfully:', response) + return response + } catch (e) { + console.log('Error sending payment', JSON.stringify(transaction)) + throw e + } +} \ No newline at end of file diff --git a/packages/sales-api-service/src/server/routes/__tests__/recurring-payments.spec.js b/packages/sales-api-service/src/server/routes/__tests__/recurring-payments.spec.js index 302f47ac7a..84ddf89d19 100644 --- a/packages/sales-api-service/src/server/routes/__tests__/recurring-payments.spec.js +++ b/packages/sales-api-service/src/server/routes/__tests__/recurring-payments.spec.js @@ -1,31 +1,106 @@ import dueRecurringPayments from '../recurring-payments.js' -import { getRecurringPayments } from '../../../services/recurring-payments.service.js' +import { getRecurringPayments, processRecurringPayment } from '../../../services/recurring-payments.service.js' +import { preparePayment } from '../../../../../gafl-webapp-service/src/processors/payment.js' +import { sendPayment } from '../../../../../gafl-webapp-service/src/services/payment/govuk-pay-service.js' jest.mock('../../../services/recurring-payments.service.js', () => ({ - getRecurringPayments: jest.fn() + getRecurringPayments: jest.fn(), + processRecurringPayment: jest.fn() })) -const getMockRequest = ({ date = '2023-10-19' }) => ({ +jest.mock('../../../../../gafl-webapp-service/src/processors/payment.js', () => ({ + preparePayment: jest.fn() +})) + +jest.mock('../../../../../gafl-webapp-service/src/services/payment/govuk-pay-service.js', () => ({ + sendPayment: jest.fn() +})) + +const getMockRequest = ({ transactionRecord = {}, contact = {}, date = '2023-10-19' } = {}) => ({ + payload: { transactionRecord, contact }, params: { date } }) -const getMockResponseToolkit = () => ({ - response: jest.fn() -}) +const getMockResponseToolkit = () => { + const code = jest.fn() + const response = jest.fn().mockReturnValue({ code }) + return { response } +} describe('recurring payments', () => { beforeEach(jest.clearAllMocks) - it('handler should return continue response', async () => { - const request = getMockRequest({}) + it('handler should return the response from getRecurringPayments', async () => { + const date = '2023-10-19' + const mockResponseData = { some: 'data' } + + getRecurringPayments.mockResolvedValue(mockResponseData) + + const request = getMockRequest({ date }) const responseToolkit = getMockResponseToolkit() - expect(await dueRecurringPayments[0].handler(request, responseToolkit)).toEqual(responseToolkit.continue) + + await dueRecurringPayments[0].handler(request, responseToolkit) + + expect(responseToolkit.response).toHaveBeenCalledWith(mockResponseData) }) it('should call getRecurringPayments with date', async () => { const date = Symbol('date') const request = getMockRequest({ date }) - await dueRecurringPayments[0].handler(request, getMockResponseToolkit()) + const responseToolkit = getMockResponseToolkit() + + await dueRecurringPayments[0].handler(request, responseToolkit) + expect(getRecurringPayments).toHaveBeenCalledWith(date) }) }) + +describe('POST /processRecurringPayment', () => { + it('should return 404 if no recurringPayment is found', async () => { + processRecurringPayment.mockResolvedValue({ recurringPayment: null }) + + const request = getMockRequest() + const responseToolkit = getMockResponseToolkit() + + await dueRecurringPayments[1].handler(request, responseToolkit) + + expect(processRecurringPayment).toHaveBeenCalledWith(request.payload.transactionRecord, request.payload.contact) + + expect(responseToolkit.response).toHaveBeenCalledWith({ error: 'No recurring payment found' }) + expect(responseToolkit.response().code).toHaveBeenCalledWith(404) + }) + + it('should process payment and return response', async () => { + const recurringPayment = { id: 'test-recurring-payment' } + const preparedPayment = { id: 'test-prepared-payment' } + const paymentResponse = { id: 'test-payment-response' } + + processRecurringPayment.mockResolvedValue({ recurringPayment }) + preparePayment.mockReturnValue(preparedPayment) + sendPayment.mockResolvedValue(paymentResponse) + + const request = getMockRequest() + const responseToolkit = getMockResponseToolkit() + + await dueRecurringPayments[1].handler(request, responseToolkit) + + expect(processRecurringPayment).toHaveBeenCalledWith(request.payload.transactionRecord, request.payload.contact) + expect(preparePayment).toHaveBeenCalledWith(request, recurringPayment) + expect(sendPayment).toHaveBeenCalledWith(preparedPayment) + + expect(responseToolkit.response).toHaveBeenCalledWith(paymentResponse) + }) + + it('should return 500 if an error occurs', async () => { + const error = new Error('Test error') + processRecurringPayment.mockRejectedValue(error) + + const request = getMockRequest() + const responseToolkit = getMockResponseToolkit() + + await dueRecurringPayments[1].handler(request, responseToolkit) + + expect(responseToolkit.response).toHaveBeenCalledWith({ error: 'Failed to process recurring payment' }) + expect(responseToolkit.response().code).toHaveBeenCalledWith(500) + }) +}) diff --git a/packages/sales-api-service/src/server/routes/recurring-payments.js b/packages/sales-api-service/src/server/routes/recurring-payments.js index fb75358977..ef9f2df437 100644 --- a/packages/sales-api-service/src/server/routes/recurring-payments.js +++ b/packages/sales-api-service/src/server/routes/recurring-payments.js @@ -1,4 +1,7 @@ -import { getRecurringPayments } from '../../services/recurring-payments.service.js' +import { getRecurringPayments, processRecurringPayment } from '../../services/recurring-payments.service.js' +import { preparePayment } from '../../../../gafl-webapp-service/src/processors/payment.js' +import { sendPayment } from '../../../../gafl-webapp-service/src/services/payment/govuk-pay-service.js' + export default [ { method: 'GET', @@ -8,5 +11,27 @@ export default [ const result = await getRecurringPayments(date) return h.response(result) } + }, + { + method: 'POST', + path: '/processRecurringPayment', + handler: async (request, h) => { + try { + const { transactionRecord, contact } = request.payload + const { recurringPayment } = await processRecurringPayment(transactionRecord, contact) + + if (!recurringPayment) { + return h.response({ error: 'No recurring payment found' }).code(404) + } + + const preparedPayment = preparePayment(request, recurringPayment) + const paymentResponse = await sendPayment(preparedPayment) + + return h.response(paymentResponse) + } catch (error) { + console.error('Error processing recurring payment:', error) + return h.response({ error: 'Failed to process recurring payment' }).code(500) + } + } } ] From 913220046a6b2f5993bbe3260c75cc1577a27035 Mon Sep 17 00:00:00 2001 From: lailien3 <138867360+lailien3@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:55:24 +0100 Subject: [PATCH 2/3] Trailing space --- .../recurring-payments-job/src/recurring-payments-processor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/recurring-payments-job/src/recurring-payments-processor.js b/packages/recurring-payments-job/src/recurring-payments-processor.js index cec72cf290..d144afec91 100644 --- a/packages/recurring-payments-job/src/recurring-payments-processor.js +++ b/packages/recurring-payments-job/src/recurring-payments-processor.js @@ -67,4 +67,4 @@ export const processPayment = async transaction => { console.log('Error sending payment', JSON.stringify(transaction)) throw e } -} \ No newline at end of file +} From 24f9aeaaa6ff739033ccb8ecc2e43bbe9b0cec44 Mon Sep 17 00:00:00 2001 From: laila aleissa <138867360+lailien3@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:14:46 +0100 Subject: [PATCH 3/3] Remove magic number issue --- .../src/server/routes/recurring-payments.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sales-api-service/src/server/routes/recurring-payments.js b/packages/sales-api-service/src/server/routes/recurring-payments.js index ef9f2df437..c8c84920f9 100644 --- a/packages/sales-api-service/src/server/routes/recurring-payments.js +++ b/packages/sales-api-service/src/server/routes/recurring-payments.js @@ -2,6 +2,9 @@ import { getRecurringPayments, processRecurringPayment } from '../../services/re import { preparePayment } from '../../../../gafl-webapp-service/src/processors/payment.js' import { sendPayment } from '../../../../gafl-webapp-service/src/services/payment/govuk-pay-service.js' +const HTTP_STATUS_NOT_FOUND = 404 +const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500 + export default [ { method: 'GET', @@ -21,7 +24,7 @@ export default [ const { recurringPayment } = await processRecurringPayment(transactionRecord, contact) if (!recurringPayment) { - return h.response({ error: 'No recurring payment found' }).code(404) + return h.response({ error: 'No recurring payment found' }).code(HTTP_STATUS_NOT_FOUND) } const preparedPayment = preparePayment(request, recurringPayment) @@ -30,7 +33,7 @@ export default [ return h.response(paymentResponse) } catch (error) { console.error('Error processing recurring payment:', error) - return h.response({ error: 'Failed to process recurring payment' }).code(500) + return h.response({ error: 'Failed to process recurring payment' }).code(HTTP_STATUS_INTERNAL_SERVER_ERROR) } } }