Created
March 3, 2020 18:06
-
-
Save TechnotronicOz/c0b7d093e3bd6e966516e7624a69aefa to your computer and use it in GitHub Desktop.
facade/lib/maker/market.index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const _ = require('lodash'); | |
const comb = require('comb'); | |
const model = require('../../../../model'); | |
const helpers = require('../../../../helpers'); | |
const pushEvents = require('../../../../push-events'); | |
const bus = require('../../../../bus')(); | |
const caching = require('../../_core/caching'); | |
const diffModelValues = require('./_helpers/helpers').diffModelValues; | |
const LOGGER = require('../../../../logger')('c2fo.facade.maker.market'); | |
const awardFacade = require('./award'); | |
const cashPoolScheduleFacade = require('./cash_pool_schedule'); | |
const takerConfigurationFacade = require('./taker_configuration'); | |
const config = require('../../../../config').loadSync(); | |
const s3 = helpers.s3; | |
const utils = helpers.utils; | |
const sql = model.patio.sql; | |
const identifier = sql.identifier; | |
const literal = sql.literal; | |
const sum = sql.sum; | |
const min = sql.min; | |
const max = sql.max; | |
const Market = model.Market; | |
const TakersMarkets = model.TakersMarkets; | |
const applyDatasetQuery = utils.applyDatasetQuery; | |
const applyDatasetOptions = utils.applyDatasetOptions; | |
const merge = comb.merge; | |
const round = comb.number.round; | |
const cache = caching.cache; | |
const redis = caching.client; | |
const when = comb.when; | |
const isGatewayEnabled = helpers.configUtils.isGatewayEnabled; | |
const DISALLOWED_NON_TPF_MARKET_FIELDS = [ 'returnAmount', 'cashPool', 'minReturnAmount', 'minimumIncomeAmount', 'maxApr', 'maxDiscount', 'calculateSettings' ]; | |
const TPF_CASH_POOL_SETTINGS_MAPPING = { | |
apr: 'returnAmount', | |
cashPool: 'cashPool', | |
minApr: 'minReturnAmount', | |
minEarn: 'minimumIncomeAmount', | |
maxApr: 'maxApr', | |
maxDiscount: 'maxDiscount', | |
}; | |
const BuyerNotifications = model.BuyerNotifications; | |
module.exports = (facade, exposed) => { | |
const updatePayDateQueue = bus.workerQueue('c2fo.model.market.updatePayDate'); | |
const tpfCalculateQueue = bus.workerQueue('c2fo.model.market.tpf.calculate'); | |
const adjustInvoicesQueue = bus.workerQueue('c2fo.model.market.adjustments'); | |
const reportDownloadQueue = bus.workerQueue('c2fo.services.email.report-download'); | |
const marketKeyGen = caching.createKeyGen('market:$0:maker:$1:currency:$2'); | |
const marketStatsKeyGen = caching.createKeyGen('market:$0:stats'); | |
facade.expose({ | |
maker: { | |
getMarketSettings, | |
market: { | |
getStats: cache('market:$0:stats', null, 30, fetchStatsForMarket), | |
getStatsForMarkets: cache('market:$0:stats', null, 30, fetchStatsForMarkets), | |
getPerformanceSummarySignedUrls: cache('market:$0:stats', null, 30, fetchPerformanceSummarySignedUrls), | |
getFilteredStats: cache('market:$0:getFilteredStats:$1:$2:$3:$4', null, 30, fetchFilteredMarketStats), | |
getMarketSuppliersCounts, | |
updateStats: updateMarketStats, | |
get: cache('market:$0:maker:$1:currency:$2', Market, 30, findMakerMarket), | |
update: updateMarket, | |
getStatus: getMarketStatus, | |
create: createMarket, | |
updateAdjustmentTolerance, | |
sendNoticationForPerformanceReport, // todo: add test coverage for this | |
getAllMakersWithUserRoles, // todo: add test coverage for this | |
getMarketForGivenUserId, // todo: add test coverage for this | |
}, | |
}, | |
}); | |
awardFacade(facade, exposed); | |
cashPoolScheduleFacade(facade, exposed); | |
takerConfigurationFacade(facade, exposed); | |
function purgeMarketCache(market) { | |
// marketId, makerId, currency | |
return when( | |
redis().del(marketKeyGen([ market.id, market.makerId, market.currency ])), | |
redis().del(marketKeyGen([ null, market.makerId, market.currency ])), | |
redis().del(marketKeyGen([ null, null, market.currency ])), | |
redis().del(marketKeyGen([ null, market.makerId, null ])), | |
redis().del(marketKeyGen([ null, null, null ])), | |
redis().del(marketKeyGen([ market.id, null, market.currency ])), | |
redis().del(marketKeyGen([ market.id, null, null ])), | |
redis().del(marketKeyGen([ market.id, market.makerId, null ])), | |
redis().del(marketStatsKeyGen([ market.id ])) | |
); | |
} | |
function fetchStatsForMarket(marketId) { | |
return _getMarketStatsDs(marketId) | |
.naked() | |
.one() | |
.chain((marketStats) => { | |
return Market | |
.select( | |
'market__id', | |
'market__makerId', | |
'organization__uuid___makerOrganizationUuid', | |
'market__isTpf' | |
) | |
.filter({ market__id: marketId }) | |
.join('division', { division__id: identifier('market__makerId') }) | |
.join('organization', { organization__id: identifier('division__organizationId') }) | |
.naked() | |
.one() | |
.chain(m => _fetchCashPoolSettings(m)) | |
.chain((market) => { | |
return market ? _calcOverallMarketStats(merge(marketStats, market)) : null; | |
}); | |
}); | |
} | |
function fetchStatsForMarkets(marketIds) { | |
const result = {}; | |
return _getMarketStatsDs(marketIds) | |
.naked() | |
.all() | |
.chain((marketsStats) => { | |
return Market | |
.select( | |
'market__id', | |
'market__makerId', | |
'organization__uuid___makerOrganizationUuid', | |
'market__uuid', | |
'market__isTpf', | |
'market__currency' | |
) | |
.filter({ market__id: marketIds }) | |
.join('division', { division__id: identifier('market__makerId') }) | |
.join('organization', { organization__id: identifier('division__organizationId') }) | |
.naked() | |
.all() | |
.chain((markets) => { | |
return markets.reduce((map, market) => { | |
map[market.uuid] = market; | |
return map; | |
}, {}); | |
}) | |
.chain((markets) => { | |
marketsStats.forEach((marketStats) => { | |
result[marketStats.marketUuid] = _calcOverallMarketStats( | |
merge(marketStats, markets[marketStats.marketUuid]) | |
); | |
}); | |
return result; | |
}); | |
}); | |
} | |
function fetchPerformanceSummarySignedUrls(uri) { | |
return s3.listFiles(uri) | |
.chain((assets = []) => { | |
if (!assets.length) { | |
return comb.rejected(); | |
} | |
const nameSortedObjects = assets.sort((a, b) => { | |
return b - a; | |
}); | |
const latestReportPrefix = uri + nameSortedObjects[0].split('-')[0]; | |
return s3.listFiles(latestReportPrefix); | |
}) | |
.chain((assets = []) => { | |
if (!assets.length) { | |
return Promise.reject(); | |
} | |
return Promise.all(assets.map((fileName) => { | |
return comb.resolved(s3.getSignedUrl(uri + fileName)) | |
.chain(fileUrl => ({ fileUrl, fileName })); | |
})); | |
}); | |
} | |
function fetchFilteredMarketStats( | |
marketId, | |
category, | |
searchOpts, | |
search, | |
filter, | |
groupOptions | |
) { | |
const grouping = _getFilteredCategoryGrouping(category); | |
return _getFilteredMarketStats( | |
grouping, | |
marketId, | |
searchOpts, | |
search, | |
filter, | |
groupOptions | |
); | |
} | |
function getMarketSuppliersCounts(marketId) { | |
const participatingFilter = '(signed_up_count > 0 AND taker_config_is_enabled = true AND ((taker_config_is_discount_bidding = false AND taker_config_max_apr > 0) OR (taker_config_is_discount_bidding = true AND taker_config_max_discount > 0)))'; | |
const stats = TakersMarkets.select( | |
// overall | |
literal('count(id)::integer').as('overallTotal'), | |
literal('sum(case when eligible_invoice_amount > 0 then 1 else 0 end)::integer').as('overallWithApCount'), | |
literal('sum(case when eligible_invoice_amount = 0 then 1 else 0 end)::integer').as('overallWithoutApCount'), | |
// registered | |
literal('sum(case when signed_up_count > 0 then 1 else 0 end)::integer').as('registeredTotal'), | |
literal('sum(case when signed_up_count > 0 AND eligible_invoice_amount > 0 then 1 else 0 end)::integer').as('registeredWithApCount'), | |
literal('sum(case when signed_up_count > 0 AND eligible_invoice_amount = 0 then 1 else 0 end)::integer').as('registeredWithoutApCount'), | |
// participating | |
literal(`sum(case when ${participatingFilter} then 1 else 0 end)::integer`).as('participatingTotal'), | |
literal(`sum(case when ${participatingFilter} AND eligible_invoice_amount > 0 then 1 else 0 end)::integer`).as('participatingWithApCount'), | |
literal(`sum(case when ${participatingFilter} AND eligible_invoice_amount = 0 then 1 else 0 end)::integer`).as('participatingWithoutApCount') | |
).filter({ marketId }); | |
return stats.naked().one().chain((marketSuppliersStats) => { | |
for (const i in marketSuppliersStats) { | |
/* eslint no-restricted-syntax:0 */ | |
if (_.has(marketSuppliersStats, i) && comb.isNull(marketSuppliersStats[i])) { | |
marketSuppliersStats[i] = 0; | |
} | |
} | |
return marketSuppliersStats; | |
}); | |
} | |
function updateMarketStats(marketId) { | |
return Market.filter({ id: marketId }).one().chain((market) => { | |
if (market) { | |
return exposed.stats.updateTakersWithConfigInMarket(marketId).chain(() => { | |
return purgeMarketCache(market); | |
}).chain((purgedKeys) => { | |
pushEvents.marketStats(market.uuid); | |
return purgedKeys; | |
}); | |
} | |
throw new Error(`unable to find market with id ${marketId}`); | |
}); | |
} | |
function findMakerMarket(marketId, makerId, currency) { | |
const query = { makerId }; | |
const hasMarketId = !comb.isUndefinedOrNull(marketId); | |
if (currency) { | |
query.currency = currency; | |
} | |
if (hasMarketId) { | |
query.id = marketId; | |
} | |
return Market.filter(query).all(); | |
} | |
/* | |
* Get settings for all markets under a maker | |
* | |
* @param makerId {Integer} The maker's ID | |
*/ | |
function getMarketSettings(makerId) { | |
return Market | |
.select( | |
identifier('market').all(), | |
'organization__uuid___makerOrganizationUuid', | |
literal('row_to_json(market_reserve.*)').as('reserveSettings'), | |
literal("adjustment_settings->>'adjustmentTolerance'").as('adjTolerance') | |
) | |
.join('division', { id: identifier('market__makerId') }) | |
.join('organization', { id: identifier('division__organizationId') }) | |
.leftJoin('marketReserve', { market_id: identifier('market__id') }) | |
.filter({ | |
market__makerId: makerId, | |
}) | |
.naked() | |
.all() | |
.chain((marketSettings) => { | |
return __addCashPoolSettingsToMarket(marketSettings).chain(() => { | |
return comb.async.array(marketSettings).map((row) => { | |
const marketId = row.id; | |
row.legacyId = row.id; | |
if (isGatewayEnabled) { | |
row.id = row.uuid; | |
} | |
row.reserveSettings = utils.camelizeObject(row.reserveSettings) || {}; | |
if (row.isTpf) { | |
row.cashPoolSchedule = {}; | |
return row; | |
} | |
return exposed.akkaCalc.cashPool.cashPoolSchedule | |
.fetchCashPoolScheduleByMarket( | |
row.makerOrganizationUuid, | |
row.makerId, | |
marketId | |
) | |
.chain((cashPoolSchedule) => { | |
return Object.assign(row, { | |
cashPoolSchedule: cashPoolSchedule || {}, | |
}); | |
}); | |
}); | |
}); | |
}); | |
} | |
function updateMarket(marketId, makerId, values) { | |
return Market.filter({ id: marketId, makerId }).one().chain((market) => { | |
if (!market) { | |
return comb.rejected(new Error(`unable to update market with id ${marketId}`)); | |
} | |
// diff the values and market | |
const diffedValues = diffModelValues(market.toObject(), values); | |
if (Object.keys(diffedValues).length === 0) { | |
LOGGER.info(`updateMarket called but no values were changed [marketId = ${marketId}]`); | |
return market.toObject(); | |
} | |
let updatePromise = null; | |
if (market.isTpf) { | |
updatePromise = _updateTpfMarket(market, diffedValues); | |
} else { | |
updatePromise = _updateNonTpfMarket(market, diffedValues); | |
} | |
return updatePromise.chain(() => purgeMarketCache(market)) | |
.chain(() => { | |
pushEvents.marketSettings(market.uuid); | |
return market.toObject(); | |
}); | |
}); | |
} | |
function getMarketStatus(marketId, makerId) { | |
return Market.filter({ id: marketId, makerId }).one().chain((market) => { | |
return market ? market.currentStatus : null; | |
}); | |
} | |
function createMarket(makerId, values) { | |
return Market.save(merge({ makerId }, values), { match: false }); | |
} | |
function updateAdjustmentTolerance(marketId, tolerance) { | |
return Market.findById(marketId).chain((market) => { | |
const adjustmentSettings = market.adjustmentSettings; | |
const shouldAdjust = adjustmentSettings.adjustmentTolerance !== tolerance; | |
adjustmentSettings.adjustmentTolerance = tolerance; | |
return market | |
.update({ adjustmentSettings: sql.json(adjustmentSettings) }) | |
.chain(() => { | |
if (shouldAdjust) { | |
return adjustInvoicesQueue.publish({ marketId }, { deliveryMode: 2 }); | |
} | |
return comb.resolved(); | |
}); | |
}); | |
} | |
/* | |
* Private API | |
*/ | |
function _getMarketStatsDs(marketIds) { | |
return TakersMarkets.select( | |
'takersMarkets__marketId', | |
'takersMarkets__marketUuid', | |
'takersMarkets__makerOrganizationUuid', | |
sum('takersMarkets__acceptedDiscountedAmount').as('cashPoolUsed'), | |
sum('takersMarkets__reservedAdjustmentAmount').as('reservedAdjustmentAmount'), | |
sum('takersMarkets__reservedAdjustmentCount').as('reservedAdjustmentCount'), | |
sum('takersMarkets__matchedAdjustmentAmount').as('matchedAdjustmentAmount'), | |
sum('takersMarkets__matchedAdjustmentCount').as('matchedAdjustmentCount'), | |
sum('takersMarkets__unmatchedAdjustmentAmount').as('unmatchedAdjustmentAmount'), | |
sum('takersMarkets__unmatchedAdjustmentCount').as('unmatchedAdjustmentCount'), | |
sum('takersMarkets__acceptedInvoiceAmount').as('acceptedInvoiceAmount'), | |
sum('takersMarkets__acceptedDiscountWeightedSum').as('acceptedDiscountWeightedSum'), | |
sum('takersMarkets__acceptedDpeWeightedSum').as('acceptedDpeWeightedSum'), | |
sum('takersMarkets__acceptedAprWeightedSum').as('acceptedAprWeightedSum'), | |
sum('takersMarkets__acceptedEarn').as('acceptedEarn'), | |
sum('takersMarkets__acceptedInvoiceCount').as('acceptedInvoiceCount'), | |
min('takersMarkets__minAcceptedApr').as('acceptedMinApr'), | |
max('takersMarkets__maxAcceptedApr').as('acceptedMaxApr'), | |
literal('sum(CASE WHEN(takers_markets.accepted_invoice_count > 0) THEN 1 ELSE 0 END)').as('acceptedTakerCount'), | |
sum('takersMarkets__awardedInvoiceAmount').as('awardedInvoiceAmount'), | |
sum('takersMarkets__awardedDiscountedAmount').as('awardedDiscountedAmount'), | |
sum('takersMarkets__awardedDiscountWeightedSum').as('awardedDiscountWeightedSum'), | |
sum('takersMarkets__awardedDpeWeightedSum').as('awardedDpeWeightedSum'), | |
sum('takersMarkets__awardedAprWeightedSum').as('awardedAprWeightedSum'), | |
sum('takersMarkets__awardedEarn').as('awardedEarn'), | |
sum('takersMarkets__awardedInvoiceCount').as('awardedInvoiceCount'), | |
literal('sum(CASE WHEN (takers_markets.awarded_invoice_count > 0) THEN 1 ELSE 0 END)').as('awardedTakerCount'), | |
sum('takersMarkets__notAcceptedInvoiceAmount').as('notAcceptedInvoiceAmount'), | |
sum('takersMarkets__notAcceptedDiscountWeightedSum').as('notAcceptedDiscountWeightedSum'), | |
sum('takersMarkets__notAcceptedDpeWeightedSum').as('notAcceptedDpeWeightedSum'), | |
sum('takersMarkets__notAcceptedAprWeightedSum').as('notAcceptedAprWeightedSum'), | |
sum('takersMarkets__notAcceptedEarn').as('notAcceptedEarn'), | |
sum('takersMarkets__notAcceptedInvoiceCount').as('notAcceptedInvoiceCount'), | |
min('takersMarkets__minNotAcceptedApr').as('notAcceptedMinApr'), | |
max('takersMarkets__maxNotAcceptedApr').as('notAcceptedMaxApr'), | |
literal('sum(CASE WHEN (takers_markets.not_accepted_invoice_count > 0) THEN 1 ELSE 0 END)').as('notAcceptedTakerCount'), | |
sum('takersMarkets__pendingInvoiceAmount').as('pendingInvoiceAmount'), | |
sum('takersMarkets__pendingDiscountedAmount').as('pendingDiscountedAmount'), | |
sum('takersMarkets__pendingDiscountWeightedSum').as('pendingDiscountWeightedSum'), | |
sum('takersMarkets__pendingDpeWeightedSum').as('pendingDpeWeightedSum'), | |
sum('takersMarkets__pendingAprWeightedSum').as('pendingAprWeightedSum'), | |
sum('takersMarkets__pendingEarn').as('pendingEarn'), | |
sum('takersMarkets__pendingInvoiceCount').as('pendingInvoiceCount'), | |
literal('sum(CASE WHEN (takers_markets.pending_invoice_count > 0) THEN 1 ELSE 0 END)').as('pendingTakerCount') | |
) | |
.filter({ takersMarkets__marketId: marketIds }) | |
.groupBy('takersMarkets__makerOrganizationUuid', 'takersMarkets__marketId', 'takersMarkets__marketUuid'); | |
} | |
function _calcOverallMarketStats(marketStats) { | |
const blendedClearingInvoiceAmountSum = marketStats.acceptedInvoiceAmount | |
+ marketStats.awardedInvoiceAmount; | |
const blendedClearingDiscountWeightedSum = marketStats.acceptedDiscountWeightedSum | |
+ marketStats.awardedDiscountWeightedSum; | |
const blendedClearingDpeWeightedSum = marketStats.acceptedDpeWeightedSum | |
+ marketStats.awardedDpeWeightedSum; | |
const blendedClearingAprWeightedSum = marketStats.acceptedAprWeightedSum | |
+ marketStats.awardedAprWeightedSum; | |
const availableInvoiceAmountSum = blendedClearingInvoiceAmountSum | |
+ marketStats.notAcceptedInvoiceAmount; | |
const availableDiscountWeightedSum = blendedClearingDiscountWeightedSum | |
+ marketStats.notAcceptedDiscountWeightedAvg; | |
const availableDpeWeightedSum = blendedClearingDpeWeightedSum | |
+ marketStats.notAcceptedDpeWeightedSum; | |
const availableAprWeightedSum = blendedClearingAprWeightedSum | |
+ marketStats.notAcceptedAprWeightedSum; | |
const overallAmountSum = marketStats.acceptedInvoiceAmount | |
+ marketStats.awardedInvoiceAmount + marketStats.pendingInvoiceAmount; | |
const overallDiscountSum = marketStats.acceptedDiscountWeightedSum | |
+ marketStats.awardedDiscountWeightedSum + marketStats.pendingDiscountWeightedSum; | |
const overallDpeSum = marketStats.acceptedDpeWeightedSum | |
+ marketStats.awardedDpeWeightedSum + marketStats.pendingDpeWeightedSum; | |
const overallAprSum = marketStats.acceptedAprWeightedSum | |
+ marketStats.awardedAprWeightedSum + marketStats.pendingAprWeightedSum; | |
marketStats.acceptedDiscountWeightedAvg = marketStats.acceptedInvoiceAmount | |
? round(marketStats.acceptedDiscountWeightedSum / marketStats.acceptedInvoiceAmount, 2) | |
: 0; | |
marketStats.acceptedDpeWeightedAvg = marketStats.acceptedInvoiceAmount | |
? round(marketStats.acceptedDpeWeightedSum / marketStats.acceptedInvoiceAmount, 2) : 0; | |
marketStats.acceptedAprWeightedAvg = marketStats.acceptedInvoiceAmount | |
? round(marketStats.acceptedAprWeightedSum / marketStats.acceptedInvoiceAmount, 2) : 0; | |
marketStats.acceptedTakerCount = parseInt(marketStats.acceptedTakerCount, 10) || 0; | |
marketStats.awardedDiscountWeightedAvg = marketStats.awardedInvoiceAmount | |
? round(marketStats.awardedDiscountWeightedSum / marketStats.awardedInvoiceAmount, 2) | |
: 0; | |
marketStats.awardedDpeWeightedAvg = marketStats.awardedInvoiceAmount | |
? round(marketStats.awardedDpeWeightedSum / marketStats.awardedInvoiceAmount, 2) : 0; | |
marketStats.awardedAprWeightedAvg = marketStats.awardedInvoiceAmount | |
? round(marketStats.awardedAprWeightedSum / marketStats.awardedInvoiceAmount, 2) : 0; | |
marketStats.awardedTakerCount = parseInt(marketStats.awardedTakerCount, 10) || 0; | |
marketStats.notAcceptedDiscountWeightedAvg = marketStats.notAcceptedInvoiceAmount | |
? round(marketStats.notAcceptedDiscountWeightedSum | |
/ marketStats.notAcceptedInvoiceAmount, 2) | |
: 0; | |
marketStats.notAcceptedDpeWeightedAvg = marketStats.notAcceptedInvoiceAmount | |
? round(marketStats.notAcceptedDpeWeightedSum / marketStats.notAcceptedInvoiceAmount, 2) | |
: 0; | |
marketStats.notAcceptedAprWeightedAvg = marketStats.notAcceptedInvoiceAmount | |
? round(marketStats.notAcceptedAprWeightedSum / marketStats.notAcceptedInvoiceAmount, 2) | |
: 0; | |
marketStats.notAcceptedTakerCount = parseInt(marketStats.notAcceptedTakerCount, 10) || 0; | |
marketStats.pendingDiscountWeightedAvg = marketStats.pendingInvoiceAmount | |
? round(marketStats.pendingDiscountWeightedSum / marketStats.pendingInvoiceAmount, 2) | |
: 0; | |
marketStats.pendingDpeWeightedAvg = marketStats.pendingInvoiceAmount | |
? round(marketStats.pendingDpeWeightedSum / marketStats.pendingInvoiceAmount, 2) : 0; | |
marketStats.pendingAprWeightedAvg = marketStats.pendingInvoiceAmount | |
? round(marketStats.pendingAprWeightedSum / marketStats.pendingInvoiceAmount, 2) : 0; | |
marketStats.pendingTakerCount = parseInt(marketStats.pendingTakerCount, 10) || 0; | |
marketStats.blendedClearingInvoiceCount = (marketStats.acceptedInvoiceCount | |
+ marketStats.awardedInvoiceCount) || 0; | |
marketStats.blendedClearingTakerCount = (marketStats.acceptedTakerCount | |
+ marketStats.awardedTakerCount) || 0; | |
marketStats.blendedClearingEarn = round(marketStats.acceptedEarn | |
+ marketStats.awardedEarn, 2) || 0; | |
marketStats.blendedClearingInvoiceAmount = round(blendedClearingInvoiceAmountSum, 2) || 0; | |
marketStats.blendedClearingDiscountedAmount = round(marketStats.cashPoolUsed | |
+ marketStats.awardedDiscountedAmount, 2) || 0; | |
marketStats.blendedClearingDiscountWeightedAvg = blendedClearingInvoiceAmountSum | |
? round(blendedClearingDiscountWeightedSum / blendedClearingInvoiceAmountSum, 2) : 0; | |
marketStats.blendedClearingDpeWeightedAvg = blendedClearingInvoiceAmountSum | |
? round(blendedClearingDpeWeightedSum / blendedClearingInvoiceAmountSum, 2) : 0; | |
marketStats.blendedClearingAprWeightedAvg = blendedClearingInvoiceAmountSum | |
? round(blendedClearingAprWeightedSum / blendedClearingInvoiceAmountSum, 2) : 0; | |
marketStats.availableInvoiceCount = (marketStats.blendedClearingInvoiceCount | |
+ marketStats.notAcceptedInvoiceCount) || 0; | |
marketStats.availableTakerCount = (marketStats.blendedClearingTakerCount | |
+ marketStats.notAcceptedTakerCount) || 0; | |
marketStats.availableEarn = (marketStats.blendedClearingEarn | |
+ marketStats.notAcceptedEarn) || 0; | |
marketStats.availableInvoiceAmount = round(availableInvoiceAmountSum, 2) || 0; | |
marketStats.availableDiscountWeightedAvg = availableInvoiceAmountSum | |
? round(availableDiscountWeightedSum / availableInvoiceAmountSum, 2) : 0; | |
marketStats.availableDpeWeightedAvg = availableInvoiceAmountSum | |
? round(availableDpeWeightedSum / availableInvoiceAmountSum, 2) : 0; | |
marketStats.availableAprWeightedAvg = availableInvoiceAmountSum | |
? round(availableAprWeightedSum / availableInvoiceAmountSum, 2) : 0; | |
marketStats.overallInvoiceCount = (marketStats.acceptedInvoiceCount | |
+ marketStats.awardedInvoiceCount + marketStats.pendingInvoiceCount) || 0; | |
marketStats.overallTakerCount = marketStats.acceptedTakerCount | |
+ marketStats.awardedTakerCount + marketStats.pendingTakerCount; | |
marketStats.overallEarn = round(marketStats.acceptedEarn + marketStats.awardedEarn | |
+ marketStats.pendingEarn, 2) || 0; | |
marketStats.overallInvoiceAmount = round(marketStats.cashPoolUsed | |
+ marketStats.awardedInvoiceAmount + marketStats.pendingInvoiceAmount, 2) || 0; | |
marketStats.overallDiscountedAmount = round(marketStats.cashPoolUsed | |
+ marketStats.awardedDiscountedAmount + marketStats.pendingDiscountedAmount, 2) || 0; | |
marketStats.overallAvgDiscount = overallAmountSum | |
? round(overallDiscountSum / overallAmountSum, 2) : 0; | |
marketStats.overallAvgDpe = overallAmountSum ? round(overallDpeSum / overallAmountSum, 2) | |
: 0; | |
marketStats.overallAvgApr = overallAmountSum ? round(overallAprSum / overallAmountSum, 2) | |
: 0; | |
delete marketStats.acceptedDiscountWeightedSum; | |
delete marketStats.acceptedDpeWeightedSum; | |
delete marketStats.acceptedAprWeightedSum; | |
delete marketStats.awardedDiscountWeightedSum; | |
delete marketStats.awardedDpeWeightedSum; | |
delete marketStats.awardedAprWeightedSum; | |
delete marketStats.pendingDiscountWeightedSum; | |
delete marketStats.pendingDpeWeightedSum; | |
delete marketStats.pendingAprWeightedSum; | |
delete marketStats.notAcceptedDiscountWeightedSum; | |
delete marketStats.notAcceptedDpeWeightedSum; | |
delete marketStats.notAcceptedAprWeightedSum; | |
return marketStats; | |
} | |
function _getFilteredCategoryGrouping(category) { | |
const SUPPLIER_CATEGORY_GROUPINGS = { | |
active: [ | |
// awarded invoices regardless of taker config | |
{ | |
awardedInvoiceCount: { gt: 0 }, | |
}, | |
// pending invoices regardless of taker config | |
{ | |
pendingInvoiceCount: { gt: 0 }, | |
}, | |
// bid enabled with eligible ap and participating | |
{ | |
takerConfigId: { isNot: null }, | |
takerConfigIsEnabled: true, | |
eligibleInvoiceAmount: { gt: 0 }, | |
}, | |
// clearing invoices and participating | |
{ | |
takerConfigId: { isNot: null }, | |
takerConfigIsEnabled: true, | |
acceptedInvoiceCount: { gt: 0 }, | |
}, | |
// not clearing invoices and participating | |
{ | |
takerConfigId: { isNot: null }, | |
takerConfigIsEnabled: true, | |
notAcceptedInvoiceCount: { gt: 0 }, | |
}, | |
], | |
inactive: [ | |
// no bid with eligible ap | |
{ | |
takerConfigId: null, | |
eligibleInvoiceAmount: { gt: 0 }, | |
}, | |
// bit not enabled with eligible ap and no pending invoices | |
{ | |
takerConfigId: { isNot: null }, | |
takerConfigIsEnabled: false, | |
eligibleInvoiceAmount: { gt: 0 }, | |
awardedInvoiceCount: { lte: 0 }, | |
pendingInvoiceCount: { lte: 0 }, | |
}, | |
], | |
'no-invoices': [ | |
// no eligible or pending | |
{ | |
eligibleInvoiceAmount: { lte: 0 }, | |
pendingInvoiceCount: { eq: 0 }, | |
}, | |
], | |
all: null, | |
}; | |
return SUPPLIER_CATEGORY_GROUPINGS[category]; | |
} | |
function _getFilteredMarketStats(andGroupedOr, marketId, search, filter, groups) { | |
let ds = _getMarketStatsDs(marketId) | |
.join('makersTakers', { | |
makerId: identifier('takersMarkets__makerId'), | |
takerId: identifier('takersMarkets__takerId'), | |
}) | |
.leftJoin('makersTakersOrganizations', { | |
makerId: identifier('takersMarkets__makerId'), | |
takerId: identifier('takersMarkets__takerId'), | |
}) | |
.naked(); | |
ds = andGroupedOr ? ds.andGroupedOr(andGroupedOr) : ds; | |
ds = utils.applyGroupQueries(ds, groups || []); | |
const datasets = applyDatasetOptions(applyDatasetQuery(ds, filter), search); | |
return datasets.ds.naked().one().chain((marketStats) => { | |
return Market.select('cashPool', 'currency').filter({ id: marketId }).naked().one() | |
.chain((market) => { | |
return _calcOverallMarketStats(merge(marketStats, market)); | |
}); | |
}); | |
} | |
/** | |
* Helper to ensure that when clearOnlyPastDueAdjustments is true that we also set the | |
* adjustment setting preventPartialsFromSplittingPayDate to true as well. | |
* | |
* @param {Market model} market | |
* @param {Object} values - diffed updated values | |
* @private | |
*/ | |
function _ensureAdjustmentClearSettingsAreInSync(market, values) { | |
if (comb.isDefined(values.clearAdjustments) | |
&& !values.clearAdjustments) { | |
values.clearOnlyPastDueAdjustments = false; | |
} | |
if (comb.isDefined(values.clearOnlyPastDueAdjustments) | |
&& values.clearOnlyPastDueAdjustments) { | |
values.adjustmentSettings = Object.assign( | |
market.adjustmentSettings, | |
{ preventPartialsFromSplittingPayDate: true } | |
); | |
} | |
} | |
/** | |
* The private market update handler | |
* | |
* @param {Market model} market | |
* @param {Object} diffedValues - a diffed | |
* @returns {Object} - returns market.toObject() | |
* @private | |
*/ | |
function _updateNonTpfMarket(market, diffedValues) { | |
LOGGER.debug(`updating non-tpf market [marketId = ${market.id}, updatedValues = ${JSON.stringify(diffedValues)}]`); | |
_ensureAdjustmentClearSettingsAreInSync(market, diffedValues); | |
// invalid fields for non-tpf market these are replaced by cashPoolSettings | |
DISALLOWED_NON_TPF_MARKET_FIELDS.forEach((k) => { | |
if (k in diffedValues) { | |
throw new Error(`Found unexpected update value when attempting to update market [value=${k}]`); | |
} | |
}); | |
return market.update(diffedValues) | |
.chain(() => _publishToUpdatePayDateIfChanged(market, diffedValues)) | |
.chain(() => _publishToRunAdjustmentsIfChanged(market, diffedValues)) | |
.chain(() => _updateCashPoolSettings(market, diffedValues)); | |
} | |
function _updateTpfMarket(market, diffedValues) { | |
LOGGER.debug(`updating TPF market [marketId = ${market.id}, updatedValues = ${JSON.stringify(diffedValues)}]`); | |
_ensureAdjustmentClearSettingsAreInSync(market, diffedValues); | |
// invalid fields for non-tpf market these are replaced by cashPoolSettings | |
[ 'returnAmount', 'cashPool', 'minReturnAmount', 'minimumIncomeAmount', 'maxApr', 'maxDiscount', 'calculateSettings' ].forEach((k) => { | |
if (k in diffedValues) { | |
throw new Error(`Found unexpected update value when attempting to update market [value=${k}]`); | |
} | |
}); | |
if (diffedValues.cashPoolSettings) { | |
_.forIn(TPF_CASH_POOL_SETTINGS_MAPPING, (marketSetting, cashPoolSetting) => { | |
if (_.has(diffedValues.cashPoolSettings, cashPoolSetting)) { | |
LOGGER.debug(`Adding ${cashPoolSetting} to diffed values [oldValue=${market[marketSetting]}, newValue=${diffedValues.cashPoolSettings[cashPoolSetting]}]`); | |
diffedValues[marketSetting] = diffedValues.cashPoolSettings[cashPoolSetting]; | |
} | |
}); | |
} | |
return market.update(diffedValues) | |
.chain(() => _publishToUpdatePayDateIfChanged(market, diffedValues)) | |
.chain(() => _publishToRunAdjustmentsIfChanged(market, diffedValues)) | |
.chain(() => tpfCalculateQueue.publish({ marketId: market.id }, { deliveryMode: 2 })); | |
} | |
function _publishToUpdatePayDateIfChanged(market, diffedValues) { | |
if ('minimumDpe' in diffedValues || 'restriction' | |
in diffedValues || 'internalRestriction' in diffedValues) { | |
LOGGER.debug(`publishing updatePayDate message [marketId = ${market.id}]`); | |
return updatePayDateQueue.publish({ marketId: market.id }, { deliveryMode: 2 }); | |
} | |
return comb.resolved(); | |
} | |
function _publishToRunAdjustmentsIfChanged(market, diffedValues) { | |
if ('adjustmentSettings' in diffedValues) { | |
LOGGER.debug(`publishing adjustment message [marketId = ${market.id}`); | |
return adjustInvoicesQueue.publish({ marketId: market.id }, { deliveryMode: 2 }); | |
} | |
return comb.resolved(); | |
} | |
function _updateCashPoolSettings(market, diffedValues) { | |
const cashPoolSettings = diffedValues.cashPoolSettings; | |
if (!comb.isHash(cashPoolSettings)) { | |
return comb.resolved(); | |
} | |
return model.Division | |
.select('organization__uuid') | |
.join('organization', { organization__id: identifier('division__organizationId') }) | |
.filter({ division__id: market.makerId }) | |
.one() | |
.chain((org) => { | |
return exposed.akkaCalc.cashPool.updateCashPoolSettings( | |
org.uuid, | |
market.makerId, | |
market.id, | |
cashPoolSettings | |
); | |
}); | |
} | |
function __addCashPoolSettingsToMarket(markets) { | |
const marketIds = markets.map(m => m.id); | |
return exposed.akkaCalc.cashPool.fetchCashPoolSettingsForMarkets(marketIds).chain((cps) => { | |
const cashPoolSettingsMap = _.indexBy(cps, 'marketUuid'); | |
return markets.map((market) => { | |
if (market.isTpf) { | |
market.cashPoolSettings = {}; | |
_.forIn(TPF_CASH_POOL_SETTINGS_MAPPING, (marketSetting, cashPoolSetting) => { | |
market.cashPoolSettings[cashPoolSetting] = market[marketSetting]; | |
}); | |
} else { | |
market.cashPoolSettings = cashPoolSettingsMap[market.uuid]; | |
} | |
delete market.calculateSettings; | |
delete market.cashPool; | |
delete market.returnAmount; | |
delete market.minReturnAmount; | |
delete market.maxDiscount; | |
delete market.maxApr; | |
delete market.minimumIncomeAmount; | |
return market; | |
}); | |
}); | |
} | |
function _fetchCashPoolSettings(market) { | |
if (!market) { | |
return comb.resolved(null); | |
} | |
if (market.isTpf) { | |
return comb.resolved(market); | |
} | |
return exposed.akkaCalc.cashPool | |
.fetchCashPoolSettings(market.makerOrganizationUuid, market.makerId, market.id) | |
.chain((cps) => { | |
market.cashPoolSettings = cps || {}; | |
return market; | |
}); | |
} | |
/** | |
* Sends users a notification report - or something specific | |
* @return {*} | |
*/ | |
function sendNoticationForPerformanceReport() { | |
return getAllMakersWithUserRoles() | |
.chain((users = []) => { | |
if (!users.length) { | |
LOGGER.info('No users found to send the notifications'); | |
return comb.resolved(); | |
} | |
return comb.async.array(users).forEach((user) => { | |
return getURLIfMarketPresents(user) | |
.chain(fileUrlName => checkIfNotificationToBeSent(user, fileUrlName)) | |
.check((shouldBeSent) => { | |
if (!shouldBeSent) { | |
LOGGER.info('not sending...'); | |
} | |
return notifyUser(user); | |
}); | |
}, 1); // 1 = parallelism factor | |
}, (error) => { | |
LOGGER.error('Report job couldn\'t get completed due to error %s', error); | |
throw error; | |
}); | |
} | |
function getAllMakersWithUserRoles() { | |
LOGGER.info('Cron job to fetch all the users with appropriate role for performance report notification started..................'); | |
return model.Market | |
.select( | |
identifier('organization__uuid').as('orgUuid'), | |
identifier('organization__name').as('orgName'), | |
identifier('division__uuid').as('divisionUuid'), | |
identifier('division__name').as('divisionName'), | |
identifier('user__uuid').as('userUuid'), | |
identifier('user__id').as('userId'), | |
identifier('role__name').as('roleName'), | |
identifier('user__emailAddress').as('email'), | |
identifier('user__locale').as('locale') | |
) | |
.leftJoin('user', { id: identifier('market__makerId') }) | |
.leftJoin('divisionsUsersRoles', { userId: identifier('user__id') }) | |
.leftJoin('role', { id: identifier('divisionsUsersRoles__roleId') }) | |
.leftJoin('division', { id: identifier('divisionsUsersRoles__divisionId') }) | |
.leftJoin('organization', { id: identifier('division__organizationId') }) | |
.filter({ role__name: [ 'download_buyer_org_report', 'download_buyer_div_report' ] }) | |
.distinct(identifier('user__emailAddress')) | |
.naked() | |
.all(); | |
} | |
/** | |
* Comment that pertains to this. Todo: change to makerId, not userId | |
* @param makerId | |
* @return {*} | |
*/ | |
function getMarketForGivenUserId(makerId) { | |
return model.Market | |
.select('uuid') | |
.filter({ makerId }) | |
.naked() | |
.one() | |
.chain(market => market.uuid); | |
} | |
function _getReportName(reportLevel, folderId) { | |
return `${config.eslap.S3Settings.uploadBucket}/buyer_reports/${reportLevel}/${folderId}/`; | |
} | |
function getURLIfMarketPresents(userInfo) { | |
return getMarketForGivenUserId(userInfo.id) | |
.chain((market) => { | |
if (!market.marketId) { | |
// this is a really odd pattern | |
return ''; | |
} | |
const url = `${config.webapp.externalUrl}/buyer/divisions/${userInfo.divisionId}/markets/${market.marketId}/insights/market/`; | |
LOGGER.info(`[downloadURL: ${url}]`); | |
const isBuyerOrgReport = userInfo.roleName === 'download_buyer_org_report'; | |
if (isBuyerOrgReport) { | |
return { | |
url: _getReportName('org', userInfo.orgId), | |
}; | |
} | |
return { | |
url: _getReportName('div', userInfo.divisionId), | |
}; | |
}, (error) => { | |
LOGGER.error('Market ID not present for User %s with role %s due to %s', userInfo.id, userInfo.roleName, error); | |
throw error; | |
}); | |
} | |
/* | |
This function will send a boolean variable, true would mean a new notification needs to be sent and | |
false signifies that notification is already sent to a given user Id for a particular period | |
*/ | |
function checkIfNotificationToBeSent(userInfo, fileUrlName) { | |
return _hasNotifications(userInfo, fileUrlName) | |
.chain((rowCountCol = {}) => { | |
if (rowCountCol.rowCount > 0) { | |
LOGGER.info(`Notification Already sent to [User ID: ${userInfo.userId}]`); | |
return comb.resolved(false); | |
} | |
return _createBuyerNotification(userInfo, fileUrlName); | |
}, (error) => { | |
LOGGER.error('we have an error message with something more descriptive', error.message); | |
return comb.rejected(error); | |
}); | |
} | |
function _hasNotifications(userInfo, fileUrlName) { | |
return BuyerNotifications | |
.select(literal('count(1) as row_count')) | |
.filter( | |
{ | |
userUuid: userInfo.userId, | |
email: userInfo.email, | |
roleName: userInfo.roleName, | |
period: fileUrlName.fileName, | |
} | |
) | |
.one() | |
.chain(({ rowCount }) => rowCount > 0); | |
} | |
function _createBuyerNotification(userInfo, fileUrlName) { | |
return new BuyerNotifications({ | |
userUuid: userInfo.userId, | |
makerUuid: userInfo.divisionId, | |
organizationUuid: userInfo.orgId, | |
email: userInfo.email, | |
period: fileUrlName.fileName, | |
roleName: userInfo.roleName, | |
}).save(); | |
} | |
/** | |
* Comment that pertains | |
*/ | |
function notifyUser(userInfo = {}) { | |
const { roleName } = userInfo; | |
const reportName = roleName === 'download_buyer_org_report' | |
? userInfo.orgName | |
: userInfo.divName; | |
return reportDownloadQueue.publish( | |
Object.assign({}, userInfo, { name: reportName }), | |
{ deliveryMode: 2 } | |
); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment