Chore: make notification center UI
Chore: fiatBalancesNotify refactor Chore: removed now-unused code in some files Feat: change column "detail" in database to use jsonb Chore: add notification center background and button Chore: notifications screen scaffolding Fix: change position of notification UI Feat: join backend and frontend Feat: notification icons and machine names Feat: add clear all button, stripe overlay on invalid notification Fix: rework notification styles Feat: use popper to render notifications Feat: make notification center UI Fix: fix css on notification center Fix: fix invalidateNotification Chore: apply PR requested changes Fix: PR fixes Fix: make toggleable body/root styles be handled by react Chore: delete old notifier file Fix: undo variable name changes for cryptobalance notifs
This commit is contained in:
parent
2a9e8dadba
commit
c457faab40
37 changed files with 1337 additions and 1332 deletions
|
|
@ -20,6 +20,14 @@ const NETWORK_DOWN_TIME = 1 * T.minute
|
|||
const STALE_STATE = 7 * T.minute
|
||||
const ALERT_SEND_INTERVAL = T.hour
|
||||
|
||||
const NOTIFICATION_TYPES = {
|
||||
HIGH_VALUE_TX: 'highValueTransaction',
|
||||
FIAT_BALANCE: 'fiatBalance',
|
||||
CRYPTO_BALANCE: 'cryptoBalance',
|
||||
COMPLIANCE: 'compliance',
|
||||
ERROR: 'error'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PING,
|
||||
STALE,
|
||||
|
|
@ -30,5 +38,6 @@ module.exports = {
|
|||
CODES_DISPLAY,
|
||||
NETWORK_DOWN_TIME,
|
||||
STALE_STATE,
|
||||
ALERT_SEND_INTERVAL
|
||||
ALERT_SEND_INTERVAL,
|
||||
NOTIFICATION_TYPES
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const _ = require('lodash/fp')
|
||||
const utils = require("./utils")
|
||||
const utils = require('./utils')
|
||||
|
||||
const email = require('../email')
|
||||
|
||||
|
|
@ -12,61 +12,47 @@ const {
|
|||
LOW_CASH_OUT
|
||||
} = require('./codes')
|
||||
|
||||
function alertSubject(alertRec, config) {
|
||||
function alertSubject (alertRec, config) {
|
||||
let alerts = []
|
||||
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.general)
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
})
|
||||
_.forEach(device => {
|
||||
alerts = _.concat(alerts, utils.deviceAlerts(config, alertRec, device))
|
||||
}, _.keys(alertRec.devices))
|
||||
|
||||
if (alerts.length === 0) return null
|
||||
|
||||
const alertTypes = _.map(codeDisplay, _.uniq(_.map('code', alerts))).sort()
|
||||
const alertTypes = _.flow(_.map('code'), _.uniq, _.map(utils.codeDisplay), _.sortBy(o => o))(alerts)
|
||||
return '[Lamassu] Errors reported: ' + alertTypes.join(', ')
|
||||
}
|
||||
|
||||
function printEmailAlerts(alertRec, config) {
|
||||
function printEmailAlerts (alertRec, config) {
|
||||
let body = 'Errors were reported by your Lamassu Machines.\n'
|
||||
|
||||
if (config.balance && alertRec.general.length !== 0) {
|
||||
body = body + '\nGeneral errors:\n'
|
||||
body = body + emailAlerts(alertRec.general) + '\n'
|
||||
body += '\nGeneral errors:\n'
|
||||
body += emailAlerts(alertRec.general) + '\n'
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
_.forEach(device => {
|
||||
const deviceName = alertRec.deviceNames[device]
|
||||
body = body + '\nErrors for ' + deviceName + ':\n'
|
||||
body += '\nErrors for ' + deviceName + ':\n'
|
||||
|
||||
let alerts = []
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
|
||||
body = body + emailAlerts(alerts)
|
||||
})
|
||||
const alerts = utils.deviceAlerts(config, alertRec, device)
|
||||
|
||||
body += emailAlerts(alerts)
|
||||
}, _.keys(alertRec.devices))
|
||||
return body
|
||||
}
|
||||
|
||||
function emailAlerts(alerts) {
|
||||
return alerts.map(emailAlert).join('\n') + '\n'
|
||||
function emailAlerts (alerts) {
|
||||
return _.join('\n', _.map(emailAlert, alerts)) + '\n'
|
||||
}
|
||||
|
||||
function emailAlert(alert) {
|
||||
function emailAlert (alert) {
|
||||
switch (alert.code) {
|
||||
case PING:
|
||||
if (alert.age) {
|
||||
|
|
@ -98,5 +84,4 @@ function emailAlert(alert) {
|
|||
|
||||
const sendMessage = email.sendMessage
|
||||
|
||||
|
||||
module.exports = { alertSubject, printEmailAlerts, sendMessage }
|
||||
|
|
|
|||
|
|
@ -11,8 +11,15 @@ const utils = require('./utils')
|
|||
const emailFuncs = require('./email')
|
||||
const smsFuncs = require('./sms')
|
||||
const { STALE, STALE_STATE, PING } = require('./codes')
|
||||
const { NOTIFICATION_TYPES: {
|
||||
HIGH_VALUE_TX,
|
||||
FIAT_BALANCE,
|
||||
CRYPTO_BALANCE,
|
||||
COMPLIANCE,
|
||||
ERROR }
|
||||
} = require('./codes')
|
||||
|
||||
function buildMessage(alerts, notifications) {
|
||||
function buildMessage (alerts, notifications) {
|
||||
const smsEnabled = utils.isActive(notifications.sms)
|
||||
const emailEnabled = utils.isActive(notifications.email)
|
||||
|
||||
|
|
@ -34,7 +41,7 @@ function buildMessage(alerts, notifications) {
|
|||
return rec
|
||||
}
|
||||
|
||||
function checkNotification(plugins) {
|
||||
function checkNotification (plugins) {
|
||||
const notifications = plugins.getNotificationConfig()
|
||||
const smsEnabled = utils.isActive(notifications.sms)
|
||||
const emailEnabled = utils.isActive(notifications.email)
|
||||
|
|
@ -50,28 +57,25 @@ function checkNotification(plugins) {
|
|||
)
|
||||
if (!currentAlertFingerprint) {
|
||||
const inAlert = !!utils.getAlertFingerprint()
|
||||
// (fingerprint = null, lastAlertTime = null)
|
||||
// variables for setAlertFingerprint: (fingerprint = null, lastAlertTime = null)
|
||||
utils.setAlertFingerprint(null, null)
|
||||
if (inAlert) {
|
||||
return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
|
||||
}
|
||||
}
|
||||
if (utils.shouldNotAlert(currentAlertFingerprint)) {
|
||||
return
|
||||
if (inAlert) return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
|
||||
}
|
||||
if (utils.shouldNotAlert(currentAlertFingerprint)) return
|
||||
|
||||
const message = buildMessage(alerts, notifications)
|
||||
utils.setAlertFingerprint(currentAlertFingerprint, Date.now())
|
||||
return plugins.sendMessage(message)
|
||||
})
|
||||
.then(results => {
|
||||
if (results && results.length > 0)
|
||||
if (results && results.length > 0) {
|
||||
logger.debug('Successfully sent alerts')
|
||||
}
|
||||
})
|
||||
.catch(logger.error)
|
||||
}
|
||||
|
||||
function getAlerts(plugins) {
|
||||
function getAlerts (plugins) {
|
||||
return Promise.all([
|
||||
plugins.checkBalances(),
|
||||
queries.machineEvents(),
|
||||
|
|
@ -82,10 +86,10 @@ function getAlerts(plugins) {
|
|||
})
|
||||
}
|
||||
|
||||
function buildAlerts(pings, balances, events, devices) {
|
||||
function buildAlerts (pings, balances, events, devices) {
|
||||
const alerts = { devices: {}, deviceNames: {} }
|
||||
alerts.general = _.filter(r => !r.deviceId, balances)
|
||||
devices.forEach(function (device) {
|
||||
_.forEach(device => {
|
||||
const deviceId = device.deviceId
|
||||
const deviceName = device.name
|
||||
const deviceEvents = events.filter(function (eventRow) {
|
||||
|
|
@ -94,83 +98,78 @@ function buildAlerts(pings, balances, events, devices) {
|
|||
const ping = pings[deviceId] || []
|
||||
const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
|
||||
|
||||
if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {}
|
||||
alerts.devices[deviceId].balanceAlerts = _.filter(
|
||||
alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter(
|
||||
['deviceId', deviceId],
|
||||
balances
|
||||
)
|
||||
), alerts.devices)
|
||||
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
|
||||
|
||||
alerts.deviceNames[deviceId] = deviceName
|
||||
})
|
||||
}, devices)
|
||||
|
||||
return alerts
|
||||
}
|
||||
|
||||
function checkPings(devices) {
|
||||
function checkPings (devices) {
|
||||
const deviceIds = _.map('deviceId', devices)
|
||||
const pings = _.map(utils.checkPing, devices)
|
||||
return _.zipObject(deviceIds)(pings)
|
||||
}
|
||||
|
||||
function checkStuckScreen(deviceEvents, machineName) {
|
||||
function checkStuckScreen (deviceEvents, machineName) {
|
||||
const sortedEvents = _.sortBy(
|
||||
utils.getDeviceTime,
|
||||
_.map(utils.parseEventNote, deviceEvents)
|
||||
)
|
||||
const lastEvent = _.last(sortedEvents)
|
||||
|
||||
if (!lastEvent) {
|
||||
return []
|
||||
}
|
||||
if (!lastEvent) return []
|
||||
|
||||
const state = lastEvent.note.state
|
||||
const isIdle = lastEvent.note.isIdle
|
||||
|
||||
if (isIdle) {
|
||||
return []
|
||||
}
|
||||
if (isIdle) return []
|
||||
|
||||
const age = Math.floor(lastEvent.age)
|
||||
if (age > STALE_STATE) {
|
||||
return [{ code: STALE, state, age, machineName }]
|
||||
}
|
||||
if (age > STALE_STATE) return [{ code: STALE, state, age, machineName }]
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function transactionNotify (tx, rec) {
|
||||
const settings = await settingsLoader.loadLatest()
|
||||
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' || highValueTx && tx.direction === 'cashOut' && rec.isRedemption) {
|
||||
queries.addHighValueTx(tx)
|
||||
}
|
||||
|
||||
// alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled
|
||||
const cashOutConfig = configManager.getCashOut(tx.deviceId, settings.config)
|
||||
const zeroConfLimit = cashOutConfig.zeroConfLimit
|
||||
const zeroConf = isCashOut && tx.fiat.lte(zeroConfLimit)
|
||||
const notificationsEnabled = notifSettings.sms.transactions || notifSettings.email.transactions
|
||||
const customerPromise = tx.customerId ? customers.getById(tx.customerId) : Promise.resolve({})
|
||||
|
||||
if (!notificationsEnabled && !highValueTx) return Promise.resolve()
|
||||
if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) return Promise.resolve()
|
||||
if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error)
|
||||
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)
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
machineLoader.getMachineName(tx.deviceId),
|
||||
customerPromise
|
||||
])
|
||||
.then(([machineName, customer]) => {
|
||||
return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer)
|
||||
// alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled
|
||||
const cashOutConfig = configManager.getCashOut(tx.deviceId, settings.config)
|
||||
const zeroConfLimit = cashOutConfig.zeroConfLimit
|
||||
const zeroConf = isCashOut && tx.fiat.lte(zeroConfLimit)
|
||||
const notificationsEnabled = notifSettings.sms.transactions || notifSettings.email.transactions
|
||||
const customerPromise = tx.customerId ? customers.getById(tx.customerId) : Promise.resolve({})
|
||||
|
||||
if (!notificationsEnabled && !highValueTx) return Promise.resolve()
|
||||
if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) return Promise.resolve()
|
||||
if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error)
|
||||
|
||||
return Promise.all([
|
||||
machineLoader.getMachineName(tx.deviceId),
|
||||
customerPromise
|
||||
]).then(([machineName, customer]) => {
|
||||
return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer)
|
||||
}).then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx))
|
||||
})
|
||||
.then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx))
|
||||
}
|
||||
|
||||
function sendRedemptionMessage(txId, error) {
|
||||
function sendRedemptionMessage (txId, error) {
|
||||
const subject = `Here's an update on transaction ${txId}`
|
||||
const body = error
|
||||
? `Error: ${error}`
|
||||
|
|
@ -188,218 +187,173 @@ function sendRedemptionMessage(txId, error) {
|
|||
return sendTransactionMessage(rec)
|
||||
}
|
||||
|
||||
async function sendTransactionMessage(rec, isHighValueTx) {
|
||||
const settings = await settingsLoader.loadLatest()
|
||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||
function sendTransactionMessage (rec, isHighValueTx) {
|
||||
return settingsLoader.loadLatest().then(settings => {
|
||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||
|
||||
let promises = []
|
||||
const promises = []
|
||||
|
||||
const emailActive =
|
||||
notifications.email.active &&
|
||||
(notifications.email.transactions || isHighValueTx)
|
||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||
const emailActive =
|
||||
notifications.email.active &&
|
||||
(notifications.email.transactions || isHighValueTx)
|
||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||
|
||||
const smsActive =
|
||||
notifications.sms.active &&
|
||||
(notifications.sms.transactions || isHighValueTx)
|
||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||
const smsActive =
|
||||
notifications.sms.active &&
|
||||
(notifications.sms.transactions || isHighValueTx)
|
||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
const cashCassettesNotify = (cassettes, deviceId) => {
|
||||
settingsLoader.loadLatest()
|
||||
.then(settings =>
|
||||
[
|
||||
configManager.getNotifications(null, deviceId, settings.config),
|
||||
configManager.getCashOut(deviceId,settings.config).active
|
||||
])
|
||||
.then(([notifications, cashOutEnabled]) => {
|
||||
const cassette1Count = cassettes.cassette1
|
||||
const cassette2Count = cassettes.cassette2
|
||||
const cassette1Threshold = notifications.fiatBalanceCassette1
|
||||
const cassette2Threshold = notifications.fiatBalanceCassette2
|
||||
|
||||
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, 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 )
|
||||
return queries.addCashCassetteWarning(1, deviceId)
|
||||
}
|
||||
if(res[1].length === 0 && cassette2Count < cassette2Threshold) {
|
||||
console.log("Adding fiatBalance alert for cashbox 2 in database - count & threshold: ", cassette2Count, cassette2Threshold )
|
||||
return queries.addCashCassetteWarning(2, deviceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
return Promise.all(promises)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
/*
|
||||
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('_')
|
||||
}
|
||||
const notInvalidated = _.filter(notification => {
|
||||
return !_.find(id => notification.id === id)(indexesToInvalidate)
|
||||
}, res)
|
||||
_.forEach(notification => {
|
||||
const idx = _.findIndex(balance => {
|
||||
return balance.code === notification.code && balance.cryptoCode === notification.cryptoCode
|
||||
}, balances)
|
||||
return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated)
|
||||
})
|
||||
}
|
||||
|
||||
if(idx !== -1) {
|
||||
return
|
||||
}
|
||||
// if the notification doesn't exist in the new balances object, then it is outdated and is not valid anymore
|
||||
return queries.invalidateNotification(notification.id)
|
||||
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 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)
|
||||
return 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)
|
||||
return queries.addCryptoBalanceWarning(`${warning.cryptoCode}_${warning.code}`, `Low balance in ${warning.cryptoCode} [${balance}]`)
|
||||
})
|
||||
})
|
||||
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) => {
|
||||
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
|
||||
return queries.invalidateNotification(notification.id)
|
||||
}, res)
|
||||
})
|
||||
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) => {
|
||||
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)
|
||||
const embedDeviceId = deviceId => _.assign({ deviceId })
|
||||
const mapToAlerts = _.map(it => _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts))
|
||||
const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices)
|
||||
|
||||
_.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`
|
||||
return 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`
|
||||
return queries.addErrorNotification(STALE, message, alert.deviceId)
|
||||
})
|
||||
default:
|
||||
return
|
||||
}
|
||||
}, alerts)
|
||||
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) => {
|
||||
let detail = ''
|
||||
let message = ''
|
||||
if(isAddressReuse) {
|
||||
detail = `${tx.cryptoCode}_REUSED_${tx.toAddress}`
|
||||
message = `Blocked reused address: ${tx.cryptoCode} ${tx.toAddress.substr(0,10)}...`
|
||||
} else {
|
||||
detail = `${tx.cryptoCode}_BLOCKED_${tx.toAddress}`
|
||||
message = `Blocked blacklisted address: ${tx.cryptoCode} ${tx.toAddress.substr(0,10)}...`
|
||||
}
|
||||
const code = isAddressReuse ? 'REUSED' : 'BLOCKED'
|
||||
const name = isAddressReuse ? 'reused' : 'blacklisted'
|
||||
|
||||
return queries.addComplianceNotification(tx.deviceId, detail, message)
|
||||
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 detail = `SUSPENDED_${customerId}`
|
||||
return queries.invalidateNotification(null, detail, deviceId)
|
||||
const detailB = utils.buildDetail({ code: 'SUSPENDED', customerId, deviceId })
|
||||
return queries.invalidateNotification(detailB, 'compliance')
|
||||
}
|
||||
|
||||
const customerComplianceNotify = (customer, deviceId, prefix, days = null) => {
|
||||
// prefix can be "BLOCKED", "SUSPENDED", etc
|
||||
const detail = `${prefix}_${customer.id}`
|
||||
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 = prefix === "SUSPENDED" ? `Customer suspended until ${date.toLocaleString()}` : `Customer blocked`
|
||||
const message = code === 'SUSPENDED' ? `Customer suspended until ${date.toLocaleString()}` : `Customer blocked`
|
||||
|
||||
// we have to clear every notification for this user where the suspension ended before the current date
|
||||
clearOldCustomerSuspendedNotifications(customer.id, deviceId).then(() => {
|
||||
return queries.getValidNotifications('compliance', detail, deviceId)
|
||||
}).then(res => {
|
||||
if (res.length > 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return queries.addComplianceNotification(deviceId, detail, message)
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
@ -408,7 +362,7 @@ module.exports = {
|
|||
checkPings,
|
||||
checkStuckScreen,
|
||||
sendRedemptionMessage,
|
||||
cashCassettesNotify,
|
||||
blacklistNotify,
|
||||
customerComplianceNotify,
|
||||
clearBlacklistNotification
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
const { v4: uuidv4 } = require('uuid')
|
||||
const pgp = require('pg-promise')()
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const dbm = require('../postgresql_interface')
|
||||
const db = require('../db')
|
||||
const { v4: uuidv4 } = require('uuid')
|
||||
|
||||
// types of notifications able to be inserted into db:
|
||||
/*
|
||||
|
|
@ -11,76 +14,68 @@ compliance - notifications related to warnings triggered by compliance settings
|
|||
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 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 message = `Cash-out cassette ${cassetteNumber} almost empty!`
|
||||
return db.oneOrNone(sql, [uuidv4(), 'fiatBalance', cassetteNumber, deviceId, message])
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
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 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 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 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 invalidateNotification = (detail, type) => {
|
||||
detail = _.omitBy(_.isEmpty, detail)
|
||||
const sql = `UPDATE notifications SET valid = 'f', read = 't', modified = CURRENT_TIMESTAMP WHERE valid = 't' AND type = $1 AND detail::jsonb @> $2::jsonb`
|
||||
return db.none(sql, [type, detail])
|
||||
}
|
||||
|
||||
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 batchInvalidate = (ids) => {
|
||||
const formattedIds = _.map(pgp.as.text, ids).join(',')
|
||||
const sql = `UPDATE notifications SET valid = 'f', read = 't', modified = CURRENT_TIMESTAMP WHERE id IN ($1^)`
|
||||
return db.none(sql, [formattedIds])
|
||||
}
|
||||
|
||||
const invalidateNotification = (id, detail = null, deviceId = null) => {
|
||||
let sql = ''
|
||||
if(id) {
|
||||
sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND id = $1`
|
||||
}
|
||||
else {
|
||||
sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND detail LIKE $2`
|
||||
sql = deviceId ? sql + ' AND device_id = $3' : sql
|
||||
}
|
||||
return db.none(sql, [id, `%${detail}%`, deviceId])
|
||||
const clearBlacklistNotification = (cryptoCode, cryptoAddress) => {
|
||||
const sql = `UPDATE notifications SET valid = 'f', read = 't', modified = CURRENT_TIMESTAMP 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 addComplianceNotification = (deviceId, detail, message) => {
|
||||
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, 'compliance', $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||
return db.oneOrNone(sql, [uuidv4(), detail, deviceId, message])
|
||||
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 getNotifications = () => {
|
||||
const sql = `SELECT * FROM notifications ORDER BY created DESC`
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
const markAsRead = (id) => {
|
||||
const sql = `UPDATE notifications SET read = 't', modified = CURRENT_TIMESTAMP WHERE id = $1`
|
||||
return db.none(sql, [id])
|
||||
}
|
||||
|
||||
const markAllAsRead = () => {
|
||||
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)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
machineEvents: dbm.machineEvents,
|
||||
addHighValueTx,
|
||||
addCashCassetteWarning,
|
||||
addCryptoBalanceWarning,
|
||||
addErrorNotification,
|
||||
getUnreadCassetteNotifications,
|
||||
getAllValidNotifications,
|
||||
getValidNotifications,
|
||||
invalidateNotification,
|
||||
addComplianceNotification
|
||||
machineEvents: dbm.machineEvents,
|
||||
addNotification,
|
||||
getAllValidNotifications,
|
||||
invalidateNotification,
|
||||
batchInvalidate,
|
||||
clearBlacklistNotification,
|
||||
getValidNotifications,
|
||||
getNotifications,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
hasUnreadNotifications
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,16 @@ const _ = require('lodash/fp')
|
|||
const utils = require('./utils')
|
||||
const sms = require('../sms')
|
||||
|
||||
function printSmsAlerts(alertRec, config) {
|
||||
function printSmsAlerts (alertRec, config) {
|
||||
let alerts = []
|
||||
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.general)
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
})
|
||||
_.forEach(device => {
|
||||
alerts = _.concat(alerts, utils.deviceAlerts(config, alertRec, device))
|
||||
}, _.keys(alertRec.devices))
|
||||
|
||||
if (alerts.length === 0) return null
|
||||
|
||||
|
|
@ -27,12 +21,12 @@ 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]),
|
||||
_.map('cryptoCode', entry[1])
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -43,16 +37,11 @@ function printSmsAlerts(alertRec, config) {
|
|||
}, _.toPairs(alertsMap))
|
||||
|
||||
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(', ')})`
|
||||
if (_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) return it.codeDisplay
|
||||
if (_.isEmpty(it.machineNames)) return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
|
||||
return `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
||||
})
|
||||
|
||||
|
||||
const displayAlertTypes = _.compose(
|
||||
_.uniq,
|
||||
mapByCodeDisplay,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ const BigNumber = require('../../../lib/bn')
|
|||
|
||||
const notifier = require('..')
|
||||
const utils = require('../utils')
|
||||
const queries = require("../queries")
|
||||
const emailFuncs = require('../email')
|
||||
const smsFuncs = require('../sms')
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -83,76 +81,79 @@ const notifSettings = {
|
|||
email_errors: false,
|
||||
sms_errors: true,
|
||||
sms_transactions: true,
|
||||
highValueTransaction: Infinity, //this will make highValueTx always false
|
||||
highValueTransaction: Infinity, // this will make highValueTx always false
|
||||
sms: {
|
||||
active: true,
|
||||
errors: true,
|
||||
transactions: false // force early return
|
||||
transactions: false // force early return
|
||||
},
|
||||
email: {
|
||||
active: false,
|
||||
errors: false,
|
||||
transactions: false // force early return
|
||||
transactions: false // force early return
|
||||
}
|
||||
}
|
||||
|
||||
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => {
|
||||
expect.assertions(1)
|
||||
await expect(
|
||||
notifier.checkNotification({
|
||||
getNotificationConfig: () => ({
|
||||
sms: { active: false, errors: false },
|
||||
email: { active: false, errors: false }
|
||||
describe('checkNotifications', () => {
|
||||
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => {
|
||||
expect.assertions(1)
|
||||
await expect(
|
||||
notifier.checkNotification({
|
||||
getNotificationConfig: () => ({
|
||||
sms: { active: false, errors: false },
|
||||
email: { active: false, errors: false }
|
||||
})
|
||||
})
|
||||
})
|
||||
).resolves.toBe(undefined)
|
||||
})
|
||||
|
||||
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled even if errors or balance are defined to something', async () => {
|
||||
expect.assertions(1)
|
||||
await expect(
|
||||
notifier.checkNotification({
|
||||
getNotificationConfig: () => ({
|
||||
sms: { active: false, errors: true, balance: true },
|
||||
email: { active: false, errors: true, balance: true }
|
||||
).resolves.toBe(undefined)
|
||||
})
|
||||
|
||||
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled even if errors or balance are defined to something', async () => {
|
||||
expect.assertions(1)
|
||||
await expect(
|
||||
notifier.checkNotification({
|
||||
getNotificationConfig: () => ({
|
||||
sms: { active: false, errors: true, balance: true },
|
||||
email: { active: false, errors: true, balance: true }
|
||||
})
|
||||
})
|
||||
})
|
||||
).resolves.toBe(undefined)
|
||||
})
|
||||
|
||||
test("Check Pings should return code PING for devices that haven't been pinged recently", () => {
|
||||
expect(
|
||||
notifier.checkPings([
|
||||
{
|
||||
deviceId:
|
||||
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
lastPing: '2020-11-16T13:11:03.169Z',
|
||||
name: 'Abc123'
|
||||
}
|
||||
])
|
||||
).toMatchObject({
|
||||
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [
|
||||
{ code: 'PING', machineName: 'Abc123' }
|
||||
]
|
||||
).resolves.toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
test('Checkpings returns empty array as the value for the id prop, if the lastPing is more recent than 60 seconds', () => {
|
||||
expect(
|
||||
notifier.checkPings([
|
||||
{
|
||||
deviceId:
|
||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
lastPing: new Date(),
|
||||
name: 'Abc123'
|
||||
}
|
||||
])
|
||||
).toMatchObject({
|
||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': []
|
||||
describe('checkPings', () => {
|
||||
test("Check Pings should return code PING for devices that haven't been pinged recently", () => {
|
||||
expect(
|
||||
notifier.checkPings([
|
||||
{
|
||||
deviceId:
|
||||
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
lastPing: '2020-11-16T13:11:03.169Z',
|
||||
name: 'Abc123'
|
||||
}
|
||||
])
|
||||
).toMatchObject({
|
||||
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [
|
||||
{ code: 'PING', machineName: 'Abc123' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('Checkpings returns empty array as the value for the id prop, if the lastPing is more recent than 60 seconds', () => {
|
||||
expect(
|
||||
notifier.checkPings([
|
||||
{
|
||||
deviceId:
|
||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
lastPing: new Date(),
|
||||
name: 'Abc123'
|
||||
}
|
||||
])
|
||||
).toMatchObject({
|
||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': []
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test('Check notification resolves to undefined if shouldNotAlert is called and is true', async () => {
|
||||
const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert')
|
||||
mockShouldNotAlert.mockReturnValue(true)
|
||||
|
|
@ -190,25 +191,42 @@ test('If no alert fingerprint and inAlert is true, exits on call to sendNoAlerts
|
|||
expect(mockSendNoAlerts).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// vvv tests for checkstuckscreen...
|
||||
test('checkStuckScreen returns [] when no events are found', () => {
|
||||
expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([])
|
||||
})
|
||||
describe('checkStuckScreen', () => {
|
||||
test('checkStuckScreen returns [] when no events are found', () => {
|
||||
expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([])
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns [] if most recent event is idle', () => {
|
||||
// device_time is what matters for the sorting of the events by recency
|
||||
expect(
|
||||
notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '1999-11-23T19:30:29.177Z',
|
||||
age: 157352628.123
|
||||
},
|
||||
test('checkStuckScreen returns [] if most recent event is idle', () => {
|
||||
// device_time is what matters for the sorting of the events by recency
|
||||
expect(
|
||||
notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '1999-11-23T19:30:29.177Z',
|
||||
age: 157352628.123
|
||||
},
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":true}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '2020-11-23T19:30:29.177Z',
|
||||
age: 157352628.123
|
||||
}
|
||||
])
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns object array of length 1 with prop code: "STALE" if age > STALE_STATE', () => {
|
||||
// there is an age 0 and an isIdle true in the first object but it will be below the second one in the sorting order and thus ignored
|
||||
const result = notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
|
|
@ -216,122 +234,106 @@ test('checkStuckScreen returns [] if most recent event is idle', () => {
|
|||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":true}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '1999-11-23T19:30:29.177Z',
|
||||
age: 0
|
||||
},
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '2020-11-23T19:30:29.177Z',
|
||||
age: 157352628.123
|
||||
}
|
||||
])
|
||||
).toEqual([])
|
||||
expect(result[0]).toMatchObject({ code: 'STALE' })
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns empty array if age < STALE_STATE', () => {
|
||||
const STALE_STATE = require('../codes').STALE_STATE
|
||||
const result1 = notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '2020-11-23T19:30:29.177Z',
|
||||
age: 0
|
||||
}
|
||||
])
|
||||
const result2 = notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '2020-11-23T19:30:29.177Z',
|
||||
age: STALE_STATE
|
||||
}
|
||||
])
|
||||
expect(result1).toEqual([])
|
||||
expect(result2).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns object array of length 1 with prop code: "STALE" if age > STALE_STATE', () => {
|
||||
// there is an age 0 and an isIdle true in the first object but it will be below the second one in the sorting order and thus ignored
|
||||
const result = notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":true}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '1999-11-23T19:30:29.177Z',
|
||||
age: 0
|
||||
},
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '2020-11-23T19:30:29.177Z',
|
||||
age: 157352628.123
|
||||
}
|
||||
])
|
||||
expect(result[0]).toMatchObject({ code: 'STALE' })
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns empty array if age < STALE_STATE', () => {
|
||||
const STALE_STATE = require('../codes').STALE_STATE
|
||||
const result1 = notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '2020-11-23T19:30:29.177Z',
|
||||
age: 0
|
||||
}
|
||||
])
|
||||
const result2 = notifier.checkStuckScreen([
|
||||
{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: '2020-11-23T19:30:29.209Z',
|
||||
device_time: '2020-11-23T19:30:29.177Z',
|
||||
age: STALE_STATE
|
||||
}
|
||||
])
|
||||
expect(result1).toEqual([])
|
||||
expect(result2).toEqual([])
|
||||
})
|
||||
|
||||
test("calls sendRedemptionMessage if !zeroConf and rec.isRedemption", async () => {
|
||||
test('calls sendRedemptionMessage if !zeroConf and rec.isRedemption', () => {
|
||||
const configManager = require('../../new-config-manager')
|
||||
const settingsLoader = require('../../new-settings-loader')
|
||||
|
||||
const loadLatest = jest.spyOn(settingsLoader, 'loadLatest')
|
||||
const loadLatest = jest.spyOn(settingsLoader, 'loadLatest')
|
||||
const getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications')
|
||||
const getCashOut = jest.spyOn(configManager, 'getCashOut')
|
||||
|
||||
// sendRedemptionMessage will cause this func to be called
|
||||
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => rec)
|
||||
|
||||
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 } })
|
||||
|
||||
const response = await notifier.transactionNotify(tx, {isRedemption: true})
|
||||
|
||||
// this type of response implies sendRedemptionMessage was called
|
||||
expect(response[0]).toMatchObject({
|
||||
sms: {
|
||||
body: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00 - It was just dispensed successfully"
|
||||
},
|
||||
email: {
|
||||
subject: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00",
|
||||
body: 'It was just dispensed successfully'
|
||||
}
|
||||
return notifier.transactionNotify(tx, { isRedemption: true }).then(response => {
|
||||
// this type of response implies sendRedemptionMessage was called
|
||||
expect(response[0]).toMatchObject({
|
||||
sms: {
|
||||
body: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00 - It was just dispensed successfully"
|
||||
},
|
||||
email: {
|
||||
subject: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00",
|
||||
body: 'It was just dispensed successfully'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("calls sendTransactionMessage if !zeroConf and !rec.isRedemption", async () => {
|
||||
test('calls sendTransactionMessage if !zeroConf and !rec.isRedemption', async () => {
|
||||
const configManager = require('../../new-config-manager')
|
||||
const settingsLoader = require('../../new-settings-loader')
|
||||
const machineLoader = require('../../machine-loader')
|
||||
|
||||
const loadLatest = jest.spyOn(settingsLoader, 'loadLatest')
|
||||
const loadLatest = jest.spyOn(settingsLoader, 'loadLatest')
|
||||
const getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications')
|
||||
const getCashOut = jest.spyOn(configManager, 'getCashOut')
|
||||
const getMachineName = jest.spyOn(machineLoader, 'getMachineName')
|
||||
const buildTransactionMessage = jest.spyOn(utils, 'buildTransactionMessage')
|
||||
|
||||
// sendMessage on emailFuncs isn't called because it is disabled in getGlobalNotifications.mockReturnValue
|
||||
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => ({prop: rec}))
|
||||
buildTransactionMessage.mockImplementation(() => ["mock message", false])
|
||||
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 } })
|
||||
|
||||
const response = await notifier.transactionNotify(tx, {isRedemption: false})
|
||||
const response = await notifier.transactionNotify(tx, { isRedemption: false })
|
||||
|
||||
// If the return object is this, it means the code went through all the functions expected to go through if
|
||||
// If the return object is this, it means the code went through all the functions expected to go through if
|
||||
// getMachineName, buildTransactionMessage and sendTransactionMessage were called, in this order
|
||||
expect(response).toEqual([{prop: 'mock message'}])
|
||||
})
|
||||
expect(response).toEqual([{ prop: 'mock message' }])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -26,75 +26,79 @@ const notifications = {
|
|||
email: { active: false, errors: false }
|
||||
}
|
||||
|
||||
test('Build alert fingerprint returns null if no sms or email alerts', () => {
|
||||
expect(
|
||||
utils.buildAlertFingerprint(
|
||||
{
|
||||
devices: {},
|
||||
deviceNames: {},
|
||||
general: []
|
||||
describe('buildAlertFingerprint', () => {
|
||||
test('Build alert fingerprint returns null if no sms or email alerts', () => {
|
||||
expect(
|
||||
utils.buildAlertFingerprint(
|
||||
{
|
||||
devices: {},
|
||||
deviceNames: {},
|
||||
general: []
|
||||
},
|
||||
notifications
|
||||
)
|
||||
).toBe(null)
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns null if sms and email are disabled', () => {
|
||||
expect(
|
||||
utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: false, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
})
|
||||
).toBe(null)
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns hash if email or [sms] are enabled and there are alerts in alertrec', () => {
|
||||
expect(
|
||||
typeof utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: true, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
})
|
||||
).toBe('string')
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns hash if [email] or sms are enabled and there are alerts in alertrec', () => {
|
||||
expect(
|
||||
typeof utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: false, errors: false },
|
||||
email: { active: true, errors: true }
|
||||
})
|
||||
).toBe('string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendNoAlerts', () => {
|
||||
test('Send no alerts returns empty object with sms and email disabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, false, false)).toEqual({})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with sms prop with sms only enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, true, false)).toEqual({
|
||||
sms: {
|
||||
body: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with sms and email prop with both enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, true, true)).toEqual({
|
||||
email: {
|
||||
body: 'No errors are reported for your machines.',
|
||||
subject: '[Lamassu] All clear'
|
||||
},
|
||||
notifications
|
||||
)
|
||||
).toBe(null)
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns null if sms and email are disabled', () => {
|
||||
expect(
|
||||
utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: false, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
sms: {
|
||||
body: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns hash if email or [sms] are enabled and there are alerts in alertrec', () => {
|
||||
expect(
|
||||
typeof utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: true, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
test('Send no alerts returns object with email prop if only email is enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, false, true)).toEqual({
|
||||
email: {
|
||||
body: 'No errors are reported for your machines.',
|
||||
subject: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
).toBe('string')
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns hash if [email] or sms are enabled and there are alerts in alertrec', () => {
|
||||
expect(
|
||||
typeof utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: false, errors: false },
|
||||
email: { active: true, errors: true }
|
||||
})
|
||||
).toBe('string')
|
||||
})
|
||||
|
||||
test('Send no alerts returns empty object with sms and email disabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, false, false)).toEqual({})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with sms prop with sms only enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, true, false)).toEqual({
|
||||
sms: {
|
||||
body: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with sms and email prop with both enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, true, true)).toEqual({
|
||||
email: {
|
||||
body: 'No errors are reported for your machines.',
|
||||
subject: '[Lamassu] All clear'
|
||||
},
|
||||
sms: {
|
||||
body: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with email prop if only email is enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, false, true)).toEqual({
|
||||
email: {
|
||||
body: 'No errors are reported for your machines.',
|
||||
subject: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,14 +11,26 @@ const {
|
|||
ALERT_SEND_INTERVAL
|
||||
} = require('./codes')
|
||||
|
||||
function parseEventNote(event) {
|
||||
const DETAIL_TEMPLATE = {
|
||||
deviceId: '',
|
||||
cryptoCode: '',
|
||||
code: '',
|
||||
cassette: '',
|
||||
age: '',
|
||||
customerId: '',
|
||||
cryptoAddress: '',
|
||||
direction: '',
|
||||
fiat: '',
|
||||
fiatCode: ''
|
||||
}
|
||||
|
||||
function parseEventNote (event) {
|
||||
return _.set('note', JSON.parse(event.note), event)
|
||||
}
|
||||
|
||||
function checkPing(device) {
|
||||
const age = +Date.now() - +new Date(device.lastPing)
|
||||
if (age > NETWORK_DOWN_TIME)
|
||||
return [{ code: PING, age, machineName: device.name }]
|
||||
function checkPing (device) {
|
||||
const age = Date.now() - (new Date(device.lastPing).getTime())
|
||||
if (age > NETWORK_DOWN_TIME) return [{ code: PING, age, machineName: device.name }]
|
||||
return []
|
||||
}
|
||||
|
||||
|
|
@ -49,28 +61,7 @@ const shouldNotAlert = currentAlertFingerprint => {
|
|||
)
|
||||
}
|
||||
|
||||
function getAlertTypes(alertRec, config) {
|
||||
let alerts = []
|
||||
if (!isActive(config)) return alerts
|
||||
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.general)
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
})
|
||||
|
||||
return alerts
|
||||
}
|
||||
|
||||
function buildAlertFingerprint(alertRec, notifications) {
|
||||
function buildAlertFingerprint (alertRec, notifications) {
|
||||
const sms = getAlertTypes(alertRec, notifications.sms)
|
||||
const email = getAlertTypes(alertRec, notifications.email)
|
||||
if (sms.length === 0 && email.length === 0) return null
|
||||
|
|
@ -82,7 +73,7 @@ function buildAlertFingerprint(alertRec, notifications) {
|
|||
return crypto.createHash('sha256').update(subject).digest('hex')
|
||||
}
|
||||
|
||||
function sendNoAlerts(plugins, smsEnabled, emailEnabled) {
|
||||
function sendNoAlerts (plugins, smsEnabled, emailEnabled) {
|
||||
const subject = '[Lamassu] All clear'
|
||||
|
||||
let rec = {}
|
||||
|
|
@ -117,8 +108,8 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
|||
status = !isCashOut
|
||||
? 'Successful'
|
||||
: !rec.isRedemption
|
||||
? 'Successful & awaiting redemption'
|
||||
: 'Successful & dispensed'
|
||||
? 'Successful & awaiting redemption'
|
||||
: 'Successful & dispensed'
|
||||
}
|
||||
|
||||
const body = `
|
||||
|
|
@ -145,7 +136,7 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
|||
}, highValueTx]
|
||||
}
|
||||
|
||||
function formatCurrency(num, code) {
|
||||
function formatCurrency (num, code) {
|
||||
return numeral(num).format('0,0.00') + ' ' + code
|
||||
}
|
||||
|
||||
|
|
@ -153,6 +144,42 @@ function formatAge (age, settings) {
|
|||
return prettyMs(age, settings)
|
||||
}
|
||||
|
||||
function buildDetail (obj) {
|
||||
// obj validation
|
||||
const objKeys = _.keys(obj)
|
||||
const detailKeys = _.keys(DETAIL_TEMPLATE)
|
||||
if ((_.difference(objKeys, detailKeys)).length > 0) {
|
||||
return Promise.reject(new Error('Error when building detail object: invalid properties'))
|
||||
}
|
||||
return { ...DETAIL_TEMPLATE, ...obj }
|
||||
}
|
||||
|
||||
function deviceAlerts (config, alertRec, device) {
|
||||
let alerts = []
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
function getAlertTypes (alertRec, config) {
|
||||
let alerts = []
|
||||
if (!isActive(config)) return alerts
|
||||
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.general)
|
||||
}
|
||||
|
||||
_.forEach(device => {
|
||||
alerts = _.concat(alerts, deviceAlerts(config, alertRec, device))
|
||||
}, _.keys(alertRec.devices))
|
||||
return alerts
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
codeDisplay,
|
||||
parseEventNote,
|
||||
|
|
@ -167,5 +194,7 @@ module.exports = {
|
|||
sendNoAlerts,
|
||||
buildTransactionMessage,
|
||||
formatCurrency,
|
||||
formatAge
|
||||
formatAge,
|
||||
buildDetail,
|
||||
deviceAlerts
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue