@defra-fish/sales-api-service 1.63.0-rc.8 → 1.63.0
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 +19 -3
- package/src/server/__tests__/server.spec.js +27 -1
- package/src/server/server.js +13 -1
- package/src/services/__tests__/permissions.service.spec.js +3 -1
- package/src/services/__tests__/recurring-payments.service.spec.js +28 -15
- package/src/services/permissions.service.js +10 -5
- package/src/services/recurring-payments.service.js +13 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/sales-api-service",
|
|
3
|
-
"version": "1.63.0
|
|
3
|
+
"version": "1.63.0",
|
|
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.63.0
|
|
39
|
-
"@defra-fish/connectors-lib": "1.63.0
|
|
40
|
-
"@defra-fish/dynamics-lib": "1.63.0
|
|
38
|
+
"@defra-fish/business-rules-lib": "1.63.0",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.63.0",
|
|
40
|
+
"@defra-fish/dynamics-lib": "1.63.0",
|
|
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": "7cfb8ef668002fc340b755dd3aaa4572063e115c"
|
|
56
56
|
}
|
|
@@ -33,10 +33,10 @@ const getResponseSampleData = () => ({
|
|
|
33
33
|
mobilePhone: null,
|
|
34
34
|
organisation: null,
|
|
35
35
|
premises: '1',
|
|
36
|
-
street: '
|
|
36
|
+
street: 'Test Street',
|
|
37
37
|
locality: null,
|
|
38
|
-
town: '
|
|
39
|
-
postcode: '
|
|
38
|
+
town: 'Testville',
|
|
39
|
+
postcode: 'TE1 1ST'
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
activePermission: {
|
|
@@ -166,3 +166,19 @@ describe('cancelRecurringPaymentRequestParamsSchema', () => {
|
|
|
166
166
|
expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData).rejects.toThrow())
|
|
167
167
|
})
|
|
168
168
|
})
|
|
169
|
+
|
|
170
|
+
describe('cancelRecurringPaymentRequestParamsSchema', () => {
|
|
171
|
+
it('validates expected object', async () => {
|
|
172
|
+
const sampleData = { id: 'abc123' }
|
|
173
|
+
expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData)).not.toThrow()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('throws an error if id missing', async () => {
|
|
177
|
+
expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync({}).rejects.toThrow())
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('throws an error if id is not the correct type', async () => {
|
|
181
|
+
const sampleData = { id: 99 }
|
|
182
|
+
expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData).rejects.toThrow())
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -2,9 +2,19 @@ import initialiseServer from '../server.js'
|
|
|
2
2
|
import Boom from '@hapi/boom'
|
|
3
3
|
import dotProp from 'dot-prop'
|
|
4
4
|
import { SERVER } from '../../config.js'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
|
|
7
|
+
jest.mock('fs', () => {
|
|
8
|
+
const actual = jest.requireActual('fs')
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
readFileSync: jest.fn(() => JSON.stringify({ name: 'sales-api-test', version: '1.2.3' }))
|
|
12
|
+
}
|
|
13
|
+
})
|
|
5
14
|
|
|
6
15
|
describe('hapi server', () => {
|
|
7
16
|
describe('initialisation', () => {
|
|
17
|
+
const serverInfoUri = 'test'
|
|
8
18
|
let serverConfigSpy
|
|
9
19
|
beforeAll(() => {
|
|
10
20
|
const Hapi = jest.requireActual('@hapi/hapi')
|
|
@@ -15,7 +25,7 @@ describe('hapi server', () => {
|
|
|
15
25
|
register: jest.fn(),
|
|
16
26
|
route: jest.fn(),
|
|
17
27
|
info: {
|
|
18
|
-
uri:
|
|
28
|
+
uri: serverInfoUri
|
|
19
29
|
},
|
|
20
30
|
listener: {}
|
|
21
31
|
}))
|
|
@@ -48,6 +58,22 @@ describe('hapi server', () => {
|
|
|
48
58
|
headersTimeout: 5123
|
|
49
59
|
})
|
|
50
60
|
})
|
|
61
|
+
|
|
62
|
+
it('logs startup details including name and version', async () => {
|
|
63
|
+
const mockPkg = { name: 'sales-api-test', version: '1.2.3' }
|
|
64
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockPkg))
|
|
65
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
66
|
+
|
|
67
|
+
await initialiseServer({ port: 4000 })
|
|
68
|
+
|
|
69
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
70
|
+
expect.stringContaining('Server started at %s. Listening on %s. name: %s. version: %s'),
|
|
71
|
+
expect.any(String),
|
|
72
|
+
serverInfoUri,
|
|
73
|
+
mockPkg.name,
|
|
74
|
+
mockPkg.version
|
|
75
|
+
)
|
|
76
|
+
})
|
|
51
77
|
})
|
|
52
78
|
|
|
53
79
|
describe('configuration', () => {
|
package/src/server/server.js
CHANGED
|
@@ -8,6 +8,8 @@ import Boom from '@hapi/boom'
|
|
|
8
8
|
import { SERVER } from '../config.js'
|
|
9
9
|
import moment from 'moment'
|
|
10
10
|
import { airbrake } from '@defra-fish/connectors-lib'
|
|
11
|
+
import path from 'path'
|
|
12
|
+
import fs from 'fs'
|
|
11
13
|
|
|
12
14
|
export default async (opts = { port: SERVER.Port }) => {
|
|
13
15
|
airbrake.initialise()
|
|
@@ -49,7 +51,17 @@ export default async (opts = { port: SERVER.Port }) => {
|
|
|
49
51
|
server.route(Routes)
|
|
50
52
|
|
|
51
53
|
await server.start()
|
|
52
|
-
|
|
54
|
+
|
|
55
|
+
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
57
|
+
|
|
58
|
+
console.log(
|
|
59
|
+
'Server started at %s. Listening on %s. name: %s. version: %s',
|
|
60
|
+
moment().toISOString(),
|
|
61
|
+
server.info.uri,
|
|
62
|
+
pkg.name,
|
|
63
|
+
pkg.version
|
|
64
|
+
)
|
|
53
65
|
|
|
54
66
|
const shutdown = async code => {
|
|
55
67
|
await server.stop()
|
|
@@ -62,7 +62,9 @@ describe('permissions service', () => {
|
|
|
62
62
|
const number = await generatePermissionNumber(
|
|
63
63
|
getSamplePermission({
|
|
64
64
|
permitId: MOCK_12MONTH_DISABLED_PERMIT.id,
|
|
65
|
-
birthDate: moment()
|
|
65
|
+
birthDate: moment()
|
|
66
|
+
.subtract(JUNIOR_MAX_AGE + 1, 'years')
|
|
67
|
+
.format('YYYY-MM-DD')
|
|
66
68
|
}),
|
|
67
69
|
'Telesales'
|
|
68
70
|
)
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
findRecurringPaymentsByAgreementId,
|
|
6
6
|
findById,
|
|
7
7
|
Permission,
|
|
8
|
+
persist,
|
|
8
9
|
RecurringPayment
|
|
9
10
|
} from '@defra-fish/dynamics-lib'
|
|
10
11
|
import {
|
|
@@ -48,7 +49,8 @@ jest.mock('@defra-fish/dynamics-lib', () => ({
|
|
|
48
49
|
findRecurringPaymentsByAgreementId: jest.fn(() => ({ toRetrieveRequest: () => {} })),
|
|
49
50
|
dynamicsClient: {
|
|
50
51
|
retrieveMultipleRequest: jest.fn(() => ({ value: [] }))
|
|
51
|
-
}
|
|
52
|
+
},
|
|
53
|
+
persist: jest.fn()
|
|
52
54
|
}))
|
|
53
55
|
|
|
54
56
|
jest.mock('@defra-fish/connectors-lib', () => ({
|
|
@@ -93,7 +95,6 @@ jest.mock('../../services/paymentjournals/payment-journals.service.js', () => ({
|
|
|
93
95
|
}))
|
|
94
96
|
|
|
95
97
|
jest.mock('@defra-fish/business-rules-lib', () => ({
|
|
96
|
-
ADVANCED_PURCHASE_MAX_DAYS: 30,
|
|
97
98
|
PAYMENT_JOURNAL_STATUS_CODES: {
|
|
98
99
|
InProgress: 'InProgressCode',
|
|
99
100
|
Cancelled: 'CancelledCode',
|
|
@@ -453,6 +454,17 @@ describe('recurring payments service', () => {
|
|
|
453
454
|
'2025-11-12T00:00:00.000Z',
|
|
454
455
|
'3456'
|
|
455
456
|
],
|
|
457
|
+
[
|
|
458
|
+
'starts thirty-one days after issue date - next due on issue date plus one year',
|
|
459
|
+
'9o8u7yhui89u8i9oiu8i8u7yhu',
|
|
460
|
+
{
|
|
461
|
+
startDate: '2024-12-14T00:00:00.000Z',
|
|
462
|
+
issueDate: '2024-11-12T15:00:45.922Z',
|
|
463
|
+
endDate: '2025-12-13T23:59:59.999Z'
|
|
464
|
+
},
|
|
465
|
+
'2025-11-12T00:00:00.000Z',
|
|
466
|
+
'4321'
|
|
467
|
+
],
|
|
456
468
|
[
|
|
457
469
|
"issued on 29th Feb '24, starts on 30th March '24 - next due on 28th Feb '25",
|
|
458
470
|
'hy7u8ijhyu78jhyu8iu8hjiujn',
|
|
@@ -509,11 +521,11 @@ describe('recurring payments service', () => {
|
|
|
509
521
|
|
|
510
522
|
it.each([
|
|
511
523
|
[
|
|
512
|
-
'start date
|
|
524
|
+
'start date equals issue date',
|
|
513
525
|
{
|
|
514
|
-
startDate: '2024-
|
|
515
|
-
issueDate: '2024-11-
|
|
516
|
-
endDate: '2025-
|
|
526
|
+
startDate: '2024-11-11T00:00:00.000Z',
|
|
527
|
+
issueDate: '2024-11-11T00:00:00.000Z',
|
|
528
|
+
endDate: '2025-11-10T23:59:59.999Z'
|
|
517
529
|
}
|
|
518
530
|
],
|
|
519
531
|
[
|
|
@@ -870,28 +882,29 @@ describe('recurring payments service', () => {
|
|
|
870
882
|
|
|
871
883
|
describe('cancelRecurringPayment', () => {
|
|
872
884
|
it('should call findById with RecurringPayment and the provided id', async () => {
|
|
885
|
+
findById.mockReturnValueOnce(getMockRecurringPayment())
|
|
873
886
|
const id = 'abc123'
|
|
874
887
|
await cancelRecurringPayment(id)
|
|
875
888
|
expect(findById).toHaveBeenCalledWith(RecurringPayment, id)
|
|
876
889
|
})
|
|
877
890
|
|
|
878
|
-
it('should
|
|
879
|
-
const
|
|
880
|
-
const recurringPayment = { entity: getMockRecurringPayment() }
|
|
891
|
+
it('should call persist with the updated RecurringPayment', async () => {
|
|
892
|
+
const recurringPayment = getMockRecurringPayment()
|
|
881
893
|
findById.mockReturnValueOnce(recurringPayment)
|
|
882
894
|
|
|
895
|
+
const cancelledDate = new Date().toISOString().split('T')[0]
|
|
896
|
+
const cancelledReason = { description: 'Payment Failure', id: 910400002, label: 'Payment Failure' }
|
|
897
|
+
const expectedUpdatedRecurringPayment = { ...recurringPayment, cancelledReason, cancelledDate }
|
|
898
|
+
|
|
883
899
|
await cancelRecurringPayment('id')
|
|
884
900
|
|
|
885
|
-
expect(
|
|
901
|
+
expect(persist).toHaveBeenCalledWith([expect.objectContaining(expectedUpdatedRecurringPayment)])
|
|
886
902
|
})
|
|
887
903
|
|
|
888
|
-
it('should
|
|
889
|
-
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn())
|
|
904
|
+
it('should raise an error when there are no matches', async () => {
|
|
890
905
|
findById.mockReturnValueOnce(undefined)
|
|
891
906
|
|
|
892
|
-
await cancelRecurringPayment('id')
|
|
893
|
-
|
|
894
|
-
expect(consoleLogSpy).toHaveBeenCalledWith('No matches found for cancellation')
|
|
907
|
+
await expect(cancelRecurringPayment('id')).rejects.toThrow('Invalid id provided for recurring payment cancellation')
|
|
895
908
|
})
|
|
896
909
|
})
|
|
897
910
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Permission, Permit } from '@defra-fish/dynamics-lib'
|
|
2
|
-
import {
|
|
2
|
+
import { SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib'
|
|
3
3
|
import { getGlobalOptionSetValue, getReferenceDataForEntityAndId } from './reference-data.service.js'
|
|
4
4
|
import { redis } from './ioredis.service.js'
|
|
5
5
|
import moment from 'moment-timezone'
|
|
@@ -76,6 +76,9 @@ export const calculateEndDateMoment = async ({ permitId, startDate }) => {
|
|
|
76
76
|
*/
|
|
77
77
|
export const calculateEndDate = async ({ permitId, startDate }) => (await calculateEndDateMoment({ permitId, startDate })).toISOString()
|
|
78
78
|
|
|
79
|
+
const ADULT_AGE = 17
|
|
80
|
+
const SENIOR_AGE = 66
|
|
81
|
+
|
|
79
82
|
/**
|
|
80
83
|
* Determine the appropriate age category code for use in a permission number
|
|
81
84
|
* @param birthDate The birth date of the licensee
|
|
@@ -85,14 +88,16 @@ export const calculateEndDate = async ({ permitId, startDate }) => (await calcul
|
|
|
85
88
|
const getAgeCategory = (birthDate, issueDate) => {
|
|
86
89
|
const dob = moment(birthDate)
|
|
87
90
|
const issue = moment(issueDate)
|
|
88
|
-
const
|
|
91
|
+
const seventeenthBirthday = dob.clone().add(ADULT_AGE, 'years')
|
|
92
|
+
const sixtysixthBirthday = dob.clone().add(SENIOR_AGE, 'years')
|
|
89
93
|
|
|
90
|
-
if (
|
|
94
|
+
if (issue.isBefore(seventeenthBirthday)) {
|
|
91
95
|
return 'J'
|
|
92
|
-
} else if (
|
|
96
|
+
} else if (issue.isSameOrAfter(sixtysixthBirthday)) {
|
|
93
97
|
return 'S'
|
|
98
|
+
} else {
|
|
99
|
+
return 'F'
|
|
94
100
|
}
|
|
95
|
-
return 'F'
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/**
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
+
dynamicsClient,
|
|
2
3
|
executeQuery,
|
|
3
4
|
findById,
|
|
4
5
|
findDueRecurringPayments,
|
|
5
6
|
findRecurringPaymentsByAgreementId,
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
persist,
|
|
8
|
+
RecurringPayment
|
|
8
9
|
} from '@defra-fish/dynamics-lib'
|
|
9
10
|
import { calculateEndDate, generatePermissionNumber } from './permissions.service.js'
|
|
10
11
|
import { getObfuscatedDob } from './contacts.service.js'
|
|
11
12
|
import { createHash } from 'node:crypto'
|
|
12
|
-
import {
|
|
13
|
+
import { PAYMENT_JOURNAL_STATUS_CODES, PAYMENT_TYPE, TRANSACTION_SOURCE } from '@defra-fish/business-rules-lib'
|
|
13
14
|
import { TRANSACTION_STAGING_TABLE, TRANSACTION_QUEUE } from '../config.js'
|
|
14
15
|
import { TRANSACTION_STATUS } from '../services/transactions/constants.js'
|
|
15
16
|
import { retrieveStagedTransaction } from '../services/transactions/retrieve-transaction.js'
|
|
16
17
|
import { createPaymentJournal, getPaymentJournal, updatePaymentJournal } from '../services/paymentjournals/payment-journals.service.js'
|
|
18
|
+
import { getGlobalOptionSetValue } from './reference-data.service.js'
|
|
17
19
|
import moment from 'moment'
|
|
18
20
|
import { AWS, govUkPayApi } from '@defra-fish/connectors-lib'
|
|
19
21
|
import db from 'debug'
|
|
@@ -24,7 +26,7 @@ export const getRecurringPayments = date => executeQuery(findDueRecurringPayment
|
|
|
24
26
|
|
|
25
27
|
const getNextDueDate = (startDate, issueDate, endDate) => {
|
|
26
28
|
const mStart = moment(startDate)
|
|
27
|
-
if (mStart.isAfter(moment(issueDate))
|
|
29
|
+
if (mStart.isAfter(moment(issueDate))) {
|
|
28
30
|
if (mStart.isSame(moment(issueDate), 'day')) {
|
|
29
31
|
return moment(startDate).add(1, 'year').subtract(10, 'days').startOf('day').toISOString()
|
|
30
32
|
}
|
|
@@ -165,9 +167,14 @@ export const findNewestExistingRecurringPaymentInCrm = async agreementId => {
|
|
|
165
167
|
export const cancelRecurringPayment = async id => {
|
|
166
168
|
const recurringPayment = await findById(RecurringPayment, id)
|
|
167
169
|
if (recurringPayment) {
|
|
168
|
-
|
|
170
|
+
const data = recurringPayment
|
|
171
|
+
data.cancelledDate = new Date().toISOString().split('T')[0]
|
|
172
|
+
data.cancelledReason = await getGlobalOptionSetValue(RecurringPayment.definition.mappings.cancelledReason.ref, 'Payment Failure')
|
|
173
|
+
const updatedRecurringPayment = Object.assign(new RecurringPayment(), data)
|
|
174
|
+
await persist([updatedRecurringPayment])
|
|
175
|
+
return updatedRecurringPayment
|
|
169
176
|
} else {
|
|
170
|
-
|
|
177
|
+
throw new Error('Invalid id provided for recurring payment cancellation')
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
|