From 34f2b84fe2619d4c59397f34b5dd8104ed48919b Mon Sep 17 00:00:00 2001 From: Cesar <26280794+csrapr@users.noreply.github.com> Date: Wed, 20 Jan 2021 12:22:02 +0000 Subject: [PATCH] Feat: add notification center row in notification settings table --- lib/machine-loader.js | 7 +- lib/notifier/codes.js | 1 + lib/notifier/index.js | 52 +++++++++--- lib/notifier/queries.js | 81 ++++++++++--------- lib/routes.js | 4 +- ...607009558538-create-notifications-table.js | 1 + .../NotificationCenter/NotificationRow.js | 31 +++---- .../src/pages/Notifications/sections/Setup.js | 1 + 8 files changed, 109 insertions(+), 69 deletions(-) diff --git a/lib/machine-loader.js b/lib/machine-loader.js index 0ba4a610..3452440e 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -3,7 +3,8 @@ const axios = require('axios') const db = require('./db') const pairing = require('./pairing') -const notifier = require('./notifier') +const checkPings = require('./notifier').checkPings +const checkStuckScreen = require('./notifier').checkStuckScreen const dbm = require('./postgresql_interface') const configManager = require('./new-config-manager') const settingsLoader = require('./new-settings-loader') @@ -41,7 +42,7 @@ function getMachineNames (config) { return Promise.all([getMachines(), getConfig(config)]) .then(([machines, config]) => Promise.all( - [machines, notifier.checkPings(machines), dbm.machineEvents(), config] + [machines, checkPings(machines), dbm.machineEvents(), config] )) .then(([machines, pings, events, config]) => { const getStatus = (ping, stuck) => { @@ -60,7 +61,7 @@ function getMachineNames (config) { const statuses = [ getStatus( _.first(pings[r.deviceId]), - _.first(notifier.checkStuckScreen(events, r.name)) + _.first(checkStuckScreen(events, r.name)) ) ] diff --git a/lib/notifier/codes.js b/lib/notifier/codes.js index 8e8a7cc4..a278ecee 100644 --- a/lib/notifier/codes.js +++ b/lib/notifier/codes.js @@ -22,6 +22,7 @@ const ALERT_SEND_INTERVAL = T.hour const NOTIFICATION_TYPES = { HIGH_VALUE_TX: 'highValueTransaction', + NORMAL_VALUE_TX: 'transaction', FIAT_BALANCE: 'fiatBalance', CRYPTO_BALANCE: 'cryptoBalance', COMPLIANCE: 'compliance', diff --git a/lib/notifier/index.js b/lib/notifier/index.js index 0466b4dd..66491632 100644 --- a/lib/notifier/index.js +++ b/lib/notifier/index.js @@ -2,7 +2,6 @@ const _ = require('lodash/fp') const configManager = require('../new-config-manager') const logger = require('../logger') -const machineLoader = require('../machine-loader') const queries = require('./queries') const settingsLoader = require('../new-settings-loader') const customers = require('../customers') @@ -10,14 +9,17 @@ const customers = require('../customers') const utils = require('./utils') const emailFuncs = require('./email') const smsFuncs = require('./sms') +const codes = require('./codes') const { STALE, STALE_STATE, PING } = require('./codes') + const { NOTIFICATION_TYPES: { HIGH_VALUE_TX, + NORMAL_VALUE_TX, FIAT_BALANCE, CRYPTO_BALANCE, COMPLIANCE, ERROR } -} = require('./codes') +} = codes function buildMessage (alerts, notifications) { const smsEnabled = utils.isActive(notifications.sms) @@ -50,7 +52,7 @@ function checkNotification (plugins) { return getAlerts(plugins) .then(alerts => { - errorAlertsNotify(alerts) + notifyIfActive('errors', alerts).catch(console.error) const currentAlertFingerprint = utils.buildAlertFingerprint( alerts, notifications @@ -81,7 +83,7 @@ function getAlerts (plugins) { queries.machineEvents(), plugins.getMachineNames() ]).then(([balances, events, devices]) => { - balancesNotify(balances) + notifyIfActive('balance', balances).catch(console.error) return buildAlerts(checkPings(devices), balances, events, devices) }) } @@ -136,17 +138,24 @@ function checkStuckScreen (deviceEvents, machineName) { return [] } +function notifCenterTransactionNotify (isHighValue, direction, fiat, fiatCode, deviceId, cryptoAddress) { + const messageSuffix = isHighValue ? 'High value' : '' + const message = `${messageSuffix} ${fiat} ${fiatCode} ${direction} transaction` + const detailB = utils.buildDetail({ deviceId: deviceId, direction, fiat, fiatCode, cryptoAddress }) + return queries.addNotification(isHighValue ? HIGH_VALUE_TX : NORMAL_VALUE_TX, message, detailB) +} + function transactionNotify (tx, rec) { return settingsLoader.loadLatest().then(settings => { const notifSettings = configManager.getGlobalNotifications(settings.config) const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity) const isCashOut = tx.direction === 'cashOut' - // high value tx on database - if (highValueTx && (tx.direction === 'cashIn' || (tx.direction === 'cashOut' && rec.isRedemption))) { - const direction = tx.direction === 'cashOut' ? 'cash-out' : 'cash-in' - const message = `${tx.fiat} ${tx.fiatCode} ${direction} transaction` - const detailB = utils.buildDetail({ deviceId: tx.deviceId, direction, fiat: tx.fiat, fiatCode: tx.fiatCode, cryptoAddress: tx.toAddress }) - queries.addNotification(HIGH_VALUE_TX, message, detailB) + + // for notification center + const directionDisplay = tx.direction === 'cashOut' ? 'cash-out' : 'cash-in' + const readyToNotify = tx.direction === 'cashIn' || (tx.direction === 'cashOut' && rec.isRedemption) + if (readyToNotify) { + notifyIfActive('transactions', highValueTx, directionDisplay, tx.fiat, tx.fiatCode, tx.deviceId, tx.toAddress).catch(console.error) } // alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled @@ -161,7 +170,7 @@ function transactionNotify (tx, rec) { if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error) return Promise.all([ - machineLoader.getMachineName(tx.deviceId), + queries.getMachineName(tx.deviceId), customerPromise ]).then(([machineName, customer]) => { return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer) @@ -356,6 +365,23 @@ const customerComplianceNotify = (customer, deviceId, code, days = null) => { .catch(console.error) } +const notificationCenterFunctions = { + 'compliance': customerComplianceNotify, + 'balance': balancesNotify, + 'errors': errorAlertsNotify, + 'transactions': notifCenterTransactionNotify +} + +// for notification center, check if type of notification is active before calling the respective notify function +const notifyIfActive = (type, ...args) => { + return settingsLoader.loadLatest().then(settings => { + const notificationSettings = configManager.getGlobalNotifications(settings.config).notificationCenter + if (!notificationCenterFunctions[type]) return Promise.reject(new Error(`Notification of type ${type} does not exist`)) + if (!(notificationSettings.active && notificationSettings[type])) return Promise.resolve() + return notificationCenterFunctions[type](...args) + }) +} + module.exports = { transactionNotify, checkNotification, @@ -363,6 +389,6 @@ module.exports = { checkStuckScreen, sendRedemptionMessage, blacklistNotify, - customerComplianceNotify, - clearBlacklistNotification + clearBlacklistNotification, + notifyIfActive } diff --git a/lib/notifier/queries.js b/lib/notifier/queries.js index 39f1bd1c..c6ae67fb 100644 --- a/lib/notifier/queries.js +++ b/lib/notifier/queries.js @@ -14,75 +14,82 @@ compliance - notifications related to warnings triggered by compliance settings error - notifications related to errors */ +function getMachineName (machineId) { + const sql = 'SELECT * FROM devices WHERE device_id=$1' + return db.oneOrNone(sql, [machineId]) + .then(it => it.name) +} + const addNotification = (type, message, detail) => { - const sql = `INSERT INTO notifications (id, type, message, detail) values ($1, $2, $3, $4)` - return db.oneOrNone(sql, [uuidv4(), type, message, detail]) + const sql = `INSERT INTO notifications (id, type, message, detail) values ($1, $2, $3, $4)` + return db.oneOrNone(sql, [uuidv4(), type, message, detail]) } const getAllValidNotifications = (type) => { - const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'` - return db.any(sql, [type]) + const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'` + return db.any(sql, [type]) } const invalidateNotification = (detail, type) => { - detail = _.omitBy(_.isEmpty, detail) - const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND type = $1 AND detail::jsonb @> $2::jsonb` - return db.none(sql, [type, detail]) + detail = _.omitBy(_.isEmpty, detail) + const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND type = $1 AND detail::jsonb @> $2::jsonb` + return db.none(sql, [type, detail]) } const batchInvalidate = (ids) => { - const formattedIds = _.map(pgp.as.text, ids).join(',') - const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE id IN ($1^)` - return db.none(sql, [formattedIds]) + const formattedIds = _.map(pgp.as.text, ids).join(',') + const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE id IN ($1^)` + return db.none(sql, [formattedIds]) } const clearBlacklistNotification = (cryptoCode, cryptoAddress) => { - const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE type = 'compliance' AND detail->>'cryptoCode' = $1 AND detail->>'cryptoAddress' = $2 AND (detail->>'code' = 'BLOCKED' OR detail->>'code' = 'REUSED')` - return db.none(sql, [cryptoCode, cryptoAddress]) + const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE type = 'compliance' AND detail->>'cryptoCode' = $1 AND detail->>'cryptoAddress' = $2 AND (detail->>'code' = 'BLOCKED' OR detail->>'code' = 'REUSED')` + return db.none(sql, [cryptoCode, cryptoAddress]) } const getValidNotifications = (type, detail) => { - const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail @> $2` - return db.any(sql, [type, detail]) + const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail @> $2` + return db.any(sql, [type, detail]) } const getNotifications = () => { - const sql = `SELECT * FROM notifications ORDER BY created DESC` - return db.any(sql) + const sql = `SELECT * FROM notifications ORDER BY created DESC` + return db.any(sql) } const markAsRead = (id) => { - const sql = `UPDATE notifications SET read = 't' WHERE id = $1` - return db.none(sql, [id]) + const sql = `UPDATE notifications SET read = 't' WHERE id = $1` + return db.none(sql, [id]) } const markAllAsRead = () => { - const sql = `UPDATE notifications SET read = 't'` - return db.none(sql) + const sql = `UPDATE notifications SET read = 't'` + return db.none(sql) } const hasUnreadNotifications = () => { - const sql = `SELECT EXISTS (SELECT 1 FROM notifications WHERE read = 'f' LIMIT 1)` - return db.oneOrNone(sql).then(res => res.exists) + const sql = `SELECT EXISTS (SELECT 1 FROM notifications WHERE read = 'f' LIMIT 1)` + return db.oneOrNone(sql).then(res => res.exists) } const getAlerts = () => { - const types = ['fiatBalance', 'cryptoBalance', 'error'] - const sql = `SELECT * FROM notifications WHERE valid = 't' AND type IN ($1:list) ORDER BY created DESC` - return db.any(sql, [types]) + const types = ['fiatBalance', 'cryptoBalance', 'error'] + const sql = `SELECT * FROM notifications WHERE valid = 't' AND type IN ($1:list) ORDER BY created DESC` + return db.any(sql, [types]) } module.exports = { - machineEvents: dbm.machineEvents, - addNotification, - getAllValidNotifications, - invalidateNotification, - batchInvalidate, - clearBlacklistNotification, - getValidNotifications, - getNotifications, - markAsRead, - markAllAsRead, - hasUnreadNotifications, - getAlerts + machineEvents: dbm.machineEvents, + addNotification, + getAllValidNotifications, + invalidateNotification, + batchInvalidate, + clearBlacklistNotification, + getValidNotifications, + getNotifications, + markAsRead, + markAllAsRead, + hasUnreadNotifications, + getAlerts, + getMachineName } diff --git a/lib/routes.js b/lib/routes.js index 5a912f56..dc07002b 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -343,7 +343,7 @@ function triggerBlock (req, res, next) { customers.update(id, { authorizedOverride: 'blocked' }) .then(customer => { - notifier.customerComplianceNotify(customer, req.deviceId, 'BLOCKED') + notifier.notifyIfActive('compliance', customer, req.deviceId, 'BLOCKED').catch(console.error) return respond(req, res, { customer }) }) .catch(next) @@ -362,7 +362,7 @@ function triggerSuspend (req, res, next) { date.setDate(date.getDate() + days); customers.update(id, { suspendedUntil: date }) .then(customer => { - notifier.customerComplianceNotify(customer, req.deviceId, 'SUSPENDED', days) + notifier.notifyIfActive('compliance', customer, req.deviceId, 'SUSPENDED', days).catch(console.error) return respond(req, res, { customer }) }) .catch(next) diff --git a/migrations/1607009558538-create-notifications-table.js b/migrations/1607009558538-create-notifications-table.js index 88231246..d4351c91 100644 --- a/migrations/1607009558538-create-notifications-table.js +++ b/migrations/1607009558538-create-notifications-table.js @@ -4,6 +4,7 @@ const singleQuotify = (item) => `'${item}'` var types = [ 'highValueTransaction', + 'transaction', 'fiatBalance', 'cryptoBalance', 'compliance', diff --git a/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js b/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js index b01312ce..1b81b325 100644 --- a/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js +++ b/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js @@ -2,6 +2,7 @@ import Grid from '@material-ui/core/Grid' import { makeStyles } from '@material-ui/core/styles' import classnames from 'classnames' import prettyMs from 'pretty-ms' +import * as R from 'ramda' import React from 'react' import { Label1, Label2, TL2 } from 'src/components/typography' @@ -14,6 +15,7 @@ import styles from './NotificationCenter.styles' const useStyles = makeStyles(styles) const types = { + transaction: { display: 'Transactions', icon: }, highValueTransaction: { display: 'Transactions', icon: }, fiatBalance: { display: 'Maintenance', icon: }, cryptoBalance: { display: 'Maintenance', icon: }, @@ -34,15 +36,18 @@ const NotificationRow = ({ }) => { const classes = useStyles() - const buildType = () => { - return types[type].display - } - - const buildAge = () => { - const createdDate = new Date(created) - const interval = +new Date() - createdDate - return prettyMs(interval, { compact: true, verbose: true }) - } + const typeDisplay = R.path([type, 'display'])(types) ?? null + const icon = R.path([type, 'icon'])(types) ?? + const age = prettyMs(new Date().getTime() - new Date(created).getTime(), { + compact: true, + verbose: true + }) + const notificationTitle = + typeDisplay && deviceName + ? `${typeDisplay} - ${deviceName}` + : !typeDisplay && deviceName + ? `${deviceName}` + : `${typeDisplay}` return ( - {types[type].icon} + {icon} - {`${buildType()} ${deviceName ? '- ' + deviceName : ''}`} + {notificationTitle} {message} - - {buildAge(created)} - + {age} diff --git a/new-lamassu-admin/src/pages/Notifications/sections/Setup.js b/new-lamassu-admin/src/pages/Notifications/sections/Setup.js index 3c5991bb..be1d1c78 100644 --- a/new-lamassu-admin/src/pages/Notifications/sections/Setup.js +++ b/new-lamassu-admin/src/pages/Notifications/sections/Setup.js @@ -87,6 +87,7 @@ const Setup = ({ wizard, forceDisable }) => { + )