@mojaloop/sdk-scheme-adapter 12.2.2 → 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 (76) hide show
  1. package/.env.example +3 -0
  2. package/CHANGELOG.md +26 -0
  3. package/audit-resolve.json +71 -1
  4. package/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json +2 -2
  5. package/docker/ml-testing-toolkit/spec_files/rules_callback/default.json +7 -7
  6. package/docker/ml-testing-toolkit/spec_files/rules_response/default.json +16 -16
  7. package/docker/ml-testing-toolkit/spec_files/rules_response/default_pisp_rules.json +5 -5
  8. package/docker/ml-testing-toolkit/spec_files/rules_validation/default.json +10 -10
  9. package/package.json +4 -1
  10. package/src/ControlAgent/index.js +2 -3
  11. package/src/ControlServer/index.js +2 -2
  12. package/src/InboundServer/handlers.js +114 -52
  13. package/src/InboundServer/index.js +7 -7
  14. package/src/InboundServer/middlewares.js +2 -2
  15. package/src/OutboundServer/api.yaml +54 -3
  16. package/src/OutboundServer/api_interfaces/openapi.d.ts +24 -3
  17. package/src/OutboundServer/api_template/components/schemas/accountsResponse.yaml +9 -0
  18. package/src/OutboundServer/api_template/components/schemas/transferRequest.yaml +3 -0
  19. package/src/OutboundServer/api_template/components/schemas/transferResponse.yaml +28 -2
  20. package/src/OutboundServer/api_template/components/schemas/transferStatusResponse.yaml +8 -1
  21. package/src/OutboundServer/handlers.js +4 -1
  22. package/src/OutboundServer/index.js +10 -11
  23. package/src/config.js +29 -12
  24. package/src/index.js +198 -10
  25. package/src/lib/cache.js +110 -52
  26. package/src/lib/metrics.js +148 -0
  27. package/src/lib/model/AccountsModel.js +17 -12
  28. package/src/lib/model/Async2SyncModel.js +4 -1
  29. package/src/lib/model/InboundTransfersModel.js +170 -25
  30. package/src/lib/model/OutboundBulkQuotesModel.js +4 -1
  31. package/src/lib/model/OutboundBulkTransfersModel.js +4 -1
  32. package/src/lib/model/OutboundRequestToPayModel.js +9 -7
  33. package/src/lib/model/OutboundRequestToPayTransferModel.js +6 -3
  34. package/src/lib/model/OutboundTransfersModel.js +318 -53
  35. package/src/lib/model/PartiesModel.js +1 -1
  36. package/src/lib/model/ProxyModel/index.js +4 -2
  37. package/src/lib/model/common/BackendError.js +28 -4
  38. package/src/lib/model/common/index.js +2 -1
  39. package/src/lib/validate.js +2 -2
  40. package/test/__mocks__/@mojaloop/sdk-standard-components.js +3 -2
  41. package/test/__mocks__/redis.js +4 -0
  42. package/test/config/integration.env +5 -0
  43. package/test/integration/lib/Outbound/parties.test.js +1 -1
  44. package/test/unit/ControlServer/index.js +3 -3
  45. package/test/unit/InboundServer.test.js +10 -10
  46. package/test/unit/TestServer.test.js +11 -13
  47. package/test/unit/api/accounts/data/postAccountsErrorMojaloopResponse.json +11 -3
  48. package/test/unit/api/accounts/data/postAccountsSuccessResponse.json +14 -0
  49. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError1.json +13 -0
  50. package/test/unit/api/accounts/data/postAccountsSuccessResponseWithError2.json +18 -0
  51. package/test/unit/api/accounts/utils.js +15 -1
  52. package/test/unit/api/transfers/data/getTransfersCommittedResponse.json +18 -15
  53. package/test/unit/api/transfers/data/getTransfersErrorNotFound.json +1 -0
  54. package/test/unit/api/transfers/data/postTransfersErrorMojaloopResponse.json +9 -0
  55. package/test/unit/api/transfers/data/postTransfersErrorTimeoutResponse.json +1 -0
  56. package/test/unit/api/transfers/data/postTransfersSuccessResponse.json +74 -47
  57. package/test/unit/api/transfers/utils.js +85 -4
  58. package/test/unit/api/utils.js +4 -1
  59. package/test/unit/config.test.js +2 -2
  60. package/test/unit/data/commonHttpHeaders.json +1 -0
  61. package/test/unit/data/defaultConfig.json +23 -7
  62. package/test/unit/inboundApi/handlers.test.js +45 -14
  63. package/test/unit/index.test.js +95 -4
  64. package/test/unit/lib/model/AccountsModel.test.js +9 -6
  65. package/test/unit/lib/model/InboundTransfersModel.test.js +210 -30
  66. package/test/unit/lib/model/OutboundRequestToPayModel.test.js +1 -1
  67. package/test/unit/lib/model/OutboundRequestToPayTransferModel.test.js +3 -3
  68. package/test/unit/lib/model/OutboundTransfersModel.test.js +863 -158
  69. package/test/unit/lib/model/data/defaultConfig.json +25 -10
  70. package/test/unit/lib/model/data/mockArguments.json +97 -40
  71. package/test/unit/lib/model/data/payeeParty.json +13 -11
  72. package/test/unit/lib/model/data/quoteResponse.json +36 -25
  73. package/test/unit/lib/model/data/transferFulfil.json +5 -3
  74. package/src/lib/api/index.js +0 -12
  75. package/src/lib/randomphrase/index.js +0 -21
  76. package/src/lib/randomphrase/words.json +0 -3397
@@ -55,6 +55,12 @@ describe('inboundModel', () => {
55
55
  beforeEach(async () => {
56
56
  expectedQuoteResponseILP = Ilp.__response;
57
57
  BackendRequests.__postQuoteRequests = jest.fn().mockReturnValue(Promise.resolve(mockArgs.internalQuoteResponse));
58
+ MojaloopRequests.__putQuotes = jest.fn().mockReturnValue(Promise.resolve({
59
+ originalRequest: {
60
+ headers: {},
61
+ body: {},
62
+ }
63
+ }));
58
64
 
59
65
  cache = new Cache({
60
66
  host: 'dummycachehost',
@@ -78,11 +84,18 @@ describe('inboundModel', () => {
78
84
  test('calls `mojaloopRequests.putQuotes` with the expected arguments.', async () => {
79
85
  await model.quoteRequest(mockArgs.quoteRequest, mockArgs.fspId);
80
86
 
87
+ expect(BackendRequests.__postQuoteRequests).toHaveBeenCalledTimes(1);
88
+ expect(BackendRequests.__postQuoteRequests.mock.calls[0][0]).toEqual(mockArgs.internalQuoteRequest);
89
+
81
90
  expect(MojaloopRequests.__putQuotes).toHaveBeenCalledTimes(1);
82
91
  expect(MojaloopRequests.__putQuotes.mock.calls[0][1].expiration).toBe(mockArgs.internalQuoteResponse.expiration);
83
92
  expect(MojaloopRequests.__putQuotes.mock.calls[0][1].ilpPacket).toBe(expectedQuoteResponseILP.ilpPacket);
84
93
  expect(MojaloopRequests.__putQuotes.mock.calls[0][1].condition).toBe(expectedQuoteResponseILP.condition);
85
94
  expect(MojaloopRequests.__putQuotes.mock.calls[0][2]).toBe(mockArgs.fspId);
95
+
96
+ // check the extension list gets translated correctly to the mojaloop form
97
+ expect(MojaloopRequests.__putQuotes.mock.calls[0][1].extensionList)
98
+ .toStrictEqual(mockArgs.internalQuoteResponse.extensionList);
86
99
  });
87
100
 
88
101
  test('adds a custom `expiration` property in case it is not defined.', async() => {
@@ -247,7 +260,12 @@ describe('inboundModel', () => {
247
260
  beforeEach(async () => {
248
261
  MojaloopRequests.__putTransfersError.mockClear();
249
262
  BackendRequests.__postTransfers = jest.fn().mockReturnValue(Promise.resolve({}));
250
- MojaloopRequests.__putTransfers = jest.fn().mockReturnValue(Promise.resolve({}));
263
+ MojaloopRequests.__putTransfers = jest.fn().mockReturnValue(Promise.resolve({
264
+ originalRequest: {
265
+ headers: {},
266
+ body: {},
267
+ }
268
+ }));
251
269
 
252
270
  cache = new Cache({
253
271
  host: 'dummycachehost',
@@ -269,13 +287,17 @@ describe('inboundModel', () => {
269
287
  logger,
270
288
  rejectTransfersOnExpiredQuotes: true,
271
289
  });
272
- cache.set(`quote_${TRANSFER_ID}`, {
273
- mojaloopResponse: {
274
- expiration: new Date(new Date().getTime() - 1000).toISOString(),
290
+ cache.set(`transferModel_in_${TRANSFER_ID}`, {
291
+ quote: {
292
+ mojaloopResponse: {
293
+ expiration: new Date(new Date().getTime() - 1000).toISOString(),
294
+ }
275
295
  }
276
296
  });
277
297
  const args = {
278
- transferId: TRANSFER_ID,
298
+ body: {
299
+ transferId: TRANSFER_ID,
300
+ }
279
301
  };
280
302
 
281
303
  await model.prepareTransfer(args, mockArgs.fspId);
@@ -358,13 +380,15 @@ describe('inboundModel', () => {
358
380
  test('fail on transfer without quote.', async () => {
359
381
  const TRANSFER_ID = 'without_quote-transfer-id';
360
382
  const args = {
361
- transferId: TRANSFER_ID,
362
- amount: {
363
- currency: 'USD',
364
- amount: 20.13
365
- },
366
- ilpPacket: 'mockBase64encodedIlpPacket',
367
- condition: 'mockGeneratedCondition'
383
+ body: {
384
+ transferId: TRANSFER_ID,
385
+ amount: {
386
+ currency: 'USD',
387
+ amount: 20.13
388
+ },
389
+ ilpPacket: 'mockBase64encodedIlpPacket',
390
+ condition: 'mockGeneratedCondition'
391
+ }
368
392
  };
369
393
 
370
394
  const model = new Model({
@@ -382,16 +406,76 @@ describe('inboundModel', () => {
382
406
  expect(call[1].errorInformation.errorCode).toEqual('2001');
383
407
  });
384
408
 
385
- test('pass on transfer without quote.', async () => {
386
- const TRANSFER_ID = 'without_quote-transfer-id';
409
+ test('stores homeTransactionId in cache when received by dfsp acting as payee', async () => {
410
+ const TRANSFER_ID = 'transfer-id';
411
+ const HOME_TRANSACTION_ID = 'mockHomeTransactionId';
412
+ shared.mojaloopPrepareToInternalTransfer = jest.fn().mockReturnValueOnce({});
413
+
414
+ // mock response from dfsp acting as payee
415
+ BackendRequests.__postTransfers = jest.fn().mockReturnValueOnce(Promise.resolve({
416
+ homeTransactionId: HOME_TRANSACTION_ID,
417
+ transferId: TRANSFER_ID
418
+ }));
419
+
387
420
  const args = {
421
+ body: {
422
+ transferId: TRANSFER_ID,
423
+ amount: {
424
+ currency: 'USD',
425
+ amount: 20.13
426
+ },
427
+ ilpPacket: 'mockBase64encodedIlpPacket',
428
+ condition: 'mockGeneratedCondition'
429
+ }
430
+ };
431
+
432
+ const model = new Model({
433
+ ...config,
434
+ cache,
435
+ logger,
436
+ checkIlp: false,
437
+ rejectTransfersOnExpiredQuotes: false
438
+ });
439
+
440
+ cache.set(`transferModel_in_${TRANSFER_ID}`, {
388
441
  transferId: TRANSFER_ID,
389
- amount: {
390
- currency: 'USD',
391
- amount: 20.13
442
+ quote: {
443
+ fulfilment: 'mockFulfilment',
444
+ mojaloopResponse: {
445
+ condition: 'mockCondition',
446
+ }
447
+ }
448
+ });
449
+
450
+ await model.prepareTransfer(args, mockArgs.fspId);
451
+
452
+ expect(MojaloopRequests.__putTransfersError).toHaveBeenCalledTimes(0);
453
+ expect(BackendRequests.__postTransfers).toHaveBeenCalledTimes(1);
454
+ expect(MojaloopRequests.__putTransfers).toHaveBeenCalledTimes(1);
455
+ expect((await cache.get(`transferModel_in_${TRANSFER_ID}`)).homeTransactionId)
456
+ .toEqual(HOME_TRANSACTION_ID);
457
+ });
458
+
459
+ test('pass on transfer without quote.', async () => {
460
+ const TRANSFER_ID = 'without_quote-transfer-id';
461
+ cache.set(`transferModel_in_${TRANSFER_ID}`, {
462
+ fulfilment: '',
463
+ mojaloopResponse: {
464
+ response: ''
392
465
  },
393
- ilpPacket: 'mockBase64encodedIlpPacket',
394
- condition: 'mockGeneratedCondition'
466
+ quote: null
467
+ });
468
+
469
+ const args = {
470
+ body: {
471
+ transferId: TRANSFER_ID,
472
+ amount: {
473
+ currency: 'USD',
474
+ amount: 20.13
475
+ },
476
+ ilpPacket: 'mockBase64encodedIlpPacket',
477
+ condition: 'mockGeneratedCondition'
478
+ }
395
479
  };
396
480
 
397
481
  const model = new Model({
@@ -413,21 +497,29 @@ describe('inboundModel', () => {
413
497
  const TRANSFER_ID = 'transfer-id';
414
498
  shared.mojaloopPrepareToInternalTransfer = jest.fn().mockReturnValueOnce({});
415
499
 
416
- cache.set(`quote_${transactionId}`, {
500
+ cache.set(`transferModel_in_${transactionId}`, {
417
501
  fulfilment: '',
418
502
  mojaloopResponse: {
419
503
  response: ''
504
+ },
505
+ quote: {
506
+ fulfilment: 'mockFulfilment',
507
+ mojaloopResponse: {
508
+ condition: 'mockCondition',
509
+ }
420
510
  }
421
511
  });
422
512
 
423
513
  const args = {
424
- transferId: TRANSFER_ID,
425
- amount: {
426
- currency: 'USD',
427
- amount: 20.13
428
- },
429
- ilpPacket: 'mockIlpPacket',
430
- condition: 'mockGeneratedCondition'
514
+ body: {
515
+ transferId: TRANSFER_ID,
516
+ amount: {
517
+ currency: 'USD',
518
+ amount: 20.13
519
+ },
520
+ ilpPacket: 'mockIlpPacket',
521
+ condition: 'mockGeneratedCondition'
522
+ }
431
523
  };
432
524
 
433
525
  const model = new Model({
@@ -648,7 +740,12 @@ describe('inboundModel', () => {
648
740
 
649
741
  test('sends notification to fsp backend', async () => {
650
742
  BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({}));
651
- const backendResponse = JSON.parse(JSON.stringify(notificationToPayee));
743
+ const notif = JSON.parse(JSON.stringify(notificationToPayee));
744
+
745
+ const expectedRequest = {
746
+ currentState: 'COMPLETED',
747
+ finalNotification: notif.data,
748
+ };
652
749
 
653
750
  const model = new Model({
654
751
  ...config,
@@ -656,11 +753,94 @@ describe('inboundModel', () => {
656
753
  logger,
657
754
  });
658
755
 
659
- await model.sendNotificationToPayee(backendResponse.data, transferId);
756
+ await model.sendNotificationToPayee(notif.data, transferId);
660
757
  expect(BackendRequests.__putTransfersNotification).toHaveBeenCalledTimes(1);
661
758
  const call = BackendRequests.__putTransfersNotification.mock.calls[0];
662
- expect(call[0]).toEqual(backendResponse.data);
759
+ expect(call[0]).toEqual(expectedRequest);
663
760
  expect(call[1]).toEqual(transferId);
664
761
  });
665
762
  });
763
+
764
+ describe('error handling:', () => {
765
+ let cache;
766
+ beforeEach(async () => {
767
+ cache = new Cache({
768
+ host: 'dummycachehost',
769
+ port: 1234,
770
+ logger,
771
+ });
772
+ await cache.connect();
773
+ });
774
+ afterEach(async () => {
775
+ await cache.disconnect();
776
+ });
777
+ test('creates mojaloop spec error body when backend returns standard error code', async () => {
778
+ const model = new Model({
779
+ ...config,
780
+ cache,
781
+ logger,
782
+ });
783
+ const testErr = new HTTPResponseError({
784
+ msg: 'Request returned non-success status code 500',
785
+ res: {
786
+ data: {
787
+ statusCode: '3200',
788
+ },
789
+ }
790
+ });
791
+ const err = await model._handleError(testErr);
792
+ expect(err).toBeDefined();
793
+ expect(err.errorInformation).toBeDefined();
794
+ expect(err.errorInformation.errorCode).toEqual('3200');
795
+ // error message should be the default one, not custom.
796
+ // it is debatibale whether this is truly correct, to overwrite
797
+ // and custom error message; but it is the case for now.
798
+ expect(err.errorInformation.errorDescription).toEqual('Generic ID not found');
799
+ });
800
+ test('creates custom error body when backend returns custom error code', async () => {
801
+ const model = new Model({
802
+ ...config,
803
+ cache,
804
+ logger,
805
+ });
806
+ const customMessage = 'some custom message';
807
+ const testErr = new HTTPResponseError({
808
+ msg: 'Request returned non-success status code 500',
809
+ res: {
810
+ data: {
811
+ statusCode: '3299',
812
+ message: customMessage,
813
+ },
814
+ }
815
+ });
816
+ const err = await model._handleError(testErr);
817
+ expect(err).toBeDefined();
818
+ expect(err.errorInformation).toBeDefined();
819
+ expect(err.errorInformation.errorCode).toEqual('3299');
820
+ expect(err.errorInformation.errorDescription).toEqual(customMessage);
821
+ });
822
+ test('creates custom error message when backend returns standard error code and message', async () => {
823
+ const model = new Model({
824
+ ...config,
825
+ cache,
826
+ logger,
827
+ });
828
+ const customMessage = 'some custom message';
829
+ const testErr = new HTTPResponseError({
830
+ msg: 'Request returned non-success status code 500',
831
+ res: {
832
+ data: {
833
+ statusCode: '3200',
834
+ message: customMessage,
835
+ },
836
+ }
837
+ });
838
+ const err = await model._handleError(testErr);
839
+ expect(err).toBeDefined();
840
+ expect(err.errorInformation).toBeDefined();
841
+ expect(err.errorInformation.errorCode).toEqual('3200');
842
+ // error message should be custom
843
+ expect(err.errorInformation.errorDescription).toEqual(customMessage);
844
+ });
845
+ });
666
846
  });
@@ -27,7 +27,7 @@ const payeeParty = require('./data/payeeParty');
27
27
  const transactionRequestResponseTemplate = require('./data/transactionRequestResponse');
28
28
 
29
29
  const genPartyId = (party) => {
30
- const { partyIdType, partyIdentifier, partySubIdOrType } = party.party.partyIdInfo;
30
+ const { partyIdType, partyIdentifier, partySubIdOrType } = party.body.party.partyIdInfo;
31
31
  return PartiesModel.channelName({
32
32
  type: partyIdType,
33
33
  id: partyIdentifier,
@@ -121,7 +121,7 @@ describe('outboundRequestToPayTransferModel', () => {
121
121
  MojaloopRequests.__postTransfers = jest.fn((postTransfersBody, destFspId) => {
122
122
  //ensure that the `MojaloopRequests.postTransfers` method has been called with the correct arguments
123
123
  // set as the destination FSPID, picked up from the header's value `fspiop-source`
124
- expect(model.data.quoteResponseSource).toBe(quoteResponse.headers['fspiop-source']);
124
+ expect(model.data.quoteResponseSource).toBe(quoteResponse.data.headers['fspiop-source']);
125
125
 
126
126
  const extensionList = postTransfersBody.extensionList.extension;
127
127
  expect(extensionList).toBeTruthy();
@@ -129,8 +129,8 @@ describe('outboundRequestToPayTransferModel', () => {
129
129
  expect(extensionList[0]).toEqual({ key: 'tkey1', value: 'tvalue1' });
130
130
  expect(extensionList[1]).toEqual({ key: 'tkey2', value: 'tvalue2' });
131
131
 
132
- expect(destFspId).toBe(quoteResponse.headers['fspiop-source']);
133
- expect(quoteResponse.headers['fspiop-source']).not.toBe(model.data.to.fspId);
132
+ expect(destFspId).toBe(quoteResponse.data.headers['fspiop-source']);
133
+ expect(quoteResponse.data.headers['fspiop-source']).not.toBe(model.data.to.fspId);
134
134
 
135
135
  // simulate a callback with the transfer fulfilment
136
136
  emitTransferFulfilCacheMessage(cache, postTransfersBody.transferId, transferFulfil);