@mojaloop/central-ledger 15.3.0-snapshot.2 → 16.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [16.2.0](https://github.com/mojaloop/central-ledger/compare/v16.1.0...v16.2.0) (2022-08-15)
6
+
7
+
8
+ ### Features
9
+
10
+ * **mojaloop/#2801:** add fulfil timestamp validation and more error handling ([#916](https://github.com/mojaloop/central-ledger/issues/916)) ([336a0a2](https://github.com/mojaloop/central-ledger/commit/336a0a27e908eedeb0dcf8b171ad8c0edfb4c3d8)), closes [mojaloop/#2801](https://github.com/mojaloop/project/issues/2801)
11
+
12
+ ## [16.1.0](https://github.com/mojaloop/central-ledger/compare/v16.0.0...v16.1.0) (2022-08-11)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **mojaloop/#2796:** duplicate transaction not getting callback for post /bulkTransfers (not forked) ([#915](https://github.com/mojaloop/central-ledger/issues/915)) ([e520aa5](https://github.com/mojaloop/central-ledger/commit/e520aa5c5e748a051534f69f5734918f0e02f379)), closes [mojaloop/#2796](https://github.com/mojaloop/project/issues/2796)
18
+
19
+ ## [16.0.0](https://github.com/mojaloop/central-ledger/compare/v15.2.0...v16.0.0) (2022-08-05)
20
+
21
+
22
+ ### ⚠ BREAKING CHANGES
23
+
24
+ * rework bulk handler validation (#913)
25
+
26
+ ### Refactors
27
+
28
+ * rework bulk handler validation ([#913](https://github.com/mojaloop/central-ledger/issues/913)) ([38d29fd](https://github.com/mojaloop/central-ledger/commit/38d29fde13128fa656b6a3cfd71c05ba52b92994))
29
+
5
30
  ## [15.2.0](https://github.com/mojaloop/central-ledger/compare/v15.1.3...v15.2.0) (2022-08-01)
6
31
 
7
32
 
package/CODEOWNERS CHANGED
@@ -5,7 +5,7 @@
5
5
  ## @global-owner1 and @global-owner2 will be requested for
6
6
  ## review when someone opens a pull request.
7
7
  #* @global-owner1 @global-owner2
8
- * @vgenev @mdebarros @elnyry-sam-k @lewisdaly @oderayi @shashi165 @vijayg10
8
+ * @mdebarros @elnyry-sam-k @vijayg10 @kleyow
9
9
  ## Order is important; the last matching pattern takes the most
10
10
  ## precedence. When someone opens a pull request that only
11
11
  ## modifies JS files, only @js-owner and not the global
package/audit-ci.jsonc CHANGED
@@ -18,6 +18,7 @@
18
18
  "GHSA-g64q-3vg8-8f93",
19
19
  "GHSA-5854-jvxx-2cg9",
20
20
  "GHSA-w5p7-h5w8-2hfq",
21
- "GHSA-p9pc-299p-vxgp"
21
+ "GHSA-p9pc-299p-vxgp",
22
+ "GHSA-f825-f98c-gj3g"
22
23
  ]
23
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-ledger",
3
- "version": "15.3.0-snapshot.2",
3
+ "version": "16.2.0",
4
4
  "description": "Central ledger hosted by a scheme to record and settle transfers",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
@@ -26,6 +26,10 @@
26
26
  "engines": {
27
27
  "node": "=16.x"
28
28
  },
29
+ "imports": {
30
+ "#src/*": "./src/*.js",
31
+ "#test/*": "./test/*.js"
32
+ },
29
33
  "pre-commit": [
30
34
  "lint",
31
35
  "dep:check",
@@ -62,6 +66,7 @@
62
66
  "docker:down": "docker-compose -f docker-compose.yml down -v",
63
67
  "docker:clean": "docker-compose -f docker-compose.yml down --rmi local",
64
68
  "generate-docs": "npx jsdoc -c jsdoc.json",
69
+ "audit:fix": "npm audit fix",
65
70
  "audit:check": "npx audit-ci --config ./audit-ci.jsonc",
66
71
  "dep:check": "npx ncu -e 2",
67
72
  "dep:update": "npx ncu -u",
@@ -80,7 +85,7 @@
80
85
  "@mojaloop/central-services-health": "14.0.1",
81
86
  "@mojaloop/central-services-logger": "11.0.1",
82
87
  "@mojaloop/central-services-metrics": "12.0.5",
83
- "@mojaloop/central-services-shared": "17.0.2",
88
+ "@mojaloop/central-services-shared": "17.3.0",
84
89
  "@mojaloop/central-services-stream": "11.0.0",
85
90
  "@mojaloop/event-sdk": "11.0.2",
86
91
  "@mojaloop/ml-number": "11.2.1",
@@ -93,7 +98,7 @@
93
98
  "catbox-memory": "4.0.1",
94
99
  "commander": "9.4.0",
95
100
  "cron": "2.1.0",
96
- "decimal.js": "10.3.1",
101
+ "decimal.js": "10.4.0",
97
102
  "docdash": "1.2.0",
98
103
  "event-stream": "4.0.1",
99
104
  "five-bells-condition": "5.0.1",
@@ -107,7 +112,7 @@
107
112
  "moment": "2.29.4",
108
113
  "rc": "1.2.8",
109
114
  "require-glob": "^4.1.0",
110
- "uuid4": "2.0.2"
115
+ "uuid4": "2.0.3"
111
116
  },
112
117
  "optionalDependencies": {
113
118
  "mysql": "2.18.1"
@@ -27,44 +27,59 @@
27
27
 
28
28
  const bulkProcessingStates = [
29
29
  {
30
+ bulkProcessingStateId: 1,
30
31
  name: 'RECEIVED',
31
32
  description: 'The switch has received the individual transfer ids part of the bulk transfer'
32
33
  },
33
34
  {
35
+ bulkProcessingStateId: 2,
34
36
  name: 'RECEIVED_DUPLICATE',
35
37
  description: 'The switch has matched individual transfer as duplicate'
36
38
  },
37
39
  {
40
+ bulkProcessingStateId: 3,
38
41
  name: 'RECEIVED_INVALID',
39
42
  description: 'The switch has matched individual transfer as invalid within Prepare or Position Handler'
40
43
  },
41
44
  {
45
+ bulkProcessingStateId: 4,
42
46
  name: 'ACCEPTED',
43
47
  description: 'The switch has reserved the funds for the transfers in the bulk'
44
48
  },
45
49
  {
50
+ bulkProcessingStateId: 5,
46
51
  name: 'PROCESSING',
47
52
  description: 'Fulfilment request has been received for the individual transfer'
48
53
  },
49
54
  {
55
+ bulkProcessingStateId: 6,
50
56
  name: 'FULFIL_DUPLICATE',
51
57
  description: 'The switch has matched individual transfer fulfil as duplicate'
52
58
  },
53
59
  {
60
+ bulkProcessingStateId: 7,
54
61
  name: 'FULFIL_INVALID',
55
62
  description: 'The switch has matched individual transfer fulfilment as invalid within Fulfil or Position Handler'
56
63
  },
57
64
  {
65
+ bulkProcessingStateId: 8,
58
66
  name: 'COMPLETED',
59
67
  description: 'The switch has marked the individual transfer as committed'
60
68
  },
61
69
  {
70
+ bulkProcessingStateId: 9,
62
71
  name: 'REJECTED',
63
72
  description: 'The switch has marked the individual transfer as rejected'
64
73
  },
65
74
  {
75
+ bulkProcessingStateId: 10,
66
76
  name: 'EXPIRED',
67
77
  description: 'The switch has marked the individual transfer as timed out'
78
+ },
79
+ {
80
+ bulkProcessingStateId: 11,
81
+ name: 'ABORTING',
82
+ description: 'The switch has marked the individual transfer as aborting due to failed validation'
68
83
  }
69
84
  ]
70
85
 
@@ -80,6 +80,11 @@ const bulkTransferStates = [
80
80
  bulkTransferStateId: 'INVALID',
81
81
  enumeration: 'REJECTED',
82
82
  description: 'Final state when the switch has completed processing of pending invalid bulk transfer'
83
+ },
84
+ {
85
+ bulkTransferStateId: 'ABORTING',
86
+ enumeration: 'PROCESSING',
87
+ description: 'The switch is attempting to abort all individual transfers'
83
88
  }
84
89
  ]
85
90
 
@@ -126,7 +126,8 @@ const getBulkTransferById = async (id) => {
126
126
  expiration: bulkTransfer.expirationDate,
127
127
  completedDate: bulkTransfer.completedTimestamp,
128
128
  payerBulkTransfer,
129
- payeeBulkTransfer
129
+ payeeBulkTransfer,
130
+ bulkTransferStateEnumeration: bulkTransfer.bulkTransferStateEnumeration
130
131
  }
131
132
  } catch (err) {
132
133
  Logger.isErrorEnabled && Logger.error(err)
@@ -162,9 +163,19 @@ const getBulkTransferExtensionListById = async (id, completedTimestamp) => {
162
163
  }
163
164
  }
164
165
 
166
+ const bulkFulfilTransitionToAborting = async (bulkFulfilPayload, stateReason = null) => {
167
+ try {
168
+ BulkTransferFacade.saveBulkTransferAborting(bulkFulfilPayload, stateReason)
169
+ } catch (err) {
170
+ Logger.isErrorEnabled && Logger.error(err)
171
+ throw err
172
+ }
173
+ }
174
+
165
175
  const BulkTransferService = {
166
176
  getBulkTransferById,
167
177
  getBulkTransferExtensionListById,
178
+ bulkFulfilTransitionToAborting,
168
179
  getBulkTransferByTransferId: BulkTransferModel.getByTransferId,
169
180
  getParticipantsById: BulkTransferModel.getParticipantsById,
170
181
  bulkPrepare: BulkTransferFacade.saveBulkTransferReceived,
@@ -144,14 +144,36 @@ const bulkFulfil = async (error, messages) => {
144
144
  const bulkTransfers = await BulkTransferService.getBulkTransferById(payload.bulkTransferId)
145
145
  for (const individualTransferFulfil of bulkTransfers.payeeBulkTransfer.individualTransferResults) {
146
146
  individualTransferFulfil.errorInformation = payload.errorInformation
147
- await sendIndividualTransfer(message, messageId, kafkaTopic, headers, payload, state, params, individualTransferFulfil, histTimerEnd)
147
+ await sendIndividualTransfer(
148
+ message,
149
+ messageId,
150
+ kafkaTopic,
151
+ headers,
152
+ payload,
153
+ state,
154
+ params,
155
+ individualTransferFulfil,
156
+ histTimerEnd,
157
+ Enum.Transfers.BulkProcessingState.PROCESSING
158
+ )
148
159
  }
149
160
  } else {
150
161
  const IndividualTransferFulfilModel = BulkTransferModels.getIndividualTransferFulfilModel()
151
162
 
152
163
  // enable async/await operations for the stream
153
164
  for await (const doc of IndividualTransferFulfilModel.find({ messageId }).cursor()) {
154
- await sendIndividualTransfer(message, messageId, kafkaTopic, headers, payload, state, params, doc.payload, histTimerEnd)
165
+ await sendIndividualTransfer(
166
+ message,
167
+ messageId,
168
+ kafkaTopic,
169
+ headers,
170
+ payload,
171
+ state,
172
+ params,
173
+ doc.payload,
174
+ histTimerEnd,
175
+ Enum.Transfers.BulkProcessingState.PROCESSING
176
+ )
155
177
  }
156
178
  }
157
179
  } catch (err) { // TODO: handle individual transfers streaming error
@@ -162,7 +184,27 @@ const bulkFulfil = async (error, messages) => {
162
184
  }
163
185
  } else {
164
186
  Logger.isErrorEnabled && Logger.error(Util.breadcrumb(location, { path: 'validationFailed' }))
165
- Logger.isErrorEnabled && Logger.error(`validationFailure Reasons - ${JSON.stringify(reasons)}`)
187
+
188
+ const validationFspiopError = reasons.shift()
189
+ if (reasons.length > 0) {
190
+ validationFspiopError.extensions = []
191
+ // If there are multiple validation errors attach them as extensions
192
+ // to the first error
193
+ reasons.forEach((reason, i) => {
194
+ validationFspiopError.extensions.push({
195
+ key: `additionalErrors${i}`,
196
+ value: reason.message
197
+ })
198
+ })
199
+ }
200
+ // Converting FSPIOPErrors to strings is verbose, so we reduce the errors
201
+ // to just their message.
202
+ const reasonsMessages = reasons.map(function (reason) {
203
+ return reason.message
204
+ })
205
+
206
+ Logger.isErrorEnabled && Logger.error(`validationFailure Reasons - ${JSON.stringify(reasonsMessages)}`)
207
+
166
208
  try {
167
209
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'saveInvalidRequest'))
168
210
  /**
@@ -171,12 +213,24 @@ const bulkFulfil = async (error, messages) => {
171
213
  * reason is "FSPIOP-Source header should match Payee". In this case we should not
172
214
  * abort the bulk as we would have accepted non-legitimate source.
173
215
  */
174
- const state = await BulkTransferService.bulkFulfil(payload, reasons.toString(), false)
216
+ const state = await BulkTransferService.bulkFulfilTransitionToAborting(payload)
175
217
  const bulkTransfers = await BulkTransferService.getBulkTransferById(payload.bulkTransferId)
176
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'Bulk fulfil failed validation')
177
218
  for (const individualTransferFulfil of bulkTransfers.payeeBulkTransfer.individualTransferResults) {
178
- individualTransferFulfil.errorInformation = fspiopError.toApiErrorObject().errorInformation
179
- await sendIndividualTransfer(message, messageId, kafkaTopic, headers, payload, state, params, individualTransferFulfil, histTimerEnd)
219
+ individualTransferFulfil.errorInformation = validationFspiopError.toApiErrorObject().errorInformation
220
+ // Abort-Reject all individual transfers
221
+ // The bulk processing handler will handle informing the payer
222
+ await sendIndividualTransfer(
223
+ message,
224
+ messageId,
225
+ kafkaTopic,
226
+ headers,
227
+ payload,
228
+ state,
229
+ params,
230
+ individualTransferFulfil,
231
+ histTimerEnd,
232
+ Enum.Transfers.BulkProcessingState.ABORTING
233
+ )
180
234
  }
181
235
  } catch (err) {
182
236
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInternal2--${actionLetter}7`))
@@ -191,12 +245,11 @@ const bulkFulfil = async (error, messages) => {
191
245
  }
192
246
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorGeneric--${actionLetter}8`))
193
247
 
