@defra-fish/pocl-job 1.35.0-rc.8 → 1.35.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/pocl-job",
3
- "version": "1.35.0-rc.8",
3
+ "version": "1.35.0",
4
4
  "description": "Post Office Counter Licence sales processor",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,8 +35,8 @@
35
35
  "test": "echo \"Error: run tests from root\" && exit 1"
36
36
  },
37
37
  "dependencies": {
38
- "@defra-fish/business-rules-lib": "1.35.0-rc.8",
39
- "@defra-fish/connectors-lib": "1.35.0-rc.8",
38
+ "@defra-fish/business-rules-lib": "1.35.0",
39
+ "@defra-fish/connectors-lib": "1.35.0",
40
40
  "commander": "^7.2.0",
41
41
  "debug": "^4.3.3",
42
42
  "filesize": "^6.4.0",
@@ -46,5 +46,5 @@
46
46
  "sax-stream": "^1.3.0",
47
47
  "ssh2-sftp-client": "^6.0.1"
48
48
  },
49
- "gitHead": "53f316d007d53e66ac57c4fbfe421f5c3fe6f2f5"
49
+ "gitHead": "be7f472adf337fc2e5b16431f2767defcab16d92"
50
50
  }
@@ -14,7 +14,7 @@ Object {
14
14
  "issueDate": "2020-01-01T14:00:00Z",
15
15
  "licensee": Object {
16
16
  "birthDate": "1989-07-01",
17
- "country": "GB",
17
+ "country": "GB-ENG",
18
18
  "email": "daniel-ricc@example.couk",
19
19
  "firstName": "Daniel",
20
20
  "lastName": "Ricciardo",
@@ -55,7 +55,7 @@ Array [
55
55
  "issueDate": "2020-01-01T14:00:00Z",
56
56
  "licensee": Object {
57
57
  "birthDate": "1989-07-01",
58
- "country": "GB",
58
+ "country": "GB-ENG",
59
59
  "email": "daniel-ricc@example.couk",
60
60
  "firstName": "Daniel",
61
61
  "lastName": "Ricciardo",
@@ -123,7 +123,7 @@ Object {
123
123
  "issueDate": "2020-01-01T14:00:00Z",
124
124
  "licensee": Object {
125
125
  "birthDate": "1989-07-01",
126
- "country": "GB",
126
+ "country": "GB-ENG",
127
127
  "email": "daniel-ricc@example.couk",
128
128
  "firstName": "Daniel",
129
129
  "lastName": "Ricciardo",
@@ -164,7 +164,7 @@ Array [
164
164
  "issueDate": "2020-01-01T14:00:00Z",
165
165
  "licensee": Object {
166
166
  "birthDate": "1989-07-01",
167
- "country": "GB",
167
+ "country": "GB-ENG",
168
168
  "email": "daniel-ricc@example.couk",
169
169
  "firstName": "Daniel",
170
170
  "lastName": "Ricciardo",
@@ -216,7 +216,7 @@ Object {
216
216
  "issueDate": "2020-01-01T14:00:00Z",
217
217
  "licensee": Object {
218
218
  "birthDate": "1989-07-01",
219
- "country": "GB",
219
+ "country": "GB-ENG",
220
220
  "email": "daniel-ricc@example.co.uk",
221
221
  "firstName": "Daniel",
222
222
  "lastName": "Ricciardo",
@@ -273,7 +273,7 @@ Array [
273
273
  "issueDate": "2020-01-01T14:00:00Z",
274
274
  "licensee": Object {
275
275
  "birthDate": "1989-07-01",
276
- "country": "GB",
276
+ "country": "GB-ENG",
277
277
  "email": "daniel-ricc@example.co.uk",
278
278
  "firstName": "Daniel",
279
279
  "lastName": "Ricciardo",
@@ -328,7 +328,7 @@ Array [
328
328
  "issueDate": "2020-01-01T14:00:00Z",
329
329
  "licensee": Object {
330
330
  "birthDate": "1989-07-01",
331
- "country": "GB",
331
+ "country": "GB-ENG",
332
332
  "email": "daniel-ricc@example.co.uk",
333
333
  "firstName": "Daniel",
334
334
  "lastName": "Ricciardo",
@@ -4,13 +4,13 @@ import { salesApi } from '@defra-fish/connectors-lib'
4
4
  jest.mock('@defra-fish/connectors-lib', () => ({
5
5
  salesApi: {
6
6
  getPoclValidationErrorsForProcessing: jest.fn(),
7
- createTransactions: jest.fn(),
7
+ createTransactions: jest.fn(() => [{ statusCode: 201, response: { id: 'test-response-id' } }]),
8
8
  finaliseTransaction: jest.fn(),
9
9
  updatePoclValidationError: jest.fn()
10
10
  }
11
11
  }))
12
12
 
13
- const getPoclValidationError = () => ({
13
+ const getPoclValidationError = overrides => ({
14
14
  id: 'test-pocl-validation-error-id',
15
15
  dataSource: { label: 'Post Office Sales' },
16
16
  serialNumber: '12345-ABCDE',
@@ -21,7 +21,7 @@ const getPoclValidationError = () => ({
21
21
  street: 'Eastmead Lane',
22
22
  town: 'Bristol',
23
23
  postcode: 'BS9 1HJ',
24
- country: 'GB',
24
+ country: { label: 'GB-ENG' },
25
25
  birthDate: '1989-07-01',
26
26
  email: 'daniel-ricc@example.co.uk',
27
27
  mobilePhone: '07722 123456',
@@ -31,14 +31,17 @@ const getPoclValidationError = () => ({
31
31
  postalFulfilment: true,
32
32
  concessions: '[{"type":"Blue Badge","referenceNumber":"123456789"}]',
33
33
  startDate: '2021-06-15',
34
+ startDateUnvalidated: '2021-06-15',
34
35
  permitId: 'test-permit-id',
35
36
  transactionDate: '2020-01-01T14:00:00Z',
36
37
  amount: 30,
37
- paymentSource: 'Post Office Sales',
38
+ paymentSource: { label: 'Post Office Sales' },
39
+ paymentSourceUnvalidated: 'Post Office Sales',
38
40
  channelId: '948594',
39
41
  methodOfPayment: { label: 'Cash' },
40
42
  status: { label: 'Ready for Processing' },
41
- transactionFile: 'test-pocl-file.xml'
43
+ transactionFile: 'test-pocl-file.xml',
44
+ ...overrides
42
45
  })
43
46
 
44
47
  const getFinalisationError = (status, error) => ({
@@ -59,46 +62,118 @@ describe('pocl-validation-errors', () => {
59
62
  })
60
63
 
61
64
  describe('when no validation errors are returned from Sales Api', () => {
62
- let result
63
- beforeEach(async () => {
65
+ beforeAll(() => {
64
66
  salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue(null)
65
- result = await processPoclValidationErrors()
66
67
  })
67
68
 
68
- it('returns undefined', () => {
69
- expect(result).toBe(undefined)
69
+ it('returns undefined', async () => {
70
+ const result = await processPoclValidationErrors()
71
+ expect(result).toBeUndefined()
70
72
  })
71
73
 
72
- it('does not attempt to create transactions', () => {
74
+ it('does not attempt to create transactions', async () => {
75
+ await processPoclValidationErrors()
73
76
  expect(salesApi.createTransactions).not.toBeCalled()
74
77
  })
75
78
 
76
- it('does not finalise transactions which have failed', () => {
79
+ it('does not finalise transactions which have failed', async () => {
80
+ await processPoclValidationErrors()
77
81
  expect(salesApi.finaliseTransaction).not.toBeCalled()
78
82
  })
79
83
  })
80
84
 
81
85
  describe('processes successfully fixed validation errors', () => {
82
- let poclValidationError
83
- beforeEach(async () => {
84
- poclValidationError = getPoclValidationError()
85
- salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
86
- salesApi.createTransactions.mockResolvedValue([{ statusCode: 201, response: { id: 'test-response-id' } }])
86
+ beforeAll(() => {
87
+ salesApi.getPoclValidationErrorsForProcessing.mockImplementation(() => [getPoclValidationError()])
88
+ })
89
+
90
+ it('creates transaction in the Sales Api', async () => {
87
91
  await processPoclValidationErrors()
92
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
93
+ expect(createTransactionPayload).toMatchSnapshot()
88
94
  })
89
95
 
90
- it('creates transaction in the Sales Api', () => {
91
- const validationError = salesApi.createTransactions.mock.calls[0][0][0]
92
- expect(validationError).toMatchSnapshot()
96
+ it('uses startDate if this is provided', async () => {
97
+ const startDate = '2023-07-15'
98
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([getPoclValidationError({ startDate })])
99
+ await processPoclValidationErrors()
100
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
101
+ expect(createTransactionPayload.permissions[0].startDate).toBe(startDate)
93
102
  })
94
103
 
95
- it('finalises the transaction in the Sales Api', () => {
96
- const record = salesApi.finaliseTransaction.mock.calls[0]
104
+ it("uses startDateUnvalidated if startDate isn't provided", async () => {
105
+ const startDateUnvalidated = '2023-07-15'
106
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([
107
+ getPoclValidationError({ startDateUnvalidated, startDate: undefined })
108
+ ])
109
+ await processPoclValidationErrors()
110
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
111
+ expect(createTransactionPayload.permissions[0].startDate).toBe(startDateUnvalidated)
112
+ })
113
+
114
+ it('uses country if this is provided', async () => {
115
+ const country = { label: 'GB-ENG' }
116
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([getPoclValidationError({ country })])
117
+ await processPoclValidationErrors()
118
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
119
+ expect(createTransactionPayload.permissions[0].licensee.country).toBe(country.label)
120
+ })
121
+
122
+ it("uses countryUnvalidated if country isn't provided", async () => {
123
+ const countryUnvalidated = 'England'
124
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([
125
+ getPoclValidationError({ countryUnvalidated, country: undefined })
126
+ ])
127
+ await processPoclValidationErrors()
128
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
129
+ expect(createTransactionPayload.permissions[0].licensee.country).toBe(countryUnvalidated)
130
+ })
131
+
132
+ it('uses paymentSource label if this is provided', async () => {
133
+ const paymentSource = { label: 'Post Office Sales' }
134
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([getPoclValidationError({ paymentSource })])
135
+ await processPoclValidationErrors()
136
+ const finaliseTransaction = salesApi.finaliseTransaction.mock.calls[0][1]
137
+ expect(finaliseTransaction.payment.source).toBe(paymentSource.label)
138
+ })
139
+
140
+ it("uses paymentSourceUnvalidated if paymentSource isn't provided", async () => {
141
+ const paymentSourceUnvalidated = 'Fire Sales'
142
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([
143
+ getPoclValidationError({ paymentSourceUnvalidated, paymentSource: undefined })
144
+ ])
145
+ await processPoclValidationErrors()
146
+ const finaliseTransaction = salesApi.finaliseTransaction.mock.calls[0][1]
147
+ expect(finaliseTransaction.payment.source).toBe(paymentSourceUnvalidated)
148
+ })
149
+
150
+ it('populates the channelId with N/A if not provided', async () => {
151
+ const error = getPoclValidationError()
152
+ delete error.channelId
153
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([error])
154
+ await processPoclValidationErrors()
155
+ const finaliseTransaction = salesApi.finaliseTransaction.mock.calls[0][1]
156
+ expect(finaliseTransaction.payment.channelId).toBe('N/A')
157
+ })
158
+
159
+ it("omits transaction file from payload if one isn't provided", async () => {
160
+ const error = getPoclValidationError()
161
+ delete error.transactionFile
162
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([error])
163
+ await processPoclValidationErrors()
164
+ const finaliseTransaction = salesApi.finaliseTransaction.mock.calls[0][1]
165
+ expect(finaliseTransaction.transactionFile).toBeUndefined()
166
+ })
167
+
168
+ it('finalises the transaction in the Sales Api', async () => {
169
+ await processPoclValidationErrors()
170
+ const [record] = salesApi.finaliseTransaction.mock.calls
97
171
  expect(record).toMatchSnapshot()
98
172
  })
99
173
 
100
- it('updates POCL validation error record in the Sales Api', () => {
101
- const record = salesApi.updatePoclValidationError.mock.calls[0]
174
+ it('updates POCL validation error record in the Sales Api', async () => {
175
+ await processPoclValidationErrors()
176
+ const [record] = salesApi.updatePoclValidationError.mock.calls
102
177
  expect(record).toMatchSnapshot()
103
178
  })
104
179
 
@@ -106,60 +181,197 @@ describe('pocl-validation-errors', () => {
106
181
  salesApi.finaliseTransaction.mockRejectedValueOnce(getFinalisationError(410, 'Gone'))
107
182
  await processPoclValidationErrors()
108
183
 
109
- const record = salesApi.updatePoclValidationError.mock.calls[0]
184
+ const [record] = salesApi.updatePoclValidationError.mock.calls
110
185
  expect(record).toMatchSnapshot()
111
186
  })
112
187
  })
113
188
 
114
189
  describe('processes failed fixes of validation errors', () => {
115
- let poclValidationError
116
- beforeEach(async () => {
117
- poclValidationError = getPoclValidationError()
118
- poclValidationError.email = 'daniel-ricc@example.couk'
119
- salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
120
- salesApi.createTransactions.mockResolvedValue([{ statusCode: 422, message: '"permissions[0].licensee.email" must be a valid email' }])
121
- await processPoclValidationErrors()
190
+ beforeEach(() => {
191
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([getPoclValidationError({ email: 'daniel-ricc@example.couk' })])
192
+ salesApi.createTransactions.mockResolvedValueOnce([
193
+ { statusCode: 422, message: '"permissions[0].licensee.email" must be a valid email' }
194
+ ])
122
195
  })
123
196
 
124
- it('creates transaction in the Sales Api', () => {
125
- const validationError = salesApi.createTransactions.mock.calls[0][0][0]
126
- expect(validationError).toMatchSnapshot()
197
+ it('creates transaction in the Sales Api', async () => {
198
+ await processPoclValidationErrors()
199
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
200
+ expect(createTransactionPayload).toMatchSnapshot()
127
201
  })
128
202
 
129
- it('does not finalise transactions which have failed', () => {
203
+ it('does not finalise transactions which have failed', async () => {
204
+ await processPoclValidationErrors()
130
205
  expect(salesApi.finaliseTransaction).not.toBeCalled()
131
206
  })
132
207
 
133
- it('updates POCL validation error record in the Sales Api', () => {
134
- const record = salesApi.updatePoclValidationError.mock.calls[0]
208
+ it('updates POCL validation error record in the Sales Api', async () => {
209
+ await processPoclValidationErrors()
210
+ const [record] = salesApi.updatePoclValidationError.mock.calls
135
211
  expect(record).toMatchSnapshot()
136
212
  })
137
213
  })
138
214
 
139
215
  describe('processes records which failed during finalising', () => {
140
- let poclValidationError
141
- beforeEach(async () => {
142
- poclValidationError = getPoclValidationError()
143
- poclValidationError.email = 'daniel-ricc@example.couk'
144
- salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
145
- salesApi.createTransactions.mockResolvedValue([{ statusCode: 201, response: { id: 'test-response-id' } }])
216
+ beforeAll(() => {
217
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([getPoclValidationError({ email: 'daniel-ricc@example.couk' })])
218
+ })
219
+
220
+ it('creates transaction in the Sales Api', async () => {
146
221
  salesApi.finaliseTransaction.mockRejectedValueOnce(getFinalisationError(500, 'Internal server error'))
147
222
  await processPoclValidationErrors()
223
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
224
+ expect(createTransactionPayload).toMatchSnapshot()
148
225
  })
149
226
 
150
- it('creates transaction in the Sales Api', () => {
151
- const validationError = salesApi.createTransactions.mock.calls[0][0][0]
152
- expect(validationError).toMatchSnapshot()
227
+ it('attempts to finalise transaction in the Sales Api', async () => {
228
+ salesApi.finaliseTransaction.mockRejectedValueOnce(getFinalisationError(500, 'Internal server error'))
229
+ await processPoclValidationErrors()
230
+ const [record] = salesApi.finaliseTransaction.mock.calls
231
+ expect(record).toMatchSnapshot()
153
232
  })
154
233
 
155
- it('attempts to finalise transaction in the Sales Api', () => {
156
- const record = salesApi.finaliseTransaction.mock.calls[0]
234
+ it('updates POCL validation error record in the Sales Api', async () => {
235
+ salesApi.finaliseTransaction.mockRejectedValueOnce(getFinalisationError(500, 'Internal server error'))
236
+ await processPoclValidationErrors()
237
+ const [record] = salesApi.updatePoclValidationError.mock.calls
157
238
  expect(record).toMatchSnapshot()
158
239
  })
240
+ })
159
241
 
160
- it('updates POCL validation error record in the Sales Api', () => {
161
- const record = salesApi.updatePoclValidationError.mock.calls[0]
162
- expect(record).toMatchSnapshot()
242
+ describe('Create transaction payload datasource', () => {
243
+ it("is set to 'Postal Order Sales' when payment source is 'Postal Order'", async () => {
244
+ const poclValidationError = getPoclValidationError({ dataSource: null, paymentSource: { label: 'Postal Order' } })
245
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([poclValidationError])
246
+ await processPoclValidationErrors()
247
+
248
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
249
+ expect(createTransactionPayload.dataSource).toBe('Postal Order Sales')
250
+ })
251
+
252
+ it("is undefined when payment source is not 'Postal Order'", async () => {
253
+ const poclValidationError = getPoclValidationError({ dataSource: null, paymentSource: { label: 'Magic beans' } })
254
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([poclValidationError])
255
+ await processPoclValidationErrors()
256
+
257
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
258
+ expect(createTransactionPayload.dataSource).toBeUndefined()
259
+ })
260
+
261
+ it('is undefined when payment source is null', async () => {
262
+ const poclValidationError = getPoclValidationError({ dataSource: null, paymentSource: null })
263
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([poclValidationError])
264
+ await processPoclValidationErrors()
265
+
266
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
267
+ expect(createTransactionPayload.dataSource).toBeUndefined()
268
+ })
269
+ })
270
+
271
+ describe('Create transaction payload serial number', () => {
272
+ it("is set to 'Postal Order Sales' when paymentSource is 'Postal Order'", async () => {
273
+ const poclValidationError = getPoclValidationError({ serialNumber: null, paymentSource: { label: 'Postal Order' } })
274
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([poclValidationError])
275
+
276
+ await processPoclValidationErrors()
277
+
278
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
279
+ expect(createTransactionPayload.serialNumber).toBe('Postal Order Sales')
280
+ })
281
+
282
+ it("is undefined if payment source is not 'Postal Order'", async () => {
283
+ const poclValidationError = getPoclValidationError({ serialNumber: null, paymentSource: { label: 'Magic Beans' } })
284
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValueOnce([poclValidationError])
285
+
286
+ await processPoclValidationErrors()
287
+
288
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
289
+ expect(createTransactionPayload.serialNumber).toBeUndefined()
290
+ })
291
+
292
+ it('is undefined if payment source is not set', async () => {
293
+ const poclValidationError = getPoclValidationError({ serialNumber: null, paymentSource: null })
294
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
295
+
296
+ await processPoclValidationErrors()
297
+
298
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
299
+ expect(createTransactionPayload.serialNumber).toBeUndefined()
300
+ })
301
+ })
302
+
303
+ describe('Finalise transaction payload payment method', () => {
304
+ it("is 'Other' if payment source is 'Postal Order'", async () => {
305
+ const poclValidationError = getPoclValidationError({ methodOfPayment: null, paymentSource: { label: 'Postal Order' } })
306
+ poclValidationError.methodOfPayment = null
307
+ poclValidationError.paymentSource = { label: 'Postal Order' }
308
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
309
+
310
+ await processPoclValidationErrors()
311
+
312
+ const finaliseTransactionPayload = salesApi.finaliseTransaction.mock.calls[0][1]
313
+ expect(finaliseTransactionPayload.payment.method).toBe('Other')
314
+ })
315
+
316
+ it("is undefined if payment source is not 'Postal Order'", async () => {
317
+ const poclValidationError = getPoclValidationError({ methodOfPayment: null, paymentSource: { label: 'Magic Beans' } })
318
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
319
+
320
+ await processPoclValidationErrors()
321
+
322
+ const finaliseTransactionPayload = salesApi.finaliseTransaction.mock.calls[0][1]
323
+ expect(finaliseTransactionPayload.payment.method).toBeUndefined()
324
+ })
325
+
326
+ it('is undefined if payment source is not set', async () => {
327
+ const poclValidationError = getPoclValidationError({ methodOfPayment: null, paymentSource: null })
328
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
329
+
330
+ await processPoclValidationErrors()
331
+
332
+ const finaliseTransactionPayload = salesApi.finaliseTransaction.mock.calls[0][1]
333
+ expect(finaliseTransactionPayload.payment.method).toBeUndefined()
334
+ })
335
+ })
336
+
337
+ describe('when a date is not in ISO format', () => {
338
+ it('converts the issueDate to ISO format without milliseconds', async () => {
339
+ const poclValidationError = getPoclValidationError()
340
+ poclValidationError.transactionDate = '01/01/2023'
341
+
342
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
343
+ salesApi.createTransactions.mockResolvedValue([{ statusCode: 201, response: { id: 'test-response-id' } }])
344
+ await processPoclValidationErrors()
345
+
346
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
347
+ const expectedDate = '2023-01-01T00:00:00Z'
348
+ expect(createTransactionPayload.permissions[0].issueDate).toBe(expectedDate)
349
+ })
350
+
351
+ it('converts the startDate to ISO format without milliseconds', async () => {
352
+ const poclValidationError = getPoclValidationError()
353
+ poclValidationError.startDate = '01/01/2023'
354
+
355
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
356
+ salesApi.createTransactions.mockResolvedValue([{ statusCode: 201, response: { id: 'test-response-id' } }])
357
+ await processPoclValidationErrors()
358
+
359
+ const [[[createTransactionPayload]]] = salesApi.createTransactions.mock.calls
360
+ const expectedDate = '2023-01-01T00:00:00Z'
361
+ expect(createTransactionPayload.permissions[0].startDate).toBe(expectedDate)
362
+ })
363
+
364
+ it('converts the payment timestamp to ISO format without milliseconds', async () => {
365
+ const poclValidationError = getPoclValidationError()
366
+ poclValidationError.transactionDate = '01/01/2023'
367
+
368
+ salesApi.getPoclValidationErrorsForProcessing.mockResolvedValue([poclValidationError])
369
+ salesApi.createTransactions.mockResolvedValue([{ statusCode: 201, response: { id: 'test-response-id' } }])
370
+ await processPoclValidationErrors()
371
+
372
+ const finaliseTransactionPayload = salesApi.finaliseTransaction.mock.calls[0][1]
373
+ const expectedDate = '2023-01-01T00:00:00Z'
374
+ expect(finaliseTransactionPayload.payment.timestamp).toBe(expectedDate)
163
375
  })
164
376
  })
165
377
  })
@@ -4,6 +4,9 @@ export const MAX_FINALISE_TRANSACTION_BATCH_SIZE = 10
4
4
  export const POST_OFFICE_DATASOURCE = 'Post Office Sales'
5
5
  export const DIRECT_DEBIT_DATASOURCE = 'DDE File'
6
6
  export const DIRECT_DEBIT_PAYMENTSOURCE = 'Direct Debit'
7
+ export const POSTAL_ORDER_DATASOURCE = 'Postal Order Sales'
8
+ export const POSTAL_ORDER_PAYMENTSOURCE = 'Postal Order'
9
+ export const POSTAL_ORDER_PAYMENTMETHOD = 'Other'
7
10
 
8
11
  export const FILE_STAGE = {
9
12
  Pending: 'Received and Pending',
@@ -1,4 +1,6 @@
1
1
  import { salesApi } from '@defra-fish/connectors-lib'
2
+ import { POSTAL_ORDER_DATASOURCE, POSTAL_ORDER_PAYMENTSOURCE, POSTAL_ORDER_PAYMENTMETHOD } from './constants.js'
3
+ import moment from 'moment'
2
4
  import db from 'debug'
3
5
  const debug = db('pocl:validation-errors')
4
6
 
@@ -6,8 +8,8 @@ const mapRecords = records =>
6
8
  records.map(record => ({
7
9
  poclValidationErrorId: record.id,
8
10
  createTransactionPayload: {
9
- dataSource: record.dataSource.label,
10
- serialNumber: record.serialNumber,
11
+ dataSource: backfillDataSource(record),
12
+ serialNumber: backfillSerialNumber(record),
11
13
  permissions: [
12
14
  {
13
15
  licensee: {
@@ -22,38 +24,88 @@ const mapRecords = records =>
22
24
  locality: record.locality,
23
25
  town: record.town,
24
26
  postcode: record.postcode,
25
- country: record.country,
27
+ country: getCountryValue(record),
26
28
  preferredMethodOfConfirmation: record.preferredMethodOfConfirmation.label,
27
29
  preferredMethodOfNewsletter: record.preferredMethodOfNewsletter.label,
28
30
  preferredMethodOfReminder: record.preferredMethodOfReminder.label,
29
31
  postalFulfilment: record.postalFulfilment
30
32
  },
31
- issueDate: record.transactionDate,
32
- startDate: record.startDate,
33
- permitId: record.permitId,
33
+ issueDate: formatDateToShortenedISO(record.transactionDate),
34
+ startDate: formatDateToShortenedISO(record.startDate || record.startDateUnvalidated),
35
+ permitId: record.permitId.replace(/(^\{|\}$)/g, '').toLowerCase(),
34
36
  ...(record.concessions && { concessions: JSON.parse(record.concessions) })
35
37
  }
36
38
  ]
37
39
  },
38
40
  finaliseTransactionPayload: {
39
- transactionFile: record.transactionFile,
41
+ ...(record.transactionFile ? { transactionFile: record.transactionFile } : {}),
40
42
  payment: {
41
- timestamp: record.transactionDate,
43
+ timestamp: formatDateToShortenedISO(record.transactionDate),
42
44
  amount: record.amount,
43
- source: record.paymentSource,
44
- channelId: record.channelId,
45
- method: record.methodOfPayment.label
45
+ source: getSourceValue(record),
46
+ channelId: record.channelId || 'N/A',
47
+ method: backfillPaymentMethod(record.methodOfPayment, record.paymentSource)
46
48
  }
47
49
  }
48
50
  }))
49
51
 
52
+ const backfillDataSource = record => {
53
+ if (record.dataSource) {
54
+ return record.dataSource.label
55
+ } else if (record.paymentSource && record.paymentSource.label === POSTAL_ORDER_PAYMENTSOURCE) {
56
+ return POSTAL_ORDER_DATASOURCE
57
+ }
58
+ return undefined
59
+ }
60
+
61
+ const backfillSerialNumber = record => {
62
+ if (record.serialNumber) {
63
+ return record.serialNumber
64
+ } else if (record.paymentSource && record.paymentSource.label === POSTAL_ORDER_PAYMENTSOURCE) {
65
+ return POSTAL_ORDER_DATASOURCE
66
+ }
67
+ return undefined
68
+ }
69
+
70
+ const backfillPaymentMethod = (method, paymentSource) => {
71
+ if (method) {
72
+ return method.label
73
+ } else if (paymentSource && paymentSource.label === POSTAL_ORDER_PAYMENTSOURCE) {
74
+ return POSTAL_ORDER_PAYMENTMETHOD
75
+ }
76
+ return undefined
77
+ }
78
+
79
+ const getCountryValue = record => {
80
+ if (record.country && record.country.label) {
81
+ return record.country.label
82
+ }
83
+ return record.countryUnvalidated
84
+ }
85
+
86
+ const getSourceValue = record => {
87
+ if (record.paymentSource && record.paymentSource.label) {
88
+ return record.paymentSource.label
89
+ }
90
+ return record.paymentSourceUnvalidated
91
+ }
92
+
93
+ const formatDateToShortenedISO = date => {
94
+ const manuallyEnteredDateFormat = /[0-9]{2}\/[0-9]{2}\/[0-9]{4}/
95
+
96
+ if (date.match(manuallyEnteredDateFormat)) {
97
+ return moment(date, 'DD/MM/YYYY').toDate().toISOString().split('.')[0] + 'Z'
98
+ }
99
+ return date
100
+ }
101
+
50
102
  /**
51
103
  * Calls Sales Api to update Dynamics record
52
104
  * @param {Array<Object>} failed
53
105
  */
54
106
  const processFailed = async failed => {
55
107
  for (const { record, result } of failed) {
56
- debug('Failed when reprocessing record: %o', record)
108
+ debug('Failed when reprocessing record: %s', JSON.stringify(record))
57
109
  await salesApi.updatePoclValidationError(record.poclValidationErrorId, { ...record, errorMessage: result.message })
58
110
  }
59
111
  }
@@ -78,26 +130,36 @@ const createTransactions = async records => {
78
130
  return { succeeded, failed }
79
131
  }
80
132
 
133
+ const finaliseTransaction = async rec => {
134
+ const payment = rec.record.finaliseTransactionPayload.payment
135
+ const finaliseTransactionPayload = {
136
+ payment: { method: backfillPaymentMethod(payment.method, payment.paymentSource) },
137
+ ...rec.record.finaliseTransactionPayload
138
+ }
139
+ debug('finalising transaction: %o', finaliseTransactionPayload)
140
+ return salesApi.finaliseTransaction(rec.result.response.id, finaliseTransactionPayload)
141
+ }
142
+
143
+ const isFulfilled = result => (result?.status === 'fulfilled' || result?.status?.id === 'FINALISED') === true
144
+
81
145
  const finaliseTransactions = async records => {
82
146
  const { succeeded: created, failed } = records
83
- const finalisationResults = await Promise.allSettled(
84
- created.map(rec => salesApi.finaliseTransaction(rec.result.response.id, rec.record.finaliseTransactionPayload))
85
- )
147
+ const finalisationResults = await Promise.allSettled(created.map(rec => finaliseTransaction(rec)))
86
148
 
87
149
  const succeeded = []
88
150
  created.forEach(({ record }, idx) => {
89
151
  const result = finalisationResults[idx]
90
- if (result.status === 'fulfilled') {
152
+ if (isFulfilled(result)) {
91
153
  succeeded.push({ record, result: result.value })
92
- } else if (result.reason.status === 410) {
154
+ } else if (result?.reason?.status === 410) {
93
155
  /*
94
156
  HTTP-410 errors indicate that the record has already been finalised. This can occur if the process is terminated while finalising records
95
157
  (between the API call and the database update.) As the transaction has already been finalised, treat these as successful. The data for the
96
158
  previously finalised record is returned under the data key of the error structure returned by the Sales API
97
159
  */
98
- succeeded.push({ record, result: result.reason.body.data })
160
+ succeeded.push({ record, result: result?.reason?.body?.data })
99
161
  } else {
100
- failed.push({ record, result: result.reason })
162
+ failed.push({ record, result: result?.reason })
101
163
  }
102
164
  })
103
165