@mojaloop/sdk-scheme-adapter 12.3.0 → 13.0.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.
Files changed (52) hide show
  1. package/.env.example +3 -0
  2. package/CHANGELOG.md +11 -0
  3. package/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json +2 -2
  4. package/docker/ml-testing-toolkit/spec_files/rules_callback/default.json +7 -7
  5. package/docker/ml-testing-toolkit/spec_files/rules_response/default.json +16 -16
  6. package/docker/ml-testing-toolkit/spec_files/rules_response/default_pisp_rules.json +5 -5
  7. package/docker/ml-testing-toolkit/spec_files/rules_validation/default.json +10 -10
  8. package/package.json +1 -1
  9. package/src/InboundServer/handlers.js +114 -52
  10. package/src/OutboundServer/api.yaml +54 -3
  11. package/src/OutboundServer/api_interfaces/openapi.d.ts +24 -3
  12. package/src/OutboundServer/api_template/components/schemas/accountsResponse.yaml +9 -0
  13. package/src/OutboundServer/api_template/components/schemas/transferRequest.yaml +3 -0
  14. package/src/OutboundServer/api_template/components/schemas/transferResponse.yaml +28 -2
  15. package/src/OutboundServer/api_template/components/schemas/transferStatusResponse.yaml +8 -1
  16. package/src/OutboundServer/handlers.js +1 -1
  17. package/src/config.js +1 -1
  18. package/src/lib/model/AccountsModel.js +13 -11
  19. package/src/lib/model/InboundTransfersModel.js +166 -24
  20. package/src/lib/model/OutboundRequestToPayModel.js +5 -6
  21. package/src/lib/model/OutboundRequestToPayTransferModel.js +2 -2
  22. package/src/lib/model/OutboundTransfersModel.js +261 -56
  23. package/src/lib/model/PartiesModel.js +1 -1
  24. package/src/lib/model/common/BackendError.js +28 -4
  25. package/src/lib/model/common/index.js +2 -1
  26. package/test/__mocks__/@mojaloop/sdk-standard-components.js +3 -2
  27. package/test/integration/lib/Outbound/parties.test.js +1 -1
  28. package/test/unit/InboundServer.test.js +9 -9
  29. package/test/unit/TestServer.test.js +11 -13
  30. package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +11 -3
  31. package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +14 -0
  32. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +13 -0
  33. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +18 -0
  34. package/test/unit/api/accounts/utils.js +15 -1
  35. package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +18 -15
  36. package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +1 -0
  37. package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +9 -0
  38. package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +1 -0
  39. package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +74 -47
  40. package/test/unit/api/transfers/utils.js +85 -4
  41. package/test/unit/data/commonHttpHeaders.json +1 -0
  42. package/test/unit/inboundApi/handlers.test.js +45 -14
  43. package/test/unit/lib/model/AccountsModel.test.js +9 -6
  44. package/test/unit/lib/model/InboundTransfersModel.test.js +210 -30
  45. package/test/unit/lib/model/OutboundRequestToPayModel.test.js +1 -1
  46. package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +3 -3
  47. package/test/unit/lib/model/OutboundTransfersModel.test.js +826 -157
  48. package/test/unit/lib/model/data/defaultConfig.json +9 -9
  49. package/test/unit/lib/model/data/mockArguments.json +97 -40
  50. package/test/unit/lib/model/data/payeeParty.json +13 -11
  51. package/test/unit/lib/model/data/quoteResponse.json +36 -25
  52. package/test/unit/lib/model/data/transferFulfil.json +5 -3
@@ -1002,6 +1002,12 @@ components:
1002
1002
  $ref: '#/components/schemas/extensionListEmptiable'
1003
1003
  transferRequestExtensions:
1004
1004
  $ref: '#/components/schemas/extensionListEmptiable'
1005
+ skipPartyLookup:
1006
+ description: >-
1007
+ Set to true if supplying an FSPID for the payee party and no party
1008
+ resolution is needed. This may be useful is a previous party
1009
+ resolution has been performed.
1010
+ type: boolean
1005
1011
  CorrelationId:
1006
1012
  title: CorrelationId
1007
1013
  type: string
@@ -1273,8 +1279,24 @@ components:
1273
1279
  $ref: '#/components/schemas/transferStatus'
1274
1280
  quoteId:
1275
1281
  $ref: '#/components/schemas/CorrelationId'
1282
+ getPartiesResponse:
1283
+ type: object
1284
+ required:
1285
+ - body
1286
+ properties:
1287
+ body:
1288
+ type: object
1289
+ headers:
1290
+ type: object
1276
1291
  quoteResponse:
1277
- $ref: '#/components/schemas/QuotesIDPutResponse'
1292
+ type: object
1293
+ required:
1294
+ - body
1295
+ properties:
1296
+ body:
1297
+ $ref: '#/components/schemas/QuotesIDPutResponse'
1298
+ headers:
1299
+ type: object
1278
1300
  quoteResponseSource:
1279
1301
  type: string
1280
1302
  description: >
@@ -1283,7 +1305,14 @@ components:
1283
1305
  account in the case of a FOREX transfer. i.e. it may be a FOREX
1284
1306
  gateway.
1285
1307
  fulfil:
1286
- $ref: '#/components/schemas/TransfersIDPutResponse'
1308
+ type: object
1309
+ required:
1310
+ - body
1311
+ properties:
1312
+ body:
1313
+ $ref: '#/components/schemas/TransfersIDPutResponse'
1314
+ headers:
1315
+ type: object
1287
1316
  lastError:
1288
1317
  description: >
1289
1318
  Object representing the last error to occur during a transfer
@@ -1291,6 +1320,12 @@ components:
1291
1320
  entity in the scheme or an object representing other types of error
1292
1321
  e.g. exceptions that may occur inside the scheme adapter.
1293
1322
  $ref: '#/components/schemas/transferError'
1323
+ skipPartyLookup:
1324
+ description: >-
1325
+ Set to true if supplying an FSPID for the payee party and no party
1326
+ resolution is needed. This may be useful is a previous party
1327
+ resolution has been performed.
1328
+ type: boolean
1294
1329
  errorResponse:
1295
1330
  type: object
1296
1331
  properties:
@@ -1321,7 +1356,14 @@ components:
1321
1356
  currentState:
1322
1357
  $ref: '#/components/schemas/transferStatus'
1323
1358
  fulfil:
1324
- $ref: '#/components/schemas/TransfersIDPutResponse'
1359
+ type: object
1360
+ required:
1361
+ - body
1362
+ properties:
1363
+ body:
1364
+ $ref: '#/components/schemas/TransfersIDPutResponse'
1365
+ headers:
1366
+ type: object
1325
1367
  transferContinuationAcceptParty:
1326
1368
  type: object
1327
1369
  required:
@@ -1993,6 +2035,15 @@ components:
1993
2035
  $ref: '#/components/schemas/accountsCreationState'
1994
2036
  lastError:
1995
2037
  $ref: '#/components/schemas/transferError'
2038
+ postAccountsResponse:
2039
+ type: object
2040
+ required:
2041
+ - body
2042
+ properties:
2043
+ body:
2044
+ type: object
2045
+ headers:
2046
+ type: object
1996
2047
  errorAccountsResponse:
1997
2048
  allOf:
1998
2049
  - $ref: '#/components/schemas/errorResponse'
@@ -633,6 +633,8 @@ export interface components {
633
633
  note?: components["schemas"]["Note"];
634
634
  quoteRequestExtensions?: components["schemas"]["extensionListEmptiable"];
635
635
  transferRequestExtensions?: components["schemas"]["extensionListEmptiable"];
636
+ /** Set to true if supplying an FSPID for the payee party and no party resolution is needed. This may be useful is a previous party resolution has been performed. */
637
+ skipPartyLookup?: boolean;
636
638
  };
637
639
  /** Identifier that correlates all messages of the same sequence. The API data type UUID (Universally Unique Identifier) is a JSON String in canonical format, conforming to [RFC 4122](https://tools.ietf.org/html/rfc4122), that is restricted by a regular expression for interoperability reasons. A UUID is always 36 characters long, 32 hexadecimal symbols and 4 dashes (‘-‘). */
638
640
  CorrelationId: string;
@@ -728,12 +730,24 @@ export interface components {
728
730
  note?: components["schemas"]["Note"];
729
731
  currentState?: components["schemas"]["transferStatus"];
730
732
  quoteId?: components["schemas"]["CorrelationId"];
731
- quoteResponse?: components["schemas"]["QuotesIDPutResponse"];
733
+ getPartiesResponse?: {
734
+ body: { [key: string]: unknown };
735
+ headers?: { [key: string]: unknown };
736
+ };
737
+ quoteResponse?: {
738
+ body: components["schemas"]["QuotesIDPutResponse"];
739
+ headers?: { [key: string]: unknown };
740
+ };
732
741
  /** FSPID of the entity that supplied the quote response. This may not be the same as the FSPID of the entity which owns the end user account in the case of a FOREX transfer. i.e. it may be a FOREX gateway. */
733
742
  quoteResponseSource?: string;
734
- fulfil?: components["schemas"]["TransfersIDPutResponse"];
743
+ fulfil?: {
744
+ body: components["schemas"]["TransfersIDPutResponse"];
745
+ headers?: { [key: string]: unknown };
746
+ };
735
747
  /** Object representing the last error to occur during a transfer process. This may be a Mojaloop API error returned from another entity in the scheme or an object representing other types of error e.g. exceptions that may occur inside the scheme adapter. */
736
748
  lastError?: components["schemas"]["transferError"];
749
+ /** Set to true if supplying an FSPID for the payee party and no party resolution is needed. This may be useful is a previous party resolution has been performed. */
750
+ skipPartyLookup?: boolean;
737
751
  };