194
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, reasons.toString())
195
248
  const eventDetail = { functionality: Enum.Events.Event.Type.NOTIFICATION, action }
196
249
  params.message.value.content.uriParams = { id: bulkTransferId }
197
250
 
198
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
199
- throw fspiopError
251
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: validationFspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
252
+ throw validationFspiopError
200
253
  }
201
254
  } catch (err) {
202
255
  Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}--BP0`)
@@ -212,13 +265,13 @@ const bulkFulfil = async (error, messages) => {
212
265
  * @async
213
266
  * @description sends individual transfers to the fulfil handler
214
267
  */
215
- const sendIndividualTransfer = async (message, messageId, kafkaTopic, headers, payload, state, params, individualTransferFulfil, histTimerEnd) => {
268
+ const sendIndividualTransfer = async (message, messageId, kafkaTopic, headers, payload, state, params, individualTransferFulfil, histTimerEnd, bulkProcessingStateId) => {
216
269
  const transferId = individualTransferFulfil.transferId
217
270
  delete individualTransferFulfil.transferId
218
271
  const bulkTransferAssociationRecord = {
219
272
  transferId,
220
273
  bulkTransferId: payload.bulkTransferId,
221
- bulkProcessingStateId: Enum.Transfers.BulkProcessingState.PROCESSING,
274
+ bulkProcessingStateId,
222
275
  errorCode: payload.errorInformation ? payload.errorInformation.errorCode : undefined,
223
276
  errorDescription: payload.errorInformation ? payload.errorInformation.errorDescription : undefined
224
277
  }
@@ -115,15 +115,11 @@ const getBulkTransfer = async (error, messages) => {
115
115
  const bulkTransferResult = await BulkTransferService.getBulkTransferById(bulkTransferId)
116
116
  const bulkTransfer = isPayeeRequest ? bulkTransferResult.payeeBulkTransfer : bulkTransferResult.payerBulkTransfer
117
117
  let payload = {
118
- bulkTransferState: bulkTransfer.bulkTransferState
118
+ bulkTransferState: bulkTransferResult.bulkTransferStateEnumeration
119
119
  }
120
120
  let fspiopError
121
- if (bulkTransfer.bulkTransferState === Enum.Transfers.BulkTransferState.REJECTED) {
122
- payload = {
123
- errorInformation: bulkTransfer.individualTransferResults[0].errorInformation
124
- }
125
- fspiopError = ErrorHandler.Factory.createFSPIOPErrorFromErrorInformation(payload.errorInformation)
126
- } else if (bulkTransfer.bulkTransferState !== Enum.Transfers.BulkTransferState.PROCESSING) {
121
+
122
+ if (bulkTransfer.bulkTransferState !== Enum.Transfers.BulkTransferState.PROCESSING) {
127
123
  payload = {
128
124
  ...payload,
129
125
  completedTimestamp: bulkTransfer.completedTimestamp,
@@ -96,6 +96,7 @@ const bulkPrepare = async (error, messages) => {
96
96
  const headers = message.value.content.headers
97
97
  const action = message.value.metadata.event.action
98
98
  const bulkTransferId = payload.bulkTransferId
99
+ const bulkTransferHash = payload.hash
99
100
  const kafkaTopic = message.topic
100
101
 
101
102
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { method: 'bulkPrepare' }))
@@ -105,9 +106,12 @@ const bulkPrepare = async (error, messages) => {
105
106
 
106
107
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'dupCheck' }))
107
108
 
108
- const { hasDuplicateId, hasDuplicateHash } = await Comparators.duplicateCheckComparator(bulkTransferId, payload, BulkTransferService.getBulkTransferDuplicateCheck, BulkTransferService.saveBulkTransferDuplicateCheck)
109
+ const { hasDuplicateId, hasDuplicateHash } = await Comparators.duplicateCheckComparator(bulkTransferId, bulkTransferHash, BulkTransferService.getBulkTransferDuplicateCheck, BulkTransferService.saveBulkTransferDuplicateCheck, {
110
+ hashOverride: true
111
+ })
112
+
109
113
  if (hasDuplicateId && hasDuplicateHash) {
110
- const eventDetail = { functionality: Enum.Events.Event.Type.NOTIFICATION, action: TransferEventAction.PREPARE_DUPLICATE }
114
+ const eventDetail = { functionality: Enum.Events.Event.Type.NOTIFICATION, action: TransferEventAction.BULK_PREPARE_DUPLICATE }
111
115
  const bulkTransferResult = await BulkTransferService.getBulkTransferById(bulkTransferId)
112
116
  const bulkTransfer = bulkTransferResult.payerBulkTransfer
113
117
  const transferStateEnum = bulkTransfer && bulkTransfer.bulkTransferState
@@ -222,11 +226,29 @@ const bulkPrepare = async (error, messages) => {
222
226
  }
223
227
  } else { // handle validation failure
224
228
  Logger.isErrorEnabled && Logger.error(Util.breadcrumb(location, { path: 'validationFailed' }))
225
- Logger.isErrorEnabled && Logger.error(`validationFailure Reasons - ${JSON.stringify(reasons)}`)
229
+ const validationFspiopError = reasons.shift()
230
+ if (reasons.length > 0) {
231
+ validationFspiopError.extensions = []
232
+ // If there are multiple validation errors attach them as extensions
233
+ // to the first error
234
+ reasons.forEach((reason, i) => {
235
+ validationFspiopError.extensions.push({
236
+ key: `additionalErrors${i}`,
237
+ value: reason.message
238
+ })
239
+ })
240
+ }
241
+ // Converting FSPIOPErrors to strings is verbose, so we reduce the errors
242
+ // to just their message.
243
+ // `bulkTransferStateChange.reason` also has a 512 character limit.
244
+ const reasonsMessages = reasons.map(function (reason) {
245
+ return reason.message
246
+ })
247
+ Logger.isErrorEnabled && Logger.error(`validationFailure Reasons - ${JSON.stringify(reasonsMessages)}`)
226
248
 
227
249
  try { // save invalid request for auditing
228
250
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'saveInvalidRequest'))
229
- await BulkTransferService.bulkPrepare(payload, { payerParticipantId, payeeParticipantId }, reasons.toString(), false)
251
+ await BulkTransferService.bulkPrepare(payload, { payerParticipantId, payeeParticipantId }, reasonsMessages.toString(), false)
230
252
  } catch (err) { // handle insert error and produce error callback notification to payer
231
253
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInternal2--${actionLetter}6`))
232
254
  Logger.isErrorEnabled && Logger.error(err)
@@ -241,12 +263,11 @@ const bulkPrepare = async (error, messages) => {
241
263
  // produce validation error callback notification to payer
242
264
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorGeneric--${actionLetter}7`))
243
265
 
244
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, reasons.toString())
245
266
  const eventDetail = { functionality: Enum.Events.Event.Type.NOTIFICATION, action }
246
267
  params.message.value.content.uriParams = { id: bulkTransferId }
247
268
 
248
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
249
- throw fspiopError
269
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: validationFspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
270
+ throw validationFspiopError
250
271
  }
251
272
  } catch (err) {
252
273
  Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}--BP0`)
@@ -150,7 +150,7 @@ const bulkProcessing = async (error, messages) => {
150
150
  errorCode = payload.errorInformation && payload.errorInformation.errorCode
151
151
  errorDescription = payload.errorInformation && payload.errorInformation.errorDescription
152
152
  } else {
153
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, `Invalid action for bulk in ${Enum.Transfers.BulkTransferState.RECEIVED} state`)
153
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, `Invalid action ${action} for bulk in ${Enum.Transfers.BulkTransferState.RECEIVED} state`)
154
154
  throw fspiopError
155
155
  }
156
156
  } else if ([Enum.Transfers.BulkTransferState.ACCEPTED].includes(bulkTransferInfo.bulkTransferStateId)) {
@@ -162,7 +162,7 @@ const bulkProcessing = async (error, messages) => {
162
162
  errorCode = payload.errorInformation && payload.errorInformation.errorCode
163
163
  errorDescription = payload.errorInformation && payload.errorInformation.errorDescription
164
164
  } else {
165
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, `Invalid action for bulk in ${Enum.Transfers.BulkTransferState.ACCEPTED} state`)
165
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, `Invalid action ${action} for bulk in ${Enum.Transfers.BulkTransferState.ACCEPTED} state`)
166
166
  throw fspiopError
167
167
  }
168
168
  } else if ([Enum.Transfers.BulkTransferState.PROCESSING, Enum.Transfers.BulkTransferState.PENDING_FULFIL, Enum.Transfers.BulkTransferState.EXPIRING].includes(bulkTransferInfo.bulkTransferStateId)) {
@@ -190,7 +190,19 @@ const bulkProcessing = async (error, messages) => {
190
190
  errorCode = payload.errorInformation && payload.errorInformation.errorCode
191
191
  errorDescription = payload.errorInformation && payload.errorInformation.errorDescription
192
192
  } else {
193
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, `Invalid action for bulk in ${Enum.Transfers.BulkTransferState.PROCESSING} state`)
193
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, `Invalid action ${action} for bulk in ${Enum.Transfers.BulkTransferState.PROCESSING} state`)
194
+ throw fspiopError
195
+ }
196
+ } else if ([Enum.Transfers.BulkTransferState.ABORTING].includes(bulkTransferInfo.bulkTransferStateId)) {
197
+ if (action === Enum.Events.Event.Action.BULK_ABORT) {
198
+ criteriaState = Enum.Transfers.BulkTransferState.ABORTING
199
+ processingStateId = Enum.Transfers.BulkProcessingState.FULFIL_INVALID
200
+ completedBulkState = Enum.Transfers.BulkTransferState.REJECTED
201
+ incompleteBulkState = Enum.Transfers.BulkTransferState.ABORTING
202
+ errorCode = payload.errorInformation && payload.errorInformation.errorCode
203
+ errorDescription = payload.errorInformation && payload.errorInformation.errorDescription
204
+ } else {
205
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, `Invalid action ${action} for bulk in ${Enum.Transfers.BulkTransferState.ABORTING} state`)
194
206
  throw fspiopError
195
207
  }
196
208
  } else if (bulkTransferInfo.bulkTransferStateId === Enum.Transfers.BulkTransferState.COMPLETED && action === Enum.Events.Event.Action.FULFIL_DUPLICATE) {
@@ -38,56 +38,104 @@ const Participant = require('../../../domain/participant')
38
38
  const BulkTransferService = require('../../../domain/bulkTransfer')
39
39
  const Config = require('../../../lib/config')
40
40
  const Enum = require('@mojaloop/central-services-shared').Enum
41
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
41
42
 
42
43
  const reasons = []
43
- const defaultLagSeconds = 300
44
44
 
45
45
  const validateDifferentFsp = (payload) => {
46
46
  const isPayerAndPayeeDifferent = (payload.payerFsp.toLowerCase() !== payload.payeeFsp.toLowerCase())
47
47
  if (!isPayerAndPayeeDifferent) {
48
- reasons.push('Payer and Payee should differ')
48
+ reasons.push(
49
+ ErrorHandler.Factory.createFSPIOPError(
50
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
51
+ 'Payer and Payee FSPs should be different'
52
+ )
53
+ )
49
54
  return false
50
55
  }
51
56
  return true
52
57
  }
58
+
53
59
  const validateExpiration = (payload) => {
54
60
  if (Date.parse(payload.expiration) < Date.parse(new Date().toDateString())) {
55
- reasons.push(`Expiration date ${new Date(payload.expiration.toString()).toISOString()} is already in the past`)
61
+ reasons.push(
62
+ ErrorHandler.Factory.createFSPIOPError(
63
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
64
+ `Expiration date ${new Date(payload.expiration.toString()).toISOString()} is already in the past`
65
+ )
66
+ )
56
67
  return false
57
68
  }
58
69
  return true
59
70
  }
71
+
60
72
  const validateFspiopSourceMatchesPayer = (payload, headers) => {
61
73
  const matched = (headers && headers[Enum.Http.Headers.FSPIOP.SOURCE] === payload.payerFsp)
62
74
  if (!matched) {
63
- reasons.push('FSPIOP-Source header should match Payer')
75
+ reasons.push(
76
+ ErrorHandler.Factory.createFSPIOPError(
77
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
78
+ 'FSPIOP-Source header should match Payer FSP'
79
+ )
80
+ )
64
81
  return false
65
82
  }
66
83
  return true
67
84
  }
85
+
68
86
  const validateFspiopSourceAndDestination = async (payload, headers) => {
69
87
  const participant = await BulkTransferService.getParticipantsById(payload.bulkTransferId)
70
88
  const matchedPayee = (headers && headers[Enum.Http.Headers.FSPIOP.SOURCE] === participant.payeeFsp)
71
89
  const matchedPayer = (headers && headers[Enum.Http.Headers.FSPIOP.DESTINATION] === participant.payerFsp)
72
90
  if (!matchedPayee) {
73
- reasons.push('FSPIOP-Source header should match Payee')
91
+ reasons.push(
92
+ ErrorHandler.Factory.createFSPIOPError(
93
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
94
+ 'FSPIOP-Source header should match Payee FSP'
95
+ )
96
+ )
74
97
  return false
75
98
  }
76
99
  if (!matchedPayer) {
77
- reasons.push('FSPIOP-Destination header should match Payer')
100
+ reasons.push(
101
+ ErrorHandler.Factory.createFSPIOPError(
102
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
103
+ 'FSPIOP-Destination header should match Payer FSP'
104
+ )
105
+ )
78
106
  return false
79
107
  }
80
108
  return true
81
109
  }
82
- const validateParticipantByName = async (participantName) => {
110
+
111
+ const validateParticipantByName = async (participantName, isPayer = null) => {
112
+ let fspiopErrorCode
113
+ if (isPayer == null) {
114
+ fspiopErrorCode = ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR
115
+ } else if (isPayer) {
116
+ fspiopErrorCode = ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_FSP_ID_NOT_FOUND
117
+ } else {
118
+ fspiopErrorCode = ErrorHandler.Enums.FSPIOPErrorCodes.PAYEE_FSP_ID_NOT_FOUND
119
+ }
120
+
83
121
  const participant = await Participant.getByName(participantName)
84
122
  let isValid = false
85
123
  let participantId
86
124
  if (!participant) {
87
- reasons.push(`Participant ${participantName} not found`)
125
+ reasons.push(
126
+ ErrorHandler.Factory.createFSPIOPError(
127
+ fspiopErrorCode,
128
+ `Participant ${participantName} not found`
129
+ )
130
+ )
88
131
  } else if (!participant.isActive) {
89
132
  participantId = participant.participantId
90
- reasons.push(`Participant ${participantName} is inactive`)
133
+ reasons.push(
134
+ ErrorHandler.Factory.createFSPIOPError(
135
+ fspiopErrorCode,
136
+ `Participant ${participantName} is inactive`
137
+ )
138
+ )
91
139
  } else {
92
140
  participantId = participant.participantId
93
141
  isValid = true
@@ -101,7 +149,12 @@ const validateBulkTransfer = async (payload, headers) => {
101
149
  let payerParticipantId = null
102
150
  let payeeParticipantId = null
103
151
  if (!payload) {
104
- reasons.push('Bulk transfer must be provided')
152
+ reasons.push(
153
+ ErrorHandler.Factory.createFSPIOPError(
154
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
155
+ 'A valid Bulk transfer message must be provided'
156
+ )
157
+ )
105
158
  isValid = false
106
159
  return { isValid, reasons, payerParticipantId, payeeParticipantId }
107
160
  }
@@ -109,7 +162,7 @@ const validateBulkTransfer = async (payload, headers) => {
109
162
  isValid = isValid && validateExpiration(payload)
110
163
  isValid = isValid && validateDifferentFsp(payload)
111
164
  if (isValid) {
112
- const result = await validateParticipantByName(payload.payerFsp)
165
+ const result = await validateParticipantByName(payload.payerFsp, true)
113
166
  isValid = result.isValid
114
167
  payerParticipantId = result.participantId
115
168
  }
@@ -122,9 +175,16 @@ const validateBulkTransfer = async (payload, headers) => {
122
175
  // and body may be different. So we can not enforce that check.
123
176
 
124
177
  // We validate that both these fspId's exist and and continue on.
125
- const payloadResult = await validateParticipantByName(payload.payeeFsp)
126
- const headerResult = await validateParticipantByName(headers[Enum.Http.Headers.FSPIOP.DESTINATION])
127
- isValid = payloadResult.isValid && headerResult.isValid
178
+ // We check the body then the header only if the body is valid to avoid
179
+ // adding two errors to `reasons`
180
+ const payloadResult = await validateParticipantByName(payload.payeeFsp, false)
181
+ isValid = payloadResult.isValid
182
+
183
+ if (isValid) {
184
+ const headerResult = await validateParticipantByName(headers[Enum.Http.Headers.FSPIOP.DESTINATION], false)
185
+ isValid = headerResult.isValid
186
+ }
187
+
128
188
  payeeParticipantId = payloadResult.participantId
129
189
  }
130
190
  return { isValid, reasons, payerParticipantId, payeeParticipantId }
@@ -134,7 +194,12 @@ const validateBulkTransferFulfilment = async (payload, headers) => {
134
194
  reasons.length = 0
135
195
  let isValid = true
136
196
  if (!payload) {
137
- reasons.push('Bulk transfer fulfilment payload must be provided')
197
+ reasons.push(
198
+ ErrorHandler.Factory.createFSPIOPError(
199
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
200
+ 'A valid Bulk transfer fulfilment message must be provided'
201
+ )
202
+ )
138
203
  isValid = false
139
204
  return { isValid, reasons }
140
205
  }
@@ -144,14 +209,24 @@ const validateBulkTransferFulfilment = async (payload, headers) => {
144
209
  }
145
210
 
146
211
  const validateCompletedTimestamp = (payload) => {
147
- const maxLag = (Config.MAX_FULFIL_TIMEOUT_DURATION_SECONDS || defaultLagSeconds) * 1000
212
+ const maxLag = Config.MAX_FULFIL_TIMEOUT_DURATION_SECONDS * 1000
148
213
  const completedTimestamp = new Date(payload.completedTimestamp)
149
214
  const now = new Date()
150
215
  if (completedTimestamp > now) {
151
- reasons.push('completedTimestamp fails because future timestamp was provided')
216
+ reasons.push(
217
+ ErrorHandler.Factory.createFSPIOPError(
218
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
219
+ 'Bulk fulfil failed validation - completedTimestamp fails because future timestamp was provided'
220
+ )
221
+ )
152
222
  return false
153
223
  } else if (completedTimestamp < now - maxLag) {
154
- reasons.push('completedTimestamp fails because provided timestamp exceeded the maximum timeout duration')
224
+ reasons.push(
225
+ ErrorHandler.Factory.createFSPIOPError(
226
+ ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR,
227
+ 'Bulk fulfil failed validation - completedTimestamp fails because provided timestamp exceeded the maximum timeout duration'
228
+ )
229
+ )
155
230
  return false
156
231
  }
157
232
  return true
package/src/lib/config.js CHANGED
@@ -3,7 +3,7 @@ const RC = require('rc')('CLEDG', require('../../config/default.json'))
3
3
  module.exports = {
4
4
  HOSTNAME: RC.HOSTNAME.replace(/\/$/, ''),
5
5
  PORT: RC.PORT,
6
- MAX_FULFIL_TIMEOUT_DURATION_SECONDS: RC.MAX_FULFIL_TIMEOUT_DURATION_SECONDS,
6
+ MAX_FULFIL_TIMEOUT_DURATION_SECONDS: RC.MAX_FULFIL_TIMEOUT_DURATION_SECONDS || 300,
7
7
  MONGODB_URI: RC.MONGODB.URI,
8
8
  MONGODB_DEBUG: (RC.MONGODB.DEBUG === true || RC.MONGODB.DEBUG === 'true'),
9
9
  MONGODB_DISABLED: RC.MONGODB.DISABLED,
@@ -34,11 +34,19 @@ const getById = async (id) => {
34
34
  .innerJoin('participant AS payee', 'payee.participantId', 'bulkTransfer.payeeParticipantId')
35
35
  .innerJoin('bulkTransferStateChange AS btsc', 'btsc.bulkTransferId', 'bulkTransfer.bulkTransferId')
36
36
  .leftJoin('bulkTransferFulfilment AS btf', 'btf.bulkTransferId', 'bulkTransfer.bulkTransferId')
37
+ .leftJoin('bulkTransferState AS bts', 'bts.bulkTransferStateId', 'btsc.bulkTransferStateId')
37
38
  .where({ 'bulkTransfer.bulkTransferId': id })
38
39
  .orderBy('btsc.bulkTransferStateChangeId', 'desc')
39
- .select('bulkTransfer.bulkTransferId', 'btsc.bulkTransferStateId', 'btf.completedDate AS completedTimestamp',
40
- 'payer.name AS payerFsp', 'payee.name AS payeeFsp', 'bulkTransfer.bulkQuoteId', 'bulkTransfer.expirationDate')
41
- .first()
40
+ .select(
41
+ 'bulkTransfer.bulkTransferId',
42
+ 'btsc.bulkTransferStateId',
43
+ 'btf.completedDate AS completedTimestamp',
44
+ 'bts.enumeration AS bulkTransferStateEnumeration',
45
+ 'payer.name AS payerFsp',
46
+ 'payee.name AS payeeFsp',
47
+ 'bulkTransfer.bulkQuoteId',
48
+ 'bulkTransfer.expirationDate'
49
+ ).first()
42
50
  return result
43
51
  })
44
52
  } catch (err) {
@@ -55,13 +63,19 @@ const getByTransferId = async (id) => {
55
63
  .innerJoin('participant AS payer', 'payer.participantId', 'bulkTransfer.payerParticipantId')
56
64
  .innerJoin('participant AS payee', 'payee.participantId', 'bulkTransfer.payeeParticipantId')
57
65
  .innerJoin('bulkTransferStateChange AS btsc', 'btsc.bulkTransferId', 'bulkTransfer.bulkTransferId')
66
+ .leftJoin('bulkTransferState AS bts', 'bts.bulkTransferStateId', 'btsc.bulkTransferStateId')
58
67
  .leftJoin('bulkTransferFulfilment AS btf', 'btf.bulkTransferId', 'bulkTransfer.bulkTransferId')
59
68
  .where({ 'bta.transferId': id })
60
69
  .orderBy('btsc.bulkTransferStateChangeId', 'desc')
61
- .select('bulkTransfer.bulkTransferId', 'btsc.bulkTransferStateId', 'btf.completedDate AS completedTimestamp',
62
- 'payer.name AS payerFsp', 'payee.name AS payeeFsp', 'bulkTransfer.bulkQuoteId',
63
- 'bulkTransfer.expirationDate AS expiration')
64
- .first()
70
+ .select(
71
+ 'bulkTransfer.bulkTransferId',
72
+ 'btsc.bulkTransferStateId',
73
+ 'btf.completedDate AS completedTimestamp',
74
+ 'bts.enumeration AS bulkTransferStateEnumeration',
75
+ 'payer.name AS payerFsp',
76
+ 'payee.name AS payeeFsp',
77
+ 'bulkTransfer.bulkQuoteId'
78
+ ).first()
65
79
  return result
66
80
  })
67
81
  } catch (err) {
@@ -172,10 +172,54 @@ const saveBulkTransferErrorProcessing = async (payload, stateReason = null, isVa
172
172
  }
173
173
  }
174
174
 
175
+ const saveBulkTransferAborting = async (payload, stateReason = null) => {
176
+ try {
177
+ const bulkTransferFulfilmentRecord = {
178
+ bulkTransferId: payload.bulkTransferId,
179
+ completedDate: Time.getUTCString(new Date(payload.completedTimestamp))
180
+ }
181
+
182
+ const state = Enum.Transfers.BulkTransferState.ABORTING
183
+ const bulkTransferStateChangeRecord = {
184
+ bulkTransferId: payload.bulkTransferId,
185
+ bulkTransferStateId: state,
186
+ reason: stateReason
187
+ }
188
+
189
+ const knex = await Db.getKnex()
190
+ return await knex.transaction(async (trx) => {
191
+ try {
192
+ await knex('bulkTransferFulfilment').transacting(trx).insert(bulkTransferFulfilmentRecord)
193
+ if (payload.extensionList && payload.extensionList.extension) {
194
+ const bulkTransferExtensionsRecordList = payload.extensionList.extension.map(ext => {
195
+ return {
196
+ bulkTransferId: payload.bulkTransferId,
197
+ isFulfilment: true,
198
+ key: ext.key,
199
+ value: ext.value
200
+ }
201
+ })
202
+ await knex.batchInsert('bulkTransferExtension', bulkTransferExtensionsRecordList).transacting(trx)
203
+ }
204
+ await knex('bulkTransferStateChange').transacting(trx).insert(bulkTransferStateChangeRecord)
205
+ await trx.commit
206
+ return state
207
+ } catch (err) {
208
+ await trx.rollback
209
+ throw err
210
+ }
211
+ })
212
+ } catch (err) {
213
+ Logger.isErrorEnabled && Logger.error(err)
214
+ throw err
215
+ }
216
+ }
217
+
175
218
  const TransferFacade = {
176
219
  saveBulkTransferReceived,
177
220
  saveBulkTransferProcessing,
178
- saveBulkTransferErrorProcessing
221
+ saveBulkTransferErrorProcessing,
222
+ saveBulkTransferAborting
179
223
  }
180
224
 
181
225
  module.exports = TransferFacade
@@ -1,156 +0,0 @@
1
- {
2
- "decisions": {
3
- "1075703|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs": {
4
- "decision": "ignore",
5
- "madeAt": 1657029291345,
6
- "expiresAt": 1659621287320
7
- },
8
- "1075703|@mojaloop/event-sdk>grpc>protobufjs": {
9
- "decision": "ignore",
10
- "madeAt": 1657029291345,
11
- "expiresAt": 1659621287320
12
- },
13
- "1075704|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs": {
14
- "decision": "ignore",
15
- "madeAt": 1657029292596,
16
- "expiresAt": 1659621287320
17
- },
18
- "1075704|@mojaloop/event-sdk>grpc>protobufjs": {
19
- "decision": "ignore",
20
- "madeAt": 1657029292596,
21
- "expiresAt": 1659621287320
22
- },
23
- "1070030|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>shins>markdown-it": {
24
- "decision": "ignore",
25
- "madeAt": 1657029293742,
26
- "expiresAt": 1659621287320
27
- },
28
- "1070030|widdershins>markdown-it": {
29
- "decision": "ignore",
30
- "madeAt": 1657547585781,
31
- "expiresAt": 1660139581210
32
- },
33
- "1068155|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>shins>markdown-it>sanitize-html": {
34
- "decision": "ignore",
35
- "madeAt": 1657029294926,
36
- "expiresAt": 1659621287320
37
- },
38
- "1070260|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>shins>markdown-it>sanitize-html": {
39
- "decision": "ignore",
40
- "madeAt": 1657029296035,
41
- "expiresAt": 1659621287320
42
- },
43
- "1070412|ejs": {
44
- "decision": "ignore",
45
- "madeAt": 1657029299037,
46
- "expiresAt": 1659621287320
47
- },
48
- "1068386|hapi-auth-basic>hapi": {
49
- "decision": "ignore",
50
- "madeAt": 1657029300211,
51
- "expiresAt": 1659621287320
52
- },
53
- "1068399|hapi-auth-basic>hapi>ammo": {
54
- "decision": "ignore",
55
- "madeAt": 1657029301421,
56
- "expiresAt": 1659621287320
57
- },
58
- "1068389|hapi-auth-basic>hapi>ammo>subtext": {
59
- "decision": "ignore",
60
- "madeAt": 1657029302848,
61
- "expiresAt": 1659621287320
62
- },
63
- "1068390|hapi-auth-basic>hapi>ammo>subtext": {
64
- "decision": "ignore",
65
- "madeAt": 1657029304128,
66
- "expiresAt": 1659621287320
67
- },
68
- "1067553|swagger2openapi>better-ajv-errors>jsonpointer": {
69
- "decision": "ignore",
70
- "madeAt": 1657029305419,
71
- "expiresAt": 1659621287320
72
- },
73
- "1067946|swagger2openapi>better-ajv-errors>jsonpointer>oas-validator>ajv": {
74
- "decision": "ignore",
75
- "madeAt": 1657029307042,
76
- "expiresAt": 1659621287320
77
- },
78
- "1068310|widdershins>markdown-it>yargs>yargs-parser": {
79
- "decision": "ignore",
80
- "madeAt": 1657029308195,
81
- "expiresAt": 1659621287320
82
- },
83
- "1080969|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment": {
84
- "decision": "ignore",
85
- "madeAt": 1657547584804,
86
- "expiresAt": 1660139581210
87
- },
88
- "1080969|@mojaloop/event-sdk>grpc>protobufjs>moment": {
89
- "decision": "ignore",
90
- "madeAt": 1657547584804,
91
- "expiresAt": 1660139581210
92
- },
93
- "1070030|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment>shins>markdown-it": {
94
- "decision": "ignore",
95
- "madeAt": 1657547585781,
96
- "expiresAt": 1660139581210
97
- },
98
- "1068155|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment>shins>markdown-it>sanitize-html": {
99
- "decision": "ignore",
100
- "madeAt": 1657547586612,
101
- "expiresAt": 1660139581210
102
- },
103
- "1070260|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment>shins>markdown-it>sanitize-html": {
104
- "decision": "ignore",
105
- "madeAt": 1657547587381,
106
- "expiresAt": 1660139581210
107
- },
108
- "1070030|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment>widdershins>markdown-it": {
109
- "decision": "ignore",
110
- "madeAt": 1657548277077,
111
- "expiresAt": 1660140273753
112
- },
113
- "1070030|shins>markdown-it": {
114
- "decision": "ignore",
115
- "madeAt": 1657548277077,
116
- "expiresAt": 1660140273753
117
- },
118
- "1068310|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment>widdershins>markdown-it>yargs>yargs-parser": {
119
- "decision": "ignore",
120
- "madeAt": 1657548277911,
121
- "expiresAt": 1660140273753
122
- },
123
- "1068155|shins>markdown-it>sanitize-html": {
124
- "decision": "ignore",
125
- "madeAt": 1657548278663,
126
- "expiresAt": 1660140273753
127
- },
128
- "1070260|shins>markdown-it>sanitize-html": {
129
- "decision": "ignore",
130
- "madeAt": 1657548279466,
131
- "expiresAt": 1660140273753
132
- },
133
- "1081008|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment": {
134
- "decision": "ignore",
135
- "madeAt": 1657629511965,
136
- "expiresAt": 1660221505784
137
- },
138
- "1081008|@mojaloop/event-sdk>grpc>protobufjs>moment": {
139
- "decision": "ignore",
140
- "madeAt": 1657629511965,
141
- "expiresAt": 1660221505784
142
- },
143
- "1081761|@mojaloop/central-services-shared>@mojaloop/event-sdk>grpc>protobufjs>moment": {
144
- "decision": "ignore",
145
- "madeAt": 1658884308135,
146
- "expiresAt": 1661476300260
147
- },
148
- "1081761|@mojaloop/event-sdk>grpc>protobufjs>moment": {
149
- "decision": "ignore",
150
- "madeAt": 1658884308135,
151
- "expiresAt": 1661476300260
152
- }
153
- },
154
- "rules": {},
155
- "version": 1
156
- }