Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(csi-633): added externalParticipant model; updated transfer/facade; added JSDocs; #1101

Merged
merged 15 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(csi-633): added externalParticipant model; added JSDocs; updated…
… transfer/facade
  • Loading branch information
geka-evk committed Sep 13, 2024
commit a0625a5bfdb665d6e1485e8b2def97f50e378d21
29 changes: 28 additions & 1 deletion src/handlers/transfers/createRemittanceEntity.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const TransferService = require('../../domain/transfer')
const cyril = require('../../domain/fx/cyril')
const { logger } = require('../../shared/logger')

/** @import { ProxyObligation } from './prepare.js' */

// abstraction on transfer and fxTransfer
const createRemittanceEntity = (isFx) => {
return {
Expand All @@ -19,14 +21,23 @@ const createRemittanceEntity = (isFx) => {
: TransferService.saveTransferDuplicateCheck(id, hash)
},

/**
* Saves prepare transfer/fxTransfer details to DB.
*
* @param {Object} payload - Message payload.
* @param {string | null} reason - Validation failure reasons.
* @param {Boolean} isValid - isValid.
* @param {DeterminingTransferCheckResult} determiningTransferCheckResult - The determining transfer check result.
* @param {ProxyObligation} proxyObligation - The proxy obligation
* @returns {Promise<void>}
*/
async savePreparedRequest (
payload,
reason,
isValid,
determiningTransferCheckResult,
proxyObligation
) {
// todo: add histoTimer and try/catch here
return isFx
? fxTransferModel.fxTransfer.savePreparedRequest(
payload,
Expand All @@ -50,6 +61,22 @@ const createRemittanceEntity = (isFx) => {
: TransferService.getByIdLight(id)
},

/**
* A determiningTransferCheckResult.
* @typedef {Object} DeterminingTransferCheckResult
* @property {boolean} determiningTransferExists - Indicates if the determining transfer exists.
* @property {Array<{participantName, currencyId}>} participantCurrencyValidationList - List of validations for participant currencies.
* @property {Object} [transferRecord] - Determining transfer for the FX transfer (optional).
* @property {Array} [watchListRecords] - Records from fxWatchList-table for the transfer (optional).
*/
/**
* Checks if a determining transfer exists based on the payload and proxy obligation.
* The function determines which method to use based on whether it is an FX transfer.
*
* @param {Object} payload - The payload data required for the transfer check.
* @param {ProxyObligation} proxyObligation - The proxy obligation details.
* @returns {DeterminingTransferCheckResult} determiningTransferCheckResult
*/
async checkIfDeterminingTransferExists (payload, proxyObligation) {
const result = isFx
? await cyril.checkIfDeterminingTransferExistsForFxTransferMessage(payload, proxyObligation)
Expand Down
19 changes: 17 additions & 2 deletions src/handlers/transfers/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ const forwardPrepare = async ({ isFx, params, ID }) => {
return true
}

/** @import { ProxyOrParticipant } from '#src/lib/proxyCache.js' */
/**
* @typedef {Object} ProxyObligation
* @property {boolean} isFx - Is FX transfer.
* @property {Object} payloadClone - A clone of the original payload.
* @property {ProxyOrParticipant} initiatingFspProxyOrParticipantId - initiating FSP: proxy or participant.
* @property {ProxyOrParticipant} counterPartyFspProxyOrParticipantId - counterparty FSP: proxy or participant.
* @property {boolean} isInitiatingFspProxy - initiatingFsp.(!inScheme && proxyId !== null).
* @property {boolean} isCounterPartyFspProxy - counterPartyFsp.(!inScheme && proxyId !== null).
*/

/**
* Calculates proxyObligation.
* @returns {ProxyObligation} proxyObligation
*/
const calculateProxyObligation = async ({ payload, isFx, params, functionality, action }) => {
const proxyObligation = {
isFx,
Expand Down Expand Up @@ -223,7 +238,7 @@ const processDuplication = async ({

let error
if (!duplication.hasDuplicateHash) {
logger.error(Util.breadcrumb(location, `callbackErrorModified1--${actionLetter}5`))
logger.warn(Util.breadcrumb(location, `callbackErrorModified1--${actionLetter}5`))
error = createFSPIOPError(FSPIOPErrorCodes.MODIFIED_REQUEST)
} else if (action === Action.BULK_PREPARE) {
logger.info(Util.breadcrumb(location, `validationError1--${actionLetter}2`))
Expand Down Expand Up @@ -286,7 +301,7 @@ const savePreparedRequest = async ({
proxyObligation
)
} catch (err) {
logger.error(`${logMessage} error - ${err.message}`)
logger.error(`${logMessage} error:`, err)
const fspiopError = reformatFSPIOPError(err, FSPIOPErrorCodes.INTERNAL_SERVER_ERROR)
await Kafka.proceed(Config.KAFKA_CONFIG, params, {
consumerCommit,
Expand Down
14 changes: 11 additions & 3 deletions src/lib/proxyCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,25 @@ const getCache = () => {
}

/**
* Check if dfspId is in scheme or proxy'.
* @typedef {Object} ProxyOrParticipant - An object containing the inScheme status, proxyId and FSP name
* @property {boolean} inScheme - Is FSP in the scheme.
* @property {string|null} proxyId - Proxy, associated with the FSP, if FSP is not in the scheme.
* @property {string} name - FSP name.
*/

/**
* Checks if dfspId is in scheme or proxy.
*
* @param {string} dfspId - The DFSP ID to check.
* @returns {Promise<{inScheme: boolean, proxyId: string|null}>} - An object containing the inScheme status and proxyId.
* @returns {ProxyOrParticipant} proxyOrParticipant details
*/
const getFSPProxy = async (dfspId) => {
logger.debug('Checking if dfspId is in scheme or proxy', { dfspId })
const participant = await ParticipantService.getByName(dfspId)
return {
inScheme: !!participant,
proxyId: !participant ? await getCache().lookupProxyByDfspId(dfspId) : null
proxyId: !participant ? await getCache().lookupProxyByDfspId(dfspId) : null,
name: dfspId
}
}

Expand Down
123 changes: 123 additions & 0 deletions src/models/participant/externalParticipant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*****
License
--------------
Copyright © 2017 Bill & Melinda Gates Foundation
The Mojaloop files are made available by the Bill & Melinda Gates 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
Gates 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>.
* Gates Foundation
- Name Surname <name.surname@gatesfoundation.com>

* Eugen Klymniuk <eugen.klymniuk@infitx.com>
--------------
**********/

const ErrorHandler = require('@mojaloop/central-services-error-handling')
const Db = require('../../lib/db')
const { logger } = require('../../shared/logger')
const { TABLE_NAMES } = require('../../shared/constants')

const TABLE = TABLE_NAMES.externalParticipant
const ID_FIELD = 'externalParticipantId'

const log = logger.child(`DB#${TABLE}`)

// todo: use caching lib
const CACHE = {}
const cache = {
get (key) {
return CACHE[key]
},
set (key, value) {
CACHE[key] = value
}
}

const create = async ({ name, proxyId }) => {
try {
const result = await Db.from(TABLE).insert({ name, proxyId })
log.debug('create result:', { result })
return result
} catch (err) {
log.error('error in create', err)
throw ErrorHandler.Factory.reformatFSPIOPError(err)
}
}

const getOneBy = async (criteria, options) => {
try {
const result = await Db.from(TABLE).findOne(criteria, options)
log.debug('getOneBy result:', { criteria, result })
return result
} catch (err) {
log.error('error in getOneBy:', err)
throw ErrorHandler.Factory.reformatFSPIOPError(err)
}
}
const getOneById = async (id, options) => getOneBy({ [ID_FIELD]: id }, options)
const getOneByName = async (name, options) => getOneBy({ name }, options)

const getOneByNameCached = async (name, options = {}) => {
let data = cache.get(name)
if (data) {
log.debug('getOneByIdCached cache hit:', { name, data })
} else {
data = await getOneByName(name, options)
cache.set(name, data)
log.debug('getOneByIdCached cache updated:', { name, data })
}
return data
}

const getIdByNameOrCreate = async ({ name, proxyId }) => {
try {
let dfsp = await getOneByNameCached(name)
if (!dfsp) {
await create({ name, proxyId })
// todo: check if create returns id (to avoid getOneByNameCached call)
dfsp = await getOneByNameCached(name)
}
const id = dfsp?.[ID_FIELD]
log.verbose('getIdByNameOrCreate result:', { id, name })
return id
} catch (err) {
log.child({ name, proxyId }).warn('error in getIdByNameOrCreate:', err)
return null
// todo: think, if we need to rethrow an error here?
}
}

const destroyBy = async (criteria) => {
try {
const result = await Db.from(TABLE).destroy(criteria)
log.debug('destroyBy result:', { criteria, result })
return result
} catch (err) {
log.error('error in destroyBy', err)
throw ErrorHandler.Factory.reformatFSPIOPError(err)
}
}
const destroyById = async (id) => destroyBy({ [ID_FIELD]: id })
const destroyByName = async (name) => destroyBy({ name })

// todo: think, if we need update method
module.exports = {
create,
getIdByNameOrCreate,
getOneByNameCached,
getOneByName,
getOneById,
destroyById,
destroyByName
}
66 changes: 39 additions & 27 deletions src/models/transfer/facade.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const Db = require('../../lib/db')
const Config = require('../../lib/config')
const ParticipantFacade = require('../participant/facade')
const ParticipantCachedModel = require('../participant/participantCached')
const externalParticipantModel = require('../participant/externalParticipant')
const TransferExtensionModel = require('./transferExtension')

const TransferEventAction = Enum.Events.Event.Action
Expand Down Expand Up @@ -404,6 +405,16 @@ const savePayeeTransferResponse = async (transferId, payload, action, fspiopErro
}
}

/**
* Saves prepare transfer details to DB.
*
* @param {Object} payload - Message payload.
* @param {string | null} stateReason - Validation failure reasons.
* @param {Boolean} hasPassedValidation - Is transfer prepare validation passed.
* @param {DeterminingTransferCheckResult} determiningTransferCheckResult - Determining transfer check result.
* @param {ProxyObligation} proxyObligation - The proxy obligation
* @returns {Promise<void>}
*/
const saveTransferPrepared = async (payload, stateReason = null, hasPassedValidation = true, determiningTransferCheckResult, proxyObligation) => {
const histTimerSaveTransferPreparedEnd = Metrics.getHistogram(
'model_transfer',
Expand All @@ -417,8 +428,7 @@ const saveTransferPrepared = async (payload, stateReason = null, hasPassedValida
}

// Iterate over the participants and get the details
const names = Object.keys(participants)
for (const name of names) {
for (const name of Object.keys(participants)) {
const participant = await ParticipantCachedModel.getByName(name)
if (participant) {
participants[name].id = participant.participantId
Expand All @@ -429,26 +439,26 @@ const saveTransferPrepared = async (payload, stateReason = null, hasPassedValida
const participantCurrencyRecord = await ParticipantFacade.getByNameAndCurrency(participantCurrency.participantName, participantCurrency.currencyId, Enum.Accounts.LedgerAccountType.POSITION)
participants[name].participantCurrencyId = participantCurrencyRecord?.participantCurrencyId
}
}

if (proxyObligation?.isInitiatingFspProxy) {
const proxyId = proxyObligation.initiatingFspProxyOrParticipantId.proxyId
const proxyParticipant = await ParticipantCachedModel.getByName(proxyId)
participants[proxyId] = {}
participants[proxyId].id = proxyParticipant.participantId
const participantCurrencyRecord = await ParticipantFacade.getByNameAndCurrency(
proxyId, payload.amount.currency, Enum.Accounts.LedgerAccountType.POSITION
)
// In a regional scheme, the stand-in initiating FSP proxy may not have a participantCurrencyId
// of the target currency of the transfer, so set to null if not found
participants[proxyId].participantCurrencyId = participantCurrencyRecord?.participantCurrencyId
}
if (proxyObligation?.isInitiatingFspProxy) {
const proxyId = proxyObligation.initiatingFspProxyOrParticipantId.proxyId
const proxyParticipant = await ParticipantCachedModel.getByName(proxyId)
participants[proxyId] = {}
participants[proxyId].id = proxyParticipant.participantId
const participantCurrencyRecord = await ParticipantFacade.getByNameAndCurrency(
proxyId, payload.amount.currency, Enum.Accounts.LedgerAccountType.POSITION
)
// In a regional scheme, the stand-in initiating FSP proxy may not have a participantCurrencyId
// of the target currency of the transfer, so set to null if not found
participants[proxyId].participantCurrencyId = participantCurrencyRecord?.participantCurrencyId
}

if (proxyObligation?.isCounterPartyFspProxy) {
const proxyId = proxyObligation.counterPartyFspProxyOrParticipantId.proxyId
const proxyParticipant = await ParticipantCachedModel.getByName(proxyId)
participants[proxyId] = {}
participants[proxyId].id = proxyParticipant.participantId
}
if (proxyObligation?.isCounterPartyFspProxy) {
const proxyId = proxyObligation.counterPartyFspProxyOrParticipantId.proxyId
const proxyParticipant = await ParticipantCachedModel.getByName(proxyId)
participants[proxyId] = {}
participants[proxyId].id = proxyParticipant.participantId
}

const transferRecord = {
Expand All @@ -464,26 +474,25 @@ const saveTransferPrepared = async (payload, stateReason = null, hasPassedValida
value: payload.ilpPacket
}

const state = hasPassedValidation
? Enum.Transfers.TransferInternalState.RECEIVED_PREPARE
: Enum.Transfers.TransferInternalState.INVALID

const transferStateChangeRecord = {
transferId: payload.transferId,
transferStateId: state,
transferStateId: hasPassedValidation ? TransferInternalState.RECEIVED_PREPARE : TransferInternalState.INVALID,
reason: stateReason,
createdDate: Time.getUTCString(new Date())
}

let payerTransferParticipantRecord
if (proxyObligation?.isInitiatingFspProxy) {
const externalParticipantId = await externalParticipantModel.getIdByNameOrCreate(proxyObligation.initiatingFspProxyOrParticipantId)
// todo: think, what if externalParticipantId is null?
payerTransferParticipantRecord = {
transferId: payload.transferId,
participantId: participants[proxyObligation.initiatingFspProxyOrParticipantId.proxyId].id,
participantCurrencyId: participants[proxyObligation.initiatingFspProxyOrParticipantId.proxyId].participantCurrencyId,
transferParticipantRoleTypeId: Enum.Accounts.TransferParticipantRoleType.PAYER_DFSP,
ledgerEntryTypeId: Enum.Accounts.LedgerEntryType.PRINCIPLE_VALUE,
amount: -payload.amount.amount
amount: -payload.amount.amount,
externalParticipantId
}
} else {
payerTransferParticipantRecord = {
Expand All @@ -499,13 +508,16 @@ const saveTransferPrepared = async (payload, stateReason = null, hasPassedValida
logger.debug('saveTransferPrepared participants:', { participants })
let payeeTransferParticipantRecord
if (proxyObligation?.isCounterPartyFspProxy) {
const externalParticipantId = await externalParticipantModel.getIdByNameOrCreate(proxyObligation.counterPartyFspProxyOrParticipantId)
// todo: think, what if externalParticipantId is null?
payeeTransferParticipantRecord = {
transferId: payload.transferId,
participantId: participants[proxyObligation.counterPartyFspProxyOrParticipantId.proxyId].id,
participantCurrencyId: null,
transferParticipantRoleTypeId: Enum.Accounts.TransferParticipantRoleType.PAYEE_DFSP,
ledgerEntryTypeId: Enum.Accounts.LedgerEntryType.PRINCIPLE_VALUE,
amount: -payload.amount.amount
amount: -payload.amount.amount,
externalParticipantId
}
} else {
payeeTransferParticipantRecord = {
Expand Down
1 change: 1 addition & 0 deletions src/shared/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { Enum } = require('@mojaloop/central-services-shared')

const TABLE_NAMES = Object.freeze({
externalParticipant: 'externalParticipant',
fxTransfer: 'fxTransfer',
fxTransferDuplicateCheck: 'fxTransferDuplicateCheck',
fxTransferErrorDuplicateCheck: 'fxTransferErrorDuplicateCheck',
Expand Down
Loading