Feat: crypto balance notifications saving in DB
Chore: add new column "detail" to transactions table migration Feat: check if older notification is valid before sending new one Feat: error saving to database Fix: fix error when invalidating notification on clearCryptoBalanceNotifications Chre: code refactor in new-settings-loader for simplicity Chore: refactor code on notifier and merge similar functions
This commit is contained in:
parent
196a05549f
commit
3b3bdf839b
9 changed files with 224 additions and 34 deletions
|
|
@ -1,5 +1,5 @@
|
|||
const _ = require('lodash/fp')
|
||||
const prettyMs = require('pretty-ms')
|
||||
const utils = require("./utils")
|
||||
|
||||
const email = require('../email')
|
||||
|
||||
|
|
@ -66,28 +66,24 @@ function emailAlerts(alerts) {
|
|||
return alerts.map(emailAlert).join('\n') + '\n'
|
||||
}
|
||||
|
||||
function formatCurrency(num, code) {
|
||||
return numeral(num).format('0,0.00') + ' ' + code
|
||||
}
|
||||
|
||||
function emailAlert(alert) {
|
||||
switch (alert.code) {
|
||||
case PING:
|
||||
if (alert.age) {
|
||||
const pingAge = prettyMs(alert.age, { compact: true, verbose: true })
|
||||
const pingAge = utils.formatAge(alert.age, { compact: true, verbose: true })
|
||||
return `Machine down for ${pingAge}`
|
||||
}
|
||||
return 'Machine down for a while.'
|
||||
case STALE: {
|
||||
const stuckAge = prettyMs(alert.age, { compact: true, verbose: true })
|
||||
const stuckAge = utils.formatAge(alert.age, { compact: true, verbose: true })
|
||||
return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
|
||||
}
|
||||
case LOW_CRYPTO_BALANCE: {
|
||||
const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
|
||||
const balance = utils.formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
|
||||
return `Low balance in ${alert.cryptoCode} [${balance}]`
|
||||
}
|
||||
case HIGH_CRYPTO_BALANCE: {
|
||||
const highBalance = formatCurrency(
|
||||
const highBalance = utils.formatCurrency(
|
||||
alert.fiatBalance.balance,
|
||||
alert.fiatCode
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const customers = require('../customers')
|
|||
const utils = require('./utils')
|
||||
const emailFuncs = require('./email')
|
||||
const smsFuncs = require('./sms')
|
||||
const { STALE, STALE_STATE } = require('./codes')
|
||||
const { STALE, STALE_STATE, PING } = require('./codes')
|
||||
|
||||
function buildMessage(alerts, notifications) {
|
||||
const smsEnabled = utils.isActive(notifications.sms)
|
||||
|
|
@ -43,6 +43,7 @@ function checkNotification(plugins) {
|
|||
|
||||
return getAlerts(plugins)
|
||||
.then(alerts => {
|
||||
errorAlertsNotify(alerts)
|
||||
const currentAlertFingerprint = utils.buildAlertFingerprint(
|
||||
alerts,
|
||||
notifications
|
||||
|
|
@ -75,9 +76,10 @@ function getAlerts(plugins) {
|
|||
plugins.checkBalances(),
|
||||
queries.machineEvents(),
|
||||
plugins.getMachineNames()
|
||||
]).then(([balances, events, devices]) =>
|
||||
buildAlerts(checkPings(devices), balances, events, devices)
|
||||
)
|
||||
]).then(([balances, events, devices]) => {
|
||||
balancesNotify(balances)
|
||||
return buildAlerts(checkPings(devices), balances, events, devices)
|
||||
})
|
||||
}
|
||||
|
||||
function buildAlerts(pings, balances, events, devices) {
|
||||
|
|
@ -220,7 +222,7 @@ const cashCassettesNotify = (cassettes, deviceId) => {
|
|||
|
||||
if(cashOutEnabled) {
|
||||
// we only want to add this notification if there isn't one already set and unread in the database
|
||||
Promise.all([queries.getUnreadCassetteNotifications(1), queries.getUnreadCassetteNotifications(2)]).then(res => {
|
||||
Promise.all([queries.getUnreadCassetteNotifications(1, deviceId), queries.getUnreadCassetteNotifications(2, deviceId)]).then(res => {
|
||||
if(res[0].length === 0 && cassette1Count < cassette1Threshold) {
|
||||
console.log("Adding fiatBalance alert for cashbox 1 in database - count & threshold: ", cassette1Count, cassette1Threshold )
|
||||
queries.addCashCassetteWarning(1, deviceId)
|
||||
|
|
@ -234,6 +236,133 @@ const cashCassettesNotify = (cassettes, deviceId) => {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Notes for new "valid" column on notifications table:
|
||||
|
||||
- We only want to add a new notification if it is present in the high or low warning consts.
|
||||
- Before we add the notification we need to see if there is no "valid" notification in the database.
|
||||
- Since the poller runs every few seconds, if the user marks it as read, this code would add a new notification
|
||||
immediately. This new column helps us decide if a new notification should be added.
|
||||
- "Valid" is defaulted to "true". When the cryptobalance goes over the low threshold or under the high threshold,
|
||||
the notification will be marked as invalid. This will allow a new one to be sent. If the cryptobalance never goes
|
||||
into the middle of the high and low thresholds, the old, "read" notification will still be relevant so we won't add
|
||||
a new one.
|
||||
*/
|
||||
|
||||
const clearOldCryptoNotifications = (balances) => {
|
||||
// get valid crypto notifications from DB
|
||||
// if that notification doesn't exist in balances, then make it invalid on the DB
|
||||
queries.getAllValidNotifications('cryptoBalance').then(res => {
|
||||
const notifications = _.map(it => {
|
||||
return {
|
||||
cryptoCode: it.detail.split('_')[0],
|
||||
code: it.detail.split('_').splice(1).join('_')
|
||||
}
|
||||
}, res)
|
||||
_.forEach(notification => {
|
||||
const idx = _.findIndex(balance => {
|
||||
return balance.code === notification.code && balance.cryptoCode === notification.cryptoCode
|
||||
}, balances)
|
||||
|
||||
if(idx !== -1) {
|
||||
return
|
||||
}
|
||||
// if the notification doesn't exist in the new balances object, then it is outdated and is not valid anymore
|
||||
queries.invalidateNotification(notification.id)
|
||||
}, notifications)
|
||||
})
|
||||
}
|
||||
|
||||
const balancesNotify = (balances) => {
|
||||
const highFilter = o => o.code === 'HIGH_CRYPTO_BALANCE'
|
||||
const lowFilter = o => o.code === 'LOW_CRYPTO_BALANCE'
|
||||
const highWarnings = _.filter(highFilter, balances)
|
||||
const lowWarnings = _.filter(lowFilter, balances)
|
||||
|
||||
clearOldCryptoNotifications(balances)
|
||||
|
||||
highWarnings.forEach(warning => {
|
||||
queries.getValidNotifications('cryptoBalance', `${warning.cryptoCode}_${warning.code}`).then(res => {
|
||||
if (res.length > 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
console.log("Adding high balance alert for " + warning.cryptoCode + " - " + warning.fiatBalance.balance)
|
||||
const balance = utils.formatCurrency(warning.fiatBalance.balance, warning.fiatCode)
|
||||
queries.addCryptoBalanceWarning(`${warning.cryptoCode}_${warning.code}`, `High balance in ${warning.cryptoCode} [${balance}]`)
|
||||
})
|
||||
})
|
||||
lowWarnings.forEach(warning => {
|
||||
queries.getValidNotifications('cryptoBalance', `${warning.cryptoCode}_${warning.code}`).then(res => {
|
||||
if (res.length > 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
console.log("Adding low balance alert for " + warning.cryptoCode + " - " + warning.fiatBalance.balance)
|
||||
const balance = utils.formatCurrency(warning.fiatBalance.balance, warning.fiatCode)
|
||||
queries.addCryptoBalanceWarning(`${warning.cryptoCode}_${warning.code}`, `Low balance in ${warning.cryptoCode} [${balance}]`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clearOldErrorNotifications = (alerts) => {
|
||||
queries.getAllValidNotifications('error').then(res => {
|
||||
_.forEach(notification => {
|
||||
const idx = _.findIndex(alert => {
|
||||
return alert.code === notification.detail.split('_')[0] && alert.deviceId === notification.device_id
|
||||
}, alerts)
|
||||
if(idx !== -1) {
|
||||
return
|
||||
}
|
||||
// if the notification doesn't exist, then it is outdated and is not valid anymore
|
||||
queries.invalidateNotification(notification.id)
|
||||
}, res)
|
||||
})
|
||||
}
|
||||
|
||||
const errorAlertsNotify = (alertRec) => {
|
||||
let alerts = []
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
// embed device ID in each alert object inside the deviceAlerts array
|
||||
alertRec.devices[device].deviceAlerts = _.map(alert => {
|
||||
return {...alert, deviceId: device}
|
||||
}, alertRec.devices[device].deviceAlerts)
|
||||
// concat every array into one
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
})
|
||||
|
||||
// now that we have all the alerts, we want to add PING and STALE alerts to the DB
|
||||
// if there is a valid alert on the DB that doesn't exist on the new alerts array,
|
||||
// that alert should be considered invalid
|
||||
// after that, for the alerts array, we have to see if there is a valid alert of
|
||||
// the sorts already on the DB
|
||||
clearOldErrorNotifications(alerts)
|
||||
|
||||
_.forEach(alert => {
|
||||
switch(alert.code) {
|
||||
case PING:
|
||||
return queries.getValidNotifications('error', PING, alert.deviceId).then(res => {
|
||||
if(res.length > 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
console.log("Adding PING alert on database for " + alert.machineName)
|
||||
const message = `Machine down`
|
||||
queries.addErrorNotification(`${PING}_${alert.age ? alert.age : '-1'}`, message, alert.deviceId)
|
||||
})
|
||||
case STALE:
|
||||
return queries.getValidNotifications('error', STALE, alert.deviceId).then(res => {
|
||||
if(res.length > 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
console.log("Adding STALE alert on database for " + alert.machineName)
|
||||
const message = `Machine is stuck on ${alert.state} screen`
|
||||
queries.addErrorNotification(STALE, message, alert.deviceId)
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, alerts)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
transactionNotify,
|
||||
checkNotification,
|
||||
|
|
|
|||
|
|
@ -12,21 +12,62 @@ error - notifications related to errors
|
|||
*/
|
||||
|
||||
const addHighValueTx = (tx) => {
|
||||
const sql = `INSERT INTO notifications (id, type, device_id, message, created) values($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
const sql = `INSERT INTO notifications (id, type, device_id, message, created) values ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
const direction = tx.direction === "cashOut" ? 'cash-out' : 'cash-in'
|
||||
const message = `${tx.fiat} ${tx.fiatCode} ${direction} transaction`
|
||||
return db.oneOrNone(sql, [uuidv4(), 'highValueTransaction', tx.deviceId, message])
|
||||
}
|
||||
|
||||
const addCashCassetteWarning = (cassetteNumber, deviceId) => {
|
||||
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)`
|
||||
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)`
|
||||
const message = `Cash-out cassette ${cassetteNumber} almost empty!`
|
||||
return db.oneOrNone(sql, [uuidv4(), 'fiatBalance', cassetteNumber, deviceId, message])
|
||||
}
|
||||
|
||||
const getUnreadCassetteNotifications = (cassetteNumber) => {
|
||||
const sql = `SELECT * FROM notifications WHERE read = 'f' AND TYPE = 'fiatBalance' AND detail = '$1'`
|
||||
return db.any(sql, [cassetteNumber])
|
||||
const getUnreadCassetteNotifications = (cassetteNumber, deviceId) => {
|
||||
const sql = `SELECT * FROM notifications WHERE read = 'f' AND device_id = $1 AND TYPE = 'fiatBalance' AND detail = '$2'`
|
||||
return db.any(sql, [deviceId, cassetteNumber])
|
||||
}
|
||||
|
||||
module.exports = { machineEvents: dbm.machineEvents, addHighValueTx, addCashCassetteWarning, getUnreadCassetteNotifications }
|
||||
const addCryptoBalanceWarning = (detail, message) => {
|
||||
const sql = `INSERT INTO notifications (id, type, detail, message, created) values ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
return db.oneOrNone(sql, [uuidv4(), 'cryptoBalance', detail, message])
|
||||
}
|
||||
|
||||
const getAllValidNotifications = (type) => {
|
||||
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'`
|
||||
return db.any(sql, [type])
|
||||
}
|
||||
|
||||
const addErrorNotification = (detail, message, deviceId) => {
|
||||
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)`
|
||||
return db.oneOrNone(sql, [uuidv4(), 'error', detail, deviceId, message])
|
||||
}
|
||||
|
||||
const getValidNotifications = (type, detail, deviceId = null) => {
|
||||
let sql;
|
||||
if(!deviceId) {
|
||||
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2`
|
||||
}
|
||||
else {
|
||||
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2 AND device_id = $3`
|
||||
}
|
||||
return db.any(sql, [type, `%${detail}%`, deviceId])
|
||||
}
|
||||
|
||||
const invalidateNotification = (id) => {
|
||||
const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE id = $1`
|
||||
return db.none(sql, [id])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
machineEvents: dbm.machineEvents,
|
||||
addHighValueTx,
|
||||
addCashCassetteWarning,
|
||||
addCryptoBalanceWarning,
|
||||
addErrorNotification,
|
||||
getUnreadCassetteNotifications,
|
||||
getAllValidNotifications,
|
||||
getValidNotifications,
|
||||
invalidateNotification,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,20 +27,31 @@ function printSmsAlerts(alertRec, config) {
|
|||
const code = entry[0]
|
||||
const machineNames = _.filter(
|
||||
_.negate(_.isEmpty),
|
||||
_.map('machineName', entry[1])
|
||||
_.map('machineName', entry[1]),
|
||||
)
|
||||
|
||||
const cryptoCodes = _.filter(
|
||||
_.negate(_.isEmpty),
|
||||
_.map('cryptoCode', entry[1]),
|
||||
)
|
||||
|
||||
return {
|
||||
codeDisplay: utils.codeDisplay(code),
|
||||
machineNames
|
||||
machineNames,
|
||||
cryptoCodes
|
||||
}
|
||||
}, _.toPairs(alertsMap))
|
||||
|
||||
const mapByCodeDisplay = _.map(it =>
|
||||
_.isEmpty(it.machineNames)
|
||||
? it.codeDisplay
|
||||
: `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
||||
)
|
||||
const mapByCodeDisplay = _.map(it => {
|
||||
if(_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) {
|
||||
return it.codeDisplay
|
||||
}
|
||||
if(_.isEmpty(it.machineNames)) {
|
||||
return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
|
||||
}
|
||||
else return `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
||||
})
|
||||
|
||||
|
||||
const displayAlertTypes = _.compose(
|
||||
_.uniq,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
const _ = require('lodash/fp')
|
||||
const crypto = require('crypto')
|
||||
const numeral = require('numeral')
|
||||
const prettyMs = require('pretty-ms')
|
||||
|
||||
const coinUtils = require('../coin-utils')
|
||||
const {
|
||||
|
|
@ -143,6 +145,14 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
|||
}, highValueTx]
|
||||
}
|
||||
|
||||
function formatCurrency(num, code) {
|
||||
return numeral(num).format('0,0.00') + ' ' + code
|
||||
}
|
||||
|
||||
function formatAge (age, settings) {
|
||||
return prettyMs(age, settings)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
codeDisplay,
|
||||
parseEventNote,
|
||||
|
|
@ -155,5 +165,7 @@ module.exports = {
|
|||
shouldNotAlert,
|
||||
buildAlertFingerprint,
|
||||
sendNoAlerts,
|
||||
buildTransactionMessage
|
||||
buildTransactionMessage,
|
||||
formatCurrency,
|
||||
formatAge
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue