From 3b3bdf839b3f5f3728028837e7a29e91f6865bae Mon Sep 17 00:00:00 2001 From: Cesar <26280794+csrapr@users.noreply.github.com> Date: Thu, 10 Dec 2020 18:26:13 +0000 Subject: [PATCH] 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 --- lib/machine-loader.js | 2 +- lib/notifier/email.js | 14 +- lib/notifier/index.js | 139 +++++++++++++++++- lib/notifier/queries.js | 53 ++++++- lib/notifier/sms.js | 25 +++- lib/notifier/utils.js | 14 +- lib/poller.js | 2 +- ...607009558538-create-notifications-table.js | 5 +- .../sections/CryptoBalanceOverrides.js | 4 +- 9 files changed, 224 insertions(+), 34 deletions(-) diff --git a/lib/machine-loader.js b/lib/machine-loader.js index 0ccdf277..62351f06 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -101,7 +101,7 @@ function renameMachine (rec) { function resetCashOutBills (rec) { const sql = ` 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]) } diff --git a/lib/notifier/email.js b/lib/notifier/email.js index e7f7417c..7790127b 100644 --- a/lib/notifier/email.js +++ b/lib/notifier/email.js @@ -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 ) diff --git a/lib/notifier/index.js b/lib/notifier/index.js index 751b9e7a..6c5a1099 100644 --- a/lib/notifier/index.js +++ b/lib/notifier/index.js @@ -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, diff --git a/lib/notifier/queries.js b/lib/notifier/queries.js index f9f0ea50..2e556451 100644 --- a/lib/notifier/queries.js +++ b/lib/notifier/queries.js @@ -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, +} diff --git a/lib/notifier/sms.js b/lib/notifier/sms.js index f4e041af..5c1e440b 100644 --- a/lib/notifier/sms.js +++ b/lib/notifier/sms.js @@ -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, diff --git a/lib/notifier/utils.js b/lib/notifier/utils.js index ca92a4aa..3f2fc866 100644 --- a/lib/notifier/utils.js +++ b/lib/notifier/utils.js @@ -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 } diff --git a/lib/poller.js b/lib/poller.js index e03bd689..7c90871b 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -1,7 +1,7 @@ const _ = require('lodash/fp') const plugins = require('./plugins') -const notifier = require('./notifier') +const notifier = require('./notifier/index') const T = require('./time') const logger = require('./logger') const cashOutTx = require('./cash-out/cash-out-tx') diff --git a/migrations/1607009558538-create-notifications-table.js b/migrations/1607009558538-create-notifications-table.js index 1435119e..fda66b84 100644 --- a/migrations/1607009558538-create-notifications-table.js +++ b/migrations/1607009558538-create-notifications-table.js @@ -22,10 +22,11 @@ exports.up = function (next) { "id" uuid NOT NULL PRIMARY KEY, "type" notification_type NOT NULL, "detail" TEXT, - "device_id" TEXT NOT NULL, + "device_id" TEXT, "message" TEXT NOT NULL, - "created" time with time zone NOT NULL, + "created" TIMESTAMP WITH TIME ZONE NOT NULL, "read" BOOLEAN NOT NULL DEFAULT 'false', + "valid" BOOLEAN NOT NULL DEFAULT 'true', CONSTRAINT fk_devices FOREIGN KEY(device_id) REFERENCES devices(device_id) diff --git a/new-lamassu-admin/src/pages/Notifications/sections/CryptoBalanceOverrides.js b/new-lamassu-admin/src/pages/Notifications/sections/CryptoBalanceOverrides.js index e8673efc..fe497913 100644 --- a/new-lamassu-admin/src/pages/Notifications/sections/CryptoBalanceOverrides.js +++ b/new-lamassu-admin/src/pages/Notifications/sections/CryptoBalanceOverrides.js @@ -9,8 +9,8 @@ import { transformNumber } from 'src/utils/number' import NotificationsCtx from '../NotificationsContext' -const HIGH_BALANCE_KEY = 'highBalance' -const LOW_BALANCE_KEY = 'lowBalance' +const HIGH_BALANCE_KEY = 'cryptoHighBalance' +const LOW_BALANCE_KEY = 'cryptoLowBalance' const CRYPTOCURRENCY_KEY = 'cryptoCurrency' const NAME = 'cryptoBalanceOverrides'