Repository: mojaloop/account-lookup-service
Pull request #535: chore(csi-1300): refactor get/put parties code to add new functionality easier
Author: login: geka-evk
Created: 2025-03-13T10:11:59Z | Merged: 2025-03-13T20:50:29Z

 - Split huge functions, which handle GET/PUT parties requests, into separate steps.
 - Add `GetPartiesService`, `PutPartiesService`, `PutPartiesErrorService` (with dependency injection) to handle all business logic.
 - Improve logging.


Diff:
diff --git a/audit-ci.jsonc b/audit-ci.jsonc
index 090d238d..01fd4792 100644
--- a/audit-ci.jsonc
+++ b/audit-ci.jsonc
@@ -4,5 +4,7 @@
   // Only use one of ["low": true, "moderate": true, "high": true, "critical": true]
   "moderate": true,
   // NOTE: Please add as much information as possible to any items added to the allowList
-  "allowlist": []
+  "allowlist": [
+    "GHSA-968p-4wvh-cqc8" // https://github.com/advisories/GHSA-968p-4wvh-cqc8
+  ]
 }
diff --git a/package-lock.json b/package-lock.json
index e13c5843..07396a79 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "account-lookup-service",
-  "version": "17.3.2",
+  "version": "17.4.1-csi-1300.6",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "account-lookup-service",
-      "version": "17.3.2",
+      "version": "17.4.1-csi-1300.6",
       "license": "Apache-2.0",
       "dependencies": {
         "@hapi/basic": "7.0.2",
@@ -20,7 +20,7 @@
         "@mojaloop/central-services-health": "15.0.4",
         "@mojaloop/central-services-logger": "11.6.2",
         "@mojaloop/central-services-metrics": "12.4.5",
-        "@mojaloop/central-services-shared": "18.22.3",
+        "@mojaloop/central-services-shared": "18.22.4",
         "@mojaloop/central-services-stream": "11.5.1",
         "@mojaloop/database-lib": "11.1.4",
         "@mojaloop/event-sdk": "14.3.2",
@@ -747,6 +747,7 @@
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.0.tgz",
       "integrity": "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@grpc/proto-loader": "^0.7.13",
         "@js-sdsl/ordered-map": "^4.4.2"
@@ -759,6 +760,7 @@
       "version": "0.7.13",
       "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
       "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
+      "license": "Apache-2.0",
       "dependencies": {
         "lodash.camelcase": "^4.3.0",
         "long": "^5.0.0",
@@ -1624,6 +1626,7 @@
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
       "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+      "license": "MIT",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/js-sdsl"
@@ -1717,6 +1720,7 @@
       "version": "11.6.2",
       "resolved": "https://registry.npmjs.org/@mojaloop/central-services-logger/-/central-services-logger-11.6.2.tgz",
       "integrity": "sha512-re3pdtWZFOW6JhGvzAWXoEdaAtHAd3UedXVCfTId4DDOr1UOITtwTE1MYXetEz2EMR4qABFplIsjKTS3bz4wNw==",
+      "license": "Apache-2.0",
       "dependencies": {
         "parse-strings-in-object": "2.0.0",
         "rc": "1.2.8",
@@ -1733,9 +1737,10 @@
       }
     },
     "node_modules/@mojaloop/central-services-shared": {
-      "version": "18.22.3",
-      "resolved": "https://registry.npmjs.org/@mojaloop/central-services-shared/-/central-services-shared-18.22.3.tgz",
-      "integrity": "sha512-W1jSR+KVsIFIyLgIGolvjE7T04Dbwgz3TWUCYQaUtaDCbYXDOaPL4OZkYbmKsS13nf6H7W8YvFeUWclbRsZwDw==",
+      "version": "18.22.4",
+      "resolved": "https://registry.npmjs.org/@mojaloop/central-services-shared/-/central-services-shared-18.22.4.tgz",
+      "integrity": "sha512-O1VCH34UncElxlL2UA4PC6cL9BQFrxtLS5NmY6gFn770d45wmSoIFIeIxaLQNn2ic1BqdYBwOenbTtaMQCC3DQ==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@hapi/catbox": "12.1.1",
         "@hapi/catbox-memory": "5.0.1",
@@ -1863,6 +1868,7 @@
       "version": "11.1.4",
       "resolved": "https://registry.npmjs.org/@mojaloop/database-lib/-/database-lib-11.1.4.tgz",
       "integrity": "sha512-x6ohcasMEHb5cPQ7KWMVHhtLOdYvv5B7MGMML9+YNX2wz0FUSdAXuzW7faigVQ13LzoqN5dxaX1te0H10YrbFQ==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@mojaloop/central-services-error-handling": "13.0.7",
         "async-exit-hook": "^2.0.1",
@@ -1875,6 +1881,7 @@
       "version": "14.3.2",
       "resolved": "https://registry.npmjs.org/@mojaloop/event-sdk/-/event-sdk-14.3.2.tgz",
       "integrity": "sha512-/DbV9HmTIwF15Kc+/0zPUfqojrejmsifvWbfi94dfipa9FxtWzKtrdV3h5xzAJoLqVxgXO4nFrb78cTHT42ojA==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@grpc/grpc-js": "1.13.0",
         "@grpc/proto-loader": "0.7.13",
@@ -1923,6 +1930,7 @@
       "version": "2.5.8",
       "resolved": "https://registry.npmjs.org/@mojaloop/ml-schema-transformer-lib/-/ml-schema-transformer-lib-2.5.8.tgz",
       "integrity": "sha512-NKLygxSrR0afMSAsmpvTquUp5T2JbpMkLh3/hgTM+anog84ph5VFveyJtPkq2Lxwy27kauCrTBarr/sRYcTbcQ==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@mojaloop/central-services-error-handling": "13.0.7",
         "@mojaloop/central-services-logger": "11.6.2",
@@ -1937,10 +1945,119 @@
         "@rollup/rollup-linux-x64-musl": "4.35.0"
       }
     },
+    "node_modules/@mojaloop/ml-schema-transformer-lib/node_modules/@hapi/boom": {
+      "version": "9.1.4",
+      "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz",
+      "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/hoek": "9.x.x"
+      }
+    },
+    "node_modules/@mojaloop/ml-schema-transformer-lib/node_modules/@hapi/catbox-memory": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz",
+      "integrity": "sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/boom": "9.x.x",
+        "@hapi/hoek": "9.x.x"
+      }
+    },
+    "node_modules/@mojaloop/ml-schema-transformer-lib/node_modules/@hapi/hoek": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
+      "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@mojaloop/ml-schema-transformer-lib/node_modules/@mojaloop/central-services-shared": {
+      "version": "18.22.3",
+      "resolved": "https://registry.npmjs.org/@mojaloop/central-services-shared/-/central-services-shared-18.22.3.tgz",
+      "integrity": "sha512-W1jSR+KVsIFIyLgIGolvjE7T04Dbwgz3TWUCYQaUtaDCbYXDOaPL4OZkYbmKsS13nf6H7W8YvFeUWclbRsZwDw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@hapi/catbox": "12.1.1",
+        "@hapi/catbox-memory": "5.0.1",
+        "@hapi/hapi": "21.4.0",
+        "@hapi/joi-date": "2.0.1",
+        "@mojaloop/inter-scheme-proxy-cache-lib": "2.3.7",
+        "axios": "1.8.3",
+        "clone": "2.1.2",
+        "dotenv": "16.4.7",
+        "env-var": "7.5.0",
+        "event-stream": "4.0.1",
+        "fast-safe-stringify": "2.1.1",
+        "immutable": "5.0.3",
+        "ioredis": "5.6.0",
+        "joi": "17.13.3",
+        "lodash": "4.17.21",
+        "mustache": "4.2.0",
+        "openapi-backend": "5.11.1",
+        "raw-body": "3.0.0",
+        "rc": "1.2.8",
+        "shins": "2.6.0",
+        "ulidx": "2.4.1",
+        "uuid4": "2.0.3",
+        "widdershins": "4.0.1",
+        "yaml": "2.7.0"
+      },
+      "peerDependencies": {
+        "@mojaloop/central-services-error-handling": ">=13.x.x",
+        "@mojaloop/central-services-metrics": ">=12.x.x",
+        "@mojaloop/event-sdk": ">=14.1.1",
+        "ajv": "8.x.x",
+        "ajv-keywords": "5.x.x"
+      },
+      "peerDependenciesMeta": {
+        "@mojaloop/central-services-error-handling": {
+          "optional": false
+        },
+        "@mojaloop/central-services-logger": {
+          "optional": false
+        },
+        "@mojaloop/central-services-metrics": {
+          "optional": false
+        },
+        "@mojaloop/event-sdk": {
+          "optional": false
+        },
+        "ajv": {
+          "optional": false
+        },
+        "ajv-keyboards": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/@mojaloop/ml-schema-transformer-lib/node_modules/ioredis": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz",
+      "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ioredis/commands": "^1.1.1",
+        "cluster-key-slot": "^1.1.0",
+        "debug": "^4.3.4",
+        "denque": "^2.1.0",
+        "lodash.defaults": "^4.2.0",
+        "lodash.isarguments": "^3.1.0",
+        "redis-errors": "^1.2.0",
+        "redis-parser": "^3.0.0",
+        "standard-as-callback": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ioredis"
+      }
+    },
     "node_modules/@mojaloop/sdk-standard-components": {
       "version": "19.10.3",
       "resolved": "https://registry.npmjs.org/@mojaloop/sdk-standard-components/-/sdk-standard-components-19.10.3.tgz",
       "integrity": "sha512-9x4d+x4aEkJn89hClZp6xPtgGoHKbCYpwZbkxoQWWoujW8FkfrIAu6rPdvj6JQCkZ8bkHPvir4VaY0CYCt9NlA==",
+      "license": "Apache-2.0",
       "dependencies": {
         "@mojaloop/central-services-logger": "11.6.2",
         "@mojaloop/ml-schema-transformer-lib": "2.5.8",
@@ -2057,27 +2174,32 @@
     "node_modules/@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
-      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/base64": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
-      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/codegen": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
-      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/eventemitter": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
-      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/fetch": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
       "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@protobufjs/aspromise": "^1.1.1",
         "@protobufjs/inquire": "^1.1.0"
@@ -2086,27 +2208,32 @@
     "node_modules/@protobufjs/float": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
-      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/inquire": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
-      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/path": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
-      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/pool": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
-      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@protobufjs/utf8": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
-      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "license": "BSD-3-Clause"
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
       "version": "4.35.0",
@@ -2115,6 +2242,7 @@
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
@@ -2831,6 +2959,7 @@
       "version": "1.8.3",
       "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
       "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+      "license": "MIT",
       "dependencies": {
         "follow-redirects": "^1.15.6",
         "form-data": "^4.0.0",
@@ -7190,6 +7319,7 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/ilp-packet/-/ilp-packet-2.2.0.tgz",
       "integrity": "sha512-QEGqY0HzGrue4r+4GWWe7lB7Xvjij4cyc2XeOTHYmwkO0BjgwzJW85mZJzR9q5HmK8zdFkN6C0CfedAaYiUv9w==",
+      "license": "Apache-2.0",
       "dependencies": {
         "bignumber.js": "^5.0.0",
         "extensible-error": "^1.0.2",
@@ -7220,6 +7350,7 @@
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
       "integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==",
+      "license": "Apache-2.0",
       "engines": {
         "node": ">=0.6"
       }
@@ -9142,7 +9273,8 @@
     "node_modules/lodash.camelcase": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
-      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "license": "MIT"
     },
     "node_modules/lodash.clonedeep": {
       "version": "4.5.0",
@@ -9209,7 +9341,8 @@
     "node_modules/long": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
-      "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="
+      "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==",
+      "license": "Apache-2.0"
     },
     "node_modules/longest": {
       "version": "1.0.1",
@@ -9286,6 +9419,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/map-any-cjs/-/map-any-cjs-1.0.1.tgz",
       "integrity": "sha512-sTO3ceFtm5rT9+sUtrBHiQgd24SlMrIBGZkoLBFSQRbT1GzORdiO0SDaQqDlUSvBsGxEywHCA7CyX4tWPolSEw==",
+      "license": "ISC",
       "engines": {
         "node": ">= 18"
       }
@@ -9311,6 +9445,7 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/map-transform-cjs/-/map-transform-cjs-0.2.0.tgz",
       "integrity": "sha512-Dn2biqrp6w0OZq6sXh3shMBgpPcN13UL9kpwoLELCoWMXWcwkzjFLPagoL6sq1bZZpEREvW3YEooZtpPlYDWpw==",
+      "license": "ISC",
       "dependencies": {
         "deepmerge": "^4.3.1",
         "map-any-cjs": "^1.0.1"
@@ -11321,6 +11456,7 @@
       "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
       "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
       "hasInstallScript": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "@protobufjs/aspromise": "^1.1.2",
         "@protobufjs/base64": "^1.1.2",
diff --git a/package.json b/package.json
index 3a7d7cab..c676383b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "account-lookup-service",
   "description": "Account Lookup Service is used to validate Party and Participant lookups.",
-  "version": "17.3.2",
+  "version": "17.4.1-csi-1300.6",
   "license": "Apache-2.0",
   "author": "ModusBox",
   "contributors": [
@@ -95,7 +95,7 @@
     "@mojaloop/central-services-health": "15.0.4",
     "@mojaloop/central-services-logger": "11.6.2",
     "@mojaloop/central-services-metrics": "12.4.5",
-    "@mojaloop/central-services-shared": "18.22.3",
+    "@mojaloop/central-services-shared": "18.22.4",
     "@mojaloop/central-services-stream": "11.5.1",
     "@mojaloop/database-lib": "11.1.4",
     "@mojaloop/event-sdk": "14.3.2",
diff --git a/src/domain/parties/deps.js b/src/domain/parties/deps.js
new file mode 100644
index 00000000..332471e3
--- /dev/null
+++ b/src/domain/parties/deps.js
@@ -0,0 +1,49 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const { proxies } = require('@mojaloop/central-services-shared').Util
+const oracle = require('../../models/oracle/facade')
+const participant = require('../../models/participantEndpoint/facade')
+const config = require('../../lib/config')
+const partiesUtils = require('./partiesUtils')
+
+const createDeps = ({ cache, proxyCache, childSpan, log, stepState }) => Object.freeze({
+  cache,
+  proxyCache,
+  childSpan,
+  log,
+  stepState,
+  config,
+  oracle,
+  participant,
+  proxies,
+  partiesUtils
+})
+
+module.exports = {
+  createDeps
+}
diff --git a/src/domain/parties/getPartiesByTypeAndID.js b/src/domain/parties/getPartiesByTypeAndID.js
index 22ea1269..016e5a8b 100644
--- a/src/domain/parties/getPartiesByTypeAndID.js
+++ b/src/domain/parties/getPartiesByTypeAndID.js
@@ -23,51 +23,11 @@
  --------------
  **********/
 
-const { Enum, Util } = require('@mojaloop/central-services-shared')
-const ErrorHandler = require('@mojaloop/central-services-error-handling')
 const Metrics = require('@mojaloop/central-services-metrics')
-
-const config = require('../../lib/config')
-const oracle = require('../../models/oracle/facade')
-const participant = require('../../models/participantEndpoint/facade')
-const { createCallbackHeaders } = require('../../lib/headers')
-const { ERROR_MESSAGES } = require('../../constants')
+const libUtil = require('../../lib/util')
 const { logger } = require('../../lib')
-const Config = require('../../lib/config')
-const utils = require('./utils')
-const util = require('../../lib/util')
-
-const { FspEndpointTypes, FspEndpointTemplates } = Enum.EndPoints
-const { Headers, RestMethods } = Enum.Http
-
-const log = logger.child('domain:get-parties')
-const handleErrorOnSendingCallback = utils.createErrorHandlerOnSendingCallback(Config, log)
-
-const proxyCacheTtlSec = 40 // todo: make configurable
-
-const validateRequester = async ({ source, proxy, proxyCache }) => {
-  const sourceParticipant = await participant.validateParticipant(source)
-  if (sourceParticipant) {
-    log.debug('source is in scheme', { source })
-    return source
-  }
-
-  if (!proxy) {
-    const errMessage = ERROR_MESSAGES.sourceFspNotFound
-    throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
-  }
-
-  const proxyParticipant = await participant.validateParticipant(proxy)
-  if (!proxyParticipant) {
-    const errMessage = ERROR_MESSAGES.partyProxyNotFound
-    throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
-  }
-
-  const isCached = await proxyCache.addDfspIdToProxyMapping(source, proxy)
-  // think, what if isCached !== true?
-  log.info('source is added to proxyMapping cache:', { source, proxy, isCached })
-  return proxy
-}
+const { createDeps } = require('./deps')
+const { GetPartiesService } = require('./services')
 
 /**
  * @function getPartiesByTypeAndID
@@ -83,173 +43,33 @@ const validateRequester = async ({ source, proxy, proxyCache }) => {
  * @param {IProxyCache} [proxyCache] - IProxyCache instance
  */
 const getPartiesByTypeAndID = async (headers, params, method, query, span, cache, proxyCache = undefined) => {
+  const component = getPartiesByTypeAndID.name
   const histTimerEnd = Metrics.getHistogram(
-    'getPartiesByTypeAndID',
+    component,
     'Get party by Type and Id',
     ['success']
   ).startTimer()
-  const proxyEnabled = !!(Config.PROXY_CACHE_CONFIG.enabled && proxyCache)
-  const type = params.Type
-  const partySubId = params.SubId
-  const callbackEndpointType = utils.getPartyCbType(partySubId)
-  const source = headers[Headers.FSPIOP.SOURCE]
-  const proxy = proxyEnabled && headers[Headers.FSPIOP.PROXY]
-  let destination = headers[Headers.FSPIOP.DESTINATION]
-  // see https://github.com/mojaloop/design-authority/issues/79
-  // the requester has specified a destination routing header. We should respect that and forward the request directly to the destination
-  // without consulting any oracles.
+  const childSpan = span ? span.getChild(component) : undefined
+  const log = logger.child({ component, params })
+  const stepState = libUtil.initStepState()
 
-  const childSpan = span ? span.getChild('getPartiesByTypeAndID') : undefined
-  log.info('parties::getPartiesByTypeAndID::begin', { source, destination, proxy, params })
+  const deps = createDeps({ cache, proxyCache, childSpan, log, stepState })
+  const service = new GetPartiesService(deps)
+  const results = {}
 
-  let requester
-  let fspiopError
-  let step
   try {
-    requester = await validateRequester({ source, proxy, proxyCache })
-
-    const options = {
-      partyIdType: type,
-      partyIdentifier: params.ID,
-      ...(partySubId && { partySubIdOrType: partySubId })
-    }
-
-    if (destination) {
-      step = 'validateParticipant-1'
-      const destParticipantModel = await participant.validateParticipant(destination)
-      if (!destParticipantModel) {
-        step = 'lookupProxyByDfspId-2'
-        const proxyId = proxyEnabled && await proxyCache.lookupProxyByDfspId(destination)
-
-        if (!proxyId) {
-          log.warn('no destination participant, and no dfsp-to-proxy mapping', { destination })
-          const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
-          throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
-        }
-        destination = proxyId
-      }
-      // all ok, go ahead and forward the request
-      step = 'sendRequest-3'
-      await participant.sendRequest(headers, destination, callbackEndpointType, RestMethods.GET, undefined, options, childSpan)
-
-      histTimerEnd({ success: true })
-      log.info('discovery getPartiesByTypeAndID request was sent to destination', { destination })
-      return
-    }
-
-    step = 'oracleRequest-4'
-    const response = await oracle.oracleRequest(headers, method, params, query, undefined, cache)
-    if (Array.isArray(response?.data?.partyList) && response.data.partyList.length > 0) {
-      // Oracle's API is a standard rest-style end-point Thus a GET /party on the oracle will return all participant-party records. We must filter the results based on the callbackEndpointType to make sure we remove records containing partySubIdOrType when we are in FSPIOP_CALLBACK_URL_PARTIES_GET mode:
-      let filteredResponsePartyList
-      switch (callbackEndpointType) {
-        case FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_GET:
-          filteredResponsePartyList = response.data.partyList.filter(party => party.partySubIdOrType == null) // Filter records that DON'T contain a partySubIdOrType
-          break
-        case FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_SUB_ID_GET:
-          filteredResponsePartyList = response.data.partyList.filter(party => party.partySubIdOrType === partySubId) // Filter records that match partySubIdOrType
-          break
-        default:
-          filteredResponsePartyList = response // Fallback to providing the standard list
-      }
-
-      if (!Array.isArray(filteredResponsePartyList) || !filteredResponsePartyList.length) {
-        const errMessage = 'Requested FSP/Party not found'
-        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
-      }
-
-      let sentCount = 0 // if sentCount === 0 after sending, should we restart the whole process?
-      const sending = filteredResponsePartyList.map(async party => {
-        const { fspId } = party
-        const clonedHeaders = { ...headers }
-        if (!destination) {
-          clonedHeaders[Headers.FSPIOP.DESTINATION] = fspId
-        }
-        step = 'validateParticipant-5'
-        const schemeParticipant = await participant.validateParticipant(fspId)
-        if (schemeParticipant) {
-          sentCount++
-          log.debug('participant is in scheme', { fspId })
-          return participant.sendRequest(clonedHeaders, party.fspId, callbackEndpointType, RestMethods.GET, undefined, options, childSpan)
-        }
-
-        // If the participant is not in the scheme and proxy routing is enabled,
-        // we should check if there is a proxy for it and send the request to the proxy
-        if (proxyEnabled) {
-          step = 'lookupProxyByDfspId-6'
-          const proxyName = await proxyCache.lookupProxyByDfspId(fspId)
-          if (!proxyName) {
-            log.warn('no proxyMapping for participant!  TODO: Delete reference in oracle...', { fspId })
-            // todo: delete reference in oracle
-          } else {
-            sentCount++
-            log.debug('participant NOT is in scheme, use proxy name', { fspId, proxyName })
-            return participant.sendRequest(clonedHeaders, proxyName, callbackEndpointType, RestMethods.GET, undefined, options, childSpan)
-          }
-        }
-      })
-      step = 'sendRequests-7'
-      await Promise.all(sending)
-      log.info('participant.sendRequests to filtered oracle partyList are done', { sentCount })
-      // todo: think what if sentCount === 0 here
-    } else {
-      log.info('empty partyList form oracle, getting proxies list...', { proxyEnabled, params })
-      let filteredProxyNames = []
-
-      if (proxyEnabled) {
-        step = 'getAllProxiesNames-8'
-        const proxyNames = await Util.proxies.getAllProxiesNames(Config.SWITCH_ENDPOINT)
-        filteredProxyNames = proxyNames.filter(name => name !== proxy)
-      }
-
-      if (!filteredProxyNames.length) {
-        const callbackHeaders = createCallbackHeaders({
-          requestHeaders: headers,
-          partyIdType: type,
-          partyIdentifier: params.ID,
-          endpointTemplate: partySubId
-            ? FspEndpointTemplates.PARTIES_SUB_ID_PUT_ERROR
-            : FspEndpointTemplates.PARTIES_PUT_ERROR
-        })
-        fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND)
-        const errorCallbackEndpointType = utils.errorPartyCbType(partySubId)
-        step = 'sendErrorToParticipant-9'
-        await participant.sendErrorToParticipant(requester, errorCallbackEndpointType,
-          fspiopError.toApiErrorObject(config.ERROR_HANDLING), callbackHeaders, params, childSpan)
-      } else {
-        const alsReq = utils.alsRequestDto(source, params)
-        log.info('starting setSendToProxiesList flow: ', { filteredProxyNames, alsReq, proxyCacheTtlSec })
-        step = 'setSendToProxiesList-10'
-        const isCached = await proxyCache.setSendToProxiesList(alsReq, filteredProxyNames, proxyCacheTtlSec)
-        if (!isCached) {
-          throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, ERROR_MESSAGES.failedToCacheSendToProxiesList)
-        }
-
-        step = 'sending-11'
-        const sending = filteredProxyNames.map(
-          proxyName => participant.sendRequest(headers, proxyName, callbackEndpointType, RestMethods.GET, undefined, options, childSpan)
-            .then(({ status, data } = {}) => ({ status, data }))
-        )
-        const results = await Promise.allSettled(sending)
-        const isOk = results.some(result => result.status === 'fulfilled')
-        // If, at least, one request is sent to proxy, we treat the whole flow as successful.
-        // Failed requests should be handled by TTL expired/timeout handler
-        // todo: - think, if we should handle failed requests here (e.g., by calling receivedErrorResponse)
-        log.info('setSendToProxiesList flow is done:', { isOk, results, filteredProxyNames, alsReq })
-        if (!isOk) {
-          throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, ERROR_MESSAGES.proxyConnectionError)
-        }
-      }
-    }
+    await service.handleRequest({ headers, params, query, results })
+    log.info('getPartiesByTypeAndID is done')
     histTimerEnd({ success: true })
-  } catch (err) {
-    fspiopError = await handleErrorOnSendingCallback(err, headers, params, requester)
+  } catch (error) {
+    const { requester } = results
+    results.fspiopError = await service.handleError({ error, requester, headers, params })
     histTimerEnd({ success: false })
-    if (fspiopError) {
-      util.countFspiopError(fspiopError, { operation: 'getPartiesByTypeAndID', step })
+    if (results.fspiopError) {
+      libUtil.countFspiopError(results.fspiopError, { operation: component, step: stepState.step })
     }
   } finally {
-    await utils.finishSpanWithError(childSpan, fspiopError)
+    await libUtil.finishSpanWithError(childSpan, results.fspiopError)
   }
 }
 
diff --git a/src/domain/parties/index.js b/src/domain/parties/index.js
index d39fe75c..2e6c41e6 100644
--- a/src/domain/parties/index.js
+++ b/src/domain/parties/index.js
@@ -25,8 +25,11 @@
 
 'use strict'
 
-const parties = require('./parties')
+const getPartiesByTypeAndID = require('./getPartiesByTypeAndID')
+const { putPartiesByTypeAndID, putPartiesErrorByTypeAndID } = require('./putParties')
 
-exports.getPartiesByTypeAndID = parties.getPartiesByTypeAndID
-exports.putPartiesByTypeAndID = parties.putPartiesByTypeAndID
-exports.putPartiesErrorByTypeAndID = parties.putPartiesErrorByTypeAndID
+module.exports = {
+  getPartiesByTypeAndID,
+  putPartiesByTypeAndID,
+  putPartiesErrorByTypeAndID
+}
diff --git a/src/domain/parties/parties.js b/src/domain/parties/parties.js
deleted file mode 100644
index df2b5468..00000000
--- a/src/domain/parties/parties.js
+++ /dev/null
@@ -1,234 +0,0 @@
-/*****
- License
- --------------
- Copyright © 2020-2025 Mojaloop Foundation
- The Mojaloop files are made available by the 2020-2025 Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- Contributors
- --------------
- This is the official list of the Mojaloop project contributors for this file.
- Names of the original copyright holders (individuals or organizations)
- should be listed with a '*' in the first column. People who have
- contributed from an organization can be listed under the organization
- that actually holds the copyright for their contributions (see the
- Mojaloop Foundation organization for an example). Those individuals should have
- their names indented and be marked with a '-'. Email address can be added
- optionally within square brackets <email>.
- * Mojaloop Foundation
- - Name Surname <name.surname@mojaloop.io>
-
- - Rajiv Mothilal <rajiv.mothilal@modusbox.com>
- - Henk Kodde <henk.kodde@modusbox.com>
- - Steven Oderayi <steven.oderayi@modusbox.com>
- - Juan Correa <juan.correa@modusbox.com>
- - James Bush <james.bush@modusbox.com>
-
- --------------
- ******/
-
-'use strict'
-
-const { Headers, RestMethods } = require('@mojaloop/central-services-shared').Enum.Http
-const { decodePayload } = require('@mojaloop/central-services-shared').Util.StreamingProtocol
-const { MojaloopApiErrorCodes } = require('@mojaloop/sdk-standard-components').Errors
-const ErrorHandler = require('@mojaloop/central-services-error-handling')
-const Metrics = require('@mojaloop/central-services-metrics')
-
-const oracle = require('../../models/oracle/facade')
-const participant = require('../../models/participantEndpoint/facade')
-const { ERROR_MESSAGES } = require('../../constants')
-const { logger } = require('../../lib')
-const Config = require('../../lib/config')
-const utils = require('./utils')
-const util = require('../../lib/util')
-const getPartiesByTypeAndID = require('./getPartiesByTypeAndID')
-
-const log = logger.child('domain:put-parties')
-const handleErrorOnSendingCallback = utils.createErrorHandlerOnSendingCallback(Config, log)
-
-/**
- * @function putPartiesByTypeAndID
- *
- * @description This sends a callback to inform participant of successful lookup
- *
- * @param {object} headers - incoming http request headers
- * @param {object} params - uri parameters of the http request
- * @param {string} method - http request method
- * @param {object} payload - payload of the request being sent out
- * @param {string} dataUri - encoded payload of the request being sent out
- * @param {CacheClient} cache - in-memory cache with CatboxMemory engine
- * @param {IProxyCache} [proxyCache] - IProxyCache instance
- */
-const putPartiesByTypeAndID = async (headers, params, method, payload, dataUri, cache, proxyCache = undefined) => {
-  const histTimerEnd = Metrics.getHistogram(
-    'putPartiesByTypeAndID',
-    'Put parties by type and id',
-    ['success']
-  ).startTimer()
-  const type = params.Type
-  const partySubId = params.SubId
-  const source = headers[Headers.FSPIOP.SOURCE]
-  const destination = headers[Headers.FSPIOP.DESTINATION]
-  const proxy = headers[Headers.FSPIOP.PROXY]
-  const proxyEnabled = !!(Config.PROXY_CACHE_CONFIG.enabled && proxyCache)
-  log.info('parties::putPartiesByTypeAndID::begin', { source, destination, proxy, params })
-
-  let sendTo
-  let step
-  try {
-    step = 'validateParticipant-1'
-    const requesterParticipant = await participant.validateParticipant(source)
-    if (!requesterParticipant) {
-      if (!proxyEnabled || !proxy) {
-        const errMessage = ERROR_MESSAGES.sourceFspNotFound
-        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
-      }
-      step = 'addDfspIdToProxyMapping-1'
-      const isCached = await proxyCache.addDfspIdToProxyMapping(source, proxy)
-      // think,if we should throw error if isCached === false?
-      log.info('addDfspIdToProxyMapping is done', { source, proxy, isCached })
-    }
-
-    if (proxyEnabled && proxy) {
-      const alsReq = utils.alsRequestDto(destination, params) // or source?
-      step = 'receivedSuccessResponse-2'
-      const isExists = await proxyCache.receivedSuccessResponse(alsReq)
-      if (!isExists) {
-        log.warn('destination is NOT in scheme, and no cached sendToProxiesList', { destination, alsReq })
-        // think, if we need to throw an error here
-      } else {
-        const mappingPayload = {
-          fspId: source
-        }
-        step = 'oracleRequest-3'
-        await oracle.oracleRequest(headers, RestMethods.POST, params, null, mappingPayload, cache)
-        log.info('oracle was updated with mappingPayload', { mappingPayload, params })
-      }
-    }
-    step = 'validateParticipant-4'
-    const destinationParticipant = await participant.validateParticipant(destination)
-    if (!destinationParticipant) {
-      step = 'lookupProxyByDfspId-5'
-      const proxyName = proxyEnabled && await proxyCache.lookupProxyByDfspId(destination)
-      if (!proxyName) {
-        const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
-        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
-      }
-      sendTo = proxyName
-    } else {
-      sendTo = destinationParticipant.name
-    }
-
-    const decodedPayload = decodePayload(dataUri, { asParsed: false })
-    const callbackEndpointType = utils.putPartyCbType(partySubId)
-    const options = {
-      partyIdType: type,
-      partyIdentifier: params.ID,
-      ...(partySubId && { partySubIdOrType: partySubId })
-    }
-    step = 'sendRequest-6'
-    await participant.sendRequest(headers, sendTo, callbackEndpointType, RestMethods.PUT, decodedPayload.body.toString(), options)
-
-    log.info('parties::putPartiesByTypeAndID::callback was sent', { sendTo, options })
-    histTimerEnd({ success: true })
-  } catch (err) {
-    const fspiopError = await handleErrorOnSendingCallback(err, headers, params, sendTo)
-    if (fspiopError) {
-      util.countFspiopError(fspiopError, { operation: 'putPartiesByTypeAndID', step })
-    }
-    histTimerEnd({ success: false })
-  }
-}
-
-/**
- * @function putPartiesErrorByTypeAndID
- *
- * @description This populates the cache of endpoints
- *
- * @param {object} headers - incoming http request headers
- * @param {object} params - uri parameters of the http request
- * @param {object} payload - payload of the request being sent out
- * @param {string} dataUri - encoded payload of the request being sent out
- * @param {object} span
- * @param {CacheClient} cache - in-memory cache with CatboxMemory engine
- * @param {IProxyCache} [proxyCache] - IProxyCache instance
- */
-const putPartiesErrorByTypeAndID = async (headers, params, payload, dataUri, span, cache, proxyCache = undefined) => {
-  const histTimerEnd = Metrics.getHistogram(
-    'putPartiesErrorByTypeAndID',
-    'Put parties error by type and id',
-    ['success']
-  ).startTimer()
-  const partySubId = params.SubId
-  const destination = headers[Headers.FSPIOP.DESTINATION]
-  const callbackEndpointType = utils.errorPartyCbType(partySubId)
-  const proxyEnabled = !!(Config.PROXY_CACHE_CONFIG.enabled && proxyCache)
-
-  const childSpan = span ? span.getChild('putPartiesErrorByTypeAndID') : undefined
-
-  let sendTo
-  let fspiopError
-  let step
-
-  try {
-    const proxy = proxyEnabled && headers[Headers.FSPIOP.PROXY]
-    if (proxy) {
-      if (isNotValidPayeeCase(payload)) {
-        const swappedHeaders = utils.swapSourceDestinationHeaders(headers)
-        step = 'oracleRequest-1'
-        await oracle.oracleRequest(swappedHeaders, RestMethods.DELETE, params, null, null, cache)
-        getPartiesByTypeAndID(swappedHeaders, params, RestMethods.GET, {}, span, cache, proxyCache)
-        // todo: - think if we need to send errorCallback?
-        //       - or sentCallback after getPartiesByTypeAndID is done
-        log.info('notValidPayee case - deleted Participants and run getPartiesByTypeAndID:', { proxy, params, payload })
-        return
-      }
-
-      const alsReq = utils.alsRequestDto(destination, params) // or source?
-      step = 'receivedErrorResponse-2'
-      const isLast = await proxyCache.receivedErrorResponse(alsReq, proxy)
-      if (!isLast) {
-        log.info('got NOT last error callback from proxy:', { proxy, alsReq })
-        return
-      }
-    }
-    step = 'validateParticipant-3'
-    const destinationParticipant = await participant.validateParticipant(destination)
-
-    if (destinationParticipant) {
-      sendTo = destination
-    } else {
-      step = 'lookupProxyByDfspId-4'
-      const proxyName = proxyEnabled && await proxyCache.lookupProxyByDfspId(destination)
-      if (!proxyName) {
-        const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
-        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
-      }
-      sendTo = proxyName
-    }
-    const decodedPayload = decodePayload(dataUri, { asParsed: false })
-    await participant.sendErrorToParticipant(sendTo, callbackEndpointType, decodedPayload.body.toString(), headers, params, childSpan)
-
-    log.info('putPartiesErrorByTypeAndID callback was sent', { sendTo })
-    histTimerEnd({ success: true })
-  } catch (err) {
-    fspiopError = await handleErrorOnSendingCallback(err, headers, params, sendTo)
-    if (fspiopError) {
-      util.countFspiopError(fspiopError, { operation: 'putPartiesErrorByTypeAndID', step })
-    }
-    histTimerEnd({ success: false })
-  } finally {
-    await utils.finishSpanWithError(childSpan, fspiopError)
-  }
-}
-
-function isNotValidPayeeCase (payload) {
-  return payload?.errorInformation?.errorCode === MojaloopApiErrorCodes.PAYEE_IDENTIFIER_NOT_VALID.code
-}
-
-module.exports = {
-  getPartiesByTypeAndID,
-  putPartiesByTypeAndID,
-  putPartiesErrorByTypeAndID
-}
diff --git a/src/domain/parties/utils.js b/src/domain/parties/partiesUtils.js
similarity index 55%
rename from src/domain/parties/utils.js
rename to src/domain/parties/partiesUtils.js
index f0cf0346..12001f6a 100644
--- a/src/domain/parties/utils.js
+++ b/src/domain/parties/partiesUtils.js
@@ -1,5 +1,33 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
 const { Enum, Util: { Hapi } } = require('@mojaloop/central-services-shared')
-const EventSdk = require('@mojaloop/event-sdk')
+const { MojaloopApiErrorCodes } = require('@mojaloop/sdk-standard-components').Errors
+// todo: check why do we need sdk-standard-components deps here !!!
 const ErrorHandler = require('@mojaloop/central-services-error-handling')
 
 const participant = require('../../models/participantEndpoint/facade')
@@ -20,18 +48,6 @@ const errorPartyCbType = (partySubId) => partySubId
   ? FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_SUB_ID_PUT_ERROR
   : FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_PUT_ERROR
 
-const finishSpanWithError = async (childSpan, fspiopError) => {
-  if (childSpan && !childSpan.isFinished) {
-    if (fspiopError) {
-      const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
-      await childSpan.error(fspiopError, state)
-      await childSpan.finish(fspiopError.message, state)
-    } else {
-      await childSpan.finish()
-    }
-  }
-}
-
 const makePutPartiesErrorPayload = async (config, fspiopError, headers, params) => {
   const body = fspiopError.toApiErrorObject(config.ERROR_HANDLING)
   return config.API_TYPE === Hapi.API_TYPES.iso20022
@@ -45,6 +61,12 @@ const alsRequestDto = (sourceId, params) => ({
   partyId: params.ID
 })
 
+const partiesRequestOptionsDto = (params) => ({
+  partyIdType: params.Type,
+  partyIdentifier: params.ID,
+  ...(params.SubId && { partySubIdOrType: params.SubId })
+})
+
 const swapSourceDestinationHeaders = (headers) => {
   const {
     [Headers.FSPIOP.SOURCE]: source,
@@ -59,10 +81,10 @@ const swapSourceDestinationHeaders = (headers) => {
   }
 }
 
-// change signature to accept object
+// todo: check if we need this function
 const createErrorHandlerOnSendingCallback = (config, logger) => async (err, headers, params, requester) => {
   try {
-    logger.error('error in sending parties callback', err)
+    logger.error('error in sending parties callback: ', err)
     const sendTo = requester || headers[Headers.FSPIOP.SOURCE]
     const errorCallbackEndpointType = errorPartyCbType(params.SubId)
     const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(err)
@@ -75,17 +97,22 @@ const createErrorHandlerOnSendingCallback = (config, logger) => async (err, head
   } catch (exc) {
     // We can't do anything else here- we _must_ handle all errors _within_ this function because
     // we've already sent a sync response- we cannot throw.
-    logger.error('error in handleErrorOnSendingCallback', exc)
+    logger.error('failed to handleErrorOnSendingCallback. No further processing! ', exc)
   }
 }
 
+function isNotValidPayeeCase (payload) {
+  return payload?.errorInformation?.errorCode === MojaloopApiErrorCodes.PAYEE_IDENTIFIER_NOT_VALID.code
+}
+
 module.exports = {
   getPartyCbType,
   putPartyCbType,
   errorPartyCbType,
   makePutPartiesErrorPayload,
-  finishSpanWithError,
   createErrorHandlerOnSendingCallback,
   alsRequestDto,
-  swapSourceDestinationHeaders
+  partiesRequestOptionsDto,
+  swapSourceDestinationHeaders,
+  isNotValidPayeeCase
 }
diff --git a/src/domain/parties/putParties.js b/src/domain/parties/putParties.js
new file mode 100644
index 00000000..f5fc69a4
--- /dev/null
+++ b/src/domain/parties/putParties.js
@@ -0,0 +1,170 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the 2020-2025 Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation organization for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+ * Mojaloop Foundation
+ - Name Surname <name.surname@mojaloop.io>
+
+ - Rajiv Mothilal <rajiv.mothilal@modusbox.com>
+ - Henk Kodde <henk.kodde@modusbox.com>
+ - Steven Oderayi <steven.oderayi@modusbox.com>
+ - Juan Correa <juan.correa@modusbox.com>
+ - James Bush <james.bush@modusbox.com>
+
+ --------------
+ ******/
+
+'use strict'
+
+const { Headers } = require('@mojaloop/central-services-shared').Enum.Http
+const Metrics = require('@mojaloop/central-services-metrics')
+const libUtil = require('../../lib/util')
+const { logger } = require('../../lib')
+const { createDeps } = require('./deps')
+const services = require('./services')
+
+/**
+ * @function putPartiesByTypeAndID
+ *
+ * @description This sends a callback to inform participant of successful lookup
+ *
+ * @param {object} headers - incoming http request headers
+ * @param {object} params - uri parameters of the http request
+ * @param {string} method - http request method
+ * @param {object} payload - payload of the request being sent out
+ * @param {string} dataUri - encoded payload of the request being sent out
+ * @param {CacheClient} cache - in-memory cache with CatboxMemory engine
+ * @param {IProxyCache} [proxyCache] - IProxyCache instance
+ */
+const putPartiesByTypeAndID = async (headers, params, method, payload, dataUri, cache, proxyCache = undefined) => {
+  // todo: think, if we need to pass span here
+  const component = putPartiesByTypeAndID.name
+  const histTimerEnd = Metrics.getHistogram(
+    component,
+    'Put parties by type and id',
+    ['success']
+  ).startTimer()
+  // const childSpan = span ? span.getChild(component) : undefined
+  const log = logger.child({ component, params })
+  const stepState = libUtil.initStepState()
+
+  const deps = createDeps({ cache, proxyCache, log, stepState })
+  const service = new services.PutPartiesService(deps)
+  const results = {}
+
+  const source = headers[Headers.FSPIOP.SOURCE]
+  const destination = headers[Headers.FSPIOP.DESTINATION]
+  const proxy = headers[Headers.FSPIOP.PROXY]
+  log.info('putPartiesByTypeAndID start', { source, destination, proxy })
+
+  try {
+    await service.validateSourceParticipant({ source, proxy })
+
+    if (proxy) {
+      await service.checkProxySuccessResponse({ destination, source, headers, params })
+    }
+
+    const sendTo = await service.identifyDestinationForSuccessCallback(destination)
+    results.requester = sendTo
+    await service.sendSuccessCallback({ sendTo, headers, params, dataUri })
+
+    log.info('putPartiesByTypeAndID callback was sent', { sendTo })
+    histTimerEnd({ success: true })
+  } catch (error) {
+    const { requester } = results
+    results.fspiopError = await service.handleError({ error, requester, headers, params })
+    if (results.fspiopError) {
+      libUtil.countFspiopError(results.fspiopError, { operation: component, step: stepState.step })
+    }
+    histTimerEnd({ success: false })
+  }
+}
+
+/**
+ * @function putPartiesErrorByTypeAndID
+ *
+ * @description This populates the cache of endpoints
+ *
+ * @param {object} headers - incoming http request headers
+ * @param {object} params - uri parameters of the http request
+ * @param {object} payload - payload of the request being sent out
+ * @param {string} dataUri - encoded payload of the request being sent out
+ * @param {object} span
+ * @param {CacheClient} cache - in-memory cache with CatboxMemory engine
+ * @param {IProxyCache} [proxyCache] - IProxyCache instance
+ */
+const putPartiesErrorByTypeAndID = async (headers, params, payload, dataUri, span, cache, proxyCache = undefined) => {
+  const component = putPartiesErrorByTypeAndID.name
+  const histTimerEnd = Metrics.getHistogram(
+    component,
+    'Put parties error by type and id',
+    ['success']
+  ).startTimer()
+  const childSpan = span ? span.getChild(component) : undefined
+  const log = logger.child({ component, params })
+  const stepState = libUtil.initStepState()
+
+  const deps = createDeps({ cache, proxyCache, childSpan, log, stepState })
+  const service = new services.PutPartiesErrorService(deps)
+  const results = {}
+
+  const destination = headers[Headers.FSPIOP.DESTINATION]
+  const proxy = headers[Headers.FSPIOP.PROXY]
+  const proxyEnabled = !!(deps.config.PROXY_CACHE_CONFIG.enabled && proxyCache)
+  log.info('putPartiesErrorByTypeAndID start', { destination, proxy, proxyEnabled })
+
+  try {
+    if (proxyEnabled && proxy) {
+      const notValid = await service.checkPayee({ headers, params, payload, proxy })
+      if (notValid) {
+        const getPartiesService = new services.GetPartiesService(deps)
+        // todo: think, if we need to remove destination header before starting new discovery
+        await getPartiesService.handleRequest({ headers, params, results })
+        log.info('putPartiesErrorByTypeAndID triggered new discovery flow')
+        histTimerEnd({ success: true })
+        return
+      }
+
+      const isLast = await service.checkLastProxyCallback({ destination, proxy, params })
+      if (!isLast) {
+        log.info('putPartiesErrorByTypeAndID proxy callback was processed', { proxy })
+        histTimerEnd({ success: true })
+        return
+      }
+    }
+
+    const sendTo = await service.identifyDestinationForErrorCallback(destination)
+    results.requester = sendTo
+    await service.sendErrorCallbackToParticipant({ sendTo, headers, params, dataUri })
+
+    log.info('putPartiesErrorByTypeAndID callback was sent', { sendTo })
+    histTimerEnd({ success: true })
+  } catch (error) {
+    const { requester } = results
+    results.fspiopError = await service.handleError({ error, requester, headers, params })
+    if (results.fspiopError) {
+      libUtil.countFspiopError(results.fspiopError, { operation: component, step: stepState.step })
+    }
+    histTimerEnd({ success: false })
+  } finally {
+    await libUtil.finishSpanWithError(childSpan, results.fspiopError)
+  }
+}
+
+module.exports = {
+  putPartiesByTypeAndID,
+  putPartiesErrorByTypeAndID
+}
diff --git a/src/domain/parties/services/BasePartiesService.js b/src/domain/parties/services/BasePartiesService.js
new file mode 100644
index 00000000..090c029b
--- /dev/null
+++ b/src/domain/parties/services/BasePartiesService.js
@@ -0,0 +1,95 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const ErrorHandler = require('@mojaloop/central-services-error-handling')
+const { decodePayload } = require('@mojaloop/central-services-shared').Util.StreamingProtocol
+const { Enum } = require('@mojaloop/central-services-shared')
+
+const { FspEndpointTypes, FspEndpointTemplates } = Enum.EndPoints
+const { Headers, RestMethods } = Enum.Http
+
+class BasePartiesService {
+  constructor (deps) {
+    this.deps = deps
+    this.log = this.deps.log.child({ component: this.constructor.name })
+    this.proxyEnabled = !!(deps.config.PROXY_CACHE_CONFIG?.enabled && deps.proxyCache)
+  }
+
+  // async handleRequest () {
+  //   throw new Error('handleRequest must be implemented by subclass')
+  // }
+
+  async handleError ({ error, requester, headers, params }) {
+    const log = this.log.child({ method: 'handleError' })
+    try {
+      log.error('error in processing parties request: ', error)
+      const sendTo = requester || headers[Headers.FSPIOP.SOURCE]
+      const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(error)
+      const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(this.deps.config, fspiopError, headers, params)
+
+      await this.sendErrorCallback({ sendTo, errorInfo, headers, params })
+      log.info('handleError in done', { sendTo, errorInfo })
+      return fspiopError
+    } catch (exc) {
+      // We can't do anything else here- we _must_ handle all errors _within_ this function because
+      // we've already sent a sync response- we cannot throw.
+      log.error('failed to handleError. No further processing! ', exc)
+    }
+  }
+
+  async validateParticipant (participantId) {
+    return this.deps.participant.validateParticipant(participantId)
+  }
+
+  async sendErrorCallback ({ sendTo, errorInfo, headers, params, payload = undefined }) {
+    const endpointType = this.deps.partiesUtils.errorPartyCbType(params.SubId)
+    await this.deps.participant.sendErrorToParticipant(
+      sendTo, endpointType, errorInfo, headers, params, payload, this.deps.childSpan
+    )
+    this.log.verbose('sendErrorCallback is done', { sendTo, errorInfo })
+  }
+
+  async sendDeleteOracleRequest (headers, params) {
+    return this.deps.oracle.oracleRequest(headers, RestMethods.DELETE, params, null, null, this.deps.cache)
+  }
+
+  static decodeDataUriPayload (dataUri) {
+    const decoded = decodePayload(dataUri, { asParsed: false })
+    return decoded.body.toString()
+  }
+
+  static enums () {
+    return {
+      FspEndpointTypes,
+      FspEndpointTemplates,
+      Headers,
+      RestMethods
+    }
+  }
+}
+
+module.exports = BasePartiesService
diff --git a/src/domain/parties/services/GetPartiesService.js b/src/domain/parties/services/GetPartiesService.js
new file mode 100644
index 00000000..af82ebd7
--- /dev/null
+++ b/src/domain/parties/services/GetPartiesService.js
@@ -0,0 +1,281 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const ErrorHandler = require('@mojaloop/central-services-error-handling')
+const { ERROR_MESSAGES } = require('../../../constants')
+const { createCallbackHeaders } = require('../../../lib/headers')
+const BasePartiesService = require('./BasePartiesService')
+
+const {
+  FspEndpointTypes, FspEndpointTemplates,
+  Headers, RestMethods
+} = BasePartiesService.enums()
+
+const proxyCacheTtlSec = 40 // todo: make configurable
+
+class GetPartiesService extends BasePartiesService {
+  async handleRequest ({ headers, params, query, results }) {
+    const source = headers[Headers.FSPIOP.SOURCE]
+    const proxy = headers[Headers.FSPIOP.PROXY]
+    const destination = headers[Headers.FSPIOP.DESTINATION]
+    // see https://github.com/mojaloop/design-authority/issues/79
+    // the requester has specified a destination routing header. We should respect that and forward the request directly to the destination
+    // without consulting any oracles.
+    this.log.info('handling getParties request', { source, destination, proxy })
+
+    const requester = await this.validateRequester({ source, proxy })
+    results.requester = requester
+
+    if (destination) {
+      await this.forwardRequestToDestination({ destination, headers, params })
+      return
+    }
+    const response = await this.sendOracleDiscoveryRequest({ headers, params, query })
+
+    if (Array.isArray(response?.data?.partyList) && response.data.partyList.length > 0) {
+      const partyList = this.filterOraclePartyList({ response, params })
+      await this.processOraclePartyList({ partyList, headers, params, destination })
+      return
+    }
+
+    this.log.info('empty partyList form oracle, getting proxyList...', { params })
+    const proxyNames = await this.getFilteredProxyList(proxy)
+
+    if (proxyNames.length) {
+      await this.triggerSendToProxiesFlow({ proxyNames, headers, params, source })
+      return
+    }
+
+    results.fspiopError = await this.sendPartyNotFoundErrorCallback({ requester, headers, params })
+  }
+
+  async validateRequester ({ source, proxy }) {
+    this.deps.stepState.inProgress('validateRequester-0')
+    const log = this.log.child({ source, proxy, method: 'validateRequester' })
+
+    const sourceParticipant = await this.validateParticipant(source)
+    if (sourceParticipant) {
+      log.debug('source is in scheme')
+      return source
+    }
+
+    if (!this.proxyEnabled || !proxy) {
+      const errMessage = ERROR_MESSAGES.sourceFspNotFound
+      log.warn(errMessage)
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+    }
+
+    const proxyParticipant = await this.validateParticipant(proxy)
+    if (!proxyParticipant) {
+      const errMessage = ERROR_MESSAGES.partyProxyNotFound
+      log.warn(errMessage)
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+    }
+
+    const isCached = await this.deps.proxyCache.addDfspIdToProxyMapping(source, proxy)
+    if (!isCached) {
+      const errMessage = 'failed to addDfspIdToProxyMapping'
+      log.warn(errMessage)
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+    }
+
+    log.info('source is added to proxyMapping cache:', { proxy, isCached })
+    return proxy
+  }
+
+  async forwardRequestToDestination ({ destination, headers, params }) {
+    this.deps.stepState.inProgress('validateDestination-1')
+    const log = this.log.child({ method: 'forwardRequestToDestination' })
+    let sendTo = destination
+
+    const destParticipantModel = await this.validateParticipant(destination)
+    if (!destParticipantModel) {
+      this.deps.stepState.inProgress('lookupProxyDestination-2')
+      const proxyId = this.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
+
+      if (!proxyId) {
+        log.warn('no destination participant, and no dfsp-to-proxy mapping', { destination })
+        const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
+        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+      }
+      sendTo = proxyId
+    }
+    // all ok, go ahead and forward the request
+    await this.#forwardGetPartiesRequest({ sendTo, headers, params })
+    log.info('discovery getPartiesByTypeAndID request was sent', { sendTo })
+  }
+
+  filterOraclePartyList ({ response, params }) {
+    // Oracle's API is a standard rest-style end-point Thus a GET /party on the oracle will return all participant-party records. We must filter the results based on the callbackEndpointType to make sure we remove records containing partySubIdOrType when we are in FSPIOP_CALLBACK_URL_PARTIES_GET mode:
+    this.deps.stepState.inProgress('filterOraclePartyList-5')
+    const callbackEndpointType = this.deps.partiesUtils.getPartyCbType(params.SubId)
+    let filteredResponsePartyList
+
+    switch (callbackEndpointType) {
+      case FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_GET:
+        filteredResponsePartyList = response.data.partyList.filter(party => party.partySubIdOrType == null) // Filter records that DON'T contain a partySubIdOrType
+        break
+      case FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_SUB_ID_GET:
+        filteredResponsePartyList = response.data.partyList.filter(party => party.partySubIdOrType === params.SubId) // Filter records that match partySubIdOrType
+        break
+      default:
+        filteredResponsePartyList = response // Fallback to providing the standard list
+    }
+
+    if (!Array.isArray(filteredResponsePartyList) || !filteredResponsePartyList.length) {
+      const errMessage = 'Requested FSP/Party not found'
+      this.log.warn(errMessage)
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+    }
+
+    return filteredResponsePartyList
+  }
+
+  async processOraclePartyList ({ partyList, headers, params, destination }) {
+    const log = this.log.child({ method: 'processOraclePartyList' })
+
+    let sentCount = 0 // if sentCount === 0 after sending, should we restart the whole process?
+    const sending = partyList.map(async party => {
+      const { fspId } = party
+      const clonedHeaders = { ...headers }
+      if (!destination) {
+        clonedHeaders[Headers.FSPIOP.DESTINATION] = fspId
+      }
+      this.deps.stepState.inProgress('validateParticipant-6')
+      const schemeParticipant = await this.validateParticipant(fspId)
+      if (schemeParticipant) {
+        sentCount++
+        log.info('participant is in scheme', { fspId })
+        return this.#forwardGetPartiesRequest({
+          sendTo: fspId,
+          headers: clonedHeaders,
+          params
+        })
+      }
+
+      // If the participant is not in the scheme and proxy routing is enabled,
+      // we should check if there is a proxy for it and send the request to the proxy
+      if (this.proxyEnabled) {
+        this.deps.stepState.inProgress('lookupProxyByDfspId-7')
+        const proxyName = await this.deps.proxyCache.lookupProxyByDfspId(fspId)
+        if (!proxyName) {
+          log.warn('no proxyMapping for participant!  TODO: Delete reference in oracle...', { fspId })
+          // todo: delete reference in oracle
+        } else {
+          sentCount++
+          log.info('participant is NOT in scheme, use proxy name', { fspId, proxyName })
+          return this.#forwardGetPartiesRequest({
+            sendTo: proxyName,
+            headers: clonedHeaders,
+            params
+          })
+        }
+      }
+    })
+    await Promise.all(sending)
+    log.verbose('processOraclePartyList is done', { sentCount })
+    // todo: think what if sentCount === 0 here
+  }
+
+  async getFilteredProxyList (proxy) {
+    this.deps.stepState.inProgress('getAllProxies-8')
+    if (!this.proxyEnabled) {
+      this.log.warn('proxyCache is not enabled')
+      return []
+    }
+
+    const proxyNames = await this.deps.proxies.getAllProxiesNames(this.deps.config.SWITCH_ENDPOINT)
+    this.log.debug('getAllProxiesNames is done', { proxyNames })
+    return proxyNames.filter(name => name !== proxy)
+  }
+
+  async triggerSendToProxiesFlow ({ proxyNames, headers, params, source }) {
+    const log = this.log.child({ method: 'triggerSendToProxiesFlow' })
+    this.deps.stepState.inProgress('setSendToProxiesList-10')
+    const alsReq = this.deps.partiesUtils.alsRequestDto(source, params)
+    log.info('starting setSendToProxiesList flow: ', { proxyNames, alsReq, proxyCacheTtlSec })
+
+    const isCached = await this.deps.proxyCache.setSendToProxiesList(alsReq, proxyNames, proxyCacheTtlSec)
+    if (!isCached) {
+      log.warn('failed to setSendToProxiesList')
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, ERROR_MESSAGES.failedToCacheSendToProxiesList)
+    }
+
+    this.deps.stepState.inProgress('sendingProxyRequests-11')
+    const sending = proxyNames.map(
+      sendTo => this.#forwardGetPartiesRequest({ sendTo, headers, params })
+        .then(({ status, data } = {}) => ({ status, data }))
+    )
+    const results = await Promise.allSettled(sending)
+    const isOk = results.some(result => result.status === 'fulfilled')
+    // If, at least, one request is sent to proxy, we treat the whole flow as successful.
+    // Failed requests should be handled by TTL expired/timeout handler
+    // todo: - think, if we should handle failed requests here (e.g., by calling receivedErrorResponse)
+    log.info('triggerSendToProxiesFlow is done:', { isOk, results, proxyNames, alsReq })
+    this.deps.stepState.inProgress('allSent-12')
+    if (!isOk) {
+      log.warn('no successful requests sent to proxies')
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, ERROR_MESSAGES.proxyConnectionError)
+    }
+  }
+
+  async sendPartyNotFoundErrorCallback ({ requester, headers, params }) {
+    this.deps.stepState.inProgress('sendErrorCallback-9')
+    const callbackHeaders = createCallbackHeaders({
+      requestHeaders: headers,
+      partyIdType: params.Type,
+      partyIdentifier: params.ID,
+      endpointTemplate: params.SubId
+        ? FspEndpointTemplates.PARTIES_SUB_ID_PUT_ERROR
+        : FspEndpointTemplates.PARTIES_PUT_ERROR
+    })
+    const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND)
+
+    await this.sendErrorCallback({
+      sendTo: requester,
+      errorInfo: fspiopError.toApiErrorObject(this.deps.config.ERROR_HANDLING),
+      headers: callbackHeaders,
+      params
+    })
+    return fspiopError
+  }
+
+  async sendOracleDiscoveryRequest ({ headers, params, query }) {
+    this.deps.stepState.inProgress('oracleRequest-4')
+    return this.deps.oracle.oracleRequest(headers, RestMethods.GET, params, query, undefined, this.deps.cache)
+  }
+
+  async #forwardGetPartiesRequest ({ sendTo, headers, params }) {
+    this.deps.stepState.inProgress('forwardRequest-3')
+    const callbackEndpointType = this.deps.partiesUtils.getPartyCbType(params.SubId)
+    const options = this.deps.partiesUtils.partiesRequestOptionsDto(params)
+
+    return this.deps.participant.sendRequest(headers, sendTo, callbackEndpointType, RestMethods.GET, undefined, options, this.deps.childSpan)
+  }
+}
+
+module.exports = GetPartiesService
diff --git a/src/domain/parties/services/PutPartiesErrorService.js b/src/domain/parties/services/PutPartiesErrorService.js
new file mode 100644
index 00000000..5a128b2c
--- /dev/null
+++ b/src/domain/parties/services/PutPartiesErrorService.js
@@ -0,0 +1,79 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const ErrorHandler = require('@mojaloop/central-services-error-handling')
+const { ERROR_MESSAGES } = require('../../../constants')
+const BasePartiesService = require('./BasePartiesService')
+
+class PutPartiesErrorService extends BasePartiesService {
+  // async handleRequest () {
+  //   // todo: add impl.
+  // }
+
+  async checkPayee ({ headers, params, payload, proxy }) {
+    const notValid = this.deps.partiesUtils.isNotValidPayeeCase(payload)
+    if (notValid) {
+      this.deps.stepState.inProgress('notValidPayeeCase-1')
+      this.log.warn('notValidPayee case - deleting Participants and run getPartiesByTypeAndID', { proxy, payload })
+      const swappedHeaders = this.deps.partiesUtils.swapSourceDestinationHeaders(headers)
+      await super.sendDeleteOracleRequest(swappedHeaders, params)
+    }
+    return notValid
+  }
+
+  async checkLastProxyCallback ({ destination, proxy, params }) {
+    this.deps.stepState.inProgress('checkLastProxyCallback-2')
+    const alsReq = this.deps.partiesUtils.alsRequestDto(destination, params) // or source?
+    const isLast = await this.deps.proxyCache.receivedErrorResponse(alsReq, proxy)
+    this.log.info(`got${isLast ? '' : 'NOT'} last error callback from proxy`, { proxy, alsReq, isLast })
+    return isLast
+  }
+
+  async identifyDestinationForErrorCallback (destination) {
+    this.deps.stepState.inProgress('validateParticipant-3')
+    const destinationParticipant = await super.validateParticipant(destination)
+    if (destinationParticipant) return destination
+
+    this.deps.stepState.inProgress('lookupProxyDestination-4')
+    const proxyName = this.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
+    if (proxyName) return proxyName
+
+    const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
+    this.log.warn(errMessage, { destination })
+    throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
+  }
+
+  async sendErrorCallbackToParticipant ({ sendTo, headers, params, dataUri }) {
+    this.deps.stepState.inProgress('sendErrorToParticipant-5')
+    const errorInfo = PutPartiesErrorService.decodeDataUriPayload(dataUri)
+    return super.sendErrorCallback({
+      sendTo, errorInfo, headers, params
+    })
+  }
+}
+
+module.exports = PutPartiesErrorService
diff --git a/src/domain/parties/services/PutPartiesService.js b/src/domain/parties/services/PutPartiesService.js
new file mode 100644
index 00000000..9ee8d1cf
--- /dev/null
+++ b/src/domain/parties/services/PutPartiesService.js
@@ -0,0 +1,113 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const ErrorHandler = require('@mojaloop/central-services-error-handling')
+const { ERROR_MESSAGES } = require('../../../constants')
+const BasePartiesService = require('./BasePartiesService')
+
+const { RestMethods } = BasePartiesService.enums()
+
+class PutPartiesService extends BasePartiesService {
+  // async handleRequest () {
+  //   // todo: add impl.
+  // }
+
+  async validateSourceParticipant ({ source, proxy }) {
+    this.deps.stepState.inProgress('validateSourceParticipant-1')
+    const requesterParticipant = await super.validateParticipant(source)
+
+    if (!requesterParticipant) {
+      if (!this.proxyEnabled || !proxy) {
+        const errMessage = ERROR_MESSAGES.sourceFspNotFound
+        this.log.warn(`${errMessage} and no proxy`, { source })
+        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+      }
+      const isCached = await this.deps.proxyCache.addDfspIdToProxyMapping(source, proxy)
+      if (!isCached) {
+        const errMessage = 'failed to addDfspIdToProxyMapping'
+        this.log.warn(errMessage, { source, proxy })
+        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+      }
+
+      this.log.info('addDfspIdToProxyMapping is done', { source, proxy })
+    }
+  }
+
+  async checkProxySuccessResponse ({ destination, source, headers, params }) {
+    if (this.proxyEnabled) {
+      this.deps.stepState.inProgress('checkProxySuccessResponse-2')
+      const alsReq = this.deps.partiesUtils.alsRequestDto(destination, params)
+
+      const isExists = await this.deps.proxyCache.receivedSuccessResponse(alsReq)
+      if (isExists) {
+        await this.#updateOracleWithParticipantMapping({ source, headers, params })
+        return
+      }
+      this.log.warn('destination is NOT in scheme, and no cached sendToProxiesList', { destination, alsReq })
+      // todo: think, if we need to throw an error here
+    }
+  }
+
+  async identifyDestinationForSuccessCallback (destination) {
+    this.deps.stepState.inProgress('validateDestinationParticipant-4')
+    const destinationParticipant = await super.validateParticipant(destination)
+    if (destinationParticipant) {
+      return destinationParticipant.name
+    }
+
+    const proxyName = this.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
+    if (!proxyName) {
+      const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
+      this.log.warn(`${errMessage} and no proxy`, { destination })
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
+    }
+    return proxyName
+  }
+
+  async sendSuccessCallback ({ sendTo, headers, params, dataUri }) {
+    this.deps.stepState.inProgress('#sendSuccessCallback-6')
+    const payload = PutPartiesService.decodeDataUriPayload(dataUri)
+    const callbackEndpointType = this.deps.partiesUtils.putPartyCbType(params.SubId)
+    const options = this.deps.partiesUtils.partiesRequestOptionsDto(params)
+
+    await this.deps.participant.sendRequest(
+      headers, sendTo, callbackEndpointType, RestMethods.PUT, payload, options
+    )
+    this.log.verbose('sendSuccessCallback is done', { sendTo, payload })
+  }
+
+  async #updateOracleWithParticipantMapping ({ source, headers, params }) {
+    this.deps.stepState.inProgress('#updateOracleWithParticipantMapping-3')
+    const mappingPayload = {
+      fspId: source
+    }
+    await this.deps.oracle.oracleRequest(headers, RestMethods.POST, params, null, mappingPayload, this.deps.cache)
+    this.log.info('oracle was updated with mappingPayload', { mappingPayload })
+  }
+}
+
+module.exports = PutPartiesService
diff --git a/src/domain/parties/services/index.js b/src/domain/parties/services/index.js
new file mode 100644
index 00000000..d56a532e
--- /dev/null
+++ b/src/domain/parties/services/index.js
@@ -0,0 +1,36 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const GetPartiesService = require('./GetPartiesService')
+const PutPartiesService = require('./PutPartiesService')
+const PutPartiesErrorService = require('./PutPartiesErrorService')
+
+module.exports = {
+  GetPartiesService,
+  PutPartiesService,
+  PutPartiesErrorService
+}
diff --git a/src/domain/timeout/dto.js b/src/domain/timeout/dto.js
index 5ae9ac96..13ea618b 100644
--- a/src/domain/timeout/dto.js
+++ b/src/domain/timeout/dto.js
@@ -13,7 +13,7 @@ const EventFrameworkUtil = require('@mojaloop/central-services-shared').Util.Eve
 
 const LibUtil = require('../../lib/util')
 const Config = require('../../lib/config')
-const partiesUtils = require('../parties/utils')
+const partiesUtils = require('../parties/partiesUtils')
 
 const timeoutCallbackDto = async ({ destination, partyId, partyType }) => {
   const headers = {
diff --git a/src/lib/headers.js b/src/lib/headers.js
index 4ab2044e..a6988fc3 100644
--- a/src/lib/headers.js
+++ b/src/lib/headers.js
@@ -46,7 +46,8 @@ exports.createCallbackHeaders = (params) => {
   callbackHeaders[Enums.Http.Headers.FSPIOP.DESTINATION] = params.requestHeaders[Enums.Http.Headers.FSPIOP.SOURCE]
   callbackHeaders[Enums.Http.Headers.FSPIOP.HTTP_METHOD] = Enums.Http.RestMethods.PUT
   callbackHeaders[Enums.Http.Headers.FSPIOP.URI] = Mustache.render(params.endpointTemplate, {
-    partyIdType: params.partyIdType, partyIdentifier: params.partyIdentifier
+    partyIdType: params.partyIdType,
+    partyIdentifier: params.partyIdentifier
   })
 
   return callbackHeaders
diff --git a/src/lib/index.js b/src/lib/index.js
index eeb9ec31..3e809ae0 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -5,7 +5,6 @@ const logger = loggerFactory('ALS') // global logger without context
 
 module.exports = {
   logger,
-  loggerFactory,
   asyncStorage,
   TransformFacades
 }
diff --git a/src/lib/util.js b/src/lib/util.js
index d149355c..de445b5b 100644
--- a/src/lib/util.js
+++ b/src/lib/util.js
@@ -1,5 +1,33 @@
-const util = require('util')
-const Path = require('path')
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const util = require('node:util')
+const Path = require('node:path')
+const EventSdk = require('@mojaloop/event-sdk')
 const Enum = require('@mojaloop/central-services-shared').Enum
 const { HeaderValidation, Hapi, rethrow } = require('@mojaloop/central-services-shared').Util
 const Config = require('../lib/config')
@@ -70,7 +98,30 @@ const countFspiopError = (error, options) => {
   rethrow.countFspiopError(error, options)
 }
 
+// todo: think better name
+const initStepState = (initStep = 'start') => {
+  let step = initStep
+  return Object.freeze({
+    get step () { return step }, // or rename to current ?
+    inProgress (nextStep) { step = nextStep }
+  })
+}
+
+const finishSpanWithError = async (childSpan, fspiopError) => {
+  if (childSpan && !childSpan.isFinished) {
+    if (fspiopError) {
+      const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
+      await childSpan.error(fspiopError, state)
+      await childSpan.finish(fspiopError.message, state)
+    } else {
+      await childSpan.finish()
+    }
+  }
+}
+
 module.exports = {
+  initStepState,
+  finishSpanWithError,
   getSpanTags,
   pathForInterface,
   getStackOrInspect,
diff --git a/test/unit/domain/parties/parties.test.js b/test/unit/domain/parties/parties.test.js
index 4acad032..46552926 100644
--- a/test/unit/domain/parties/parties.test.js
+++ b/test/unit/domain/parties/parties.test.js
@@ -41,8 +41,8 @@ const Metrics = require('@mojaloop/central-services-metrics')
 
 const Config = require('../../../../src/lib/config')
 const Db = require('../../../../src/lib/db')
-const partiesDomain = require('../../../../src/domain/parties/parties')
-const partiesUtils = require('../../../../src/domain/parties/utils')
+const partiesDomain = require('../../../../src/domain/parties')
+const partiesUtils = require('../../../../src/domain/parties/partiesUtils')
 const participant = require('../../../../src/models/participantEndpoint/facade')
 const oracle = require('../../../../src/models/oracle/facade')
 const oracleEndpointCached = require('../../../../src/models/oracle/oracleEndpointCached')
diff --git a/test/unit/domain/parties/utils.test.js b/test/unit/domain/parties/utils.test.js
index bbdce7cd..cb79d061 100644
--- a/test/unit/domain/parties/utils.test.js
+++ b/test/unit/domain/parties/utils.test.js
@@ -36,7 +36,7 @@ jest.mock('@mojaloop/central-services-shared', () => ({
 
 const { API_TYPES } = require('@mojaloop/central-services-shared').Util.Hapi
 const { logger } = require('../../../../src/lib')
-const utils = require('../../../../src/domain/parties/utils')
+const partiesUtils = require('../../../../src/domain/parties/partiesUtils')
 const config = require('../../../../src/lib/config')
 const fixtures = require('../../../fixtures')
 
@@ -48,7 +48,7 @@ describe('parties utils Tests -->', () => {
     const headers = fixtures.partiesCallHeadersDto({ source })
     const params = { ID: '1234', Type: 'MSISDN' }
 
-    const handleError = utils.createErrorHandlerOnSendingCallback(isoConfig, logger)
+    const handleError = partiesUtils.createErrorHandlerOnSendingCallback(isoConfig, logger)
     await handleError(err, headers, params, source)
 
     expect(mockSendRequest).toHaveBeenCalledTimes(1)
diff --git a/test/unit/lib/util.test.js b/test/unit/lib/util.test.js
index c26d97f9..fb65f3bc 100644
--- a/test/unit/lib/util.test.js
+++ b/test/unit/lib/util.test.js
@@ -65,4 +65,29 @@ describe('Util', () => {
     const expectedApiExtendedAdminMockPath = path.join('interface', 'thirdparty', 'admin_swagger.json')
     expect(apiExtendedAdminMockPath).toContain(expectedApiExtendedAdminMockPath)
   })
+
+  describe('stepState Tests -->', () => {
+    it('should initiate stepState with default step', async () => {
+      const stepState = Util.initStepState()
+      expect(stepState.step).toBe('start')
+    })
+
+    it('should initiate stepState with custom step', async () => {
+      const step = 'customStep'
+      const stepState = Util.initStepState(step)
+      expect(stepState.step).toBe(step)
+    })
+
+    it('should set ongoing step using inProgress() method', async () => {
+      const step = 'nextStep'
+      const stepState = Util.initStepState()
+      stepState.inProgress(step)
+      expect(stepState.step).toBe(step)
+    })
+
+    it('should not be able to change internal state directly', async () => {
+      const stepState = Util.initStepState()
+      expect(() => { stepState.step = 'impossible' }).toThrowError()
+    })
+  })
 })


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the get/put parties code to simplify the addition of new functionality and improve overall consistency in error handling and state management. Key changes include new implementations for PutPartiesService, GetPartiesService, and PutPartiesErrorService; updates to dependency injection via deps.js; and modifications in utility functions and tests for better maintainability.

### Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                      | Description                                                   |
| ----------------------------------------- | ------------------------------------------------------------- |
| src/domain/parties/services/PutPartiesService.js | Refactored validation and callback methods with updated error handling TODOs. |
| src/domain/parties/services/GetPartiesService.js | Updated request flow, proxy lookup, and added TODOs for corner-case handling. |
| src/domain/parties/deps.js                | Expanded dependencies injected into services for improved modularity. |
| src/domain/parties/services/index.js      | Updated exports for new service naming consistency.         |
| src/lib/util.js                           | Enhanced utility functions and updated licensing/header changes. |
| (Others: Several files)                   | Various minor adjustments in headers, utils, tests, and file removals for consistency. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/domain/parties/services/GetPartiesService.js:196**
* Add explicit error handling or a fallback strategy for when 'sentCount' remains zero after processing the oracle party list, ensuring that failed request paths are properly managed.
```
// todo: think what if sentCount === 0 here
```
**src/domain/parties/parties.js:1**
* Verify that the removal of 'parties.js' is intentional and ensure that all references to this module are updated to prevent potential inconsistencies.
```
File removal diff
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T16:56:59Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the GET/PUT parties logic by splitting the monolithic functions into separate services and improving dependency injection and logging. Key changes include:
- Extraction of business logic into GetPartiesService, PutPartiesService, and PutPartiesErrorService.
- Introduction of a new createDeps helper to provide dependencies via DI.
- Reorganization of modules and updates to utilities and tests for improved modularity and clarity.

### Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/domain/parties/services/index.js | New service index file exporting the individual parties services. |
| src/domain/parties/deps.js | Added createDeps function to safely inject dependencies. |
| src/domain/parties/services/PutPartiesService.js | Refactored PUT request handling including a private method for updating oracle mapping. |
| src/domain/parties/services/GetPartiesService.js | New implementation handling GET parties requests with improved flow control. |
| src/lib/util.js | Updated utility functions to use modern Node modules and improved step-state management. |
| src/domain/parties/index.js | Updated exports to reflect the new modular structure. |
| Others | Minor updates in partiesUtils and test files for consistency. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/lib/util.js:102**
* [nitpick] The naming and behavior of initStepState could be confusing because although the returned object is frozen, its internal state (the variable 'step') can still be updated via the inProgress method. Consider renaming or adding documentation to clarify that this design is intentional.
```
const initStepState = (initStep = 'start') => {
```
**src/domain/parties/services/PutPartiesService.js:92**
* [nitpick] The step identifier '#sendSuccessCallback-6' includes a leading '#' which is inconsistent with other step names. Consider using a consistent naming convention for step labels to improve log readability and maintainability.
```
this.deps.stepState.inProgress('#sendSuccessCallback-6')
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T17:46:42Z
user: login: kleyow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:46:45Z
user: login: kleyow
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:56:50Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the GET/PUT parties code by splitting huge functions into smaller, dedicated service classes to improve maintainability and enable easier extension of functionality. Key changes include:
- Introducing GetPartiesService, PutPartiesService, and PutPartiesErrorService with dependency injection to encapsulate business logic.
- Refactoring dependency management (deps.js) and updating routing/exports in the parties domain.
- Improving logging and reorganizing utility functions across the codebase.

### Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                        | Description                                                    |
|---------------------------------------------|----------------------------------------------------------------|
| src/domain/parties/services/index.js        | Exposes new service classes to handle GET/PUT parties requests. |
| src/domain/parties/deps.js                   | Provides a frozen dependency object for services.             |
| src/domain/parties/services/*.js             | Implements new service classes with clear separation of concerns.|
| src/domain/parties/putParties.js             | Introduces higher-level orchestration using the new services.   |
| src/domain/parties/getPartiesByTypeAndID.js    | Refactored to delegate business logic to GetPartiesService.      |
| src/domain/parties/partiesUtils.js           | Updates function names and paths to support refactored services.  |
| src/lib/util.js                              | Adds helper functions (initStepState, finishSpanWithError) used in services. |
| Other files (tests, headers, index.js, timeout DTO)| Various updates to align with refactored paths and improved utils.   |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/lib/util.js:110**
* Consider wrapping calls to childSpan.error() and childSpan.finish() in try-catch blocks to handle potential rejections, ensuring that errors during span finishing do not propagate unexpectedly.
```
const finishSpanWithError = async (childSpan, fspiopError) => { ... }
```
**src/domain/parties/getPartiesByTypeAndID.js:64**
* [nitpick] Ensure that 'results.requester' is reliably set before entering the try-catch block so that error handling in the catch block can reference a valid requester; otherwise, consider initializing the requester variable prior to the try block.
```
catch (error) { const { requester } = results; results.fspiopError = await service.handleError({ error, requester, headers, params }); ... }
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T20:20:53Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the parties handling logic by splitting the monolithic GET/PUT parties code into separate service classes, making it easier to add new functionality and improve logging and error handling. Key changes include:
- Introduction of dedicated service classes (GetPartiesService, PutPartiesService, PutPartiesErrorService) that encapsulate business logic.
- A new dependency injection mechanism via createDeps to consistently pass required dependencies.
- Removal of the legacy parties module and reorganization of utility functions and tests for better modularity.

### Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/domain/parties/services/* | New service classes to handle GET/PUT parties flows and error handling |
| src/domain/parties/deps.js | Added a helper to construct frozen dependency objects for services |
| src/domain/parties/putParties.js, getPartiesByTypeAndID.js | Rewritten endpoints that delegate processing to the new service classes |
| src/lib/util.js | Updated utility functions (initStepState, finishSpanWithError) with added license and refactored imports |
| Other test and index files | Adjusted to match the new module organization and dependency paths |
</details>



<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**src/domain/parties/partiesUtils.js:85**
* [nitpick] Enhance the log message within createErrorHandlerOnSendingCallback by including additional context (e.g., current step or request details) to aid in debugging callback failures.
```
const createErrorHandlerOnSendingCallback = (config, logger) => async (err, headers, params, requester) => {
```
**src/domain/parties/getPartiesByTypeAndID.js:64**
* Ensure that 'results.requester' is properly set before destructuring it in the catch block so that undefined is not passed to service.handleError. A guard or default value may help prevent unintended behavior.
```
} catch (error) {
```
**src/lib/util.js:102**
* [nitpick] Document that initStepState intentionally allows controlled internal state mutation via the inProgress method even though the returned object is frozen, so that future maintainers understand this design choice.
```
const initStepState = (initStep = 'start') => {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-03-13T20:24:20Z
user: login: kleyow
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T20:49:35Z

Review comments:
diff_hunk: @@ -0,0 +1,108 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const ErrorHandler = require('@mojaloop/central-services-error-handling')
+const { ERROR_MESSAGES } = require('../../../constants')
+const BasePartiesService = require('./BasePartiesService')
+
+const { RestMethods } = BasePartiesService.enums()
+
+class PutPartiesService extends BasePartiesService {
+  async handleRequest () {
+    // todo: add impl.
+  }
+
+  async validateSourceParticipant ({ source, proxy }) {
+    this.deps.stepState.inProgress('validateSourceParticipant-1')
+    const requesterParticipant = await super.validateParticipant(source)
+
+    if (!requesterParticipant) {
+      if (!this.proxyEnabled || !proxy) {
+        const errMessage = ERROR_MESSAGES.sourceFspNotFound
+        this.log.warn(`${errMessage} and no proxy`, { source })
+        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+      }
+      const isCached = await this.deps.proxyCache.addDfspIdToProxyMapping(source, proxy)
+      // todo: think,if we should throw error if isCached === false?
path: src/domain/parties/services/PutPartiesService.js
user: login: Copilot
body: Consider handling the case when 'isCached' is false by either throwing an error or explicitly managing the failure to avoid unintended silent issues.
```suggestion
      if (!isCached) {
        const errMessage = ERROR_MESSAGES.cacheUpdateFailed
        this.log.warn(`${errMessage}`, { source, proxy })
        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, errMessage)
      }
```
created_at: 2025-03-13T16:56:59Z
author_association: NONE
diff_hunk: @@ -1,5 +1,33 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
 const { Enum, Util: { Hapi } } = require('@mojaloop/central-services-shared')
-const EventSdk = require('@mojaloop/event-sdk')
+const { MojaloopApiErrorCodes } = require('@mojaloop/sdk-standard-components').Errors
+// todo: check why do we need sdk-standard-components deps here !!!
path: src/domain/parties/partiesUtils.js
user: login: kleyow
body: Most likely because the Error objects were contributed there. They should be moved to css lib imo.
created_at: 2025-03-13T17:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,113 @@
+/*****
+ License
+ --------------
+ Copyright © 2020-2025 Mojaloop Foundation
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+ Contributors
+ --------------
+ This is the official list of the Mojaloop project contributors for this file.
+ Names of the original copyright holders (individuals or organizations)
+ should be listed with a '*' in the first column. People who have
+ contributed from an organization can be listed under the organization
+ that actually holds the copyright for their contributions (see the
+ Mojaloop Foundation for an example). Those individuals should have
+ their names indented and be marked with a '-'. Email address can be added
+ optionally within square brackets <email>.
+
+ * Mojaloop Foundation
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
+
+ --------------
+ ******/
+
+const ErrorHandler = require('@mojaloop/central-services-error-handling')
+const { ERROR_MESSAGES } = require('../../../constants')
+const BasePartiesService = require('./BasePartiesService')
+
+const { RestMethods } = BasePartiesService.enums()
+
+class PutPartiesService extends BasePartiesService {
+  // async handleRequest () {
+  //   // todo: add impl.
+  // }
+
+  async validateSourceParticipant ({ source, proxy }) {
+    this.deps.stepState.inProgress('validateSourceParticipant-1')
+    const requesterParticipant = await super.validateParticipant(source)
+
+    if (!requesterParticipant) {
+      if (!this.proxyEnabled || !proxy) {
+        const errMessage = ERROR_MESSAGES.sourceFspNotFound
+        this.log.warn(`${errMessage} and no proxy`, { source })
+        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+      }
+      const isCached = await this.deps.proxyCache.addDfspIdToProxyMapping(source, proxy)
+      if (!isCached) {
+        const errMessage = 'failed to addDfspIdToProxyMapping'
+        this.log.warn(errMessage, { source, proxy })
+        throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
+      }
+
+      this.log.info('addDfspIdToProxyMapping is done', { source, proxy })
+    }
+  }
+
+  async checkProxySuccessResponse ({ destination, source, headers, params }) {
+    if (this.proxyEnabled) {
+      this.deps.stepState.inProgress('checkProxySuccessResponse-2')
+      const alsReq = this.deps.partiesUtils.alsRequestDto(destination, params)
+
+      const isExists = await this.deps.proxyCache.receivedSuccessResponse(alsReq)
+      if (isExists) {
+        await this.#updateOracleWithParticipantMapping({ source, headers, params })
+        return
+      }
+      this.log.warn('destination is NOT in scheme, and no cached sendToProxiesList', { destination, alsReq })
+      // todo: think, if we need to throw an error here
+    }
+  }
+
+  async identifyDestinationForSuccessCallback (destination) {
+    this.deps.stepState.inProgress('validateDestinationParticipant-4')
+    const destinationParticipant = await super.validateParticipant(destination)
+    if (destinationParticipant) {
+      return destinationParticipant.name
+    }
+
+    const proxyName = this.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
+    if (!proxyName) {
+      const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
+      this.log.warn(`${errMessage} and no proxy`, { destination })
+      throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
+    }
+    return proxyName
+  }
+
+  async sendSuccessCallback ({ sendTo, headers, params, dataUri }) {
+    this.deps.stepState.inProgress('#sendSuccessCallback-6')
+    const payload = PutPartiesService.decodeDataUriPayload(dataUri)
+    const callbackEndpointType = this.deps.partiesUtils.putPartyCbType(params.SubId)
+    const options = this.deps.partiesUtils.partiesRequestOptionsDto(params)
+
+    await this.deps.participant.sendRequest(
+      headers, sendTo, callbackEndpointType, RestMethods.PUT, payload, options
+    )
+    this.log.verbose('sendSuccessCallback is done', { sendTo, payload })
+  }
+
+  async #updateOracleWithParticipantMapping ({ source, headers, params }) {
path: src/domain/parties/services/PutPartiesService.js
user: login: Copilot
body: Consider adding error handling inside #updateOracleWithParticipantMapping to catch and log potential failures from deps.oracle.oracleRequest. This would help ensure that subsequent processing is correctly managed if the update fails.
created_at: 2025-03-13T20:24:20Z
author_association: NONE

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-13T20:39:49Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=mojaloop_account-lookup-service&pullRequest=535) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [13 New issues](https://sonarcloud.io/project/issues?id=mojaloop_account-lookup-service&pullRequest=535&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=mojaloop_account-lookup-service&pullRequest=535&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mojaloop_account-lookup-service&pullRequest=535&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=mojaloop_account-lookup-service&pullRequest=535&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=mojaloop_account-lookup-service&pullRequest=535&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=mojaloop_account-lookup-service&pullRequest=535)


author_association: NONE