@defra-fish/sales-api-service 1.62.0-rc.0 → 1.62.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/schema/__tests__/recurring-payments.schema.spec.js +18 -1
- package/src/schema/recurring-payments.schema.js +4 -0
- package/src/server/routes/__tests__/recurring-payments.spec.js +38 -6
- package/src/server/routes/recurring-payments.js +27 -2
- package/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap +4 -3
- package/src/services/__tests__/recurring-payments.service.spec.js +180 -30
- package/src/services/recurring-payments.service.js +41 -4
- package/src/services/transactions/process-transaction-queue.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/sales-api-service",
|
|
3
|
-
"version": "1.62.0-rc.
|
|
3
|
+
"version": "1.62.0-rc.10",
|
|
4
4
|
"description": "Rod Licensing Sales API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defra-fish/business-rules-lib": "1.62.0-rc.
|
|
39
|
-
"@defra-fish/connectors-lib": "1.62.0-rc.
|
|
40
|
-
"@defra-fish/dynamics-lib": "1.62.0-rc.
|
|
38
|
+
"@defra-fish/business-rules-lib": "1.62.0-rc.10",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.62.0-rc.10",
|
|
40
|
+
"@defra-fish/dynamics-lib": "1.62.0-rc.10",
|
|
41
41
|
"@hapi/boom": "^9.1.2",
|
|
42
42
|
"@hapi/hapi": "^20.1.3",
|
|
43
43
|
"@hapi/inert": "^6.0.3",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"moment-timezone": "^0.5.34",
|
|
53
53
|
"uuid": "^8.3.2"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "7be5ffd5ab38a7b7690cb2b7f7c711c5d8473860"
|
|
56
56
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dueRecurringPaymentsRequestParamsSchema,
|
|
3
3
|
dueRecurringPaymentsResponseSchema,
|
|
4
|
-
processRPResultRequestParamsSchema
|
|
4
|
+
processRPResultRequestParamsSchema,
|
|
5
|
+
cancelRecurringPaymentRequestParamsSchema
|
|
5
6
|
} from '../recurring-payments.schema.js'
|
|
6
7
|
|
|
7
8
|
jest.mock('../validators/validators.js', () => ({
|
|
@@ -149,3 +150,19 @@ describe('processRPResultRequestParamsSchema', () => {
|
|
|
149
150
|
expect(() => processRPResultRequestParamsSchema.validateAsync(sampleData).rejects.toThrow())
|
|
150
151
|
})
|
|
151
152
|
})
|
|
153
|
+
|
|
154
|
+
describe('cancelRecurringPaymentRequestParamsSchema', () => {
|
|
155
|
+
it('validates expected object', async () => {
|
|
156
|
+
const sampleData = { id: 'abc123' }
|
|
157
|
+
expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData)).not.toThrow()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('throws an error if id missing', async () => {
|
|
161
|
+
expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync({}).rejects.toThrow())
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('throws an error if id is not the correct type', async () => {
|
|
165
|
+
const sampleData = { id: 99 }
|
|
166
|
+
expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData).rejects.toThrow())
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -29,3 +29,7 @@ export const processRPResultRequestParamsSchema = Joi.object({
|
|
|
29
29
|
paymentId: Joi.string().required(),
|
|
30
30
|
createdDate: Joi.string().isoDate().required()
|
|
31
31
|
})
|
|
32
|
+
|
|
33
|
+
export const cancelRecurringPaymentRequestParamsSchema = Joi.object({
|
|
34
|
+
id: Joi.string().required()
|
|
35
|
+
})
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import recurringPayments from '../recurring-payments.js'
|
|
2
|
-
import { getRecurringPayments, processRPResult } from '../../../services/recurring-payments.service.js'
|
|
3
|
-
import {
|
|
2
|
+
import { getRecurringPayments, processRPResult, cancelRecurringPayment } from '../../../services/recurring-payments.service.js'
|
|
3
|
+
import {
|
|
4
|
+
dueRecurringPaymentsRequestParamsSchema,
|
|
5
|
+
processRPResultRequestParamsSchema,
|
|
6
|
+
cancelRecurringPaymentRequestParamsSchema
|
|
7
|
+
} from '../../../schema/recurring-payments.schema.js'
|
|
4
8
|
|
|
5
9
|
const [
|
|
6
10
|
{
|
|
@@ -8,17 +12,22 @@ const [
|
|
|
8
12
|
},
|
|
9
13
|
{
|
|
10
14
|
options: { handler: prpHandler }
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
options: { handler: crpHandler }
|
|
11
18
|
}
|
|
12
19
|
] = recurringPayments
|
|
13
20
|
|
|
14
21
|
jest.mock('../../../services/recurring-payments.service.js', () => ({
|
|
15
22
|
getRecurringPayments: jest.fn(),
|
|
16
|
-
processRPResult: jest.fn()
|
|
23
|
+
processRPResult: jest.fn(),
|
|
24
|
+
cancelRecurringPayment: jest.fn()
|
|
17
25
|
}))
|
|
18
26
|
|
|
19
27
|
jest.mock('../../../schema/recurring-payments.schema.js', () => ({
|
|
20
28
|
dueRecurringPaymentsRequestParamsSchema: jest.fn(),
|
|
21
|
-
processRPResultRequestParamsSchema: jest.fn()
|
|
29
|
+
processRPResultRequestParamsSchema: jest.fn(),
|
|
30
|
+
cancelRecurringPaymentRequestParamsSchema: jest.fn()
|
|
22
31
|
}))
|
|
23
32
|
|
|
24
33
|
const getMockRequest = ({
|
|
@@ -27,9 +36,10 @@ const getMockRequest = ({
|
|
|
27
36
|
paymentId = 'payment-id',
|
|
28
37
|
createdDate = 'created-date',
|
|
29
38
|
existingRecurringPaymentId = 'existing-recurring-payment-id',
|
|
30
|
-
agreementId = 'agreement-id'
|
|
39
|
+
agreementId = 'agreement-id',
|
|
40
|
+
id = 'abc123'
|
|
31
41
|
}) => ({
|
|
32
|
-
params: { date, transactionId, paymentId, createdDate, existingRecurringPaymentId, agreementId }
|
|
42
|
+
params: { date, transactionId, paymentId, createdDate, existingRecurringPaymentId, agreementId, id }
|
|
33
43
|
})
|
|
34
44
|
|
|
35
45
|
const getMockResponseToolkit = () => ({
|
|
@@ -86,4 +96,26 @@ describe('recurring payments', () => {
|
|
|
86
96
|
expect(recurringPayments[1].options.validate.params).toBe(processRPResultRequestParamsSchema)
|
|
87
97
|
})
|
|
88
98
|
})
|
|
99
|
+
|
|
100
|
+
describe('cancelRecurringPayment', () => {
|
|
101
|
+
it('handler should return continue response', async () => {
|
|
102
|
+
const request = getMockRequest({})
|
|
103
|
+
const responseToolkit = getMockResponseToolkit()
|
|
104
|
+
expect(await crpHandler(request, responseToolkit)).toEqual(responseToolkit.continue)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should call cancelRecurringPayment with id', async () => {
|
|
108
|
+
const id = Symbol('recurring-payment-id')
|
|
109
|
+
const request = getMockRequest({ id })
|
|
110
|
+
await crpHandler(request, getMockResponseToolkit())
|
|
111
|
+
expect(cancelRecurringPayment).toHaveBeenCalledWith(id)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should validate with cancelRecurringPaymentRequestParamsSchema', async () => {
|
|
115
|
+
const id = Symbol('recurring-payment-id')
|
|
116
|
+
const request = getMockRequest({ id })
|
|
117
|
+
await crpHandler(request, getMockResponseToolkit())
|
|
118
|
+
expect(recurringPayments[2].options.validate.params).toBe(cancelRecurringPaymentRequestParamsSchema)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
89
121
|
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dueRecurringPaymentsRequestParamsSchema,
|
|
3
3
|
dueRecurringPaymentsResponseSchema,
|
|
4
|
-
processRPResultRequestParamsSchema
|
|
4
|
+
processRPResultRequestParamsSchema,
|
|
5
|
+
cancelRecurringPaymentRequestParamsSchema
|
|
5
6
|
} from '../../schema/recurring-payments.schema.js'
|
|
6
|
-
import { getRecurringPayments, processRPResult } from '../../services/recurring-payments.service.js'
|
|
7
|
+
import { getRecurringPayments, processRPResult, cancelRecurringPayment } from '../../services/recurring-payments.service.js'
|
|
7
8
|
|
|
8
9
|
const SWAGGER_TAGS = ['api', 'recurring-payments']
|
|
9
10
|
|
|
@@ -55,5 +56,29 @@ export default [
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
method: 'GET',
|
|
62
|
+
path: '/cancelRecurringPayment/{id}',
|
|
63
|
+
options: {
|
|
64
|
+
handler: async (request, h) => {
|
|
65
|
+
const { id } = request.params
|
|
66
|
+
const result = await cancelRecurringPayment(id)
|
|
67
|
+
return h.response(result)
|
|
68
|
+
},
|
|
69
|
+
description: 'Cancel a recurring payment',
|
|
70
|
+
tags: SWAGGER_TAGS,
|
|
71
|
+
validate: {
|
|
72
|
+
params: cancelRecurringPaymentRequestParamsSchema
|
|
73
|
+
},
|
|
74
|
+
plugins: {
|
|
75
|
+
'hapi-swagger': {
|
|
76
|
+
responses: {
|
|
77
|
+
200: { description: 'Recurring payment cancelled' }
|
|
78
|
+
},
|
|
79
|
+
order: 3
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
58
83
|
}
|
|
59
84
|
]
|
|
@@ -5,9 +5,10 @@ Object {
|
|
|
5
5
|
"agreementId": "435678",
|
|
6
6
|
"cancelledDate": null,
|
|
7
7
|
"cancelledReason": null,
|
|
8
|
-
"endDate": 2023-11-12T00:00:00.000Z,
|
|
9
|
-
"
|
|
10
|
-
"
|
|
8
|
+
"endDate": "2023-11-12T00:00:00.000Z",
|
|
9
|
+
"lastDigitsCardNumbers": "0128",
|
|
10
|
+
"name": "Fester Tester 2023",
|
|
11
|
+
"nextDueDate": "2023-11-02T00:00:00.000Z",
|
|
11
12
|
"publicId": "abcdef99987",
|
|
12
13
|
"status": 0,
|
|
13
14
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
executeQuery,
|
|
4
4
|
findDueRecurringPayments,
|
|
5
5
|
findRecurringPaymentsByAgreementId,
|
|
6
|
+
findById,
|
|
6
7
|
Permission,
|
|
7
8
|
RecurringPayment
|
|
8
9
|
} from '@defra-fish/dynamics-lib'
|
|
@@ -11,22 +12,38 @@ import {
|
|
|
11
12
|
processRecurringPayment,
|
|
12
13
|
generateRecurringPaymentRecord,
|
|
13
14
|
processRPResult,
|
|
14
|
-
findNewestExistingRecurringPaymentInCrm
|
|
15
|
+
findNewestExistingRecurringPaymentInCrm,
|
|
16
|
+
getRecurringPaymentAgreement,
|
|
17
|
+
cancelRecurringPayment
|
|
15
18
|
} from '../recurring-payments.service.js'
|
|
16
19
|
import { calculateEndDate, generatePermissionNumber } from '../permissions.service.js'
|
|
17
20
|
import { getObfuscatedDob } from '../contacts.service.js'
|
|
18
21
|
import { createHash } from 'node:crypto'
|
|
19
|
-
import { AWS } from '@defra-fish/connectors-lib'
|
|
22
|
+
import { AWS, govUkPayApi } from '@defra-fish/connectors-lib'
|
|
20
23
|
import { TRANSACTION_STAGING_TABLE, TRANSACTION_QUEUE } from '../../config.js'
|
|
21
24
|
import { TRANSACTION_STATUS } from '../../services/transactions/constants.js'
|
|
22
25
|
import { retrieveStagedTransaction } from '../../services/transactions/retrieve-transaction.js'
|
|
23
26
|
import { createPaymentJournal, getPaymentJournal, updatePaymentJournal } from '../../services/paymentjournals/payment-journals.service.js'
|
|
24
27
|
import { PAYMENT_JOURNAL_STATUS_CODES, TRANSACTION_SOURCE, PAYMENT_TYPE } from '@defra-fish/business-rules-lib'
|
|
28
|
+
import db from 'debug'
|
|
29
|
+
|
|
30
|
+
jest.mock('ioredis', () => ({
|
|
31
|
+
built: {
|
|
32
|
+
utils: {
|
|
33
|
+
debug: jest.fn()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
jest.mock('debug', () => jest.fn(() => jest.fn()))
|
|
39
|
+
const { value: debug } = db.mock.results[db.mock.calls.findIndex(c => c[0] === 'sales:recurring')]
|
|
40
|
+
|
|
25
41
|
const { docClient, sqs } = AWS.mock.results[0].value
|
|
26
42
|
|
|
27
43
|
jest.mock('@defra-fish/dynamics-lib', () => ({
|
|
28
44
|
...jest.requireActual('@defra-fish/dynamics-lib'),
|
|
29
45
|
executeQuery: jest.fn(),
|
|
46
|
+
findById: jest.fn(),
|
|
30
47
|
findDueRecurringPayments: jest.fn(),
|
|
31
48
|
findRecurringPaymentsByAgreementId: jest.fn(() => ({ toRetrieveRequest: () => {} })),
|
|
32
49
|
dynamicsClient: {
|
|
@@ -43,7 +60,10 @@ jest.mock('@defra-fish/connectors-lib', () => ({
|
|
|
43
60
|
sqs: {
|
|
44
61
|
sendMessage: jest.fn()
|
|
45
62
|
}
|
|
46
|
-
}))
|
|
63
|
+
})),
|
|
64
|
+
govUkPayApi: {
|
|
65
|
+
getRecurringPaymentAgreementInformation: jest.fn()
|
|
66
|
+
}
|
|
47
67
|
}))
|
|
48
68
|
|
|
49
69
|
jest.mock('node:crypto', () => ({
|
|
@@ -88,6 +108,7 @@ jest.mock('@defra-fish/business-rules-lib', () => ({
|
|
|
88
108
|
debit: Symbol('debit')
|
|
89
109
|
}
|
|
90
110
|
}))
|
|
111
|
+
global.structuredClone = obj => JSON.parse(JSON.stringify(obj))
|
|
91
112
|
|
|
92
113
|
const getMockRecurringPayment = (overrides = {}) => ({
|
|
93
114
|
name: 'Test Name',
|
|
@@ -224,7 +245,14 @@ const getMockResponse = () => ({
|
|
|
224
245
|
})
|
|
225
246
|
|
|
226
247
|
describe('recurring payments service', () => {
|
|
227
|
-
const createSimpleSampleTransactionRecord = () => ({
|
|
248
|
+
const createSimpleSampleTransactionRecord = () => ({
|
|
249
|
+
payment: {
|
|
250
|
+
recurring: {
|
|
251
|
+
nextDueDate: '2025-01-01T00:00:00.000Z'
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
permissions: [{}]
|
|
255
|
+
})
|
|
228
256
|
const createSamplePermission = overrides => {
|
|
229
257
|
const p = new Permission()
|
|
230
258
|
p.referenceNumber = 'ABC123'
|
|
@@ -244,6 +272,11 @@ describe('recurring payments service', () => {
|
|
|
244
272
|
beforeAll(() => {
|
|
245
273
|
TRANSACTION_QUEUE.Url = 'TestUrl'
|
|
246
274
|
TRANSACTION_STAGING_TABLE.TableName = 'TestTable'
|
|
275
|
+
const mockResponse = {
|
|
276
|
+
ok: true,
|
|
277
|
+
json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
|
|
278
|
+
}
|
|
279
|
+
govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
|
|
247
280
|
})
|
|
248
281
|
|
|
249
282
|
describe('getRecurringPayments', () => {
|
|
@@ -281,13 +314,13 @@ describe('recurring payments service', () => {
|
|
|
281
314
|
const transactionRecord = {
|
|
282
315
|
payment: {
|
|
283
316
|
recurring: {
|
|
284
|
-
|
|
285
|
-
nextDueDate: new Date('2023-11-02'),
|
|
317
|
+
nextDueDate: '2023-11-02T00:00:00.000Z',
|
|
286
318
|
cancelledDate: null,
|
|
287
319
|
cancelledReason: null,
|
|
288
|
-
endDate:
|
|
320
|
+
endDate: '2023-11-12T00:00:00.000Z',
|
|
289
321
|
agreementId: '435678',
|
|
290
|
-
status: 0
|
|
322
|
+
status: 0,
|
|
323
|
+
last_digits_card_number: '0128'
|
|
291
324
|
}
|
|
292
325
|
},
|
|
293
326
|
permissions: [getMockPermission()]
|
|
@@ -297,6 +330,19 @@ describe('recurring payments service', () => {
|
|
|
297
330
|
expect(result.recurringPayment).toMatchSnapshot()
|
|
298
331
|
})
|
|
299
332
|
|
|
333
|
+
it('should set a valid name on the recurringPayment', async () => {
|
|
334
|
+
const transactionRecord = {
|
|
335
|
+
payment: {
|
|
336
|
+
recurring: {
|
|
337
|
+
nextDueDate: '2023-07-07T00:00:00.000Z'
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
permissions: [getMockPermission()]
|
|
341
|
+
}
|
|
342
|
+
const result = await processRecurringPayment(transactionRecord, getMockContact())
|
|
343
|
+
expect(result.recurringPayment.name).toBe('Fester Tester 2023')
|
|
344
|
+
})
|
|
345
|
+
|
|
300
346
|
it.each(['abc-123', 'def-987'])('generates a publicId %s for the recurring payment', async samplePublicId => {
|
|
301
347
|
createHash.mockReturnValue({
|
|
302
348
|
update: () => {},
|
|
@@ -333,7 +379,7 @@ describe('recurring payments service', () => {
|
|
|
333
379
|
})
|
|
334
380
|
|
|
335
381
|
describe('generateRecurringPaymentRecord', () => {
|
|
336
|
-
const createFinalisedSampleTransaction = (agreementId, permission) => ({
|
|
382
|
+
const createFinalisedSampleTransaction = (agreementId, permission, lastDigitsCardNumbers) => ({
|
|
337
383
|
expires: 1732892402,
|
|
338
384
|
cost: 35.8,
|
|
339
385
|
isRecurringPaymentSupported: true,
|
|
@@ -356,7 +402,8 @@ describe('recurring payments service', () => {
|
|
|
356
402
|
id: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757',
|
|
357
403
|
dataSource: 'Web Sales',
|
|
358
404
|
transactionId: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757',
|
|
359
|
-
status: { id: 'FINALISED' }
|
|
405
|
+
status: { id: 'FINALISED' },
|
|
406
|
+
lastDigitsCardNumbers
|
|
360
407
|
})
|
|
361
408
|
|
|
362
409
|
it.each([
|
|
@@ -368,7 +415,8 @@ describe('recurring payments service', () => {
|
|
|
368
415
|
issueDate: '2024-11-22T15:00:45.922Z',
|
|
369
416
|
endDate: '2025-11-21T23:59:59.999Z'
|
|
370
417
|
},
|
|
371
|
-
'2025-11-12T00:00:00.000Z'
|
|
418
|
+
'2025-11-12T00:00:00.000Z',
|
|
419
|
+
'1234'
|
|
372
420
|
],
|
|
373
421
|
[
|
|
374
422
|
'next day start - next due on end date minus ten days',
|
|
@@ -378,7 +426,8 @@ describe('recurring payments service', () => {
|
|
|
378
426
|
issueDate: '2024-11-22T15:00:45.922Z',
|
|
379
427
|
endDate: '2025-11-22T23:59:59.999Z'
|
|
380
428
|
},
|
|
381
|
-
'2025-11-12T00:00:00.000Z'
|
|
429
|
+
'2025-11-12T00:00:00.000Z',
|
|
430
|
+
'5678'
|
|
382
431
|
],
|
|
383
432
|
[
|
|
384
433
|
'starts ten days after issue - next due on issue date plus one year',
|
|
@@ -388,7 +437,8 @@ describe('recurring payments service', () => {
|
|
|
388
437
|
issueDate: '2024-11-12T15:00:45.922Z',
|
|
389
438
|
endDate: '2025-11-21T23:59:59.999Z'
|
|
390
439
|
},
|
|
391
|
-
'2025-11-12T00:00:00.000Z'
|
|
440
|
+
'2025-11-12T00:00:00.000Z',
|
|
441
|
+
'9012'
|
|
392
442
|
],
|
|
393
443
|
[
|
|
394
444
|
'starts twenty days after issue - next due on issue date plus one year',
|
|
@@ -398,7 +448,8 @@ describe('recurring payments service', () => {
|
|
|
398
448
|
issueDate: '2024-11-12T15:00:45.922Z',
|
|
399
449
|
endDate: '2025-01-30T23:59:59.999Z'
|
|
400
450
|
},
|
|
401
|
-
'2025-11-12T00:00:00.000Z'
|
|
451
|
+
'2025-11-12T00:00:00.000Z',
|
|
452
|
+
'3456'
|
|
402
453
|
],
|
|
403
454
|
[
|
|
404
455
|
"issued on 29th Feb '24, starts on 30th March '24 - next due on 28th Feb '25",
|
|
@@ -408,7 +459,8 @@ describe('recurring payments service', () => {
|
|
|
408
459
|
issueDate: '2024-02-29T12:38:24.123Z',
|
|
409
460
|
endDate: '2025-03-29T23:59:59.999Z'
|
|
410
461
|
},
|
|
411
|
-
'2025-02-28T00:00:00.000Z'
|
|
462
|
+
'2025-02-28T00:00:00.000Z',
|
|
463
|
+
'7890'
|
|
412
464
|
],
|
|
413
465
|
[
|
|
414
466
|
"issued on 30th March '25 at 1am, starts at 1:30am - next due on 20th March '26",
|
|
@@ -418,13 +470,21 @@ describe('recurring payments service', () => {
|
|
|
418
470
|
issueDate: '2025-03-30T01:00:00.000Z',
|
|
419
471
|
endDate: '2026-03-29T23:59:59.999Z'
|
|
420
472
|
},
|
|
421
|
-
'2026-03-20T00:00:00.000Z'
|
|
473
|
+
'2026-03-20T00:00:00.000Z',
|
|
474
|
+
'1199'
|
|
422
475
|
]
|
|
423
|
-
])('creates record from transaction with %s', (_d, agreementId, permissionData, expectedNextDueDate) => {
|
|
424
|
-
const
|
|
476
|
+
])('creates record from transaction with %s', async (_d, agreementId, permissionData, expectedNextDueDate, lastDigitsCardNumbers) => {
|
|
477
|
+
const mockResponse = {
|
|
478
|
+
ok: true,
|
|
479
|
+
json: jest
|
|
480
|
+
.fn()
|
|
481
|
+
.mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: lastDigitsCardNumbers } } })
|
|
482
|
+
}
|
|
483
|
+
govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
|
|
484
|
+
const sampleTransaction = createFinalisedSampleTransaction(agreementId, permissionData, lastDigitsCardNumbers)
|
|
425
485
|
const permission = createSamplePermission(permissionData)
|
|
426
486
|
|
|
427
|
-
const rpRecord = generateRecurringPaymentRecord(sampleTransaction, permission)
|
|
487
|
+
const rpRecord = await generateRecurringPaymentRecord(sampleTransaction, permission)
|
|
428
488
|
|
|
429
489
|
expect(rpRecord).toEqual(
|
|
430
490
|
expect.objectContaining({
|
|
@@ -436,7 +496,8 @@ describe('recurring payments service', () => {
|
|
|
436
496
|
cancelledReason: null,
|
|
437
497
|
endDate: permissionData.endDate,
|
|
438
498
|
agreementId,
|
|
439
|
-
status: 1
|
|
499
|
+
status: 1,
|
|
500
|
+
last_digits_card_number: lastDigitsCardNumbers
|
|
440
501
|
})
|
|
441
502
|
}),
|
|
442
503
|
permissions: expect.arrayContaining([permission])
|
|
@@ -461,22 +522,26 @@ describe('recurring payments service', () => {
|
|
|
461
522
|
endDate: '2025-11-10T23:59:59.999Z'
|
|
462
523
|
}
|
|
463
524
|
]
|
|
464
|
-
])('throws an error for invalid dates when %s', (_d, permission) => {
|
|
525
|
+
])('throws an error for invalid dates when %s', async (_d, permission) => {
|
|
465
526
|
const sampleTransaction = createFinalisedSampleTransaction('hyu78ijhyu78ijuhyu78ij9iu6', permission)
|
|
466
527
|
|
|
467
|
-
expect(
|
|
528
|
+
await expect(generateRecurringPaymentRecord(sampleTransaction)).rejects.toThrow('Invalid dates provided for permission')
|
|
468
529
|
})
|
|
469
530
|
|
|
470
|
-
it('returns a false flag when agreementId is not present', () => {
|
|
471
|
-
const sampleTransaction = createFinalisedSampleTransaction(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
531
|
+
it('returns a false flag when agreementId is not present', async () => {
|
|
532
|
+
const sampleTransaction = createFinalisedSampleTransaction(
|
|
533
|
+
null,
|
|
534
|
+
{
|
|
535
|
+
startDate: '2024-11-22T15:30:45.922Z',
|
|
536
|
+
issueDate: '2024-11-22T15:00:45.922Z',
|
|
537
|
+
endDate: '2025-11-21T23:59:59.999Z'
|
|
538
|
+
},
|
|
539
|
+
'0123'
|
|
540
|
+
)
|
|
476
541
|
|
|
477
|
-
const rpRecord = generateRecurringPaymentRecord(sampleTransaction)
|
|
542
|
+
const rpRecord = await generateRecurringPaymentRecord(sampleTransaction)
|
|
478
543
|
|
|
479
|
-
expect(rpRecord.payment
|
|
544
|
+
expect(rpRecord.payment?.recurring).toBeFalsy()
|
|
480
545
|
})
|
|
481
546
|
})
|
|
482
547
|
|
|
@@ -733,4 +798,89 @@ describe('recurring payments service', () => {
|
|
|
733
798
|
expect(rcp).toBeFalsy()
|
|
734
799
|
})
|
|
735
800
|
})
|
|
801
|
+
|
|
802
|
+
describe('getRecurringPaymentAgreement', () => {
|
|
803
|
+
const agreementId = '1234'
|
|
804
|
+
|
|
805
|
+
it('should send provided agreement id data to Gov.UK Pay', async () => {
|
|
806
|
+
const mockResponse = {
|
|
807
|
+
ok: true,
|
|
808
|
+
json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
|
|
809
|
+
}
|
|
810
|
+
govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
|
|
811
|
+
await getRecurringPaymentAgreement(agreementId)
|
|
812
|
+
expect(govUkPayApi.getRecurringPaymentAgreementInformation).toHaveBeenCalledWith(agreementId)
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
it('should return response body when payment creation is successful', async () => {
|
|
816
|
+
const mockResponse = {
|
|
817
|
+
ok: true,
|
|
818
|
+
json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
|
|
819
|
+
}
|
|
820
|
+
govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
|
|
821
|
+
|
|
822
|
+
const result = await getRecurringPaymentAgreement(agreementId)
|
|
823
|
+
|
|
824
|
+
expect(result).toEqual({
|
|
825
|
+
success: true,
|
|
826
|
+
payment_instrument: {
|
|
827
|
+
card_details: {
|
|
828
|
+
last_digits_card_number: '1234'
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
it('debug should output message when response.ok is true without card details', async () => {
|
|
835
|
+
const mockResponse = {
|
|
836
|
+
ok: true,
|
|
837
|
+
json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
|
|
838
|
+
}
|
|
839
|
+
govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
|
|
840
|
+
|
|
841
|
+
await getRecurringPaymentAgreement(agreementId)
|
|
842
|
+
|
|
843
|
+
expect(debug).toHaveBeenCalledWith('Successfully got recurring payment agreement information: %o', {
|
|
844
|
+
success: true,
|
|
845
|
+
payment_instrument: {}
|
|
846
|
+
})
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
it('should throw an error when the response is not ok', async () => {
|
|
850
|
+
const mockResponse = {
|
|
851
|
+
ok: false,
|
|
852
|
+
json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
|
|
853
|
+
}
|
|
854
|
+
govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
|
|
855
|
+
|
|
856
|
+
await expect(getRecurringPaymentAgreement(agreementId)).rejects.toThrow('Failure getting agreement in the GOV.UK API service')
|
|
857
|
+
})
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
describe('cancelRecurringPayment', () => {
|
|
861
|
+
it('should call findById with RecurringPayment and the provided id', async () => {
|
|
862
|
+
const id = 'abc123'
|
|
863
|
+
await cancelRecurringPayment(id)
|
|
864
|
+
expect(findById).toHaveBeenCalledWith(RecurringPayment, id)
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
it('should log a RecurringPayment record when there is one match', async () => {
|
|
868
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn())
|
|
869
|
+
const recurringPayment = { entity: getMockRecurringPayment() }
|
|
870
|
+
findById.mockReturnValueOnce(recurringPayment)
|
|
871
|
+
|
|
872
|
+
await cancelRecurringPayment('id')
|
|
873
|
+
|
|
874
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('RecurringPayment for cancellation: ', recurringPayment)
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
it('should log no matches when there are no matches', async () => {
|
|
878
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn())
|
|
879
|
+
findById.mockReturnValueOnce(undefined)
|
|
880
|
+
|
|
881
|
+
await cancelRecurringPayment('id')
|
|
882
|
+
|
|
883
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('No matches found for cancellation')
|
|
884
|
+
})
|
|
885
|
+
})
|
|
736
886
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
executeQuery,
|
|
3
|
+
findById,
|
|
3
4
|
findDueRecurringPayments,
|
|
4
5
|
findRecurringPaymentsByAgreementId,
|
|
5
6
|
RecurringPayment,
|
|
@@ -14,7 +15,9 @@ import { TRANSACTION_STATUS } from '../services/transactions/constants.js'
|
|
|
14
15
|
import { retrieveStagedTransaction } from '../services/transactions/retrieve-transaction.js'
|
|
15
16
|
import { createPaymentJournal, getPaymentJournal, updatePaymentJournal } from '../services/paymentjournals/payment-journals.service.js'
|
|
16
17
|
import moment from 'moment'
|
|
17
|
-
import { AWS } from '@defra-fish/connectors-lib'
|
|
18
|
+
import { AWS, govUkPayApi } from '@defra-fish/connectors-lib'
|
|
19
|
+
import db from 'debug'
|
|
20
|
+
const debug = db('sales:recurring')
|
|
18
21
|
const { sqs, docClient } = AWS()
|
|
19
22
|
|
|
20
23
|
export const getRecurringPayments = date => executeQuery(findDueRecurringPayments(date))
|
|
@@ -33,8 +36,10 @@ const getNextDueDate = (startDate, issueDate, endDate) => {
|
|
|
33
36
|
throw new Error('Invalid dates provided for permission')
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
export const generateRecurringPaymentRecord = (transactionRecord, permission) => {
|
|
39
|
+
export const generateRecurringPaymentRecord = async (transactionRecord, permission) => {
|
|
37
40
|
if (transactionRecord.agreementId) {
|
|
41
|
+
const agreementResponse = await getRecurringPaymentAgreement(transactionRecord.agreementId)
|
|
42
|
+
const lastDigitsCardNumbers = agreementResponse.payment_instrument?.card_details?.last_digits_card_number
|
|
38
43
|
const [{ startDate, issueDate, endDate }] = transactionRecord.permissions
|
|
39
44
|
return {
|
|
40
45
|
payment: {
|
|
@@ -45,7 +50,8 @@ export const generateRecurringPaymentRecord = (transactionRecord, permission) =>
|
|
|
45
50
|
cancelledReason: null,
|
|
46
51
|
endDate,
|
|
47
52
|
agreementId: transactionRecord.agreementId,
|
|
48
|
-
status: 1
|
|
53
|
+
status: 1,
|
|
54
|
+
last_digits_card_number: lastDigitsCardNumbers
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
permissions: [permission]
|
|
@@ -64,7 +70,7 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
|
|
|
64
70
|
if (transactionRecord.payment?.recurring) {
|
|
65
71
|
const recurringPayment = new RecurringPayment()
|
|
66
72
|
hash.update(recurringPayment.uniqueContentId)
|
|
67
|
-
recurringPayment.name = transactionRecord
|
|
73
|
+
recurringPayment.name = determineRecurringPaymentName(transactionRecord, contact)
|
|
68
74
|
recurringPayment.nextDueDate = transactionRecord.payment.recurring.nextDueDate
|
|
69
75
|
recurringPayment.cancelledDate = transactionRecord.payment.recurring.cancelledDate
|
|
70
76
|
recurringPayment.cancelledReason = transactionRecord.payment.recurring.cancelledReason
|
|
@@ -72,6 +78,7 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
|
|
|
72
78
|
recurringPayment.agreementId = transactionRecord.payment.recurring.agreementId
|
|
73
79
|
recurringPayment.publicId = hash.digest('base64')
|
|
74
80
|
recurringPayment.status = transactionRecord.payment.recurring.status
|
|
81
|
+
recurringPayment.lastDigitsCardNumbers = transactionRecord.payment.recurring.last_digits_card_number
|
|
75
82
|
const [permission] = transactionRecord.permissions
|
|
76
83
|
recurringPayment.bindToEntity(RecurringPayment.definition.relationships.activePermission, permission)
|
|
77
84
|
recurringPayment.bindToEntity(RecurringPayment.definition.relationships.contact, contact)
|
|
@@ -80,6 +87,22 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
|
|
|
80
87
|
return { recurringPayment: null }
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
export const getRecurringPaymentAgreement = async agreementId => {
|
|
91
|
+
const response = await govUkPayApi.getRecurringPaymentAgreementInformation(agreementId)
|
|
92
|
+
if (response.ok) {
|
|
93
|
+
const resBody = await response.json()
|
|
94
|
+
const resBodyNoCardDetails = structuredClone(resBody)
|
|
95
|
+
|
|
96
|
+
if (resBodyNoCardDetails.payment_instrument?.card_details) {
|
|
97
|
+
delete resBodyNoCardDetails.payment_instrument.card_details
|
|
98
|
+
}
|
|
99
|
+
debug('Successfully got recurring payment agreement information: %o', resBodyNoCardDetails)
|
|
100
|
+
return resBody
|
|
101
|
+
} else {
|
|
102
|
+
throw new Error('Failure getting agreement in the GOV.UK API service')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
export const processRPResult = async (transactionId, paymentId, createdDate) => {
|
|
84
107
|
const transactionRecord = await retrieveStagedTransaction(transactionId)
|
|
85
108
|
if (await getPaymentJournal(transactionId)) {
|
|
@@ -138,3 +161,17 @@ export const findNewestExistingRecurringPaymentInCrm = async agreementId => {
|
|
|
138
161
|
}
|
|
139
162
|
return false
|
|
140
163
|
}
|
|
164
|
+
|
|
165
|
+
export const cancelRecurringPayment = async id => {
|
|
166
|
+
const recurringPayment = await findById(RecurringPayment, id)
|
|
167
|
+
if (recurringPayment) {
|
|
168
|
+
console.log('RecurringPayment for cancellation: ', recurringPayment)
|
|
169
|
+
} else {
|
|
170
|
+
console.log('No matches found for cancellation')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const determineRecurringPaymentName = (transactionRecord, contact) => {
|
|
175
|
+
const [dueYear] = transactionRecord.payment.recurring.nextDueDate.split('-')
|
|
176
|
+
return [contact.firstName, contact.lastName, dueYear].join(' ')
|
|
177
|
+
}
|
|
@@ -77,7 +77,7 @@ export async function processQueue ({ id }) {
|
|
|
77
77
|
|
|
78
78
|
entities.push(contact, permission)
|
|
79
79
|
|
|
80
|
-
const { recurringPayment } = await processRecurringPayment(generateRecurringPaymentRecord(transactionRecord, permission), contact)
|
|
80
|
+
const { recurringPayment } = await processRecurringPayment(await generateRecurringPaymentRecord(transactionRecord, permission), contact)
|
|
81
81
|
|
|
82
82
|
if (recurringPayment && permit.isRecurringPaymentSupported) {
|
|
83
83
|
entities.push(recurringPayment)
|