From 6b0b1dfef83a33d8d401fed11576210c9c6529bc Mon Sep 17 00:00:00 2001 From: Phil Benson Date: Fri, 29 Nov 2024 18:47:57 +0000 Subject: [PATCH] add transformer for transaction to calculate next due date as well as put transaction in a shape accepted by processRecurringPayment. Also modified processRecurringPayment to set publicId to be a hash of the recurring payment id --- .../recurring-payments.service.spec.js.snap | 2 +- .../recurring-payments.service.spec.js | 127 +++++++++++++++++- .../services/recurring-payments.service.js | 36 ++++- 3 files changed, 161 insertions(+), 4 deletions(-) diff --git a/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap b/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap index e67aacad92..7c91321273 100644 --- a/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap +++ b/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap @@ -8,7 +8,7 @@ Object { "endDate": 2023-11-12T00:00:00.000Z, "name": "Test Name", "nextDueDate": 2023-11-02T00:00:00.000Z, - "publicId": "1234456", + "publicId": "abcdef99987", "status": 0, } `; diff --git a/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js b/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js index 6631d0c5e9..0e1a8fa83a 100644 --- a/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js +++ b/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js @@ -1,5 +1,6 @@ import { findDueRecurringPayments } from '@defra-fish/dynamics-lib' -import { getRecurringPayments, processRecurringPayment } from '../recurring-payments.service.js' +import { getRecurringPayments, processRecurringPayment, generateRecurringPaymentRecord } from '../recurring-payments.service.js' +import { createHash } from 'node:crypto' jest.mock('@defra-fish/dynamics-lib', () => ({ ...jest.requireActual('@defra-fish/dynamics-lib'), @@ -8,6 +9,13 @@ jest.mock('@defra-fish/dynamics-lib', () => ({ findDueRecurringPayments: jest.fn() })) +jest.mock('node:crypto', () => ({ + createHash: jest.fn(() => ({ + update: () => {}, + digest: () => 'abcdef99987' + })) +})) + const dynamicsLib = jest.requireMock('@defra-fish/dynamics-lib') const getMockRecurringPayment = () => ({ @@ -81,6 +89,8 @@ const getMockPermission = () => ({ }) describe('recurring payments service', () => { + const createSampleTransactionRecord = () => ({ payment: { recurring: true }, permissions: [{}] }) + beforeEach(jest.clearAllMocks) describe('getRecurringPayments', () => { it('should equal result of findDueRecurringPayments query', async () => { @@ -123,7 +133,6 @@ describe('recurring payments service', () => { cancelledReason: null, endDate: new Date('2023-11-12'), agreementId: '435678', - publicId: '1234456', status: 0 } }, @@ -133,5 +142,119 @@ describe('recurring payments service', () => { const result = await processRecurringPayment(transactionRecord, contact) expect(result.recurringPayment).toMatchSnapshot() }) + + it.each(['abc-123', 'def-987'])('generates a publicId %s for the recurring payment', async samplePublicId => { + createHash.mockReturnValue({ + update: () => {}, + digest: () => samplePublicId + }) + const result = await processRecurringPayment(createSampleTransactionRecord(), getMockContact()) + expect(result.recurringPayment.publicId).toBe(samplePublicId) + }) + + it('passes the unique id of the entity to the hash.update function', async () => { + const update = jest.fn() + createHash.mockReturnValueOnce({ + update, + digest: () => {} + }) + const { recurringPayment } = await processRecurringPayment(createSampleTransactionRecord(), getMockContact()) + expect(update).toHaveBeenCalledWith(recurringPayment.uniqueContentId) + }) + + it('hashes using sha256', async () => { + await processRecurringPayment(createSampleTransactionRecord(), getMockContact()) + expect(createHash).toHaveBeenCalledWith('sha256') + }) + + it('uses base64 hash string', async () => { + const digest = jest.fn() + createHash.mockReturnValueOnce({ + update: () => {}, + digest + }) + await processRecurringPayment(createSampleTransactionRecord(), getMockContact()) + expect(digest).toHaveBeenCalledWith('base64') + }) + }) + + describe('generateRecurringPaymentRecord', () => { + it.each([ + [ + 'same day start - next due on issue date plus one year minus ten days', + 'iujhy7u8ijhy7u8iuuiuu8ie89', + { + startDate: '2024-11-22T15:30:45.922Z', + issueDate: '2024-11-22T15:00:45.922Z', + endDate: '2025-11-21T23:59:59.999Z' + }, + '2025-11-12T00:00:00.000Z' + ], + [ + 'next day start - next due on end date minus ten days', + '89iujhy7u8i87yu9iokjuij901', + { + startDate: '2024-11-23T00:00:00.000Z', + issueDate: '2024-11-22T15:00:45.922Z', + endDate: '2025-11-22T23:59:59.999Z' + }, + '2025-11-12T00:00:00.000Z' + ], + [ + 'starts ten days after issue - next due on issue date plus one year', + '9o8u7yhui89u8i9oiu8i8u7yhu', + { + startDate: '2024-12-23T00:00:00.000Z', + issueDate: '2024-11-12T15:00:45.922Z', + endDate: '2025-12-22T23:59:59.999Z' + }, + '2025-11-12T00:00:00.000Z' + ] + ])('creates record from transaction with %s', (_d, agreementId, permission, expectedNextDueDate) => { + const sampleTransaction = { + expires: 1732892402, + cost: 35.8, + isRecurringPaymentSupported: true, + permissions: [ + { + permitId: 'permit-id-1', + licensee: {}, + referenceNumber: '23211125-2WC3FBP-ABNDT8', + isLicenceForYou: true, + ...permission + } + ], + agreementId, + payment: { + amount: 35.8, + source: 'Gov Pay', + method: 'Debit card', + timestamp: '2024-11-22T15:00:45.922Z' + }, + id: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757', + dataSource: 'Web Sales', + transactionId: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757', + status: { id: 'FINALISED' } + } + + const rpRecord = generateRecurringPaymentRecord(sampleTransaction) + + expect(rpRecord).toEqual( + expect.objectContaining({ + payment: expect.objectContaining({ + recurring: expect.objectContaining({ + name: '', + nextDueDate: expectedNextDueDate, + cancelledDate: null, + cancelledReason: null, + endDate: permission.endDate, + agreementId, + status: 1 + }) + }), + permissions: expect.arrayContaining([expect.objectContaining(permission)]) + }) + ) + }) }) }) diff --git a/packages/sales-api-service/src/services/recurring-payments.service.js b/packages/sales-api-service/src/services/recurring-payments.service.js index aee997d2a7..49f025deac 100644 --- a/packages/sales-api-service/src/services/recurring-payments.service.js +++ b/packages/sales-api-service/src/services/recurring-payments.service.js @@ -1,22 +1,56 @@ import { executeQuery, findDueRecurringPayments, RecurringPayment } from '@defra-fish/dynamics-lib' +import { createHash } from 'node:crypto' +import moment from 'moment' export const getRecurringPayments = date => executeQuery(findDueRecurringPayments(date)) +const getNextDueDate = (startDate, issueDate, endDate) => { + if (moment(startDate).isSame(moment(issueDate), 'day')) { + return moment(startDate).add(1, 'year').subtract(10, 'days').startOf('day').toISOString() + } + if (moment(startDate).isBefore(moment(issueDate).add(10, 'days'), 'day')) { + return moment(endDate).subtract(10, 'days').startOf('day').toISOString() + } + if (moment(startDate).isSameOrAfter(moment(issueDate).add(10, 'days'), 'day')) { + return moment(issueDate).add(1, 'year').startOf('day').toISOString() + } +} + +export const generateRecurringPaymentRecord = transactionRecord => { + const [{ startDate, issueDate, endDate }] = transactionRecord.permissions + return { + payment: { + recurring: { + name: '', + nextDueDate: getNextDueDate(startDate, issueDate, endDate), + cancelledDate: null, + cancelledReason: null, + endDate, + agreementId: transactionRecord.agreementId, + status: 1 + } + }, + permissions: transactionRecord.permissions + } +} + /** * Process a recurring payment instruction * @param transactionRecord * @returns {Promise<{recurringPayment: RecurringPayment | null}>} */ export const processRecurringPayment = async (transactionRecord, contact) => { + const hash = createHash('sha256') if (transactionRecord.payment?.recurring) { const recurringPayment = new RecurringPayment() + hash.update(recurringPayment.uniqueContentId) recurringPayment.name = transactionRecord.payment.recurring.name recurringPayment.nextDueDate = transactionRecord.payment.recurring.nextDueDate recurringPayment.cancelledDate = transactionRecord.payment.recurring.cancelledDate recurringPayment.cancelledReason = transactionRecord.payment.recurring.cancelledReason recurringPayment.endDate = transactionRecord.payment.recurring.endDate recurringPayment.agreementId = transactionRecord.payment.recurring.agreementId - recurringPayment.publicId = transactionRecord.payment.recurring.publicId + recurringPayment.publicId = hash.digest('base64') recurringPayment.status = transactionRecord.payment.recurring.status const [permission] = transactionRecord.permissions recurringPayment.bindToEntity(RecurringPayment.definition.relationships.activePermission, permission)