@defra-fish/sales-api-service 1.62.0-rc.7 → 1.62.0-rc.8
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/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap +1 -0
- package/src/services/__tests__/recurring-payments.service.spec.js +126 -25
- package/src/services/recurring-payments.service.js +25 -3
- 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.8",
|
|
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.8",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.62.0-rc.8",
|
|
40
|
+
"@defra-fish/dynamics-lib": "1.62.0-rc.8",
|
|
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": "5d9e947d9ce87f6f43aa6ca7ac8d78f797ecebdf"
|
|
56
56
|
}
|
|
@@ -13,17 +13,31 @@ import {
|
|
|
13
13
|
generateRecurringPaymentRecord,
|
|
14
14
|
processRPResult,
|
|
15
15
|
findNewestExistingRecurringPaymentInCrm,
|
|
16
|
+
getRecurringPaymentAgreement,
|
|
16
17
|
cancelRecurringPayment
|
|
17
18
|
} from '../recurring-payments.service.js'
|
|
18
19
|
import { calculateEndDate, generatePermissionNumber } from '../permissions.service.js'
|
|
19
20
|
import { getObfuscatedDob } from '../contacts.service.js'
|
|
20
21
|
import { createHash } from 'node:crypto'
|
|
21
|
-
import { AWS } from '@defra-fish/connectors-lib'
|
|
22
|
+
import { AWS, govUkPayApi } from '@defra-fish/connectors-lib'
|
|
22
23
|
import { TRANSACTION_STAGING_TABLE, TRANSACTION_QUEUE } from '../../config.js'
|
|
23
24
|
import { TRANSACTION_STATUS } from '../../services/transactions/constants.js'
|
|
24
25
|
import { retrieveStagedTransaction } from '../../services/transactions/retrieve-transaction.js'
|
|
25
26
|
import { createPaymentJournal, getPaymentJournal, updatePaymentJournal } from '../../services/paymentjournals/payment-journals.service.js'
|
|
26
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
|
+
|
|
27
41
|
const { docClient, sqs } = AWS.mock.results[0].value
|
|
28
42
|
|
|
29
43
|
jest.mock('@defra-fish/dynamics-lib', () => ({
|
|
@@ -46,7 +60,10 @@ jest.mock('@defra-fish/connectors-lib', () => ({
|
|
|
46
60
|
sqs: {
|
|
47
61
|
sendMessage: jest.fn()
|
|
48
62
|
}
|
|
49
|
-
}))
|
|
63
|
+
})),
|
|
64
|
+
govUkPayApi: {
|
|
65
|
+
getRecurringPaymentAgreementInformation: jest.fn()
|
|
66
|
+
}
|
|
50
67
|
}))
|
|
51
68
|
|
|
52
69
|
jest.mock('node:crypto', () => ({
|
|
@@ -91,6 +108,7 @@ jest.mock('@defra-fish/business-rules-lib', () => ({
|
|
|
91
108
|
debit: Symbol('debit')
|
|
92
109
|
}
|
|
93
110
|
}))
|
|
111
|
+
global.structuredClone = obj => JSON.parse(JSON.stringify(obj))
|
|
94
112
|
|
|
95
113
|
const getMockRecurringPayment = (overrides = {}) => ({
|
|
96
114
|
name: 'Test Name',
|
|
@@ -254,6 +272,11 @@ describe('recurring payments service', () => {
|
|
|
254
272
|
beforeAll(() => {
|
|
255
273
|
TRANSACTION_QUEUE.Url = 'TestUrl'
|
|
256
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)
|
|
257
280
|
})
|
|
258
281
|
|
|
259
282
|
describe('getRecurringPayments', () => {
|
|
@@ -296,7 +319,8 @@ describe('recurring payments service', () => {
|
|
|
296
319
|
cancelledReason: null,
|
|
297
320
|
endDate: '2023-11-12T00:00:00.000Z',
|
|
298
321
|
agreementId: '435678',
|
|
299
|
-
status: 0
|
|
322
|
+
status: 0,
|
|
323
|
+
last_digits_card_number: '0128'
|
|
300
324
|
}
|
|
301
325
|
},
|
|
302
326
|
permissions: [getMockPermission()]
|
|
@@ -355,7 +379,7 @@ describe('recurring payments service', () => {
|
|
|
355
379
|
})
|
|
356
380
|
|
|
357
381
|
describe('generateRecurringPaymentRecord', () => {
|
|
358
|
-
const createFinalisedSampleTransaction = (agreementId, permission) => ({
|
|
382
|
+
const createFinalisedSampleTransaction = (agreementId, permission, lastDigitsCardNumbers) => ({
|
|
359
383
|
expires: 1732892402,
|
|
360
384
|
cost: 35.8,
|
|
361
385
|
isRecurringPaymentSupported: true,
|
|
@@ -378,7 +402,8 @@ describe('recurring payments service', () => {
|
|
|
378
402
|
id: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757',
|
|
379
403
|
dataSource: 'Web Sales',
|
|
380
404
|
transactionId: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757',
|
|
381
|
-
status: { id: 'FINALISED' }
|
|
405
|
+
status: { id: 'FINALISED' },
|
|
406
|
+
lastDigitsCardNumbers
|
|
382
407
|
})
|
|
383
408
|
|
|
384
409
|
it.each([
|
|
@@ -390,7 +415,8 @@ describe('recurring payments service', () => {
|
|
|
390
415
|
issueDate: '2024-11-22T15:00:45.922Z',
|
|
391
416
|
endDate: '2025-11-21T23:59:59.999Z'
|
|
392
417
|
},
|
|
393
|
-
'2025-11-12T00:00:00.000Z'
|
|
418
|
+
'2025-11-12T00:00:00.000Z',
|
|
419
|
+
'1234'
|
|
394
420
|
],
|
|
395
421
|
[
|
|
396
422
|
'next day start - next due on end date minus ten days',
|
|
@@ -400,7 +426,8 @@ describe('recurring payments service', () => {
|
|
|
400
426
|
issueDate: '2024-11-22T15:00:45.922Z',
|
|
401
427
|
endDate: '2025-11-22T23:59:59.999Z'
|
|
402
428
|
},
|
|
403
|
-
'2025-11-12T00:00:00.000Z'
|
|
429
|
+
'2025-11-12T00:00:00.000Z',
|
|
430
|
+
'5678'
|
|
404
431
|
],
|
|
405
432
|
[
|
|
406
433
|
'starts ten days after issue - next due on issue date plus one year',
|
|
@@ -410,7 +437,8 @@ describe('recurring payments service', () => {
|
|
|
410
437
|
issueDate: '2024-11-12T15:00:45.922Z',
|
|
411
438
|
endDate: '2025-11-21T23:59:59.999Z'
|
|
412
439
|
},
|
|
413
|
-
'2025-11-12T00:00:00.000Z'
|
|
440
|
+
'2025-11-12T00:00:00.000Z',
|
|
441
|
+
'9012'
|
|
414
442
|
],
|
|
415
443
|
[
|
|
416
444
|
'starts twenty days after issue - next due on issue date plus one year',
|
|
@@ -420,7 +448,8 @@ describe('recurring payments service', () => {
|
|
|
420
448
|
issueDate: '2024-11-12T15:00:45.922Z',
|
|
421
449
|
endDate: '2025-01-30T23:59:59.999Z'
|
|
422
450
|
},
|
|
423
|
-
'2025-11-12T00:00:00.000Z'
|
|
451
|
+
'2025-11-12T00:00:00.000Z',
|
|
452
|
+
'3456'
|
|
424
453
|
],
|
|
425
454
|
[
|
|
426
455
|
"issued on 29th Feb '24, starts on 30th March '24 - next due on 28th Feb '25",
|
|
@@ -430,7 +459,8 @@ describe('recurring payments service', () => {
|
|
|
430
459
|
issueDate: '2024-02-29T12:38:24.123Z',
|
|
431
460
|
endDate: '2025-03-29T23:59:59.999Z'
|
|
432
461
|
},
|
|
433
|
-
'2025-02-28T00:00:00.000Z'
|
|
462
|
+
'2025-02-28T00:00:00.000Z',
|
|
463
|
+
'7890'
|
|
434
464
|
],
|
|
435
465
|
[
|
|
436
466
|
"issued on 30th March '25 at 1am, starts at 1:30am - next due on 20th March '26",
|
|
@@ -440,13 +470,21 @@ describe('recurring payments service', () => {
|
|
|
440
470
|
issueDate: '2025-03-30T01:00:00.000Z',
|
|
441
471
|
endDate: '2026-03-29T23:59:59.999Z'
|
|
442
472
|
},
|
|
443
|
-
'2026-03-20T00:00:00.000Z'
|
|
473
|
+
'2026-03-20T00:00:00.000Z',
|
|
474
|
+
'1199'
|
|
444
475
|
]
|
|
445
|
-
])('creates record from transaction with %s', (_d, agreementId, permissionData, expectedNextDueDate) => {
|
|
446
|
-
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)
|
|
447
485
|
const permission = createSamplePermission(permissionData)
|
|
448
486
|
|
|
449
|
-
const rpRecord = generateRecurringPaymentRecord(sampleTransaction, permission)
|
|
487
|
+
const rpRecord = await generateRecurringPaymentRecord(sampleTransaction, permission)
|
|
450
488
|
|
|
451
489
|
expect(rpRecord).toEqual(
|
|
452
490
|
expect.objectContaining({
|
|
@@ -458,7 +496,8 @@ describe('recurring payments service', () => {
|
|
|
458
496
|
cancelledReason: null,
|
|
459
497
|
endDate: permissionData.endDate,
|
|
460
498
|
agreementId,
|
|
461
|
-
status: 1
|
|
499
|
+
status: 1,
|
|
500
|
+
last_digits_card_number: lastDigitsCardNumbers
|
|
462
501
|
})
|
|
463
502
|
}),
|
|
464
503
|
permissions: expect.arrayContaining([permission])
|
|
@@ -483,22 +522,26 @@ describe('recurring payments service', () => {
|
|
|
483
522
|
endDate: '2025-11-10T23:59:59.999Z'
|
|
484
523
|
}
|
|
485
524
|
]
|
|
486
|
-
])('throws an error for invalid dates when %s', (_d, permission) => {
|
|
525
|
+
])('throws an error for invalid dates when %s', async (_d, permission) => {
|
|
487
526
|
const sampleTransaction = createFinalisedSampleTransaction('hyu78ijhyu78ijuhyu78ij9iu6', permission)
|
|
488
527
|
|
|
489
|
-
expect(
|
|
528
|
+
await expect(generateRecurringPaymentRecord(sampleTransaction)).rejects.toThrow('Invalid dates provided for permission')
|
|
490
529
|
})
|
|
491
530
|
|
|
492
|
-
it('returns a false flag when agreementId is not present', () => {
|
|
493
|
-
const sampleTransaction = createFinalisedSampleTransaction(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
)
|
|
498
541
|
|
|
499
|
-
const rpRecord = generateRecurringPaymentRecord(sampleTransaction)
|
|
542
|
+
const rpRecord = await generateRecurringPaymentRecord(sampleTransaction)
|
|
500
543
|
|
|
501
|
-
expect(rpRecord.payment
|
|
544
|
+
expect(rpRecord.payment?.recurring).toBeFalsy()
|
|
502
545
|
})
|
|
503
546
|
})
|
|
504
547
|
|
|
@@ -756,6 +799,64 @@ describe('recurring payments service', () => {
|
|
|
756
799
|
})
|
|
757
800
|
})
|
|
758
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
|
+
|
|
759
860
|
describe('cancelRecurringPayment', () => {
|
|
760
861
|
it('should call findById with RecurringPayment and the provided id', async () => {
|
|
761
862
|
const id = 'abc123'
|
|
@@ -15,7 +15,9 @@ import { TRANSACTION_STATUS } from '../services/transactions/constants.js'
|
|
|
15
15
|
import { retrieveStagedTransaction } from '../services/transactions/retrieve-transaction.js'
|
|
16
16
|
import { createPaymentJournal, getPaymentJournal, updatePaymentJournal } from '../services/paymentjournals/payment-journals.service.js'
|
|
17
17
|
import moment from 'moment'
|
|
18
|
-
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')
|
|
19
21
|
const { sqs, docClient } = AWS()
|
|
20
22
|
|
|
21
23
|
export const getRecurringPayments = date => executeQuery(findDueRecurringPayments(date))
|
|
@@ -34,8 +36,10 @@ const getNextDueDate = (startDate, issueDate, endDate) => {
|
|
|
34
36
|
throw new Error('Invalid dates provided for permission')
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
export const generateRecurringPaymentRecord = (transactionRecord, permission) => {
|
|
39
|
+
export const generateRecurringPaymentRecord = async (transactionRecord, permission) => {
|
|
38
40
|
if (transactionRecord.agreementId) {
|
|
41
|
+
const agreementResponse = await getRecurringPaymentAgreement(transactionRecord.agreementId)
|
|
42
|
+
const lastDigitsCardNumbers = agreementResponse.payment_instrument?.card_details?.last_digits_card_number
|
|
39
43
|
const [{ startDate, issueDate, endDate }] = transactionRecord.permissions
|
|
40
44
|
return {
|
|
41
45
|
payment: {
|
|
@@ -46,7 +50,8 @@ export const generateRecurringPaymentRecord = (transactionRecord, permission) =>
|
|
|
46
50
|
cancelledReason: null,
|
|
47
51
|
endDate,
|
|
48
52
|
agreementId: transactionRecord.agreementId,
|
|
49
|
-
status: 1
|
|
53
|
+
status: 1,
|
|
54
|
+
last_digits_card_number: lastDigitsCardNumbers
|
|
50
55
|
}
|
|
51
56
|
},
|
|
52
57
|
permissions: [permission]
|
|
@@ -73,6 +78,7 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
|
|
|
73
78
|
recurringPayment.agreementId = transactionRecord.payment.recurring.agreementId
|
|
74
79
|
recurringPayment.publicId = hash.digest('base64')
|
|
75
80
|
recurringPayment.status = transactionRecord.payment.recurring.status
|
|
81
|
+
recurringPayment.lastDigitsCardNumbers = transactionRecord.payment.recurring.last_digits_card_number
|
|
76
82
|
const [permission] = transactionRecord.permissions
|
|
77
83
|
recurringPayment.bindToEntity(RecurringPayment.definition.relationships.activePermission, permission)
|
|
78
84
|
recurringPayment.bindToEntity(RecurringPayment.definition.relationships.contact, contact)
|
|
@@ -81,6 +87,22 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
|
|
|
81
87
|
return { recurringPayment: null }
|
|
82
88
|
}
|
|
83
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
|
+
|
|
84
106
|
export const processRPResult = async (transactionId, paymentId, createdDate) => {
|
|
85
107
|
const transactionRecord = await retrieveStagedTransaction(transactionId)
|
|
86
108
|
if (await getPaymentJournal(transactionId)) {
|
|
@@ -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)
|