@mojaloop/sdk-scheme-adapter 12.3.0 → 13.0.2

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 (60) hide show
  1. package/.env.example +3 -0
  2. package/CHANGELOG.md +25 -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 +3 -3
  9. package/src/InboundServer/handlers.js +114 -52
  10. package/src/OutboundServer/api.yaml +105 -32
  11. package/src/OutboundServer/api_interfaces/openapi.d.ts +46 -16
  12. package/src/OutboundServer/api_template/components/schemas/accountsResponse.yaml +9 -0
  13. package/src/OutboundServer/api_template/components/schemas/partiesByIdResponse.yaml +10 -3
  14. package/src/OutboundServer/api_template/components/schemas/quotesPostResponse.yaml +42 -34
  15. package/src/OutboundServer/api_template/components/schemas/simpleTransfersPostResponse.yaml +9 -2
  16. package/src/OutboundServer/api_template/components/schemas/transferRequest.yaml +3 -0
  17. package/src/OutboundServer/api_template/components/schemas/transferResponse.yaml +28 -2
  18. package/src/OutboundServer/api_template/components/schemas/transferStatusResponse.yaml +8 -1
  19. package/src/OutboundServer/handlers.js +1 -1
  20. package/src/config.js +1 -1
  21. package/src/lib/model/AccountsModel.js +13 -11
  22. package/src/lib/model/InboundTransfersModel.js +166 -24
  23. package/src/lib/model/OutboundRequestToPayModel.js +5 -6
  24. package/src/lib/model/OutboundRequestToPayTransferModel.js +2 -2
  25. package/src/lib/model/OutboundTransfersModel.js +261 -56
  26. package/src/lib/model/PartiesModel.js +15 -2
  27. package/src/lib/model/common/BackendError.js +28 -4
  28. package/src/lib/model/common/index.js +2 -1
  29. package/test/__mocks__/@mojaloop/sdk-standard-components.js +3 -2
  30. package/test/integration/lib/Outbound/parties.test.js +2 -0
  31. package/test/integration/lib/Outbound/quotes.test.js +2 -0
  32. package/test/integration/lib/Outbound/simpleTransfers.test.js +2 -0
  33. package/test/unit/InboundServer.test.js +9 -9
  34. package/test/unit/TestServer.test.js +11 -13
  35. package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +11 -3
  36. package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +14 -0
  37. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +13 -0
  38. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +18 -0
  39. package/test/unit/api/accounts/utils.js +15 -1
  40. package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +18 -15
  41. package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +1 -0
  42. package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +9 -0
  43. package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +1 -0
  44. package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +74 -47
  45. package/test/unit/api/transfers/utils.js +85 -4
  46. package/test/unit/data/commonHttpHeaders.json +1 -0
  47. package/test/unit/inboundApi/handlers.test.js +45 -14
  48. package/test/unit/lib/model/AccountsModel.test.js +9 -6
  49. package/test/unit/lib/model/InboundTransfersModel.test.js +210 -30
  50. package/test/unit/lib/model/OutboundRequestToPayModel.test.js +1 -1
  51. package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +3 -3
  52. package/test/unit/lib/model/OutboundTransfersModel.test.js +826 -157
  53. package/test/unit/lib/model/PartiesModel.test.js +13 -7
  54. package/test/unit/lib/model/QuotesModel.test.js +8 -2
  55. package/test/unit/lib/model/TransfersModel.test.js +8 -2
  56. package/test/unit/lib/model/data/defaultConfig.json +9 -9
  57. package/test/unit/lib/model/data/mockArguments.json +97 -40
  58. package/test/unit/lib/model/data/payeeParty.json +13 -11
  59. package/test/unit/lib/model/data/quoteResponse.json +36 -25
  60. package/test/unit/lib/model/data/transferFulfil.json +5 -3
@@ -15,17 +15,9 @@ const { uuid } = require('uuidv4');
15
15
  const StateMachine = require('javascript-state-machine');
16
16
  const { Ilp, MojaloopRequests } = require('@mojaloop/sdk-standard-components');
17
17
  const shared = require('./lib/shared');
18
- const { BackendError } = require('./common');
18
+ const { BackendError, TransferStateEnum } = require('./common');
19
19
  const PartiesModel = require('./PartiesModel');
20
20
 
21
- const transferStateEnum = {
22
- 'WAITING_FOR_PARTY_ACEPTANCE': 'WAITING_FOR_PARTY_ACCEPTANCE',
23
- 'WAITING_FOR_QUOTE_ACCEPTANCE': 'WAITING_FOR_QUOTE_ACCEPTANCE',
24
- 'ERROR_OCCURRED': 'ERROR_OCCURRED',
25
- 'COMPLETED': 'COMPLETED',
26
- };
27
-
28
-
29
21
  /**
30
22
  * Models the state machine and operations required for performing an outbound transfer
31
23
  */
@@ -42,6 +34,13 @@ class OutboundTransfersModel {
42
34
  this._autoAcceptParty = config.autoAcceptParty;
43
35
  this._useQuoteSourceFSPAsTransferPayeeFSP = config.useQuoteSourceFSPAsTransferPayeeFSP;
44
36
  this._checkIlp = config.checkIlp;
37
+ this._multiplePartiesResponse = config.multiplePartiesResponse;
38
+ this._multiplePartiesResponseSeconds = config.multiplePartiesResponseSeconds;
39
+ this._sendFinalNotificationIfRequested = config.sendFinalNotificationIfRequested;
40
+
41
+ if (this._autoAcceptParty && this._multiplePartiesResponse) {
42
+ throw new Error('Conflicting config options provided: autoAcceptParty and multiplePartiesResponse');
43
+ }
45
44
 
46
45
  this._requests = new MojaloopRequests({
47
46
  logger: this._logger,
@@ -108,9 +107,11 @@ class OutboundTransfersModel {
108
107
  transitions: [
109
108
  { name: 'resolvePayee', from: 'start', to: 'payeeResolved' },
110
109
  { name: 'requestQuote', from: 'payeeResolved', to: 'quoteReceived' },
110
+ { name: 'requestQuote', from: 'start', to: 'quoteReceived' },
111
111
  { name: 'executeTransfer', from: 'quoteReceived', to: 'succeeded' },
112
112
  { name: 'getTransfer', to: 'succeeded' },
113
113
  { name: 'error', from: '*', to: 'errored' },
114
+ { name: 'abort', from: '*', to: 'aborted' },
114
115
  ],
115
116
  methods: {
116
117
  onTransition: this._handleTransition.bind(this),
@@ -159,6 +160,13 @@ class OutboundTransfersModel {
159
160
  this.data.initiatedTimestamp = new Date().toISOString();
160
161
  }
161
162
 
163
+ if(!this.data.hasOwnProperty('direction')) {
164
+ this.data.direction = 'OUTBOUND';
165
+ }
166
+ if(this.data.skipPartyLookup && typeof(this.data.to.fspId) === undefined) {
167
+ throw new Error('fspId of to party must be specific id when skipPartyLookup is truthy');
168
+ }
169
+
162
170
  this._initStateMachine(this.data.currentState);
163
171
  }
164
172
 
@@ -176,6 +184,9 @@ class OutboundTransfersModel {
176
184
 
177
185
  case 'resolvePayee':
178
186
  // resolve the payee
187
+ if (this._multiplePartiesResponse) {
188
+ return this._resolveBatchPayees();
189
+ }
179
190
  return this._resolvePayee();
180
191
 
181
192
  case 'requestQuote':
@@ -189,6 +200,11 @@ class OutboundTransfersModel {
189
200
  // prepare a transfer and wait for fulfillment
190
201
  return this._executeTransfer();
191
202
 
203
+ case 'abort':
204
+ this._logger.log('State machine is aborting transfer');
205
+ this.data.abortedReason = args[0];
206
+ break;
207
+
192
208
  case 'error':
193
209
  this._logger.log(`State machine is erroring with error: ${util.inspect(args)}`);
194
210
  this.data.lastError = args[0] || new Error('unspecified error');
@@ -225,17 +241,16 @@ class OutboundTransfersModel {
225
241
  }
226
242
  this.metrics.partyLookupResponses.inc();
227
243
 
228
- let payee = JSON.parse(msg);
229
-
230
- if(payee.errorInformation) {
244
+ this.data.getPartiesResponse = JSON.parse(msg);
245
+ if(this.data.getPartiesResponse.body && this.data.getPartiesResponse.body.errorInformation) {
231
246
  // this is an error response to our GET /parties request
232
- const err = new BackendError(`Got an error response resolving party: ${util.inspect(payee, { depth: Infinity })}`, 500);
233
- err.mojaloopError = payee;
234
-
247
+ const err = new BackendError(`Got an error response resolving party: ${util.inspect(this.data.getPartiesResponse.body, { depth: Infinity })}`, 500);
248
+ err.mojaloopError = this.data.getPartiesResponse.body;
235
249
  // cancel the timeout handler
236
250
  clearTimeout(timeout);
237
251
  return reject(err);
238
252
  }
253
+ let payee = this.data.getPartiesResponse.body;
239
254
 
240
255
  if(!payee.party) {
241
256
  // we should never get a non-error response without a party, but just in case...
@@ -310,6 +325,10 @@ class OutboundTransfersModel {
310
325
  this._logger.log(`Error unsubscribing (in timeout handler) ${payeeKey} ${subId}: ${e.stack || util.inspect(e)}`);
311
326
  });
312
327
 
328
+ if(latencyTimerDone) {
329
+ latencyTimerDone();
330
+ }
331
+
313
332
  return reject(err);
314
333
  }, this._requestProcessingTimeoutSeconds * 1000);
315
334
 
@@ -319,11 +338,14 @@ class OutboundTransfersModel {
319
338
  latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
320
339
  const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
321
340
  this.data.to.idSubValue, this.data.to.fspId);
341
+
342
+ this.data.getPartiesRequest = res.originalRequest;
343
+
322
344
  this.metrics.partyLookupRequests.inc();
323
345
  this._logger.push({ peer: res }).log('Party lookup sent to peer');
324
346
  }
325
347
  catch(err) {
326
- // cancel the timout and unsubscribe before rejecting the promise
348
+ // cancel the timeout and unsubscribe before rejecting the promise
327
349
  clearTimeout(timeout);
328
350
 
329
351
  // we dont really care if the unsubscribe fails but we should log it regardless
@@ -336,6 +358,107 @@ class OutboundTransfersModel {
336
358
  });
337
359
  }
338
360
 
361
+ /**
362
+ * Resolves multiple payees.
363
+ * Starts the payee resolution process by sending a GET /parties request to the switch;
364
+ * then waits for a specified number of seconds and resolve payees with responses from the cache.
365
+ */
366
+ _resolveBatchPayees() {
367
+ // eslint-disable-next-line no-async-promise-executor
368
+ return new Promise(async (resolve, reject) => {
369
+ let latencyTimerDone;
370
+ // hook up a timer to handle response messages
371
+ // const timer = setTimeout((cn, msg, subId) => {
372
+ const payeeResolver = (msg) => {
373
+ this.data.getPartiesResponse = JSON.parse(msg);
374
+
375
+ if(this.data.getPartiesResponse.body.errorInformation) {
376
+ // this is an error response to our GET /parties request
377
+ const err = new BackendError(`Got an error response resolving party: ${util.inspect(this.data.getPartiesResponse.body, { depth: Infinity })}`, 500);
378
+ err.mojaloopError = this.data.getPartiesResponse.body;
379
+ throw err;
380
+ }
381
+ let payee = this.data.getPartiesResponse.body;
382
+
383
+ if(!payee.party) {
384
+ // we should never get a non-error response without a party, but just in case...
385
+ // cancel the timeout handler
386
+ throw new Error(`Resolved payee has no party object: ${util.inspect(payee)}`);
387
+ }
388
+ payee = payee.party;
389
+ // check we got the right payee and info we need
390
+ if(payee.partyIdInfo.partyIdType !== this.data.to.idType) {
391
+ throw new Error(`Expecting resolved payee party IdType to be ${this.data.to.idType} but got ${payee.partyIdInfo.partyIdType}`);
392
+ }
393
+ if(payee.partyIdInfo.partyIdentifier !== this.data.to.idValue) {
394
+ throw new Error(`Expecting resolved payee party identifier to be ${this.data.to.idValue} but got ${payee.partyIdInfo.partyIdentifier}`);
395
+ }
396
+ if(payee.partyIdInfo.partySubIdOrType !== this.data.to.idSubValue) {
397
+ throw new Error(`Expecting resolved payee party subTypeId to be ${this.data.to.idSubValue} but got ${payee.partyIdInfo.partySubIdOrType}`);
398
+ }
399
+ if(!payee.partyIdInfo.fspId) {
400
+ throw new Error(`Expecting resolved payee party to have an FSPID: ${util.inspect(payee.partyIdInfo)}`);
401
+ }
402
+ // now we got the payee, add the details to our data so we can use it
403
+ // in the quote request
404
+ const to = {};
405
+ to.fspId = payee.partyIdInfo.fspId;
406
+ if(payee.partyIdInfo.extensionList) {
407
+ to.extensionList = payee.partyIdInfo.extensionList.extension;
408
+ }
409
+ if(payee.personalInfo) {
410
+ if(payee.personalInfo.complexName) {
411
+ to.firstName = payee.personalInfo.complexName.firstName || this.data.to.firstName;
412
+ to.middleName = payee.personalInfo.complexName.middleName || this.data.to.middleName;
413
+ to.lastName = payee.personalInfo.complexName.lastName || this.data.to.lastName;
414
+ }
415
+ to.dateOfBirth = payee.personalInfo.dateOfBirth;
416
+ }
417
+ return to;
418
+ };
419
+ // listen for resolution events on the payee idType and idValue
420
+ // const payeeKey = `${this.data.to.idType}_${this.data.to.idValue}`
421
+ // + (this.data.to.idSubValue ? `_${this.data.to.idSubValue}` : '');
422
+ const payeeKey = PartiesModel.channelName({
423
+ type: this.data.to.idType,
424
+ id: this.data.to.idValue,
425
+ subId: this.data.to.idSubValue
426
+ });
427
+ const timer = setTimeout(async () => {
428
+ if(latencyTimerDone) {
429
+ latencyTimerDone();
430
+ }
431
+ this.metrics.partyLookupResponses.inc();
432
+ let payeeList;
433
+ try {
434
+ payeeList = await this._cache.members(payeeKey);
435
+ } catch (e) {
436
+ return reject(e);
437
+ }
438
+ if (!payeeList.length) {
439
+ return reject(new BackendError(`Timeout resolving payees for transfer ${this.data.transferId}`, 504));
440
+ }
441
+ this._logger.push({ payeeList }).log('Payees resolved');
442
+ this.data.to = payeeList.map(payeeResolver);
443
+ resolve();
444
+ }, this._multiplePartiesResponseSeconds * 1000);
445
+ // now we have a timeout handler we can fire off
446
+ // a GET /parties request to the switch
447
+ try {
448
+ latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
449
+ const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
450
+ this.data.to.idSubValue);
451
+ this.data.getPartiesRequest = res.originalRequest;
452
+ this.metrics.partyLookupRequests.inc();
453
+ this._logger.push({ peer: res }).log('Party lookup sent to peer');
454
+ }
455
+ catch(err) {
456
+ // cancel the timer before rejecting the promise
457
+ clearTimeout(timer);
458
+ return reject(err);
459
+ }
460
+ });
461
+ }
339
462
 
340
463
  /**
341
464
  * Requests a quote
@@ -396,12 +519,13 @@ class OutboundTransfersModel {
396
519
  return reject(error);
397
520
  }
398
521
 
399
- const quoteResponseBody = message.data;
400
- const quoteResponseHeaders = message.headers;
401
- this._logger.push({ quoteResponseBody }).log('Quote response received');
522
+ this.data.quoteResponse = {
523
+ headers: message.data.headers,
524
+ body: message.data.body
525
+ };
526
+ this._logger.push({ quoteResponse: this.data.quoteResponse.body }).log('Quote response received');
402
527
 
403
- this.data.quoteResponse = quoteResponseBody;
404
- this.data.quoteResponseSource = quoteResponseHeaders['fspiop-source'];
528
+ this.data.quoteResponseSource = this.data.quoteResponse.headers['fspiop-source'];
405
529
 
406
530
  return resolve(quote);
407
531
  }
@@ -419,6 +543,10 @@ class OutboundTransfersModel {
419
543
  this._logger.log(`Error unsubscribing (in timeout handler) ${quoteKey} ${subId}: ${e.stack || util.inspect(e)}`);
420
544
  });
421
545
 
546
+ if(latencyTimerDone) {
547
+ latencyTimerDone();
548
+ }
549
+
422
550
  return reject(err);
423
551
  }, this._requestProcessingTimeoutSeconds * 1000);
424
552
 
@@ -427,6 +555,9 @@ class OutboundTransfersModel {
427
555
  try {
428
556
  latencyTimerDone = this.metrics.quoteRequestLatency.startTimer();
429
557
  const res = await this._requests.postQuotes(quote, this.data.to.fspId);
558
+
559
+ this.data.quoteRequest = res.originalRequest;
560
+
430
561
  this.metrics.quoteRequests.inc();
431
562
  this._logger.push({ res }).log('Quote request sent to peer');
432
563
  }
@@ -510,13 +641,13 @@ class OutboundTransfersModel {
510
641
 
511
642
  const subId = await this._cache.subscribe(transferKey, async (cn, msg, subId) => {
512
643
  try {
513
- let error;
514
- let message = JSON.parse(msg);
515
-
516
644
  if(latencyTimerDone) {
517
645
  latencyTimerDone();
518
646
  }
519
647
 
648
+ let error;
649
+ let message = JSON.parse(msg);
650
+
520
651
  if (message.type === 'transferFulfil') {
521
652
  this.metrics.transferFulfils.inc();
522
653
 
@@ -529,10 +660,10 @@ class OutboundTransfersModel {
529
660
  }
530
661
  }
531
662
  } else if (message.type === 'transferError') {
532
- error = new BackendError(`Got an error response preparing transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
533
- error.mojaloopError = message.data;
663
+ error = new BackendError(`Got an error response preparing transfer: ${util.inspect(message.data.body, { depth: Infinity })}`, 500);
664
+ error.mojaloopError = message.data.body;
534
665
  } else {
535
- this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Uknokwn message type ${message.type}.`);
666
+ this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Unknown message type ${message.type}.`);
536
667
  return;
537
668
  }
538
669
 
@@ -549,14 +680,29 @@ class OutboundTransfersModel {
549
680
  }
550
681
 
551
682
  const fulfil = message.data;
552
- this._logger.push({ fulfil }).log('Transfer fulfil received');
683
+ this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
553
684
  this.data.fulfil = fulfil;
554
-
555
- if(this._checkIlp && !this._ilp.validateFulfil(fulfil.fulfilment, this.data.quoteResponse.condition)) {
685
+ if(this._checkIlp && !this._ilp.validateFulfil(fulfil.body.fulfilment, this.data.quoteResponse.body.condition)) {
556
686
  throw new Error('Invalid fulfilment received from peer DFSP.');
557
687
  }
558
-
559
- return resolve(fulfil);
688
+ if(this._sendFinalNotificationIfRequested && fulfil.body.transferState === 'RESERVED') {
689
+ // we need to send a PATCH notification back to say we have committed the transfer.
690
+ // Note that this is normally a switch only responsibility but the capability is
691
+ // implemented here to support testing use cases where the mojaloop-connector is
692
+ // acting in a peer-to-peer scenario and it is desirable for the other peer to
693
+ // receive this notification.
694
+ // Note that the transfer is considered committed as far as this (payer) side is concerned
695
+ // we will use the current server time as committed timestamp.
696
+ const patchNotification = {
697
+ completedTimestamp: (new Date()).toISOString(),
698
+ transferState: 'COMMITTED',
699
+ };
700
+ const res = this._requests.patchTransfers(this.data.transferId,
701
+ patchNotification, this.data.quoteResponseSource);
702
+ this.data.patch = res.originalRequest;
703
+ this._logger.log(`PATCH final notification sent to peer for transfer ${this.data.transferId}`);
704
+ }
705
+ return resolve(fulfil.body);
560
706
  }
561
707
  catch(err) {
562
708
  return reject(err);
@@ -572,6 +718,10 @@ class OutboundTransfersModel {
572
718
  this._logger.log(`Error unsubscribing (in timeout handler) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
573
719
  });
574
720
 
721
+ if(latencyTimerDone) {
722
+ latencyTimerDone();
723
+ }
724
+
575
725
  return reject(err);
576
726
  }, this._requestProcessingTimeoutSeconds * 1000);
577
727
 
@@ -580,11 +730,14 @@ class OutboundTransfersModel {
580
730
  try {
581
731
  latencyTimerDone = this.metrics.transferLatency.startTimer();
582
732
  const res = await this._requests.postTransfers(prepare, this.data.quoteResponseSource);
733
+
734
+ this.data.prepare = res.originalRequest;
735
+
583
736
  this.metrics.transferPrepares.inc();
584
737
  this._logger.push({ res }).log('Transfer prepare sent to peer');
585
738
  }
586
739
  catch(err) {
587
- // cancel the timout and unsubscribe before rejecting the promise
740
+ // cancel the timeout and unsubscribe before rejecting the promise
588
741
  clearTimeout(timeout);
589
742
 
590
743
  // we dont really care if the unsubscribe fails but we should log it regardless
@@ -610,32 +763,26 @@ class OutboundTransfersModel {
610
763
  try {
611
764
  let error;
612
765
  let message = JSON.parse(msg);
613
-
614
766
  if (message.type === 'transferError') {
615
- error = new BackendError(`Got an error response retrieving transfer: ${util.inspect(message.data, { depth: Infinity })}`, 500);
616
- error.mojaloopError = message.data;
767
+ error = new BackendError(`Got an error response retrieving transfer: ${util.inspect(message.data.body, { depth: Infinity })}`, 500);
768
+ error.mojaloopError = message.data.body;
617
769
  } else if (message.type !== 'transferFulfil') {
618
770
  this._logger.push({ message }).log(`Ignoring cache notification for transfer ${transferKey}. Uknokwn message type ${message.type}.`);
619
771
  return;
620
772
  }
621
-
622
773
  // cancel the timeout handler
623
774
  clearTimeout(timeout);
624
-
625
775
  // stop listening for transfer fulfil messages
626
776
  this._cache.unsubscribe(transferKey, subId).catch(e => {
627
777
  this._logger.log(`Error unsubscribing (in callback) ${transferKey} ${subId}: ${e.stack || util.inspect(e)}`);
628
778
  });
629
-
630
779
  if (error) {
631
780
  return reject(error);
632
781
  }
633
-
634
782
  const fulfil = message.data;
635
- this._logger.push({ fulfil }).log('Transfer fulfil received');
783
+ this._logger.push({ fulfil: fulfil.body }).log('Transfer fulfil received');
636
784
  this.data.fulfil = fulfil;
637
-
638
- return resolve(this.data);
785
+ return resolve(this.data.fulfil);
639
786
  }
640
787
  catch(err) {
641
788
  return reject(err);
@@ -690,11 +837,11 @@ class OutboundTransfersModel {
690
837
  // rather than the original request. In Forex cases we may have requested
691
838
  // a RECEIVE amount in a currency we cannot send. FXP should always give us
692
839
  // a quote response with transferAmount in the correct currency.
693
- currency: this.data.quoteResponse.transferAmount.currency,
694
- amount: this.data.quoteResponse.transferAmount.amount
840
+ currency: this.data.quoteResponse.body.transferAmount.currency,
841
+ amount: this.data.quoteResponse.body.transferAmount.amount
695
842
  },
696
- ilpPacket: this.data.quoteResponse.ilpPacket,
697
- condition: this.data.quoteResponse.condition,
843
+ ilpPacket: this.data.quoteResponse.body.ilpPacket,
844
+ condition: this.data.quoteResponse.body.condition,
698
845
  expiration: this._getExpirationTimestamp()
699
846
  };
700
847
 
@@ -738,24 +885,28 @@ class OutboundTransfersModel {
738
885
 
739
886
  switch(this.data.currentState) {
740
887
  case 'payeeResolved':
741
- resp.currentState = transferStateEnum.WAITING_FOR_PARTY_ACEPTANCE;
888
+ resp.currentState = TransferStateEnum.WAITING_FOR_PARTY_ACCEPTANCE;
742
889
  break;
743
890
 
744
891
  case 'quoteReceived':
745
- resp.currentState = transferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
892
+ resp.currentState = TransferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
746
893
  break;
747
894
 
748
895
  case 'succeeded':
749
- resp.currentState = transferStateEnum.COMPLETED;
896
+ resp.currentState = TransferStateEnum.COMPLETED;
897
+ break;
898
+
899
+ case 'aborted':
900
+ resp.currentState = TransferStateEnum.ABORTED;
750
901
  break;
751
902
 
752
903
  case 'errored':
753
- resp.currentState = transferStateEnum.ERROR_OCCURRED;
904
+ resp.currentState = TransferStateEnum.ERROR_OCCURRED;
754
905
  break;
755
906
 
756
907
  default:
757
908
  this._logger.log(`Transfer model response being returned from an unexpected state: ${this.data.currentState}. Returning ERROR_OCCURRED state`);
758
- resp.currentState = transferStateEnum.ERROR_OCCURRED;
909
+ resp.currentState = TransferStateEnum.ERROR_OCCURRED;
759
910
  break;
760
911
  }
761
912
 
@@ -769,7 +920,7 @@ class OutboundTransfersModel {
769
920
  async _save() {
770
921
  try {
771
922
  this.data.currentState = this.stateMachine.state;
772
- const res = await this._cache.set(`transferModel_${this.data.transferId}`, this.data);
923
+ const res = await this._cache.set(`transferModel_out_${this.data.transferId}`, this.data);
773
924
  this._logger.push({ res }).log('Persisted transfer model in cache');
774
925
  }
775
926
  catch(err) {
@@ -786,7 +937,8 @@ class OutboundTransfersModel {
786
937
  */
787
938
  async load(transferId) {
788
939
  try {
789
- const data = await this._cache.get(`transferModel_${transferId}`);
940
+ const data = await this._cache.get(`transferModel_out_${transferId}`);
941
+
790
942
  if(!data) {
791
943
  throw new Error(`No cached data found for transferId: ${transferId}`);
792
944
  }
@@ -802,12 +954,39 @@ class OutboundTransfersModel {
802
954
 
803
955
  /**
804
956
  * Returns a promise that resolves when the state machine has reached a terminal state
957
+ *
958
+ * @param mergeDate {object} - an object to merge with the model state (data) before running the state machine
805
959
  */
806
- async run() {
960
+ async run(mergeData) {
807
961
  try {
962
+ // if we were passed a mergeData object...
963
+ // merge it with our existing state, overwriting any existing matching root level keys
964
+ if(mergeData) {
965
+ // first remove any merge keys that we do not want to allow to be changed
966
+ // note that we could do this in the swagger also. this is to put a responsibility
967
+ // on this model to defend itself.
968
+ const permittedMergeKeys = ['acceptParty', 'acceptQuote', 'amount', 'to'];
969
+ Object.keys(mergeData).forEach(k => {
970
+ if(permittedMergeKeys.indexOf(k) === -1) {
971
+ delete mergeData[k];
972
+ }
973
+ });
974
+ this.data = {
975
+ ...this.data,
976
+ ...mergeData,
977
+ };
978
+ }
808
979
  // run transitions based on incoming state
809
980
  switch(this.data.currentState) {
810
981
  case 'start':
982
+ // first transition is to resolvePayee
983
+ if(typeof(this.data.to.fspId) !== 'undefined' && this.data.skipPartyLookup) {
984
+ // we already have the payee DFSP and we have bee asked to skip party resolution
985
+ this._logger.log(`Skipping payee resolution for transfer ${this.data.transferId} as to.fspId was provided and skipPartyLookup is truthy`);
986
+ this.data.currentState = 'payeeResolved';
987
+ break;
988
+ }
989
+
811
990
  // next transition is to resolvePayee
812
991
  await this.stateMachine.resolvePayee();
813
992
  this._logger.log(`Payee resolved for transfer ${this.data.transferId}`);
@@ -820,6 +999,13 @@ class OutboundTransfersModel {
820
999
  break;
821
1000
 
822
1001
  case 'payeeResolved':
1002
+ if(!this._autoAcceptParty && !this.data.acceptParty && !this.data.skipPartyLookup) {
1003
+ // resuming after a party resolution halt, backend did not accept the party.
1004
+ await this.stateMachine.abort('Payee rejected by backend');
1005
+ await this._save();
1006
+ return this.getResponse();
1007
+ }
1008
+
823
1009
  // next transition is to requestQuote
824
1010
  await this.stateMachine.requestQuote();
825
1011
  this._logger.log(`Quote received for transfer ${this.data.transferId}`);
@@ -832,6 +1018,13 @@ class OutboundTransfersModel {
832
1018
  break;
833
1019
 
834
1020
  case 'quoteReceived':
1021
+ if(!this._autoAcceptQuotes && !this.data.acceptQuote) {
1022
+ // resuming after a party resolution halt, backend did not accept the party.
1023
+ await this.stateMachine.abort('Quote rejected by backend');
1024
+ await this._save();
1025
+ return this.getResponse();
1026
+ }
1027
+
835
1028
  // next transition is executeTransfer
836
1029
  await this.stateMachine.executeTransfer();
837
1030
  this._logger.log(`Transfer ${this.data.transferId} has been completed`);
@@ -853,9 +1046,21 @@ class OutboundTransfersModel {
853
1046
  await this._save();
854
1047
  this._logger.log('State machine in errored state');
855
1048
  return;
1049
+
1050
+ case 'aborted':
1051
+ // stopped in aborted state
1052
+ await this._save();
1053
+ this._logger.log('State machine in aborted state');
1054
+ return this.getResponse();
1055
+
1056
+ default:
1057
+ // The state is not handled here, throwing an error to avoid an infinite recursion of this function
1058
+ await this._save();
1059
+ this._logger.error(`State machine in unhandled(${this.data.currentState}) state`);
1060
+ return;
856
1061
  }
857
1062
 
858
- // now call ourslves recursively to deal with the next transition
1063
+ // now call ourselves recursively to deal with the next transition
859
1064
  this._logger.log(`Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recusring to handle next transition.`);
860
1065
  return this.run();
861
1066
  }
@@ -58,12 +58,25 @@ function argsValidation({ type, id, subId }) {
58
58
  }
59
59
  }
60
60
 
61
- // generate model
61
+ /**
62
+ * @name reformatMessage
63
+ * @description reformats message received from PUB/SUB channel, it is optional method, if not specified identify function is used by default
64
+ * @param {object} message - message received
65
+ * @returns {object} - reformatted message
66
+ */
67
+ function reformatMessage(message) {
68
+ return {
69
+ party: { ...message }
70
+ };
71
+ }
72
+
73
+ // generate model
62
74
  const PartiesModel = Async2SyncModel.generate({
63
75
  modelName: 'PartiesModel',
64
76
  channelNameMethod: channelName,
65
77
  requestActionMethod: requestAction,
66
- argsValidationMethod: argsValidation
78
+ argsValidationMethod: argsValidation,
79
+ reformatMessageMethod: reformatMessage
67
80
  });
68
81
 
69
82
  module.exports = PartiesModel;
@@ -10,6 +10,17 @@
10
10
 
11
11
  'use strict';
12
12
 
13
+ const TransferStateEnum = {
14
+ 'WAITING_FOR_PARTY_ACCEPTANCE': 'WAITING_FOR_PARTY_ACCEPTANCE',
15
+ 'QUOTE_REQUEST_RECEIVED': 'QUOTE_REQUEST_RECEIVED',
16
+ 'WAITING_FOR_QUOTE_ACCEPTANCE': 'WAITING_FOR_QUOTE_ACCEPTANCE',
17
+ 'PREPARE_RECEIVED': 'PREPARE_RECEIVED',
18
+ 'ERROR_OCCURRED': 'ERROR_OCCURRED',
19
+ 'COMPLETED': 'COMPLETED',
20
+ 'ABORTED': 'ABORTED',
21
+ 'RESERVED': 'RESERVED',
22
+ };
23
+
13
24
  class BackendError extends Error {
14
25
  constructor(msg, httpStatusCode) {
15
26
  super(msg);
@@ -17,10 +28,23 @@ class BackendError extends Error {
17
28
  }
18
29
 
19
30
  toJSON() {
20
- // return shallow clone of `this`, from `this` are only taken enumerable owned properties
21
- return Object.assign({}, this);
22
-
31
+ const ret = {
32
+ httpStatusCode: this.httpStatusCode
33
+ };
34
+
35
+ // copy across any other properties
36
+ for(let prop in this) {
37
+ if(this.hasOwnProperty(prop)) {
38
+ ret[prop] = this[prop];
39
+ }
40
+ }
41
+
42
+ return ret;
23
43
  }
24
44
  }
25
45
 
26
- module.exports = BackendError;
46
+
47
+ module.exports = {
48
+ BackendError,
49
+ TransferStateEnum,
50
+ };
@@ -9,10 +9,11 @@
9
9
  **************************************************************************/
10
10
 
11
11
  'use strict';
12
- const BackendError = require('./BackendError');
12
+ const { BackendError, TransferStateEnum } = require('./BackendError');
13
13
  const PersistentStateMachine = require('./PersistentStateMachine');
14
14
 
15
15
  module.exports = {
16
16
  BackendError,
17
+ TransferStateEnum,
17
18
  PersistentStateMachine
18
19
  };
@@ -40,6 +40,7 @@ class MockMojaloopRequests extends MojaloopRequests {
40
40
  this.postBulkTransfers = MockMojaloopRequests.__postBulkTransfers;
41
41
  this.putBulkTransfers = MockMojaloopRequests.__putBulkTransfers;
42
42
  this.putBulkTransfersError = MockMojaloopRequests.__putBulkTransfersError;
43
+ this.patchTransfers = MockMojaloopRequests.__patchTransfers;
43
44
  }
44
45
  }
45
46
  MockMojaloopRequests.__postParticipants = jest.fn(() => Promise.resolve());
@@ -63,7 +64,7 @@ MockMojaloopRequests.__getBulkTransfers = jest.fn(() => Promise.resolve());
63
64
  MockMojaloopRequests.__postBulkTransfers = jest.fn(() => Promise.resolve());
64
65
  MockMojaloopRequests.__putBulkTransfers = jest.fn(() => Promise.resolve());
65
66
  MockMojaloopRequests.__putBulkTransfersError = jest.fn(() => Promise.resolve());
66
-
67
+ MockMojaloopRequests.__patchTransfers = jest.fn(() => Promise.resolve());
67
68
 
68
69
  class MockIlp {
69
70
  constructor(config) {
@@ -146,5 +147,5 @@ module.exports = {
146
147
  },
147
148
  Errors,
148
149
  WSO2Auth,
149
- Logger
150
+ Logger,
150
151
  };