diff --git a/lib/blacklist.js b/lib/blacklist.js index d5d45027..ad4edb9f 100644 --- a/lib/blacklist.js +++ b/lib/blacklist.js @@ -1,5 +1,5 @@ const db = require('./db') -const notifier = require('./notifier') +const notifierQueries = require('./notifier/queries') // Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator const getBlacklist = () => { @@ -15,7 +15,7 @@ const getBlacklist = () => { // Delete row from blacklist table by crypto code and address const deleteFromBlacklist = (cryptoCode, address) => { const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2` - notifier.clearBlacklistNotification(cryptoCode, address) + notifierQueries.clearBlacklistNotification(cryptoCode, address).catch(console.error) return db.none(sql, [cryptoCode, address]) } diff --git a/lib/cash-in/cash-in-tx.js b/lib/cash-in/cash-in-tx.js index 3c9f8463..01e18f0a 100644 --- a/lib/cash-in/cash-in-tx.js +++ b/lib/cash-in/cash-in-tx.js @@ -31,9 +31,9 @@ function post (machineTx, pi) { if (_.some(it => it.created_by_operator)(blacklistItems)) { blacklisted = true - notifier.blacklistNotify(r.tx, false) + notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false).catch(console.error) } else if (_.some(it => !it.created_by_operator)(blacklistItems) && rejectAddressReuseActive) { - notifier.blacklistNotify(r.tx, true) + notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true).catch(console.error) addressReuse = true } return postProcess(r, pi, blacklisted, addressReuse) diff --git a/lib/notifier/index.js b/lib/notifier/index.js index 66491632..2e2aaf52 100644 --- a/lib/notifier/index.js +++ b/lib/notifier/index.js @@ -6,20 +6,11 @@ const queries = require('./queries') const settingsLoader = require('../new-settings-loader') const customers = require('../customers') +const notificationCenter = require('./notificationCenter') 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 } -} = codes +const { STALE, STALE_STATE } = require('./codes') function buildMessage (alerts, notifications) { const smsEnabled = utils.isActive(notifications.sms) @@ -52,7 +43,7 @@ function checkNotification (plugins) { return getAlerts(plugins) .then(alerts => { - notifyIfActive('errors', alerts).catch(console.error) + notifyIfActive('errors', 'errorAlertsNotify', alerts).catch(console.error) const currentAlertFingerprint = utils.buildAlertFingerprint( alerts, notifications @@ -83,7 +74,7 @@ function getAlerts (plugins) { queries.machineEvents(), plugins.getMachineNames() ]).then(([balances, events, devices]) => { - notifyIfActive('balance', balances).catch(console.error) + notifyIfActive('balance', 'balancesNotify', balances).catch(console.error) return buildAlerts(checkPings(devices), balances, events, devices) }) } @@ -138,13 +129,6 @@ 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) @@ -154,8 +138,12 @@ function transactionNotify (tx, rec) { // 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) + // awaiting for redesign. notification should not be sent if toggle in the settings table is disabled, + // but currently we're sending notifications of high value tx even with the toggle disabled + if (readyToNotify && !highValueTx) { + notifyIfActive('transactions', 'notifCenterTransactionNotify', highValueTx, directionDisplay, tx.fiat, tx.fiatCode, tx.deviceId, tx.toAddress).catch(console.error) + } else if (readyToNotify && highValueTx) { + notificationCenter.notifCenterTransactionNotify(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 @@ -216,169 +204,21 @@ function sendTransactionMessage (rec, isHighValueTx) { }) } -const clearOldCryptoNotifications = balances => { - return queries.getAllValidNotifications(CRYPTO_BALANCE).then(res => { - const filterByBalance = _.filter(notification => { - const { cryptoCode, code } = notification.detail - return !_.find(balance => balance.cryptoCode === cryptoCode && balance.code === code)(balances) - }) - const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(res) - - const notInvalidated = _.filter(notification => { - return !_.find(id => notification.id === id)(indexesToInvalidate) - }, res) - return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated) - }) -} - -const cryptoBalancesNotify = (cryptoWarnings) => { - return clearOldCryptoNotifications(cryptoWarnings).then(notInvalidated => { - return cryptoWarnings.forEach(balance => { - // if notification exists in DB and wasnt invalidated then don't add a duplicate - if (_.find(o => { - const { code, cryptoCode } = o.detail - return code === balance.code && cryptoCode === balance.cryptoCode - }, notInvalidated)) return - - const fiat = utils.formatCurrency(balance.fiatBalance.balance, balance.fiatCode) - const message = `${balance.code === 'HIGH_CRYPTO_BALANCE' ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]` - const detailB = utils.buildDetail({ cryptoCode: balance.cryptoCode, code: balance.code }) - return queries.addNotification(CRYPTO_BALANCE, message, detailB) - }) - }) -} - -const clearOldFiatNotifications = (balances) => { - return queries.getAllValidNotifications(FIAT_BALANCE).then(notifications => { - const filterByBalance = _.filter(notification => { - const { cassette, deviceId } = notification.detail - return !_.find(balance => balance.cassette === cassette && balance.deviceId === deviceId)(balances) - }) - const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(notifications) - const notInvalidated = _.filter(notification => { - return !_.find(id => notification.id === id)(indexesToInvalidate) - }, notifications) - return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated) - }) -} - -const fiatBalancesNotify = (fiatWarnings) => { - return clearOldFiatNotifications(fiatWarnings).then(notInvalidated => { - return fiatWarnings.forEach(balance => { - if (_.find(o => { - const { cassette, deviceId } = o.detail - return cassette === balance.cassette && deviceId === balance.deviceId - }, notInvalidated)) return - const message = `Cash-out cassette ${balance.cassette} almost empty!` - const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette }) - return queries.addNotification(FIAT_BALANCE, message, detailB) - }) - }) -} - -const balancesNotify = (balances) => { - const cryptoFilter = o => o.code === 'HIGH_CRYPTO_BALANCE' || o.code === 'LOW_CRYPTO_BALANCE' - const fiatFilter = o => o.code === 'LOW_CASH_OUT' - const cryptoWarnings = _.filter(cryptoFilter, balances) - const fiatWarnings = _.filter(fiatFilter, balances) - return Promise.all([cryptoBalancesNotify(cryptoWarnings), fiatBalancesNotify(fiatWarnings)]).catch(console.error) -} - -const clearOldErrorNotifications = alerts => { - return queries.getAllValidNotifications(ERROR) - .then(res => { - // for each valid notification in DB see if it exists in alerts - // if the notification doesn't exist in alerts, it is not valid anymore - const filterByAlert = _.filter(notification => { - const { code, deviceId } = notification.detail - return !_.find(alert => alert.code === code && alert.deviceId === deviceId)(alerts) - }) - const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res) - if (!indexesToInvalidate.length) return Promise.resolve() - return queries.batchInvalidate(indexesToInvalidate) - }) - .catch(console.error) -} - -const errorAlertsNotify = (alertRec) => { - const embedDeviceId = deviceId => _.assign({ deviceId }) - const mapToAlerts = _.map(it => _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts)) - const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices) - - return clearOldErrorNotifications(alerts).then(() => { - _.forEach(alert => { - switch (alert.code) { - case PING: { - const detailB = utils.buildDetail({ code: PING, age: alert.age ? alert.age : -1, deviceId: alert.deviceId }) - return queries.getValidNotifications(ERROR, _.omit(['age'], detailB)).then(res => { - if (res.length > 0) return Promise.resolve() - const message = `Machine down` - return queries.addNotification(ERROR, message, detailB) - }) - } - case STALE: { - const detailB = utils.buildDetail({ code: STALE, deviceId: alert.deviceId }) - return queries.getValidNotifications(ERROR, detailB).then(res => { - if (res.length > 0) return Promise.resolve() - const message = `Machine is stuck on ${alert.state} screen` - return queries.addNotification(ERROR, message, detailB) - }) - } - } - }, alerts) - }).catch(console.error) -} - -const blacklistNotify = (tx, isAddressReuse) => { - const code = isAddressReuse ? 'REUSED' : 'BLOCKED' - const name = isAddressReuse ? 'reused' : 'blacklisted' - - const detailB = utils.buildDetail({ cryptoCode: tx.cryptoCode, code, cryptoAddress: tx.toAddress }) - const message = `Blocked ${name} address: ${tx.cryptoCode} ${tx.toAddress.substr(0, 10)}...` - return queries.addNotification(COMPLIANCE, message, detailB) -} - -const clearBlacklistNotification = (cryptoCode, cryptoAddress) => { - return queries.clearBlacklistNotification(cryptoCode, cryptoAddress).catch(console.error) -} - -const clearOldCustomerSuspendedNotifications = (customerId, deviceId) => { - const detailB = utils.buildDetail({ code: 'SUSPENDED', customerId, deviceId }) - return queries.invalidateNotification(detailB, 'compliance') -} - -const customerComplianceNotify = (customer, deviceId, code, days = null) => { - // code for now can be "BLOCKED", "SUSPENDED" - const detailB = utils.buildDetail({ customerId: customer.id, code, deviceId }) - const date = new Date() - if (days) { - date.setDate(date.getDate() + days) - } - const message = code === 'SUSPENDED' ? `Customer suspended until ${date.toLocaleString()}` : `Customer blocked` - - return clearOldCustomerSuspendedNotifications(customer.id, deviceId) - .then(() => queries.getValidNotifications(COMPLIANCE, detailB)) - .then(res => { - if (res.length > 0) return Promise.resolve() - return queries.addNotification(COMPLIANCE, message, detailB) - }) - .catch(console.error) -} - const notificationCenterFunctions = { - 'compliance': customerComplianceNotify, - 'balance': balancesNotify, - 'errors': errorAlertsNotify, - 'transactions': notifCenterTransactionNotify + 'customerComplianceNotify': notificationCenter.customerComplianceNotify, + 'blacklistNotify': notificationCenter.blacklistNotify, + 'balancesNotify': notificationCenter.balancesNotify, + 'errorAlertsNotify': notificationCenter.errorAlertsNotify, + 'notifCenterTransactionNotify': notificationCenter.notifCenterTransactionNotify } // for notification center, check if type of notification is active before calling the respective notify function -const notifyIfActive = (type, ...args) => { +const notifyIfActive = (type, fnName, ...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 (!notificationCenterFunctions[fnName]) return Promise.reject(new Error(`Notification function ${fnName} for type ${type} does not exist`)) if (!(notificationSettings.active && notificationSettings[type])) return Promise.resolve() - return notificationCenterFunctions[type](...args) + return notificationCenterFunctions[fnName](...args) }) } @@ -388,7 +228,5 @@ module.exports = { checkPings, checkStuckScreen, sendRedemptionMessage, - blacklistNotify, - clearBlacklistNotification, notifyIfActive } diff --git a/lib/notifier/notificationCenter.js b/lib/notifier/notificationCenter.js new file mode 100644 index 00000000..e9eae580 --- /dev/null +++ b/lib/notifier/notificationCenter.js @@ -0,0 +1,170 @@ +const _ = require('lodash/fp') + +const queries = require('./queries') +const utils = require('./utils') +const codes = require('./codes') + +const { NOTIFICATION_TYPES: { + COMPLIANCE, + CRYPTO_BALANCE, + FIAT_BALANCE, + ERROR, + HIGH_VALUE_TX, + NORMAL_VALUE_TX } +} = codes + +const { STALE, PING } = codes + +const clearOldCustomerSuspendedNotifications = (customerId, deviceId) => { + const detailB = utils.buildDetail({ code: 'SUSPENDED', customerId, deviceId }) + return queries.invalidateNotification(detailB, 'compliance') +} + +const customerComplianceNotify = (customer, deviceId, code, days = null) => { + // code for now can be "BLOCKED", "SUSPENDED" + const detailB = utils.buildDetail({ customerId: customer.id, code, deviceId }) + const date = new Date() + if (days) { + date.setDate(date.getDate() + days) + } + const message = code === 'SUSPENDED' ? `Customer suspended until ${date.toLocaleString()}` : `Customer blocked` + + return clearOldCustomerSuspendedNotifications(customer.id, deviceId) + .then(() => queries.getValidNotifications(COMPLIANCE, detailB)) + .then(res => { + if (res.length > 0) return Promise.resolve() + return queries.addNotification(COMPLIANCE, message, detailB) + }) + .catch(console.error) +} + +const clearOldFiatNotifications = (balances) => { + return queries.getAllValidNotifications(FIAT_BALANCE).then(notifications => { + const filterByBalance = _.filter(notification => { + const { cassette, deviceId } = notification.detail + return !_.find(balance => balance.cassette === cassette && balance.deviceId === deviceId)(balances) + }) + const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(notifications) + const notInvalidated = _.filter(notification => { + return !_.find(id => notification.id === id)(indexesToInvalidate) + }, notifications) + return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated) + }) +} + +const fiatBalancesNotify = (fiatWarnings) => { + return clearOldFiatNotifications(fiatWarnings).then(notInvalidated => { + return fiatWarnings.forEach(balance => { + if (_.find(o => { + const { cassette, deviceId } = o.detail + return cassette === balance.cassette && deviceId === balance.deviceId + }, notInvalidated)) return + const message = `Cash-out cassette ${balance.cassette} almost empty!` + const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette }) + return queries.addNotification(FIAT_BALANCE, message, detailB) + }) + }) +} + +const clearOldCryptoNotifications = balances => { + return queries.getAllValidNotifications(CRYPTO_BALANCE).then(res => { + const filterByBalance = _.filter(notification => { + const { cryptoCode, code } = notification.detail + return !_.find(balance => balance.cryptoCode === cryptoCode && balance.code === code)(balances) + }) + const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(res) + + const notInvalidated = _.filter(notification => { + return !_.find(id => notification.id === id)(indexesToInvalidate) + }, res) + return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated) + }) +} + +const cryptoBalancesNotify = (cryptoWarnings) => { + return clearOldCryptoNotifications(cryptoWarnings).then(notInvalidated => { + return cryptoWarnings.forEach(balance => { + // if notification exists in DB and wasnt invalidated then don't add a duplicate + if (_.find(o => { + const { code, cryptoCode } = o.detail + return code === balance.code && cryptoCode === balance.cryptoCode + }, notInvalidated)) return + + const fiat = utils.formatCurrency(balance.fiatBalance.balance, balance.fiatCode) + const message = `${balance.code === 'HIGH_CRYPTO_BALANCE' ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]` + const detailB = utils.buildDetail({ cryptoCode: balance.cryptoCode, code: balance.code }) + return queries.addNotification(CRYPTO_BALANCE, message, detailB) + }) + }) +} + +const balancesNotify = (balances) => { + const cryptoFilter = o => o.code === 'HIGH_CRYPTO_BALANCE' || o.code === 'LOW_CRYPTO_BALANCE' + const fiatFilter = o => o.code === 'LOW_CASH_OUT' + const cryptoWarnings = _.filter(cryptoFilter, balances) + const fiatWarnings = _.filter(fiatFilter, balances) + return Promise.all([cryptoBalancesNotify(cryptoWarnings), fiatBalancesNotify(fiatWarnings)]).catch(console.error) +} + +const clearOldErrorNotifications = alerts => { + return queries.getAllValidNotifications(ERROR) + .then(res => { + // for each valid notification in DB see if it exists in alerts + // if the notification doesn't exist in alerts, it is not valid anymore + const filterByAlert = _.filter(notification => { + const { code, deviceId } = notification.detail + return !_.find(alert => alert.code === code && alert.deviceId === deviceId)(alerts) + }) + const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res) + if (!indexesToInvalidate.length) return Promise.resolve() + return queries.batchInvalidate(indexesToInvalidate) + }) + .catch(console.error) +} + +const errorAlertsNotify = (alertRec) => { + const embedDeviceId = deviceId => _.assign({ deviceId }) + const mapToAlerts = _.map(it => _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts)) + const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices) + + return clearOldErrorNotifications(alerts).then(() => { + _.forEach(alert => { + switch (alert.code) { + case PING: { + const detailB = utils.buildDetail({ code: PING, age: alert.age ? alert.age : -1, deviceId: alert.deviceId }) + return queries.getValidNotifications(ERROR, _.omit(['age'], detailB)).then(res => { + if (res.length > 0) return Promise.resolve() + const message = `Machine down` + return queries.addNotification(ERROR, message, detailB) + }) + } + case STALE: { + const detailB = utils.buildDetail({ code: STALE, deviceId: alert.deviceId }) + return queries.getValidNotifications(ERROR, detailB).then(res => { + if (res.length > 0) return Promise.resolve() + const message = `Machine is stuck on ${alert.state} screen` + return queries.addNotification(ERROR, message, detailB) + }) + } + } + }, alerts) + }).catch(console.error) +} + +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) +} + +const blacklistNotify = (tx, isAddressReuse) => { + const code = isAddressReuse ? 'REUSED' : 'BLOCKED' + const name = isAddressReuse ? 'reused' : 'blacklisted' + + const detailB = utils.buildDetail({ cryptoCode: tx.cryptoCode, code, cryptoAddress: tx.toAddress }) + const message = `Blocked ${name} address: ${tx.cryptoCode} ${tx.toAddress.substr(0, 10)}...` + return queries.addNotification(COMPLIANCE, message, detailB) +} + +module.exports = { customerComplianceNotify, balancesNotify, errorAlertsNotify, notifCenterTransactionNotify, blacklistNotify } diff --git a/lib/notifier/test/notifier.test.js b/lib/notifier/test/notifier.test.js index f7314af8..47068779 100644 --- a/lib/notifier/test/notifier.test.js +++ b/lib/notifier/test/notifier.test.js @@ -291,9 +291,15 @@ test("calls sendRedemptionMessage if !zeroConf and rec.isRedemption", async () = // sendRedemptionMessage will cause this func to be called jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => rec) +<<<<<<< HEAD getCashOut.mockReturnValue({zeroConfLimit: -Infinity}) loadLatest.mockReturnValue({}) getGlobalNotifications.mockReturnValue({... notifSettings, sms: { active: true, errors: true, transactions: true }}) +======= + getCashOut.mockReturnValue({ zeroConfLimit: -Infinity }) + loadLatest.mockReturnValue(Promise.resolve({})) + getGlobalNotifications.mockReturnValue({ ...notifSettings, sms: { active: true, errors: true, transactions: true }, notificationCenter: { active: true } }) +>>>>>>> a7a9fd3... Feat: move notif center fns to own file on the notifier module const response = await notifier.transactionNotify(tx, {isRedemption: true}) @@ -324,10 +330,10 @@ test("calls sendTransactionMessage if !zeroConf and !rec.isRedemption", async () jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => ({prop: rec})) buildTransactionMessage.mockImplementation(() => ["mock message", false]) - getMachineName.mockReturnValue("mockMachineName") - getCashOut.mockReturnValue({zeroConfLimit: -Infinity}) - loadLatest.mockReturnValue({}) - getGlobalNotifications.mockReturnValue({... notifSettings, sms: { active: true, errors: true, transactions: true }}) + getMachineName.mockReturnValue('mockMachineName') + getCashOut.mockReturnValue({ zeroConfLimit: -Infinity }) + loadLatest.mockReturnValue(Promise.resolve({})) + getGlobalNotifications.mockReturnValue({ ...notifSettings, sms: { active: true, errors: true, transactions: true }, notificationCenter: { active: true } }) const response = await notifier.transactionNotify(tx, {isRedemption: false}) diff --git a/lib/routes.js b/lib/routes.js index dc07002b..3b454a95 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.notifyIfActive('compliance', customer, req.deviceId, 'BLOCKED').catch(console.error) + notifier.notifyIfActive('compliance', 'customerComplianceNotify', 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.notifyIfActive('compliance', customer, req.deviceId, 'SUSPENDED', days).catch(console.error) + notifier.notifyIfActive('compliance', 'customerComplianceNotify', customer, req.deviceId, 'SUSPENDED', days).catch(console.error) return respond(req, res, { customer }) }) .catch(next)