Skip to content

Commit

Permalink
add transformer for transaction to calculate next due date as well as…
Browse files Browse the repository at this point in the history
… put transaction in a shape accepted by processRecurringPayment. Also modified processRecurringPayment to set publicId to be a hash of the recurring payment id
  • Loading branch information
jaucourt committed Nov 29, 2024
1 parent 766396e commit 6b0b1df
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
`;
Original file line number Diff line number Diff line change
@@ -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'),
Expand All @@ -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 = () => ({
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -123,7 +133,6 @@ describe('recurring payments service', () => {
cancelledReason: null,
endDate: new Date('2023-11-12'),
agreementId: '435678',
publicId: '1234456',
status: 0
}
},
Expand All @@ -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)])
})
)
})
})
})
Original file line number Diff line number Diff line change
@@ -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)
Expand Down

0 comments on commit 6b0b1df

Please sign in to comment.