Skip to content

Instantly share code, notes, and snippets.

@TechnotronicOz
Created March 3, 2020 18:06
Show Gist options
  • Save TechnotronicOz/c0b7d093e3bd6e966516e7624a69aefa to your computer and use it in GitHub Desktop.
Save TechnotronicOz/c0b7d093e3bd6e966516e7624a69aefa to your computer and use it in GitHub Desktop.
facade/lib/maker/market.index.js
'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