738
752
  errorResponse: {
739
753
  /** Error code as string. */
@@ -747,7 +761,10 @@ export interface components {
747
761
  transferStatusResponse: {
748
762
  transferId: components["schemas"]["CorrelationId"];
749
763
  currentState: components["schemas"]["transferStatus"];
750
- fulfil: components["schemas"]["TransfersIDPutResponse"];
764
+ fulfil: {
765
+ body: components["schemas"]["TransfersIDPutResponse"];
766
+ headers?: { [key: string]: unknown };
767
+ };
751
768
  };
752
769
  transferContinuationAcceptParty: {
753
770
  acceptParty: true;
@@ -1011,6 +1028,10 @@ export interface components {
1011
1028
  response?: components["schemas"]["accountCreationStatus"];
1012
1029
  currentState?: components["schemas"]["accountsCreationState"];
1013
1030
  lastError?: components["schemas"]["transferError"];
1031
+ postAccountsResponse?: {
1032
+ body: { [key: string]: unknown };
1033
+ headers?: { [key: string]: unknown };
1034
+ };
1014
1035
  };
1015
1036
  errorAccountsResponse: components["schemas"]["errorResponse"] & {
1016
1037
  executionState: components["schemas"]["accountsResponse"];
@@ -13,3 +13,12 @@ properties:
13
13
  $ref: ./accountsCreationState.yaml
14
14
  lastError:
15
15
  $ref: ./transferError.yaml
16
+ postAccountsResponse:
17
+ type: object
18
+ required:
19
+ - body
20
+ properties:
21
+ body:
22
+ type: object
23
+ headers:
24
+ type: object
@@ -35,3 +35,6 @@ properties:
35
35
  $ref: ./extensionListEmptiable.yaml
36
36
  transferRequestExtensions:
37
37
  $ref: ./extensionListEmptiable.yaml
38
+ skipPartyLookup:
39
+ description: Set to true if supplying an FSPID for the payee party and no party resolution is needed. This may be useful is a previous party resolution has been performed.
40
+ type: boolean
@@ -39,8 +39,24 @@ properties:
39
39
  quoteId:
40
40
  $ref: >-
41
41
  ../../../../../node_modules/@mojaloop/api-snippets/fspiop/v1_1/openapi3/components/schemas/CorrelationId.yaml
42
+ getPartiesResponse:
43
+ type: object
44
+ required:
45
+ - body
46
+ properties:
47
+ body:
48
+ type: object
49
+ headers:
50
+ type: object
42
51
  quoteResponse:
43
- $ref: ./quote.yaml
52
+ type: object
53
+ required:
54
+ - body
55
+ properties:
56
+ body:
57
+ $ref: './quote.yaml'
58
+ headers:
59
+ type: object
44
60
  quoteResponseSource:
45
61
  type: string
46
62
  description: >
@@ -48,7 +64,14 @@ properties:
48
64
  same as the FSPID of the entity which owns the end user account in the
49
65
  case of a FOREX transfer. i.e. it may be a FOREX gateway.
50
66
  fulfil:
51
- $ref: ./transferFulfilment.yaml
67
+ type: object
68
+ required:
69
+ - body
70
+ properties:
71
+ body:
72
+ $ref: ./transferFulfilment.yaml
73
+ headers:
74
+ type: object
52
75
  lastError:
53
76
  description: >
54
77
  Object representing the last error to occur during a transfer process.
@@ -56,3 +79,6 @@ properties:
56
79
  scheme or an object representing other types of error e.g. exceptions that
57
80
  may occur inside the scheme adapter.
58
81
  $ref: ./transferError.yaml
82
+ skipPartyLookup:
83
+ description: Set to true if supplying an FSPID for the payee party and no party resolution is needed. This may be useful is a previous party resolution has been performed.
84
+ type: boolean
@@ -10,4 +10,11 @@ properties:
10
10
  currentState:
11
11
  $ref: ./transferStatus.yaml
12
12
  fulfil:
13
- $ref: ./transferFulfilment.yaml
13
+ type: object
14
+ required:
15
+ - body
16
+ properties:
17
+ body:
18
+ $ref: ./transferFulfilment.yaml
19
+ headers:
20
+ type: object
@@ -182,7 +182,7 @@ const putTransfers = async (ctx) => {
182
182
  // load the transfer model from cache and start it running again
183
183
  await model.load(ctx.state.path.params.transferId);
184
184
 
185
- const response = await model.run();
185
+ const response = await model.run(ctx.request.body);
186
186
 
187
187
  // return the result
188
188
  ctx.response.status = 200;
package/src/config.js CHANGED
@@ -182,5 +182,5 @@ module.exports = {
182
182
  // the `transactionId` to retrieve the quote from cache
183
183
  allowDifferentTransferTransactionId: env.get('ALLOW_DIFFERENT_TRANSFER_TRANSACTION_ID').default('false').asBool(),
184
184
 
185
- pm4mlEnabled: env.get('PM4ML_ENABLED').default('false').asBool()
185
+ pm4mlEnabled: env.get('PM4ML_ENABLED').default('false').asBool(),
186
186
  };
@@ -133,20 +133,23 @@ class AccountsModel {
133
133
 
134
134
 
135
135
  async _executeCreateAccountsRequest(request) {
136
+ const accountRequest = request;
137
+
136
138
  // eslint-disable-next-line no-async-promise-executor
137
139
  return new Promise(async (resolve, reject) => {
138
- const requestKey = `ac_${request.requestId}`;
140
+ const requestKey = `ac_${accountRequest.requestId}`;
139
141
 
140
142
  const subId = await this._cache.subscribe(requestKey, async (cn, msg, subId) => {
141
143
  try {
142
144
  let error;
143
- let message = JSON.parse(msg);
145
+ const message = JSON.parse(msg);
146
+ this._data.postAccountsResponse = message.data;
144
147
 
145
148
  if (message.type === 'accountsCreationErrorResponse') {
146
- error = new BackendError(`Got an error response creating accounts: ${util.inspect(message.data)}`, 500);
147
- error.mojaloopError = message.data;
149
+ error = new BackendError(`Got an error response creating accounts: ${util.inspect(this._data.postAccountsResponse.body)}`, 500);
150
+ error.mojaloopError = this._data.postAccountsResponse.body;
148
151
  } else if (message.type !== 'accountsCreationSuccessfulResponse') {
149
- this._logger.push({ message }).log(
152
+ this._logger.push(util.inspect(this._data.postAccountsResponse)).log(
150
153
  `Ignoring cache notification for request ${requestKey}. ` +
151
154
  `Unknown message type ${message.type}.`
152
155
  );
@@ -167,7 +170,7 @@ class AccountsModel {
167
170
  return reject(error);
168
171
  }
169
172
 
170
- const response = message.data;
173
+ const response = this._data.postAccountsResponse;
171
174
  this._logger.push({ response }).log('Account creation response received');
172
175
  return resolve(response);
173
176
  }
@@ -178,7 +181,7 @@ class AccountsModel {
178
181
 
179
182
  // set up a timeout for the request
180
183
  const timeout = setTimeout(() => {
181
- const err = new BackendError(`Timeout waiting for response to account creation request ${request.requestId}`, 504);
184
+ const err = new BackendError(`Timeout waiting for response to account creation request ${accountRequest.requestId}`, 504);
182
185
 
183
186
  // we dont really care if the unsubscribe fails but we should log it regardless
184
187
  this._cache.unsubscribe(requestKey, subId).catch(e => {
@@ -191,7 +194,7 @@ class AccountsModel {
191
194
  // now we have a timeout handler and a cache subscriber hooked up we can fire off
192
195
  // a POST /participants request to the switch
193
196
  try {
194
- const res = await this._requests.postParticipants(request);
197
+ const res = await this._requests.postParticipants(accountRequest);
195
198
  this._logger.push({ res }).log('Account creation request sent to peer');
196
199
  }
197
200
  catch(err) {
@@ -218,11 +221,11 @@ class AccountsModel {
218
221
  }
219
222
 
220
223
  _buildClientResponse(response) {
221
- return response.partyList.map(party => ({
224
+ return response.body.partyList.map(party => ({
222
225
  idType: party.partyId.partyIdType,
223
226
  idValue: party.partyId.partyIdentifier,
224
227
  idSubValue: party.partyId.partySubIdOrType,
225
- ...!response.currency && {
228
+ ...!response.body.currency && {
226
229
  error: {
227
230
  statusCode: Errors.MojaloopApiErrorCodes.CLIENT_ERROR.code,
228
231
  message: 'Provided currency not supported',
@@ -295,7 +298,6 @@ class AccountsModel {
295
298
  resp.currentState = stateEnum.ERROR_OCCURRED;
296
299
  break;
297
300
  }
298
-
299
301
  return resp;
300
302
  }
301
303
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  'use strict';
12
12
 
13
+ const util = require('util');
13
14
 
14
15
  const {
15
16
  BackendRequests,
@@ -21,6 +22,7 @@ const {
21
22
  Errors,
22
23
  } = require('@mojaloop/sdk-standard-components');
23
24
  const shared = require('./lib/shared');
25
+ const { TransferStateEnum } = require('./common');
24
26
 
25
27
  /**
26
28
  * Models the operations required for performing inbound transfers
@@ -70,6 +72,12 @@ class InboundTransfersModel {
70
72
  });
71
73
  }
72
74
 
75
+ updateStateWithError(err) {
76
+ this.data.lastError = err;
77
+ this.data.currentState = TransferStateEnum.ERROR_OCCURRED;
78
+ return this._save();
79
+ }
80
+
73
81
  /**
74
82
  * Queries the backend API for the specified party and makes a callback to the originator with the result
75
83
  */
@@ -162,7 +170,32 @@ class InboundTransfersModel {
162
170
  * Asks the backend for a response to an incoming quote request and makes a callback to the originator with
163
171
  * the result
164
172
  */
165
- async quoteRequest(quoteRequest, sourceFspId) {
173
+ async quoteRequest(request, sourceFspId) {
174
+ const quoteRequest = request.body;
175
+
176
+ // keep track of our state.
177
+ // note that instances of this model typically only live as long as it takes to
178
+ // handle an incoming request and send a response asynchronously, but we hold onto
179
+ // some state across async ops
180
+
181
+ this.data = {
182
+ // transferId: this follows the slightly dodgy assumption that transferId will be same as this transactionId.
183
+ // so far this has held in moja implementations but may not always be the case. regardless, future FSPIOP API
184
+ // versions MUST deal with this cleanly so we can expect to eliminate this assumption at some point.
185
+ transferId: quoteRequest.transactionId,
186
+ direction: 'INBOUND',
187
+ quoteRequest: {
188
+ headers: request.headers,
189
+ body: request.body
190
+ },
191
+ currentState: TransferStateEnum.QUOTE_REQUEST_RECEIVED,
192
+ initiatedTimestamp: new Date().toISOString(),
193
+ };
194
+
195
+ // persist the transfer record in the cache. if we crash after this at least we will
196
+ // have a record of the request in the cache.
197
+ await this._save();
198
+
166
199
  try {
167
200
  const internalForm = shared.mojaloopQuoteRequestToInternal(quoteRequest);
168
201
 
@@ -189,19 +222,24 @@ class InboundTransfersModel {
189
222
  mojaloopResponse.condition = condition;
190
223
 
191
224
  // now store the fulfilment and the quote data against the quoteId in our cache
192
- await this._cache.set(`quote_${quoteRequest.transactionId}`, {
225
+ this.data.quote = {
193
226
  request: quoteRequest,
194
227
  internalRequest: internalForm,
195
228
  response: response,
196
229
  mojaloopResponse: mojaloopResponse,
197
230
  fulfilment: fulfilment
198
- });
199
-
200
- // now store the quoteResponse data against the quoteId in our cache to be sent as a response to GET /quotes/{ID}
201
- await this._cache.set(`quoteResponse_${quoteRequest.quoteId}`, mojaloopResponse);
231
+ };
232
+ await this._save();
202
233
 
203
234
  // make a callback to the source fsp with the quote response
204
- return this._mojaloopRequests.putQuotes(quoteRequest.quoteId, mojaloopResponse, sourceFspId);
235
+ const res = await this._mojaloopRequests.putQuotes(quoteRequest.quoteId, mojaloopResponse, sourceFspId);
236
+ this.data.quoteResponse = {
237
+ headers: res.originalRequest.headers,
238
+ body: res.originalRequest.body,
239
+ };
240
+ this.data.currentState = TransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
241
+ await this._save();
242
+ return res;
205
243
  }
206
244
  catch(err) {
207
245
  this._logger.push({ err }).log('Error in quoteRequest');
@@ -257,7 +295,7 @@ class InboundTransfersModel {
257
295
  return 'No response from backend';
258
296
  }
259
297
 
260
- // project our internal quote reponse into mojaloop quote response form
298
+ // project our internal quote response into mojaloop quote response form
261
299
  const mojaloopResponse = shared.internalTransactionRequestResponseToMojaloop(response);
262
300
 
263
301
  // make a callback to the source fsp with the quote response
@@ -277,25 +315,36 @@ class InboundTransfersModel {
277
315
  * Validates an incoming transfer prepare request and makes a callback to the originator with
278
316
  * the result
279
317
  */
280
- async prepareTransfer(prepareRequest, sourceFspId) {
318
+ async prepareTransfer(request, sourceFspId) {
319
+ const prepareRequest = request.body;
281
320
  try {
282
321
  // retrieve our quote data
283
- let quote;
284
-
285
322
  if (this._allowDifferentTransferTransactionId) {
286
323
  const transactionId = this._ilp.getTransactionObject(prepareRequest.ilpPacket).transactionId;
287
- quote = await this._cache.get(`quote_${transactionId}`);
324
+ this.data = await this._load(transactionId);
288
325
  } else {
289
- quote = await this._cache.get(`quote_${prepareRequest.transferId}`);
326
+ this.data = await this._load(prepareRequest.transferId);
290
327
  }
291
328
 
292
- if(!quote) {
329
+ const quote = this.data.quote;
330
+
331
+ if(!this.data || !quote) {
332
+ // If using the sdk-scheme-adapter in place of the deprecated `mojaloop-connector`
333
+ // make sure this is false. Scenarios that use `mojaloop-connector`
334
+ // absolutely requires a previous quote before allowing a transfer to proceed.
335
+ // This is a different to the a typical mojaloop sdk-scheme-adapter setup which allows this as an option.
336
+
293
337
  // Check whether to allow transfers without a previous quote.
294
338
  if(!this._allowTransferWithoutQuote) {
295
339
  throw new Error(`Corresponding quote not found for transfer ${prepareRequest.transferId}`);
296
340
  }
297
341
  }
298
342
 
343
+ // persist our state so we have a record if we crash during processing the prepare
344
+ this.data.prepare = request;
345
+ this.data.currentState = TransferStateEnum.PREPARE_RECEIVED;
346
+ await this._save();
347
+
299
348
  // Calculate or retrieve fulfilment and condition
300
349
  let fulfilment = null;
301
350
  let condition = null;
@@ -313,13 +362,13 @@ class InboundTransfersModel {
313
362
  throw new Error(`ILP condition in transfer prepare for ${prepareRequest.transferId} does not match quote`);
314
363
  }
315
364
 
316
-
317
- if (quote && this._rejectTransfersOnExpiredQuotes) {
365
+ if (this._rejectTransfersOnExpiredQuotes) {
318
366
  const now = new Date().toISOString();
319
367
  const expiration = quote.mojaloopResponse.expiration;
320
368
  if (now > expiration) {
321
369
  const error = Errors.MojaloopApiErrorObjectFromCode(Errors.MojaloopApiErrorCodes.QUOTE_EXPIRED);
322
370
  this._logger.error(`Error in prepareTransfer: quote expired for transfer ${prepareRequest.transferId}, system time=${now} > quote time=${expiration}`);
371
+ await this.updateStateWithError(error);
323
372
  return this._mojaloopRequests.putTransfersError(prepareRequest.transferId, error, sourceFspId);
324
373
  }
325
374
  }
@@ -336,6 +385,7 @@ class InboundTransfersModel {
336
385
  }
337
386
 
338
387
  this._logger.log(`Transfer accepted by backend returning homeTransactionId: ${response.homeTransactionId} for mojaloop transferId: ${prepareRequest.transferId}`);
388
+ this.data.homeTransactionId = response.homeTransactionId;
339
389
 
340
390
  // create a mojaloop transfer fulfil response
341
391
  const mojaloopResponse = {
@@ -350,10 +400,16 @@ class InboundTransfersModel {
350
400
  };
351
401
 
352
402
  // make a callback to the source fsp with the transfer fulfilment
353
- return this._mojaloopRequests.putTransfers(prepareRequest.transferId, mojaloopResponse,
403
+ const res = await this._mojaloopRequests.putTransfers(prepareRequest.transferId, mojaloopResponse,
354
404
  sourceFspId);
355
- }
356
- catch(err) {
405
+ this.data.fulfil = {
406
+ headers: res.originalRequest.headers,
407
+ body: res.originalRequest.body,
408
+ };
409
+ this.data.currentState = this._reserveNotification ? TransferStateEnum.RESERVED : TransferStateEnum.COMPLETED;
410
+ await this._save();
411
+ return res;
412
+ } catch(err) {
357
413
  this._logger.push({ err }).log('Error in prepareTransfer');
358
414
  const mojaloopError = await this._handleError(err);
359
415
  this._logger.push({ mojaloopError }).log(`Sending error response to ${sourceFspId}`);
@@ -717,22 +773,108 @@ class InboundTransfersModel {
717
773
  */
718
774
  async sendNotificationToPayee(body, transferId) {
719
775
  try {
720
- const res = await this._backendRequests.putTransfersNotification(body, transferId);
776
+ // load any cached state for this transfer e.g. quote request/response etc...
777
+ this.data = await this._load(transferId);
778
+
779
+ // if we didnt have anything cached, start from scratch
780
+ if(!this.data) {
781
+ this.data = {};
782
+ }
783
+
784
+ // tag the final notification body on to the state
785
+ this.data.finalNotification = body;
786
+
787
+ if(body.transferState === 'COMMITTED') {
788
+ // if the transfer was successful in the switch, set the overall transfer state to COMPLETED
789
+ this.data.currentState = TransferStateEnum.COMPLETED;
790
+ }
791
+ else {
792
+ // if the final notification has anything other than COMMITTED as the final state, set an error
793
+ // in the transfer state.
794
+ this.data.currentState == TransferStateEnum.ERROR_OCCURED;
795
+ this.data.lastError = 'Final notification state not COMMITTED';
796
+ }
797
+
798
+ await this._save();
799
+
800
+ const res = await this._backendRequests.putTransfersNotification(this.data, transferId);
721
801
  return res;
722
802
  } catch (err) {
723
- this._logger.push({ err }).log('Error in sendNotificationToPayee');
803
+ this._logger.push({ err }).log('Error notifying backend of final transfer state');
724
804
  }
725
805
  }
726
806
 
727
- async _handleError(err, mojaloopErrorCode = Errors.MojaloopApiErrorCodes.INTERNAL_SERVER_ERROR) {
807
+ async _handleError(err) {
808
+ // by default use a generic server error
809
+ let mojaloopError = (new Errors.MojaloopFSPIOPError(err, null, null, Errors.MojaloopApiErrorCodes.INTERNAL_SERVER_ERROR)).toApiErrorObject();
728
810
  if(err instanceof HTTPResponseError) {
811
+ // this is an http response error e.g. from calling DFSP backend
729
812
  const e = err.getData();
730
813
  if(e.res && e.res.data) {
731
- mojaloopErrorCode = Errors.MojaloopApiErrorCodeFromCode(`${e.res.data.statusCode}`);
814
+ // look for a standard mojaloop error that matches the statusCode
815
+ let mojaloopErrorCode = Errors.MojaloopApiErrorCodeFromCode(`${e.res.data.statusCode}`);
816
+ let errorDescription = e.res.data.message;
817
+ if(mojaloopErrorCode) {
818
+ // use the standard mojaloop error object
819
+ mojaloopError = (new Errors.MojaloopFSPIOPError(err, null, null, mojaloopErrorCode)).toApiErrorObject();
820
+ if(errorDescription) {
821
+ // if the error has a description, use that instead of the default mojaloop description
822
+ // note that the mojaloop API spec allows any string up to 128 utf8 characters to be sent
823
+ // in the errorDescription field.
824
+ mojaloopError.errorInformation.errorDescription = errorDescription;
825
+ }
826
+ }
827
+ else {
828
+ // this is a custom error, so construct a mojaloop spec body
829
+ mojaloopError = {
830
+ errorInformation: {
831
+ errorCode: e.res.data.statusCode,
832
+ errorDescription: e.res.data.message,
833
+ }
834
+ };
835
+ }
732
836
  }
733
837
  }
838
+ if(this.data) {
839
+ //we have persisted state so update that with this error
840
+ this.data.lastError = {
841
+ originalError: err.stack || util.inspect(err),
842
+ mojaloopError: mojaloopError,
843
+ };
844
+ this.data.currentState = TransferStateEnum.ERROR_OCCURRED;
845
+ await this._save();
846
+ }
847
+ return mojaloopError;
848
+ }
734
849
 
735
- return new Errors.MojaloopFSPIOPError(err, null, null, mojaloopErrorCode).toApiErrorObject();
850
+ /**
851
+ * Persists the model state to cache for reinstantiation at a later point
852
+ */
853
+ async _save() {
854
+ try {
855
+ const res = await this._cache.set(`transferModel_in_${this.data.transferId}`, this.data);
856
+ this._logger.push({ res }).log('Persisted transfer model in cache');
857
+ }
858
+ catch(err) {
859
+ this._logger.push({ err }).log('Error saving transfer model');
860
+ throw err;
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Loads a transfer model from cache for resumption of the transfer process
866
+ *
867
+ * @param transferId {string} - UUID transferId of the model to load from cache
868
+ */
869
+ async _load(transferId) {
870
+ try {
871
+ const data = await this._cache.get(`transferModel_in_${transferId}`);
872
+ return data;
873
+ }
874
+ catch(err) {
875
+ this._logger.push({ err }).log('Error loading transfer model');
876
+ throw err;
877
+ }
736
878
  }
737
879
  }
738
880
 
@@ -155,17 +155,16 @@ class OutboundRequestToPayModel {
155
155
  // hook up a subscriber to handle response messages
156
156
  const subId = await this._cache.subscribe(payeeKey, (cn, msg, subId) => {
157
157
  try {
158
- let payee = JSON.parse(msg);
159
-
160
- if(payee.errorInformation) {
158
+ this.data.getPartiesResponse = JSON.parse(msg);
159
+ if(this.data.getPartiesResponse.body.errorInformation) {
161
160
  // this is an error response to our GET /parties request
162
- const err = new BackendError(`Got an error response resolving party: ${util.inspect(payee)}`, 500);
163
- err.mojaloopError = payee;
164
-
161
+ const err = new BackendError(`Got an error response resolving party: ${util.inspect(this.data.getPartiesResponse.body, { depth: Infinity })}`, 500);
162
+ err.mojaloopError = this.data.getPartiesResponse.body;
165
163
  // cancel the timeout handler
166
164
  clearTimeout(timeout);
167
165
  return reject(err);
168
166
  }
167
+ let payee = this.data.getPartiesResponse.body;
169
168
 
170
169
  if(!payee.party) {
171
170
  // we should never get a non-error response without a party, but just in case...