@defra-fish/sales-api-service
Version:
Rod Licensing Sales API
343 lines (312 loc) • 11.2 kB
JavaScript
import { findDueRecurringPayments, Permission } from '@defra-fish/dynamics-lib'
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'),
executeQuery: jest.fn(),
findById: jest.fn(),
findDueRecurringPayments: jest.fn()
}))
jest.mock('node:crypto', () => ({
createHash: jest.fn(() => ({
update: () => {},
digest: () => 'abcdef99987'
}))
}))
const dynamicsLib = jest.requireMock('@defra-fish/dynamics-lib')
const getMockRecurringPayment = () => ({
name: 'Test Name',
nextDueDate: '2019-12-14T00:00:00Z',
cancelledDate: null,
cancelledReason: null,
endDate: '2019-12-15T00:00:00Z',
agreementId: 'c9267c6e-573d-488b-99ab-ea18431fc472',
publicId: '649-213',
status: 1,
expanded: {
contact: {
entity: getMockContact()
},
activePermission: {
entity: getMockPermission()
}
}
})
const getMockRPContactPermission = (contact, permission) => ({
name: 'Test Name',
nextDueDate: '2019-12-14T00:00:00Z',
cancelledDate: null,
cancelledReason: null,
endDate: '2019-12-15T00:00:00Z',
agreementId: 'c9267c6e-573d-488b-99ab-ea18431fc472',
publicId: '649-213',
status: 1,
expanded: {
contact,
activePermission: permission
}
})
const getMockContact = () => ({
firstName: 'Fester',
lastName: 'Tester',
birthDate: '2000-01-01',
email: 'person@example.com',
mobilePhone: '+44 7700 900088',
premises: 'Example House',
street: 'Example Street',
locality: 'Near Sample',
town: 'Exampleton',
postcode: 'AB12 3CD',
country: 'GB-ENG',
preferredMethodOfConfirmation: 'Text',
preferredMethodOfNewsletter: 'Email',
preferredMethodOfReminder: 'Letter',
postalFulfilment: true
})
const getMockPermission = () => ({
referenceNumber: '12345678',
licenceLength: '12M',
isLicenceForYou: true,
licensee: {
firstName: 'Fester',
lastName: 'Tester',
premises: '14 Howecroft Court',
street: 'Eastmead Lane',
town: 'Bristol',
postcode: 'BS9 1HJ',
email: 'fester@tester.com',
mobilePhone: '01234567890',
birthDate: '1996-01-01',
postalFulfilment: true
}
})
describe('recurring payments service', () => {
const createSimpleSampleTransactionRecord = () => ({ payment: { recurring: true }, permissions: [{}] })
const createSamplePermission = overrides => {
const p = new Permission()
p.referenceNumber = 'ABC123'
p.issueDate = '2024-12-04T11:15:12Z'
p.startDate = '2024-12-04T11:45:12Z'
p.endDate = '2025-12-03T23:59:59.999Z'
p.stagingId = 'aaa-111-bbb-222'
p.isRenewal = false
p.isLicenseForYou = 1
for (const key in overrides) {
p[key] = overrides[key]
}
return p
}
beforeEach(jest.clearAllMocks)
describe('getRecurringPayments', () => {
it('should equal result of findDueRecurringPayments query', async () => {
const mockRecurringPayments = [getMockRecurringPayment()]
const mockContact = mockRecurringPayments[0].expanded.contact
const mockPermission = mockRecurringPayments[0].expanded.activePermission
dynamicsLib.executeQuery.mockResolvedValueOnce(mockRecurringPayments)
const result = await getRecurringPayments(new Date())
expect(result).toEqual([getMockRPContactPermission(mockContact, mockPermission)])
})
it('executeQuery is called with findDueRecurringPayments with a date', async () => {
const mockRecurringPayments = [getMockRecurringPayment()]
const mockDate = new Date()
dynamicsLib.executeQuery.mockResolvedValueOnce(mockRecurringPayments)
await getRecurringPayments(mockDate)
expect(dynamicsLib.executeQuery).toHaveBeenCalledWith(findDueRecurringPayments(mockDate))
})
})
describe('processRecurringPayment', () => {
it('should return null when transactionRecord.payment.recurring is not present', async () => {
const transactionRecord = { payment: null }
const result = await processRecurringPayment(transactionRecord, getMockContact())
expect(result.recurringPayment).toBeNull()
})
it('should return a valid recurringPayment when transactionRecord.payment.recurring is present', async () => {
const transactionRecord = {
payment: {
recurring: {
name: 'Test Name',
nextDueDate: new Date('2023-11-02'),
cancelledDate: null,
cancelledReason: null,
endDate: new Date('2023-11-12'),
agreementId: '435678',
status: 0
}
},
permissions: [getMockPermission()]
}
const contact = getMockContact()
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(createSimpleSampleTransactionRecord(), 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(createSimpleSampleTransactionRecord(), getMockContact())
expect(update).toHaveBeenCalledWith(recurringPayment.uniqueContentId)
})
it('hashes using sha256', async () => {
await processRecurringPayment(createSimpleSampleTransactionRecord(), getMockContact())
expect(createHash).toHaveBeenCalledWith('sha256')
})
it('uses base64 hash string', async () => {
const digest = jest.fn()
createHash.mockReturnValueOnce({
update: () => {},
digest
})
await processRecurringPayment(createSimpleSampleTransactionRecord(), getMockContact())
expect(digest).toHaveBeenCalledWith('base64')
})
})
describe('generateRecurringPaymentRecord', () => {
const createFinalisedSampleTransaction = (agreementId, permission) => ({
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' }
})
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-11-22T00:00:00.000Z',
issueDate: '2024-11-12T15:00:45.922Z',
endDate: '2025-11-21T23:59:59.999Z'
},
'2025-11-12T00:00:00.000Z'
],
[
'starts twenty days after issue - next due on issue date plus one year',
'9o8u7yhui89u8i9oiu8i8u7yhu',
{
startDate: '2024-12-01T00:00:00.000Z',
issueDate: '2024-11-12T15:00:45.922Z',
endDate: '2025-01-30T23:59:59.999Z'
},
'2025-11-12T00:00:00.000Z'
],
[
"issued on 29th Feb '24, starts on 30th March '24 - next due on 28th Feb '25",
'hy7u8ijhyu78jhyu8iu8hjiujn',
{
startDate: '2024-03-30T00:00:00.000Z',
issueDate: '2024-02-29T12:38:24.123Z',
endDate: '2025-03-29T23:59:59.999Z'
},
'2025-02-28T00:00:00.000Z'
],
[
"issued on 30th March '25 at 1am, starts at 1:30am - next due on 20th March '26",
'jhy67uijhy67u87yhtgjui8u7j',
{
startDate: '2025-03-30T01:30:00.000Z',
issueDate: '2025-03-30T01:00:00.000Z',
endDate: '2026-03-29T23:59:59.999Z'
},
'2026-03-20T00:00:00.000Z'
]
])('creates record from transaction with %s', (_d, agreementId, permissionData, expectedNextDueDate) => {
const sampleTransaction = createFinalisedSampleTransaction(agreementId, permissionData)
const permission = createSamplePermission(permissionData)
const rpRecord = generateRecurringPaymentRecord(sampleTransaction, permission)
expect(rpRecord).toEqual(
expect.objectContaining({
payment: expect.objectContaining({
recurring: expect.objectContaining({
name: '',
nextDueDate: expectedNextDueDate,
cancelledDate: null,
cancelledReason: null,
endDate: permissionData.endDate,
agreementId,
status: 1
})
}),
permissions: expect.arrayContaining([permission])
})
)
})
it.each([
[
'start date is thirty one days after issue date',
{
startDate: '2024-12-14T00:00:00.000Z',
issueDate: '2024-11-12T15:00:45.922Z',
endDate: '2025-12-13T23:59:59.999Z'
}
],
[
'start date precedes issue date',
{
startDate: '2024-11-11T00:00:00.000Z',
issueDate: '2024-11-12T15:00:45.922Z',
endDate: '2025-11-10T23:59:59.999Z'
}
]
])('throws an error for invalid dates when %s', (_d, permission) => {
const sampleTransaction = createFinalisedSampleTransaction('hyu78ijhyu78ijuhyu78ij9iu6', permission)
expect(() => generateRecurringPaymentRecord(sampleTransaction)).toThrow('Invalid dates provided for permission')
})
it('returns a false flag when agreementId is not present', () => {
const sampleTransaction = createFinalisedSampleTransaction(null, {
startDate: '2024-11-22T15:30:45.922Z',
issueDate: '2024-11-22T15:00:45.922Z',
endDate: '2025-11-21T23:59:59.999Z'
})
const rpRecord = generateRecurringPaymentRecord(sampleTransaction)
expect(rpRecord.payment.recurring).toBeFalsy()
})
})
})