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:
Cesar 2020-12-10 18:26:13 +00:00 committed by Josh Harvey
parent 196a05549f
commit 3b3bdf839b
9 changed files with 224 additions and 34 deletions

View file

@ -101,7 +101,7 @@ function renameMachine (rec) {
function resetCashOutBills (rec) { function resetCashOutBills (rec) {
const sql = ` const sql = `
update devices set cassette1=$1, cassette2=$2 where device_id=$3; update devices set cassette1=$1, cassette2=$2 where device_id=$3;
update notifications set read = 't' where device_id = $3 AND type = 'fiatBalance' AND read = 'f'; update notifications set read = 't', valid = 'f' where read = 'f' AND device_id = $3 AND type = 'fiatBalance';
` `
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId]) return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
} }

View file

@ -1,5 +1,5 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const prettyMs = require('pretty-ms') const utils = require("./utils")
const email = require('../email') const email = require('../email')
@ -66,28 +66,24 @@ function emailAlerts(alerts) {
return alerts.map(emailAlert).join('\n') + '\n' return alerts.map(emailAlert).join('\n') + '\n'
} }
function formatCurrency(num, code) {
return numeral(num).format('0,0.00') + ' ' + code
}
function emailAlert(alert) { function emailAlert(alert) {
switch (alert.code) { switch (alert.code) {
case PING: case PING:
if (alert.age) { 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 ${pingAge}`
} }
return 'Machine down for a while.' return 'Machine down for a while.'
case STALE: { 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}` return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
} }
case LOW_CRYPTO_BALANCE: { 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}]` return `Low balance in ${alert.cryptoCode} [${balance}]`
} }
case HIGH_CRYPTO_BALANCE: { case HIGH_CRYPTO_BALANCE: {
const highBalance = formatCurrency( const highBalance = utils.formatCurrency(
alert.fiatBalance.balance, alert.fiatBalance.balance,
alert.fiatCode alert.fiatCode
) )

View file

@ -10,7 +10,7 @@ const customers = require('../customers')
const utils = require('./utils') const utils = require('./utils')
const emailFuncs = require('./email') const emailFuncs = require('./email')
const smsFuncs = require('./sms') const smsFuncs = require('./sms')
const { STALE, STALE_STATE } = require('./codes') const { STALE, STALE_STATE, PING } = require('./codes')
function buildMessage(alerts, notifications) { function buildMessage(alerts, notifications) {
const smsEnabled = utils.isActive(notifications.sms) const smsEnabled = utils.isActive(notifications.sms)
@ -43,6 +43,7 @@ function checkNotification(plugins) {
return getAlerts(plugins) return getAlerts(plugins)
.then(alerts => { .then(alerts => {
errorAlertsNotify(alerts)
const currentAlertFingerprint = utils.buildAlertFingerprint( const currentAlertFingerprint = utils.buildAlertFingerprint(
alerts, alerts,
notifications notifications
@ -75,9 +76,10 @@ function getAlerts(plugins) {
plugins.checkBalances(), plugins.checkBalances(),
queries.machineEvents(), queries.machineEvents(),
plugins.getMachineNames() plugins.getMachineNames()
]).then(([balances, events, devices]) => ]).then(([balances, events, devices]) => {
buildAlerts(checkPings(devices), balances, events, devices) balancesNotify(balances)
) return buildAlerts(checkPings(devices), balances, events, devices)
})
} }
function buildAlerts(pings, balances, events, devices) { function buildAlerts(pings, balances, events, devices) {
@ -220,7 +222,7 @@ const cashCassettesNotify = (cassettes, deviceId) => {
if(cashOutEnabled) { if(cashOutEnabled) {
// we only want to add this notification if there isn't one already set and unread in the database // 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) { if(res[0].length === 0 && cassette1Count < cassette1Threshold) {
console.log("Adding fiatBalance alert for cashbox 1 in database - count & threshold: ", cassette1Count, cassette1Threshold ) console.log("Adding fiatBalance alert for cashbox 1 in database - count & threshold: ", cassette1Count, cassette1Threshold )
queries.addCashCassetteWarning(1, deviceId) 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 = { module.exports = {
transactionNotify, transactionNotify,
checkNotification, checkNotification,

View file

@ -24,9 +24,50 @@ const addCashCassetteWarning = (cassetteNumber, deviceId) => {
return db.oneOrNone(sql, [uuidv4(), 'fiatBalance', cassetteNumber, deviceId, message]) return db.oneOrNone(sql, [uuidv4(), 'fiatBalance', cassetteNumber, deviceId, message])
} }
const getUnreadCassetteNotifications = (cassetteNumber) => { const getUnreadCassetteNotifications = (cassetteNumber, deviceId) => {
const sql = `SELECT * FROM notifications WHERE read = 'f' AND TYPE = 'fiatBalance' AND detail = '$1'` const sql = `SELECT * FROM notifications WHERE read = 'f' AND device_id = $1 AND TYPE = 'fiatBalance' AND detail = '$2'`
return db.any(sql, [cassetteNumber]) 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,
}

View file

@ -27,20 +27,31 @@ function printSmsAlerts(alertRec, config) {
const code = entry[0] const code = entry[0]
const machineNames = _.filter( const machineNames = _.filter(
_.negate(_.isEmpty), _.negate(_.isEmpty),
_.map('machineName', entry[1]) _.map('machineName', entry[1]),
)
const cryptoCodes = _.filter(
_.negate(_.isEmpty),
_.map('cryptoCode', entry[1]),
) )
return { return {
codeDisplay: utils.codeDisplay(code), codeDisplay: utils.codeDisplay(code),
machineNames machineNames,
cryptoCodes
} }
}, _.toPairs(alertsMap)) }, _.toPairs(alertsMap))
const mapByCodeDisplay = _.map(it => const mapByCodeDisplay = _.map(it => {
_.isEmpty(it.machineNames) if(_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) {
? it.codeDisplay return it.codeDisplay
: `${it.codeDisplay} (${it.machineNames.join(', ')})` }
) if(_.isEmpty(it.machineNames)) {
return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
}
else return `${it.codeDisplay} (${it.machineNames.join(', ')})`
})
const displayAlertTypes = _.compose( const displayAlertTypes = _.compose(
_.uniq, _.uniq,

View file

@ -1,5 +1,7 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const crypto = require('crypto') const crypto = require('crypto')
const numeral = require('numeral')
const prettyMs = require('pretty-ms')
const coinUtils = require('../coin-utils') const coinUtils = require('../coin-utils')
const { const {
@ -143,6 +145,14 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
}, highValueTx] }, highValueTx]
} }
function formatCurrency(num, code) {
return numeral(num).format('0,0.00') + ' ' + code
}
function formatAge (age, settings) {
return prettyMs(age, settings)
}
module.exports = { module.exports = {
codeDisplay, codeDisplay,
parseEventNote, parseEventNote,
@ -155,5 +165,7 @@ module.exports = {
shouldNotAlert, shouldNotAlert,
buildAlertFingerprint, buildAlertFingerprint,
sendNoAlerts, sendNoAlerts,
buildTransactionMessage buildTransactionMessage,
formatCurrency,
formatAge
} }

View file

@ -1,7 +1,7 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const plugins = require('./plugins') const plugins = require('./plugins')
const notifier = require('./notifier') const notifier = require('./notifier/index')
const T = require('./time') const T = require('./time')
const logger = require('./logger') const logger = require('./logger')
const cashOutTx = require('./cash-out/cash-out-tx') const cashOutTx = require('./cash-out/cash-out-tx')

View file

@ -22,10 +22,11 @@ exports.up = function (next) {
"id" uuid NOT NULL PRIMARY KEY, "id" uuid NOT NULL PRIMARY KEY,
"type" notification_type NOT NULL, "type" notification_type NOT NULL,
"detail" TEXT, "detail" TEXT,
"device_id" TEXT NOT NULL, "device_id" TEXT,
"message" TEXT NOT NULL, "message" TEXT NOT NULL,
"created" time with time zone NOT NULL, "created" TIMESTAMP WITH TIME ZONE NOT NULL,
"read" BOOLEAN NOT NULL DEFAULT 'false', "read" BOOLEAN NOT NULL DEFAULT 'false',
"valid" BOOLEAN NOT NULL DEFAULT 'true',
CONSTRAINT fk_devices CONSTRAINT fk_devices
FOREIGN KEY(device_id) FOREIGN KEY(device_id)
REFERENCES devices(device_id) REFERENCES devices(device_id)

View file

@ -9,8 +9,8 @@ import { transformNumber } from 'src/utils/number'
import NotificationsCtx from '../NotificationsContext' import NotificationsCtx from '../NotificationsContext'
const HIGH_BALANCE_KEY = 'highBalance' const HIGH_BALANCE_KEY = 'cryptoHighBalance'
const LOW_BALANCE_KEY = 'lowBalance' const LOW_BALANCE_KEY = 'cryptoLowBalance'
const CRYPTOCURRENCY_KEY = 'cryptoCurrency' const CRYPTOCURRENCY_KEY = 'cryptoCurrency'
const NAME = 'cryptoBalanceOverrides' const NAME = 'cryptoBalanceOverrides'