@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra-fish/sales-api-service",
3
- "version": "1.63.0-rc.8",
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-rc.8",
39
- "@defra-fish/connectors-lib": "1.63.0-rc.8",
40
- "@defra-fish/dynamics-lib": "1.63.0-rc.8",
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": "4376fb63f6e1867708241521e43c4bce5791e08a"
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: 'Catharine Place',
36
+ street: 'Test Street',
37
37
  locality: null,
38
- town: 'Bath',
39
- postcode: 'BA1 2PR'
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: 'test'
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', () => {
@@ -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
- console.log('Server started at %s. Listening on %s', moment().toISOString(), server.info.uri)
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().subtract(JUNIOR_MAX_AGE, 'years').format('YYYY-MM-DD')
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 is thirty one days after issue date',
524
+ 'start date equals issue date',
513
525
  {
514
- startDate: '2024-12-14T00:00:00.000Z',
515
- issueDate: '2024-11-12T15:00:45.922Z',
516
- endDate: '2025-12-13T23:59:59.999Z'
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 log a RecurringPayment record when there is one match', async () => {
879
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn())
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(consoleLogSpy).toHaveBeenCalledWith('RecurringPayment for cancellation: ', recurringPayment)
901
+ expect(persist).toHaveBeenCalledWith([expect.objectContaining(expectedUpdatedRecurringPayment)])
886
902
  })
887
903
 
888
- it('should log no matches when there are no matches', async () => {
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 { isJunior, isSenior, SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib'
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 diff = issue.diff(dob, 'years', true)
91
+ const seventeenthBirthday = dob.clone().add(ADULT_AGE, 'years')
92
+ const sixtysixthBirthday = dob.clone().add(SENIOR_AGE, 'years')
89
93
 
90
- if (isJunior(diff)) {
94
+ if (issue.isBefore(seventeenthBirthday)) {
91
95
  return 'J'
92
- } else if (isSenior(diff)) {
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
- RecurringPayment,
7
- dynamicsClient
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 { ADVANCED_PURCHASE_MAX_DAYS, PAYMENT_JOURNAL_STATUS_CODES, PAYMENT_TYPE, TRANSACTION_SOURCE } from '@defra-fish/business-rules-lib'
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)) && mStart.isSameOrBefore(moment(issueDate).add(ADVANCED_PURCHASE_MAX_DAYS, 'days'), 'day')) {
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
- console.log('RecurringPayment for cancellation: ', recurringPayment)
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
- console.log('No matches found for cancellation')
177
+ throw new Error('Invalid id provided for recurring payment cancellation')
171
178
  }
172
179
  }
173
180