From 47ebcca9b72e7b1693d6799ee6ccb0ee8d3014e2 Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Fri, 18 Oct 2024 16:50:42 +0000
Subject: [PATCH 01/16] feat: call rules during fx quote

---
 src/handlers/QuotingHandler.js   |  2 +-
 src/model/executeRules.js        | 80 ++++++++++++++++++++++++++++++++
 src/model/fxQuotes.js            | 30 +++++++++++-
 src/model/quotes.js              | 80 ++------------------------------
 src/model/rules.js               |  3 ++
 test/unit/model/fxQuotes.test.js |  3 ++
 test/unit/model/quotes.test.js   | 14 ++++--
 7 files changed, 131 insertions(+), 81 deletions(-)
 create mode 100644 src/model/executeRules.js

diff --git a/src/handlers/QuotingHandler.js b/src/handlers/QuotingHandler.js
index 0daeb8e5..4cdf858c 100644
--- a/src/handlers/QuotingHandler.js
+++ b/src/handlers/QuotingHandler.js
@@ -212,7 +212,7 @@ class QuotingHandler {
 
     try {
       span = await this.createSpan(requestData)
-      await model.handleFxQuoteRequest(headers, payload, span, originalPayload)
+      await model.handleFxQuoteRequest(headers, payload, span, originalPayload, this.cache)
       this.logger.debug('handlePostFxQuotes is done')
     } catch (err) {
       this.logger.error(`error in handlePostFxQuotes: ${err?.stack}`)
diff --git a/src/model/executeRules.js b/src/model/executeRules.js
new file mode 100644
index 00000000..6b3f8565
--- /dev/null
+++ b/src/model/executeRules.js
@@ -0,0 +1,80 @@
+const ErrorHandler = require('@mojaloop/central-services-error-handling')
+
+const rules = require('../../config/rules.json')
+const RulesEngine = require('./rules.js')
+
+module.exports.executeRules = async function executeRules (headers, quoteRequest, originalPayload, payer, payee, operation) {
+  if (rules.length === 0) {
+    return []
+  }
+
+  const facts = {
+    operation,
+    payer,
+    payee,
+    payload: quoteRequest,
+    headers
+  }
+
+  const { events } = await RulesEngine.run(rules, facts)
+
+  this.writeLog(`Rules engine returned events ${JSON.stringify(events)}`)
+
+  return await this.handleRuleEvents(events, headers, quoteRequest, originalPayload)
+}
+
+module.exports.handleRuleEvents = async function handleRuleEvents (events, headers, payload, originalPayload) {
+  const quoteRequest = originalPayload || payload
+  // todo: pass only originalPayload (added this logic only for passing tests)
+
+  // At the time of writing, all events cause the "normal" flow of execution to be interrupted.
+  // So we'll return false when there have been no events whatsoever.
+  if (events.length === 0) {
+    return { terminate: false, quoteRequest, headers }
+  }
+
+  const { INVALID_QUOTE_REQUEST, INTERCEPT_QUOTE } = RulesEngine.events
+
+  const unhandledEvents = events.filter(ev => !(ev.type in RulesEngine.events))
+
+  if (unhandledEvents.length > 0) {
+    // The rules configuration contains events not handled in the code
+    // TODO: validate supplied rules at startup and fail if any invalid rules are discovered.
+    throw new Error('Unhandled event returned by rules engine')
+  }
+
+  const invalidQuoteRequestEvents = events.filter(ev => ev.type === INVALID_QUOTE_REQUEST)
+  if (invalidQuoteRequestEvents.length > 0) {
+    // Use the first event, ignore the others for now. This is ergonomically worse for someone
+    // developing against this service, as they can't see all reasons their quote was invalid at
+    // once. But is a valid solution in the short-term.
+    const { FSPIOPError: code, message } = invalidQuoteRequestEvents[0].params
+    // Will throw an internal server error if property doesn't exist
+    throw ErrorHandler.CreateFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes[code],
+      message, null, headers['fspiop-source'])
+  }
+
+  const interceptQuoteEvents = events.filter(ev => ev.type === INTERCEPT_QUOTE)
+  if (interceptQuoteEvents.length > 1) {
+    // TODO: handle priority. Can we stream events?
+    throw new Error('Multiple intercept quote events received')
+  }
+  if (interceptQuoteEvents.length > 0) {
+    // send the quote request to the recipient in the event
+    const result = {
+      terminate: false,
+      quoteRequest,
+      headers: {
+        ...headers,
+        'fspiop-destination': interceptQuoteEvents[0].params.rerouteToFsp
+      }
+    }
+    // if additionalHeaders are present then add the additional non-standard headers (e.g. used by forex)
+    // Note these headers are not part of the mojaloop specification
+    if (interceptQuoteEvents[0].params.additionalHeaders) {
+      result.headers = { ...result.headers, ...interceptQuoteEvents[0].params.additionalHeaders }
+      result.additionalHeaders = interceptQuoteEvents[0].params.additionalHeaders
+    }
+    return result
+  }
+}
diff --git a/src/model/fxQuotes.js b/src/model/fxQuotes.js
index 105327fa..e9c1f752 100644
--- a/src/model/fxQuotes.js
+++ b/src/model/fxQuotes.js
@@ -29,9 +29,10 @@ const Metrics = require('@mojaloop/central-services-metrics')
 const Config = require('../lib/config')
 const { logger } = require('../lib')
 const { httpRequest } = require('../lib/http')
-const { getStackOrInspect, generateRequestHeadersForJWS, generateRequestHeaders, getParticipantEndpoint, calculateRequestHash } = require('../lib/util')
+const { getStackOrInspect, generateRequestHeadersForJWS, generateRequestHeaders, getParticipantEndpoint, calculateRequestHash, fetchParticipantInfo } = require('../lib/util')
 const LOCAL_ENUM = require('../lib/enum')
 const { RESOURCES, ERROR_MESSAGES } = require('../constants')
+const { executeRules, handleRuleEvents } = require('./executeRules')
 
 axios.defaults.headers.common = {}
 
@@ -48,6 +49,10 @@ class FxQuotesModel {
     })
   }
 
+  executeRules = executeRules
+  handleRuleEvents = handleRuleEvents
+  _fetchParticipantInfo = fetchParticipantInfo
+
   /**
    * Validates the fxQuote request object
    *
@@ -160,7 +165,7 @@ class FxQuotesModel {
    *
    * @returns {undefined}
    */
-  async handleFxQuoteRequest (headers, fxQuoteRequest, span, originalPayload = fxQuoteRequest) {
+  async handleFxQuoteRequest (headers, fxQuoteRequest, span, originalPayload = fxQuoteRequest, cache) {
     // todo: remove default value for originalPayload (added just for passing tests)
     const histTimer = Metrics.getHistogram(
       'model_fxquote',
@@ -227,6 +232,17 @@ class FxQuotesModel {
         await txn.commit()
       }
 
+      const { payer, payee } = await this._fetchParticipantInfo(fspiopSource, fspiopDestination, cache)
+      this.writeLog(`Got payer ${payer} and payee ${payee}`)
+
+      // Run the rules engine. If the user does not want to run the rules engine, they need only to
+      // supply a rules file containing an empty array.
+      const handledRuleEvents = await this.executeRules(headers, fxQuoteRequest, originalPayload, payer, payee, 'fxQuoteRequest')
+
+      if (handledRuleEvents.terminate) {
+        return
+      }
+
       await this.forwardFxQuoteRequest(headers, fxQuoteRequest.conversionRequestId, originalPayload, childSpan)
       histTimer({ success: true, queryName: 'handleFxQuoteRequest' })
     } catch (err) {
@@ -810,6 +826,16 @@ class FxQuotesModel {
       opts.headers['fspiop-signature'] = jwsSigner.getSignature(opts)
     }
   }
+
+  /**
+   * Writes a formatted message to the console
+   *
+   * @returns {undefined}
+   */
+  // eslint-disable-next-line no-unused-vars
+  writeLog (message) {
+    Logger.isDebugEnabled && Logger.debug(`(${this.requestId}) [quotesmodel]: ${message}`)
+  }
 }
 
 module.exports = FxQuotesModel
diff --git a/src/model/quotes.js b/src/model/quotes.js
index f5ce3605..9ef19a99 100644
--- a/src/model/quotes.js
+++ b/src/model/quotes.js
@@ -52,8 +52,7 @@ const { httpRequest } = require('../lib/http')
 const { getStackOrInspect, generateRequestHeadersForJWS, generateRequestHeaders, calculateRequestHash, fetchParticipantInfo, getParticipantEndpoint } = require('../lib/util')
 const { RESOURCES } = require('../constants')
 const LOCAL_ENUM = require('../lib/enum')
-const rules = require('../../config/rules.json')
-const RulesEngine = require('./rules.js')
+const { executeRules, handleRuleEvents } = require('./executeRules')
 
 delete axios.defaults.headers.common.Accept
 delete axios.defaults.headers.common['Content-Type']
@@ -75,79 +74,11 @@ class QuotesModel {
     })
   }
 
-  async executeRules (headers, quoteRequest, payer, payee) {
-    if (rules.length === 0) {
-      return []
-    }
-
-    const facts = {
-      payer,
-      payee,
-      payload: quoteRequest,
-      headers
-    }
-
-    const { events } = await RulesEngine.run(rules, facts)
-    this.log.debug('Rules engine returned events:', { events })
-
-    return events
+  executeRules () {
+    return executeRules.apply(this, arguments)
   }
 
-  async handleRuleEvents (events, headers, payload, originalPayload) {
-    const quoteRequest = originalPayload || payload
-    // todo: pass only originalPayload (added this logic only for passing tests)
-
-    // At the time of writing, all events cause the "normal" flow of execution to be interrupted.
-    // So we'll return false when there have been no events whatsoever.
-    if (events.length === 0) {
-      return { terminate: false, quoteRequest, headers }
-    }
-
-    const { INVALID_QUOTE_REQUEST, INTERCEPT_QUOTE } = RulesEngine.events
-
-    const unhandledEvents = events.filter(ev => !(ev.type in RulesEngine.events))
-
-    if (unhandledEvents.length > 0) {
-      // The rules configuration contains events not handled in the code
-      // TODO: validate supplied rules at startup and fail if any invalid rules are discovered.
-      throw new Error('Unhandled event returned by rules engine')
-    }
-
-    const invalidQuoteRequestEvents = events.filter(ev => ev.type === INVALID_QUOTE_REQUEST)
-    if (invalidQuoteRequestEvents.length > 0) {
-      // Use the first event, ignore the others for now. This is ergonomically worse for someone
-      // developing against this service, as they can't see all reasons their quote was invalid at
-      // once. But is a valid solution in the short-term.
-      const { FSPIOPError: code, message } = invalidQuoteRequestEvents[0].params
-      // Will throw an internal server error if property doesn't exist
-      throw ErrorHandler.CreateFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes[code],
-        message, null, headers['fspiop-source'])
-    }
-
-    const interceptQuoteEvents = events.filter(ev => ev.type === INTERCEPT_QUOTE)
-    if (interceptQuoteEvents.length > 1) {
-      // TODO: handle priority. Can we stream events?
-      throw new Error('Multiple intercept quote events received')
-    }
-    if (interceptQuoteEvents.length > 0) {
-      // send the quote request to the recipient in the event
-      const result = {
-        terminate: false,
-        quoteRequest,
-        headers: {
-          ...headers,
-          'fspiop-destination': interceptQuoteEvents[0].params.rerouteToFsp
-        }
-      }
-      // if additionalHeaders are present then add the additional non-standard headers (e.g. used by forex)
-      // Note these headers are not part of the mojaloop specification
-      if (interceptQuoteEvents[0].params.additionalHeaders) {
-        result.headers = { ...result.headers, ...interceptQuoteEvents[0].params.additionalHeaders }
-        result.additionalHeaders = interceptQuoteEvents[0].params.additionalHeaders
-      }
-      return result
-    }
-  }
+  handleRuleEvents = handleRuleEvents
 
   /**
    * Validates the quote request object
@@ -281,9 +212,8 @@ class QuotesModel {
 
       // Run the rules engine. If the user does not want to run the rules engine, they need only to
       // supply a rules file containing an empty array.
-      const events = await this.executeRules(headers, quoteRequest, payer, payee)
+      handledRuleEvents = await this.executeRules(headers, quoteRequest, originalPayload, payer, payee, 'quoteRequest')
 
-      handledRuleEvents = await this.handleRuleEvents(events, headers, quoteRequest, originalPayload)
       if (handledRuleEvents.terminate) {
         return
       }
diff --git a/src/model/rules.js b/src/model/rules.js
index c8d0a430..aceebe64 100644
--- a/src/model/rules.js
+++ b/src/model/rules.js
@@ -59,6 +59,9 @@ const createEngine = () => {
     }
   }
 
+  engine.addOperator('truthy', (factValue, ruleValue) => {
+    return !!factValue === ruleValue
+  })
   engine.addOperator('notDeepEqual', (factValue, ruleValue) => {
     return !deepEqual(factValue, ruleValue)
   })
diff --git a/test/unit/model/fxQuotes.test.js b/test/unit/model/fxQuotes.test.js
index dd141e8d..fd225751 100644
--- a/test/unit/model/fxQuotes.test.js
+++ b/test/unit/model/fxQuotes.test.js
@@ -136,6 +136,7 @@ describe('FxQuotesModel Tests -->', () => {
   describe('handleFxQuoteRequest', () => {
     test('should handle fx quote request', async () => {
       fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log })
+      fxQuotesModel._fetchParticipantInfo = jest.fn(() => ({ payer: 'payer', payee: 'payee' }))
       jest.spyOn(fxQuotesModel, 'forwardFxQuoteRequest').mockResolvedValue()
       jest.spyOn(fxQuotesModel, 'validateFxQuoteRequest')
 
@@ -175,6 +176,7 @@ describe('FxQuotesModel Tests -->', () => {
 
     test('should handle fx quote request in persistent mode', async () => {
       fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log })
+      fxQuotesModel._fetchParticipantInfo = jest.fn(() => ({ payer: 'payer', payee: 'payee' }))
       fxQuotesModel.envConfig.simpleRoutingMode = false
 
       jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteRequest').mockResolvedValue({
@@ -215,6 +217,7 @@ describe('FxQuotesModel Tests -->', () => {
 
     test('should handle error thrown', async () => {
       fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log })
+      fxQuotesModel._fetchParticipantInfo = jest.fn(() => ({ payer: 'payer', payee: 'payee' }))
       jest.spyOn(fxQuotesModel, 'forwardFxQuoteRequest').mockRejectedValue(new Error('Forward Error'))
       jest.spyOn(fxQuotesModel, 'validateFxQuoteRequest')
       jest.spyOn(fxQuotesModel, 'handleException').mockResolvedValue()
diff --git a/test/unit/model/quotes.test.js b/test/unit/model/quotes.test.js
index a5eae643..aad75fa5 100644
--- a/test/unit/model/quotes.test.js
+++ b/test/unit/model/quotes.test.js
@@ -428,9 +428,8 @@ describe('QuotesModel', () => {
           const payer = { accounts: [{ accountId: 1, ledgerAccountType: 'POSITION', isActive: 1 }] }
           const payee = { accounts: [{ accountId: 2, ledgerAccountType: 'POSITION', isActive: 1 }] }
 
-          await expect(quotesModel.executeRules(mockData.headers, mockData.quoteRequest, payer, payee))
-            .resolves
-            .toEqual(expectedEvents)
+          await quotesModel.executeRules(mockData.headers, mockData.quoteRequest, payer, payee)
+          expect(quotesModel.handleRuleEvents).toHaveBeenCalledWith(expectedEvents, expect.anything(), expect.anything(), expect.anything())
         })
       })
     })
@@ -718,6 +717,10 @@ describe('QuotesModel', () => {
 
     describe('Failures:', () => {
       describe('Before forwarding the request:', () => {
+        beforeEach(() => {
+          quotesModel.executeRules.mockRestore()
+        })
+
         it('throws an exception if `executeRules` fails', async () => {
           expect.assertions(1)
 
@@ -1012,6 +1015,7 @@ describe('QuotesModel', () => {
         describe('In case environment is configured for simple routing mode', () => {
           beforeEach(() => {
             mockConfig.simpleRoutingMode = true
+            quotesModel.executeRules.mockRestore()
           })
 
           it('calls `handleException` with the proper arguments if `span.audit` fails', async () => {
@@ -1058,6 +1062,7 @@ describe('QuotesModel', () => {
 
           beforeEach(() => {
             mockConfig.simpleRoutingMode = false
+            quotesModel.executeRules.mockRestore()
 
             expectedResult = {
               amountTypeId: mockData.amountTypeId,
@@ -1138,6 +1143,7 @@ describe('QuotesModel', () => {
             }
           }))
 
+          quotesModel.executeRules.mockRestore()
           const result = await quotesModel.handleQuoteRequest(mockData.headers, mockData.quoteRequest, mockSpan)
 
           expect(quotesModel.db.createQuoteDuplicateCheck.mock.calls.length).toBe(0)
@@ -1149,6 +1155,7 @@ describe('QuotesModel', () => {
         describe('In case environment is configured for simple routing mode', () => {
           beforeEach(() => {
             mockConfig.simpleRoutingMode = true
+            quotesModel.executeRules.mockRestore()
           })
 
           it('forwards the quote request properly', async () => {
@@ -1174,6 +1181,7 @@ describe('QuotesModel', () => {
 
           beforeEach(() => {
             mockConfig.simpleRoutingMode = false
+            quotesModel.executeRules.mockRestore()
 
             expectedResult = {
               amountTypeId: mockData.amountTypeId,

From d4440d84a7106a5308bc7c1fa533c9891647b005 Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 08:26:54 +0000
Subject: [PATCH 02/16] test: fix integration tests

---
 docker-compose.yml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/docker-compose.yml b/docker-compose.yml
index c4a5573c..5acb4b6a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -130,6 +130,7 @@ services:
 
 #  To use with proxyCache.type === 'redis-cluster'
   redis-node-0:
+    container_name: redis-node-0
     <<: *REDIS_NODE
     environment:
       <<: *REDIS_ENVS
@@ -142,6 +143,7 @@ services:
       - redis-node-4
       - redis-node-5
   redis-node-1:
+    container_name: redis-node-1
     <<: *REDIS_NODE
     environment:
       <<: *REDIS_ENVS
@@ -149,6 +151,7 @@ services:
     ports:
       - "16380:16380"
   redis-node-2:
+    container_name: redis-node-2
     <<: *REDIS_NODE
     environment:
       <<: *REDIS_ENVS
@@ -156,6 +159,7 @@ services:
     ports:
       - "16381:16381"
   redis-node-3:
+    container_name: redis-node-3
     <<: *REDIS_NODE
     environment:
       <<: *REDIS_ENVS
@@ -163,6 +167,7 @@ services:
     ports:
       - "16382:16382"
   redis-node-4:
+    container_name: redis-node-4
     <<: *REDIS_NODE
     environment:
       <<: *REDIS_ENVS
@@ -170,6 +175,7 @@ services:
     ports:
       - "16383:16383"
   redis-node-5:
+    container_name: redis-node-5
     <<: *REDIS_NODE
     environment:
       <<: *REDIS_ENVS

From 5514c840d9537867eb85d65f4ec859e4078c0c40 Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 08:47:01 +0000
Subject: [PATCH 03/16] test: fix integration tests

---
 test/integration/fxQuotes.test.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/test/integration/fxQuotes.test.js b/test/integration/fxQuotes.test.js
index 190ea68a..a0a64f08 100644
--- a/test/integration/fxQuotes.test.js
+++ b/test/integration/fxQuotes.test.js
@@ -122,6 +122,7 @@ describe('POST /fxQuotes request tests --> ', () => {
       expect(isOk).toBe(true)
 
       response = await getResponseWithRetry()
+      if (response.data.history.length !== 1) console.log(response.data.history)
       expect(response.data.history.length).toBe(1)
 
       // assert that the request was received by the proxy
@@ -194,6 +195,7 @@ describe('POST /fxQuotes request tests --> ', () => {
       expect(isOk).toBe(true)
 
       response = await getResponseWithRetry()
+      if (response.data.history.length !== 1) console.log(response.data.history)
       expect(response.data.history.length).toBe(1)
 
       // assert that the callback was received by the payer dfsp
@@ -266,6 +268,7 @@ describe('POST /fxQuotes request tests --> ', () => {
     expect(isOk).toBe(true)
 
     response = await getResponseWithRetry()
+    if (response.data.history.length !== 1) console.log(response.data.history)
     expect(response.data.history.length).toBe(1)
 
     // assert that the request was received by the payee dfsp

From 6ed18ae94076354afa5ac256c83db9412bbf7f9a Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 09:17:25 +0000
Subject: [PATCH 04/16] test: fix integration tests

---
 test/integration/fxQuotes.test.js    | 6 ++----
 test/integration/postRequest.test.js | 1 +
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/test/integration/fxQuotes.test.js b/test/integration/fxQuotes.test.js
index a0a64f08..80b59557 100644
--- a/test/integration/fxQuotes.test.js
+++ b/test/integration/fxQuotes.test.js
@@ -122,8 +122,7 @@ describe('POST /fxQuotes request tests --> ', () => {
       expect(isOk).toBe(true)
 
       response = await getResponseWithRetry()
-      if (response.data.history.length !== 1) console.log(response.data.history)
-      expect(response.data.history.length).toBe(1)
+      expect(response.data.history.length).toBe(3) // count 2 extra calls to redbank and pinkbank
 
       // assert that the request was received by the proxy
       const request = response.data.history[0]
@@ -268,8 +267,7 @@ describe('POST /fxQuotes request tests --> ', () => {
     expect(isOk).toBe(true)
 
     response = await getResponseWithRetry()
-    if (response.data.history.length !== 1) console.log(response.data.history)
-    expect(response.data.history.length).toBe(1)
+    expect(response.data.history.length).toBe(2) // count 1 extra call to greenbank
 
     // assert that the request was received by the payee dfsp
     const request = response.data.history[0]
diff --git a/test/integration/postRequest.test.js b/test/integration/postRequest.test.js
index aed22f13..57530f54 100644
--- a/test/integration/postRequest.test.js
+++ b/test/integration/postRequest.test.js
@@ -92,6 +92,7 @@ describe('POST request tests --> ', () => {
 
     expect(response.data.history.length).toBeGreaterThan(0)
     const { url } = response.data.history[0]
+    console.log('95 ===>', response.data.history)
     expect(url).toBe(`/${message.to}/quotes`)
   })
 

From e97519bfa27e3f88a99f08b043891f3e4006004c Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 12:22:56 +0000
Subject: [PATCH 05/16] fix: github-advanced-security alerts

---
 test/integration/postRequest.test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/integration/postRequest.test.js b/test/integration/postRequest.test.js
index 57530f54..6baca154 100644
--- a/test/integration/postRequest.test.js
+++ b/test/integration/postRequest.test.js
@@ -92,7 +92,7 @@ describe('POST request tests --> ', () => {
 
     expect(response.data.history.length).toBeGreaterThan(0)
     const { url } = response.data.history[0]
-    console.log('95 ===>', response.data.history)
+    console.log('95 ===>', response.data.history[0])
     expect(url).toBe(`/${message.to}/quotes`)
   })
 

From 7c3b66d363bcd14df9f6c7177783549db2f9a1a4 Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 13:41:57 +0000
Subject: [PATCH 06/16] fix: executeRules

---
 src/model/executeRules.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/model/executeRules.js b/src/model/executeRules.js
index 6b3f8565..fe9c9b09 100644
--- a/src/model/executeRules.js
+++ b/src/model/executeRules.js
@@ -5,7 +5,7 @@ const RulesEngine = require('./rules.js')
 
 module.exports.executeRules = async function executeRules (headers, quoteRequest, originalPayload, payer, payee, operation) {
   if (rules.length === 0) {
-    return []
+    this.handleRuleEvents([], headers, quoteRequest, originalPayload)
   }
 
   const facts = {

From 9ebe28cf068fc73807365169fd9b1ef74fb3d58c Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 14:28:20 +0000
Subject: [PATCH 07/16] fix: executeRules

---
 src/model/executeRules.js      | 2 +-
 test/unit/model/quotes.test.js | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/model/executeRules.js b/src/model/executeRules.js
index fe9c9b09..3032e5f1 100644
--- a/src/model/executeRules.js
+++ b/src/model/executeRules.js
@@ -5,7 +5,7 @@ const RulesEngine = require('./rules.js')
 
 module.exports.executeRules = async function executeRules (headers, quoteRequest, originalPayload, payer, payee, operation) {
   if (rules.length === 0) {
-    this.handleRuleEvents([], headers, quoteRequest, originalPayload)
+    return await this.handleRuleEvents([], headers, quoteRequest, originalPayload)
   }
 
   const facts = {
diff --git a/test/unit/model/quotes.test.js b/test/unit/model/quotes.test.js
index aad75fa5..556e5eb5 100644
--- a/test/unit/model/quotes.test.js
+++ b/test/unit/model/quotes.test.js
@@ -406,9 +406,10 @@ describe('QuotesModel', () => {
         it('stops execution', async () => {
           expect(rules.length).toBe(0)
 
+          quotesModel.handleRuleEvents.mockRestore()
           await expect(quotesModel.executeRules(mockData.headers, mockData.quoteRequest))
             .resolves
-            .toEqual([])
+            .toEqual({ terminate: false, headers: mockData.headers, quoteRequest: mockData.quoteRequest })
 
           expect(axios.request.mock.calls.length).toBe(0)
         })

From c8d0b3336e35ed149064a3c5d8b526cb83bfbd65 Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 14:34:49 +0000
Subject: [PATCH 08/16] fix: executeRules

---
 package-lock.json | 9 +++++----
 package.json      | 4 ++--
 2 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index fab4cb26..b03865c0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
         "@mojaloop/event-sdk": "14.1.1",
         "@mojaloop/inter-scheme-proxy-cache-lib": "2.3.0",
         "@mojaloop/ml-number": "11.2.4",
-        "@mojaloop/ml-schema-transformer-lib": "1.1.1",
+        "@mojaloop/ml-schema-transformer-lib": "1.1.3",
         "@mojaloop/sdk-standard-components": "18.1.0",
         "ajv": "8.17.1",
         "ajv-keywords": "5.1.0",
@@ -2042,9 +2042,10 @@
       }
     },
     "node_modules/@mojaloop/ml-schema-transformer-lib": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@mojaloop/ml-schema-transformer-lib/-/ml-schema-transformer-lib-1.1.1.tgz",
-      "integrity": "sha512-+zKE2IVwV6sXwwdWnwyXN2qRjLhfztqwHPVciBici3bGx4S7LNSaQ1dfQSueZJA1qwR4TGARZjv2WiKxC4eOxw==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@mojaloop/ml-schema-transformer-lib/-/ml-schema-transformer-lib-1.1.3.tgz",
+      "integrity": "sha512-vAFnooDnGtfzrxAhsBg7ER3ufhzDaFv/Kdi2DF4/UTVb/EKVNb4JnLs2rcRgL3Mhvn439pw19iqOiK8b0rS+lg==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@mojaloop/central-services-error-handling": "^13.0.1",
         "@mojaloop/central-services-logger": "^11.5.1",
diff --git a/package.json b/package.json
index 31f11656..4e9a28f6 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
     "test:coverage": "jest --coverage --coverageThreshold='{}' --testMatch '**/test/unit/**/*.test.js'",
     "test:coverage-check": "jest --runInBand --forceExit --coverage --testMatch '**/test/unit/**/*.test.js'",
     "test:junit": "jest --runInBand --forceExit --reporters=default --reporters=jest-junit --testMatch '**/test/unit/**/*.test.js'",
-    "test:int": "jest --runInBand --testMatch '**/test/integration/**/*.test.js'",
+    "test:int": "jest --runInBand --testMatch '**/test/integration/postRequest.test.js'",
     "test:integration": "./test/integration/scripts/start.sh && npm run test:int",
     "test:functional": "true",
     "regenerate": "yo swaggerize:test --framework hapi --apiPath './src/interface/swagger.json'",
@@ -116,7 +116,7 @@
     "@mojaloop/event-sdk": "14.1.1",
     "@mojaloop/inter-scheme-proxy-cache-lib": "2.3.0",
     "@mojaloop/ml-number": "11.2.4",
-    "@mojaloop/ml-schema-transformer-lib": "1.1.1",
+    "@mojaloop/ml-schema-transformer-lib": "1.1.3",
     "@mojaloop/sdk-standard-components": "18.1.0",
     "ajv": "8.17.1",
     "ajv-keywords": "5.1.0",

From 2d3bbe6cafe898eb5af5a8cce0aaa0e9ab977beb Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 15:01:27 +0000
Subject: [PATCH 09/16] fix: executeRules

---
 test/mocks.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/mocks.js b/test/mocks.js
index 51396997..43ea3455 100644
--- a/test/mocks.js
+++ b/test/mocks.js
@@ -140,7 +140,8 @@ const postFxQuotesPayloadDto = ({
     amount: 300
   },
   targetAmount = {
-    currency: 'ZMW'
+    currency: 'ZMW',
+    amount: 0
   },
   expiration = new Date(Date.now() + 5 * 60 * 1000).toISOString(),
   extensionList = {

From 54a31da239834ff56e0513252bdaed24183c804d Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 15:27:48 +0000
Subject: [PATCH 10/16] fix: executeRules

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4e9a28f6..72ddb2be 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
     "test:coverage": "jest --coverage --coverageThreshold='{}' --testMatch '**/test/unit/**/*.test.js'",
     "test:coverage-check": "jest --runInBand --forceExit --coverage --testMatch '**/test/unit/**/*.test.js'",
     "test:junit": "jest --runInBand --forceExit --reporters=default --reporters=jest-junit --testMatch '**/test/unit/**/*.test.js'",
-    "test:int": "jest --runInBand --testMatch '**/test/integration/postRequest.test.js'",
+    "test:int": "jest --runInBand --testMatch '**/test/integration/**/*.test.js'",
     "test:integration": "./test/integration/scripts/start.sh && npm run test:int",
     "test:functional": "true",
     "regenerate": "yo swaggerize:test --framework hapi --apiPath './src/interface/swagger.json'",

From 77831fdfb8df6ba06535af01e3bd48266958c42e Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 15:29:54 +0000
Subject: [PATCH 11/16] fix: executeRules

---
 test/integration/postRequest.test.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/test/integration/postRequest.test.js b/test/integration/postRequest.test.js
index 6baca154..aed22f13 100644
--- a/test/integration/postRequest.test.js
+++ b/test/integration/postRequest.test.js
@@ -92,7 +92,6 @@ describe('POST request tests --> ', () => {
 
     expect(response.data.history.length).toBeGreaterThan(0)
     const { url } = response.data.history[0]
-    console.log('95 ===>', response.data.history[0])
     expect(url).toBe(`/${message.to}/quotes`)
   })
 

From b97575a89369c5cb3784582f1493d96ff1f26d2f Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Mon, 21 Oct 2024 15:49:09 +0000
Subject: [PATCH 12/16] fix: executeRules

---
 test/integration/fxQuotes.test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/integration/fxQuotes.test.js b/test/integration/fxQuotes.test.js
index 80b59557..83e4bfd8 100644
--- a/test/integration/fxQuotes.test.js
+++ b/test/integration/fxQuotes.test.js
@@ -143,7 +143,7 @@ describe('POST /fxQuotes request tests --> ', () => {
         counterPartyFsp: payload.conversionTerms.counterPartyFsp,
         sourceAmount: payload.conversionTerms.sourceAmount.amount,
         sourceCurrency: payload.conversionTerms.sourceAmount.currency,
-        targetAmount: null,
+        targetAmount: 0,
         targetCurrency: payload.conversionTerms.targetAmount.currency,
         extensions: expect.anything(),
         expirationDate: expect.anything(),

From 3888928d03365e5c2a18025dc97c85ed7e72c24e Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Tue, 22 Oct 2024 06:19:22 +0000
Subject: [PATCH 13/16] test: allow rules.js

---
 src/model/executeRules.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/model/executeRules.js b/src/model/executeRules.js
index 3032e5f1..9fd5e686 100644
--- a/src/model/executeRules.js
+++ b/src/model/executeRules.js
@@ -1,6 +1,6 @@
 const ErrorHandler = require('@mojaloop/central-services-error-handling')
 
-const rules = require('../../config/rules.json')
+const rules = require('../../config/rules')
 const RulesEngine = require('./rules.js')
 
 module.exports.executeRules = async function executeRules (headers, quoteRequest, originalPayload, payer, payee, operation) {

From 85a525035a93342070c54de528930a181a802bfd Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Wed, 13 Nov 2024 08:36:46 +0000
Subject: [PATCH 14/16] test: coverage

---
 test/unit/lib/payloadCache.test.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/test/unit/lib/payloadCache.test.js b/test/unit/lib/payloadCache.test.js
index 6e9daf01..5b5e88f2 100644
--- a/test/unit/lib/payloadCache.test.js
+++ b/test/unit/lib/payloadCache.test.js
@@ -28,6 +28,12 @@ describe('Payload Cache Tests -->', () => {
     ])
   })
 
+  test('should should throw for invalid type', () => {
+    expect(() => {
+      createPayloadCache('invalid', connectionConfig)
+    }).toThrow()
+  })
+
   test('should create an instance of PayloadCache', () => {
     const payloadCache = createPayloadCache(type, connectionConfig)
     expect(payloadCache).toBeInstanceOf(PayloadCache)

From 7d7a7adcf6a173282e8c5caebee43d3dff4d2df9 Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Wed, 13 Nov 2024 09:00:13 +0000
Subject: [PATCH 15/16] test: coverage

---
 test/unit/data/cachedDatabase.test.js | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/test/unit/data/cachedDatabase.test.js b/test/unit/data/cachedDatabase.test.js
index 59d3d771..2d4c5832 100644
--- a/test/unit/data/cachedDatabase.test.js
+++ b/test/unit/data/cachedDatabase.test.js
@@ -201,19 +201,26 @@ describe('cachedDatabase', () => {
     it('tries to get a value where none is cached', async () => {
       // Arrange
       // Mocking superclasses is a little tricky -- so we directly override the prototype here
-      Database.prototype.getLedgerEntryType = jest.fn().mockReturnValueOnce({ ledgerEntryType: true })
-      const expected = { ledgerEntryType: true }
+      const expectedLedgerEntryType = { ledgerEntryType: true }
+      Database.prototype.getLedgerEntryType = jest.fn().mockReturnValueOnce(expectedLedgerEntryType)
+      const expectedParticipant = {}
+      Database.prototype.getParticipant = jest.fn().mockReturnValueOnce(expectedParticipant)
 
       // Act
       const result = await cachedDb.getCacheValue('getLedgerEntryType', ['paramA'])
+      const result3 = await cachedDb.getCacheValue('getParticipant', [])
       // Result should now be cached
       const result2 = await cachedDb.getCacheValue('getLedgerEntryType', ['paramA'])
+      const result4 = await cachedDb.getCacheValue('getParticipant', [])
 
       // Assert
       // Check that we only called the super method once, the 2nd time should be cached
       expect(Database.prototype.getLedgerEntryType).toBeCalledTimes(1)
-      expect(result).toStrictEqual(expected)
-      expect(result2).toStrictEqual(expected)
+      expect(Database.prototype.getParticipant).toBeCalledTimes(1)
+      expect(result).toStrictEqual(expectedLedgerEntryType)
+      expect(result2).toStrictEqual(expectedLedgerEntryType)
+      expect(result3).toStrictEqual(expectedParticipant)
+      expect(result4).toStrictEqual(expectedParticipant)
 
       // invalidate to stop jest open handles
       await cachedDb.invalidateCache()

From 1e58d94daac34c5503e8a0374cb2a7316b58e847 Mon Sep 17 00:00:00 2001
From: Kalin Krustev <kalin.krustev@gmail.com>
Date: Wed, 13 Nov 2024 09:07:44 +0000
Subject: [PATCH 16/16] chore: update dependencies

---
 package-lock.json | 32 ++++++++++++++++----------------
 package.json      |  6 +++---
 2 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 9cc26878..4cb1e5c7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
         "@mojaloop/event-sdk": "14.1.1",
         "@mojaloop/inter-scheme-proxy-cache-lib": "2.3.0",
         "@mojaloop/ml-number": "11.2.4",
-        "@mojaloop/ml-schema-transformer-lib": "2.0.1",
+        "@mojaloop/ml-schema-transformer-lib": "2.3.4",
         "@mojaloop/sdk-standard-components": "18.1.0",
         "ajv": "8.17.1",
         "ajv-keywords": "5.1.0",
@@ -47,11 +47,11 @@
         "audit-ci": "^7.1.0",
         "eslint": "8.16.0",
         "eslint-config-standard": "17.1.0",
-        "eslint-plugin-jest": "28.8.3",
+        "eslint-plugin-jest": "28.9.0",
         "ioredis-mock": "8.9.0",
         "jest": "29.7.0",
         "jest-junit": "16.0.0",
-        "npm-check-updates": "17.1.6",
+        "npm-check-updates": "17.1.11",
         "nyc": "17.1.0",
         "pre-commit": "1.2.2",
         "proxyquire": "2.1.3",
@@ -2035,9 +2035,9 @@
       }
     },
     "node_modules/@mojaloop/ml-schema-transformer-lib": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@mojaloop/ml-schema-transformer-lib/-/ml-schema-transformer-lib-2.0.1.tgz",
-      "integrity": "sha512-3LJBQv8x/+60DvS3gOCySSF8KYf6FNOHe/L2h+g7Kr4PqZdbISf1DNAqUi9i7d7YVP1Gx3RDBIkKEq7dPvoAZA==",
+      "version": "2.3.4",
+      "resolved": "https://registry.npmjs.org/@mojaloop/ml-schema-transformer-lib/-/ml-schema-transformer-lib-2.3.4.tgz",
+      "integrity": "sha512-ficIupSFyrSRsrRVqVqxaFT6xDbyGCaZKIWX5x4aKU/bH4VoJQ5fpYxeDLPg+HTyvm4qYpj3KDUVKyCc7mMKlQ==",
       "dependencies": {
         "@mojaloop/central-services-error-handling": "^13.0.2",
         "@mojaloop/central-services-logger": "^11.5.1",
@@ -2049,7 +2049,7 @@
         "node": ">=18.x"
       },
       "optionalDependencies": {
-        "@rollup/rollup-linux-x64-musl": "4.24.0"
+        "@rollup/rollup-linux-x64-musl": "4.24.3"
       }
     },
     "node_modules/@mojaloop/ml-schema-transformer-lib/node_modules/@hapi/boom": {
@@ -2227,9 +2227,9 @@
       "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.24.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
-      "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
+      "version": "4.24.3",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz",
+      "integrity": "sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==",
       "cpu": [
         "x64"
       ],
@@ -5418,9 +5418,9 @@
       }
     },
     "node_modules/eslint-plugin-jest": {
-      "version": "28.8.3",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz",
-      "integrity": "sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==",
+      "version": "28.9.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz",
+      "integrity": "sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==",
       "dev": true,
       "dependencies": {
         "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -10046,9 +10046,9 @@
       }
     },
     "node_modules/npm-check-updates": {
-      "version": "17.1.6",
-      "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.6.tgz",
-      "integrity": "sha512-9I0b60plxMrOLuQT3W7f0Aq5FQSMeR1HWGXbkhq9xPTGyOPbS/Oz9wCy2aK6I2hu+vLRHMhU27ObDs1+oU3QfA==",
+      "version": "17.1.11",
+      "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.11.tgz",
+      "integrity": "sha512-TR2RuGIH7P3Qrb0jfdC/nT7JWqXPKjDlxuNQt3kx4oNVf1Pn5SBRB7KLypgYZhruivJthgTtfkkyK4mz342VjA==",
       "dev": true,
       "bin": {
         "ncu": "build/cli.js",
diff --git a/package.json b/package.json
index 9de45b63..49692be6 100644
--- a/package.json
+++ b/package.json
@@ -116,7 +116,7 @@
     "@mojaloop/event-sdk": "14.1.1",
     "@mojaloop/inter-scheme-proxy-cache-lib": "2.3.0",
     "@mojaloop/ml-number": "11.2.4",
-    "@mojaloop/ml-schema-transformer-lib": "2.0.1",
+    "@mojaloop/ml-schema-transformer-lib": "2.3.4",
     "@mojaloop/sdk-standard-components": "18.1.0",
     "ajv": "8.17.1",
     "ajv-keywords": "5.1.0",
@@ -141,11 +141,11 @@
     "audit-ci": "^7.1.0",
     "eslint": "8.16.0",
     "eslint-config-standard": "17.1.0",
-    "eslint-plugin-jest": "28.8.3",
+    "eslint-plugin-jest": "28.9.0",
     "ioredis-mock": "8.9.0",
     "jest": "29.7.0",
     "jest-junit": "16.0.0",
-    "npm-check-updates": "17.1.6",
+    "npm-check-updates": "17.1.11",
     "nyc": "17.1.0",
     "pre-commit": "1.2.2",
     "proxyquire": "2.1.3",