diff --git a/lib/blacklist.js b/lib/blacklist.js
index d6fa4f18..d5d45027 100644
--- a/lib/blacklist.js
+++ b/lib/blacklist.js
@@ -1,4 +1,5 @@
const db = require('./db')
+const notifier = require('./notifier')
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
const getBlacklist = () => {
@@ -13,11 +14,9 @@ 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;
- UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND detail IN ($3^)`
-
- const detail = `'${cryptoCode}_BLOCKED_${address}', '${cryptoCode}_REUSED_${address}'`
- return db.none(sql, [cryptoCode, address, detail])
+ const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2`
+ notifier.clearBlacklistNotification(cryptoCode, address)
+ return db.none(sql, [cryptoCode, address])
}
const insertIntoBlacklist = (cryptoCode, address) => {
diff --git a/lib/cash-in/cash-in-tx.js b/lib/cash-in/cash-in-tx.js
index 77e6276a..3c9f8463 100644
--- a/lib/cash-in/cash-in-tx.js
+++ b/lib/cash-in/cash-in-tx.js
@@ -8,7 +8,7 @@ const plugins = require('../plugins')
const logger = require('../logger')
const settingsLoader = require('../new-settings-loader')
const configManager = require('../new-config-manager')
-const notifier = require("../notifier/index")
+const notifier = require('../notifier')
const cashInAtomic = require('./cash-in-atomic')
const cashInLow = require('./cash-in-low')
@@ -16,7 +16,7 @@ const cashInLow = require('./cash-in-low')
const PENDING_INTERVAL = '60 minutes'
const MAX_PENDING = 10
-module.exports = {post, monitorPending, cancel, PENDING_INTERVAL}
+module.exports = { post, monitorPending, cancel, PENDING_INTERVAL }
function post (machineTx, pi) {
return db.tx(cashInAtomic.atomic(machineTx, pi))
@@ -29,10 +29,10 @@ function post (machineTx, pi) {
.then(([{ config }, blacklistItems]) => {
const rejectAddressReuseActive = configManager.getCompliance(config).rejectAddressReuse
- if (_.some(it => it.created_by_operator === true)(blacklistItems)) {
+ if (_.some(it => it.created_by_operator)(blacklistItems)) {
blacklisted = true
notifier.blacklistNotify(r.tx, false)
- } else if (_.some(it => it.created_by_operator === false)(blacklistItems) && rejectAddressReuseActive) {
+ } else if (_.some(it => !it.created_by_operator)(blacklistItems) && rejectAddressReuseActive) {
notifier.blacklistNotify(r.tx, true)
addressReuse = true
}
@@ -65,7 +65,7 @@ function logAction (rec, tx) {
}
function logActionById (action, _rec, txId) {
- const rec = _.assign(_rec, {action, tx_id: txId})
+ const rec = _.assign(_rec, { action, tx_id: txId })
const sql = pgp.helpers.insert(rec, null, 'cash_in_actions')
return db.none(sql)
diff --git a/lib/cash-out/cash-out-atomic.js b/lib/cash-out/cash-out-atomic.js
index ea65402f..f3365123 100644
--- a/lib/cash-out/cash-out-atomic.js
+++ b/lib/cash-out/cash-out-atomic.js
@@ -9,8 +9,6 @@ const helper = require('./cash-out-helper')
const cashOutActions = require('./cash-out-actions')
const cashOutLow = require('./cash-out-low')
-const notifier = require("../notifier/index")
-
const toObj = helper.toObj
module.exports = {atomic}
@@ -124,10 +122,8 @@ function updateCassettes (t, tx) {
tx.deviceId
]
- return t.one(sql, values).then(r => {
- notifier.cashCassettesNotify(r, tx.deviceId)
- return socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId}))
- })
+ return t.one(sql, values)
+ .then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
}
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
diff --git a/lib/customers.js b/lib/customers.js
index 0e3b535a..6a336a3c 100644
--- a/lib/customers.js
+++ b/lib/customers.js
@@ -15,7 +15,8 @@ const complianceOverrides = require('./compliance_overrides')
const users = require('./users')
const options = require('./options')
const writeFile = util.promisify(fs.writeFile)
-
+const notifierQueries = require('./notifier/queries')
+const notifierUtils = require('./notifier/utils')
const NUM_RESULTS = 1000
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
const frontCameraBaseDir = _.get('frontCameraDir', options)
@@ -115,7 +116,7 @@ async function updateCustomer (id, data, userToken) {
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
' where id=$1'
- invalidateCustomerNotifications(id, formattedData)
+ invalidateCustomerNotifications(id, formattedData).catch(console.error)
await db.none(sql, [id])
@@ -123,12 +124,10 @@ async function updateCustomer (id, data, userToken) {
}
const invalidateCustomerNotifications = (id, data) => {
- let detail = '';
- if(data.authorized_override === 'verified') {
- detail = `BLOCKED_${id}`
- }
- const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND detail = $1`
- return db.none(sql, [detail])
+ if (data.authorized_override !== 'verified') return Promise.resolve()
+
+ const detailB = notifierUtils.buildDetail({ code: 'BLOCKED', customerId: id })
+ return notifierQueries.invalidateNotification(detailB, 'compliance').catch(console.error)
}
/**
@@ -409,7 +408,7 @@ function populateOverrideUsernames (customer) {
return users.getByIds(queryTokens)
.then(usersList => {
return _.map(userField => {
- const user = _.find({token: userField.token}, usersList)
+ const user = _.find({ token: userField.token }, usersList)
return {
[userField.field]: user ? user.name : null
}
@@ -443,7 +442,7 @@ function batch () {
// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored
/**
- * Query all customers, ordered by last activity
+ * Query all customers, ordered by last activity
* and with aggregate columns based on their
* transactions
*
@@ -481,7 +480,7 @@ function getCustomersList () {
}
/**
- * Query all customers, ordered by last activity
+ * Query all customers, ordered by last activity
* and with aggregate columns based on their
* transactions
*
diff --git a/lib/machine-loader.js b/lib/machine-loader.js
index e3b787f9..0ba4a610 100644
--- a/lib/machine-loader.js
+++ b/lib/machine-loader.js
@@ -1,18 +1,17 @@
const _ = require('lodash/fp')
const axios = require('axios')
-const logger = require('./logger')
const db = require('./db')
const pairing = require('./pairing')
const notifier = require('./notifier')
const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader')
-
-module.exports = {getMachineName, getMachines, getMachine, getMachineNames, setMachine}
+const notifierUtils = require('./notifier/utils')
+const notifierQueries = require('./notifier/queries')
function getMachines () {
- return db.any('select * from devices where display=TRUE order by created')
+ return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
.then(rr => rr.map(r => ({
deviceId: r.device_id,
cashbox: r.cashbox,
@@ -36,10 +35,10 @@ function getConfig (defaultConfig) {
}
function getMachineNames (config) {
- const fullyFunctionalStatus = {label: 'Fully functional', type: 'success'}
- const unresponsiveStatus = {label: 'Unresponsive', type: 'error'}
- const stuckStatus = {label: 'Stuck', type: 'error'}
-
+ const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
+ const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
+ const stuckStatus = { label: 'Stuck', type: 'error' }
+
return Promise.all([getMachines(), getConfig(config)])
.then(([machines, config]) => Promise.all(
[machines, notifier.checkPings(machines), dbm.machineEvents(), config]
@@ -49,7 +48,7 @@ function getMachineNames (config) {
if (ping && ping.age) return unresponsiveStatus
if (stuck && stuck.age) return stuckStatus
-
+
return fullyFunctionalStatus
}
@@ -57,7 +56,7 @@ function getMachineNames (config) {
const cashOutConfig = configManager.getCashOut(r.deviceId, config)
const cashOut = !!cashOutConfig.active
-
+
const statuses = [
getStatus(
_.first(pings[r.deviceId]),
@@ -65,7 +64,7 @@ function getMachineNames (config) {
)
]
- return _.assign(r, {cashOut, statuses})
+ return _.assign(r, { cashOut, statuses })
}
return _.map(addName, machines)
@@ -83,31 +82,29 @@ function getMachineNames (config) {
* @returns {string} machine name
*/
function getMachineName (machineId) {
- const sql = 'select * from devices where device_id=$1'
+ const sql = 'SELECT * FROM devices WHERE device_id=$1'
return db.oneOrNone(sql, [machineId])
.then(it => it.name)
}
function getMachine (machineId) {
- const sql = 'select * from devices where device_id=$1'
+ const sql = 'SELECT * FROM devices WHERE device_id=$1'
return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res))
}
function renameMachine (rec) {
- const sql = 'update devices set name=$1 where device_id=$2'
+ const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
return db.none(sql, [rec.newName, rec.deviceId])
}
function resetCashOutBills (rec) {
- const sql = `
- update devices set cassette1=$1, cassette2=$2 where device_id=$3;
- update notifications set read = 't', valid = 'f' where read = 'f' AND valid = 't' AND device_id = $3 AND type = 'fiatBalance';
- `
- return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
+ const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
+ const sql = `UPDATE devices SET cassette1=$1, cassette2=$2 WHERE device_id=$3;`
+ return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
}
function emptyCashInBills (rec) {
- const sql = 'update devices set cashbox=0 where device_id=$1'
+ const sql = 'UPDATE devices SET cashbox=0 WHERE device_id=$1'
return db.none(sql, [rec.deviceId])
}
@@ -145,3 +142,5 @@ function setMachine (rec) {
default: throw new Error('No such action: ' + rec.action)
}
}
+
+module.exports = { getMachineName, getMachines, getMachine, getMachineNames, setMachine }
diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js
index 32250765..c363faff 100644
--- a/lib/new-admin/graphql/schema.js
+++ b/lib/new-admin/graphql/schema.js
@@ -12,8 +12,9 @@ const logs = require('../../logs')
const settingsLoader = require('../../new-settings-loader')
// const tokenManager = require('../../token-manager')
const blacklist = require('../../blacklist')
-const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch
+const machineEventsByIdBatch = require('../../postgresql_interface').machineEventsByIdBatch
const promoCodeManager = require('../../promo-codes')
+const notifierQueries = require('../../notifier/queries')
const serverVersion = require('../../../package.json').version
const transactions = require('../transactions')
@@ -247,6 +248,17 @@ const typeDefs = gql`
rate: Float
}
+ type Notification {
+ id: ID!
+ deviceName: String
+ type: String
+ detail: JSON
+ message: String
+ created: Date
+ read: Boolean
+ valid: Boolean
+ }
+
type Query {
countries: [Country]
currencies: [Currency]
@@ -279,6 +291,8 @@ const typeDefs = gql`
promoCodes: [PromoCode]
cryptoRates: JSONObject
fiatRates: [Rate]
+ notifications: [Notification]
+ hasUnreadNotifications: Boolean
}
type SupportLogsResponse {
@@ -312,6 +326,8 @@ const typeDefs = gql`
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
createPromoCode(code: String!, discount: Int!): PromoCode
deletePromoCode(codeId: ID!): PromoCode
+ clearNotification(id: ID!): Notification
+ clearAllNotifications: Notification
}
`
@@ -344,9 +360,9 @@ const resolvers = {
customers: () => customers.getCustomersList(),
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
funding: () => funding.getFunding(),
- machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
+ machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset),
- machineLogsCsv: (...[, { deviceId, from, until, limit, offset }]) =>
+ machineLogsCsv: (...[, { deviceId, from, until, limit, offset }]) =>
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset).then(parseAsync),
serverVersion: () => serverVersion,
uptime: () => supervisor.getAllProcessInfo(),
@@ -373,7 +389,9 @@ const resolvers = {
}
})
}),
- fiatRates: () => forex.getFiatRates()
+ fiatRates: () => forex.getFiatRates(),
+ notifications: () => notifierQueries.getNotifications(),
+ hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications()
},
Mutation: {
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }),
@@ -397,7 +415,9 @@ const resolvers = {
blacklist.insertIntoBlacklist(cryptoCode, address),
// revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
createPromoCode: (...[, { code, discount }]) => promoCodeManager.createPromoCode(code, discount),
- deletePromoCode: (...[, { codeId }]) => promoCodeManager.deletePromoCode(codeId)
+ deletePromoCode: (...[, { codeId }]) => promoCodeManager.deletePromoCode(codeId),
+ clearNotification: (...[, { id }]) => notifierQueries.markAsRead(id),
+ clearAllNotifications: () => notifierQueries.markAllAsRead()
}
}
diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js
index 1f88ce8b..916c6c5b 100644
--- a/lib/new-settings-loader.js
+++ b/lib/new-settings-loader.js
@@ -44,18 +44,6 @@ const configSql = 'insert into user_config (type, data, valid, schema_version) v
function saveConfig (config) {
return loadLatestConfigOrNone()
.then(currentConfig => {
- if(config.notifications_cryptoHighBalance || config.notifications_cryptoLowBalance) {
- clearCryptoBalanceNotifications(currentConfig, config, false)
- }
- if(config.notifications_cryptoBalanceOverrides) {
- clearCryptoBalanceNotifications(currentConfig.notifications_cryptoBalanceOverrides, config.notifications_cryptoBalanceOverrides, true)
- }
- if(config.notifications_fiatBalanceCassette1 || config.notifications_fiatBalanceCassette2) {
- clearCassetteNotifications(currentConfig, config, false)
- }
- if(config.notifications_fiatBalanceOverrides) {
- clearCassetteNotifications(currentConfig.notifications_fiatBalanceOverrides, config.notifications_fiatBalanceOverrides, true)
- }
const newConfig = _.assign(currentConfig, config)
return db.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
})
diff --git a/lib/notifier.js b/lib/notifier.js
deleted file mode 100644
index 96941bea..00000000
--- a/lib/notifier.js
+++ /dev/null
@@ -1,351 +0,0 @@
-const crypto = require('crypto')
-const _ = require('lodash/fp')
-const prettyMs = require('pretty-ms')
-const numeral = require('numeral')
-
-const dbm = require('./postgresql_interface')
-const db = require('./db')
-const T = require('./time')
-const logger = require('./logger')
-
-const STALE_STATE = 7 * T.minute
-const NETWORK_DOWN_TIME = 1 * T.minute
-const ALERT_SEND_INTERVAL = T.hour
-
-const PING = 'PING'
-const STALE = 'STALE'
-const LOW_CRYPTO_BALANCE = 'LOW_CRYPTO_BALANCE'
-const HIGH_CRYPTO_BALANCE = 'HIGH_CRYPTO_BALANCE'
-const CASH_BOX_FULL = 'CASH_BOX_FULL'
-const LOW_CASH_OUT = 'LOW_CASH_OUT'
-
-const CODES_DISPLAY = {
- PING: 'Machine Down',
- STALE: 'Machine Stuck',
- LOW_CRYPTO_BALANCE: 'Low Crypto Balance',
- HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
- CASH_BOX_FULL: 'Cash box full',
- LOW_CASH_OUT: 'Low Cash-out'
-}
-
-let alertFingerprint
-let lastAlertTime
-
-function codeDisplay (code) {
- return CODES_DISPLAY[code]
-}
-
-function jsonParse (event) {
- return _.set('note', JSON.parse(event.note), event)
-}
-
-function sameState (a, b) {
- return a.note.txId === b.note.txId && a.note.state === b.note.state
-}
-
-function sendNoAlerts (plugins, smsEnabled, emailEnabled) {
- const subject = '[Lamassu] All clear'
-
- let rec = {}
- if (smsEnabled) {
- rec = _.set(['sms', 'body'])(subject)(rec)
- }
-
- if (emailEnabled) {
- rec = _.set(['email', 'subject'])(subject)(rec)
- rec = _.set(['email', 'body'])('No errors are reported for your machines.')(rec)
- }
-
- return plugins.sendMessage(rec)
-}
-
-function checkNotification (plugins) {
- const notifications = plugins.getNotificationConfig()
- const isActive = it => it.active && (it.balance || it.errors)
- const smsEnabled = isActive(notifications.sms)
- const emailEnabled = isActive(notifications.email)
-
- if (!smsEnabled && !emailEnabled) return Promise.resolve()
-
- return checkStatus(plugins)
- .then(alertRec => {
- const currentAlertFingerprint = buildAlertFingerprint(alertRec, notifications)
- if (!currentAlertFingerprint) {
- const inAlert = !!alertFingerprint
- alertFingerprint = null
- lastAlertTime = null
- if (inAlert) return sendNoAlerts(plugins, smsEnabled, emailEnabled)
- }
-
- const alertChanged = currentAlertFingerprint === alertFingerprint &&
- lastAlertTime - Date.now() < ALERT_SEND_INTERVAL
- if (alertChanged) return
-
- let rec = {}
- if (smsEnabled) {
- rec = _.set(['sms', 'body'])(printSmsAlerts(alertRec, notifications.sms))(rec)
- }
-
- if (emailEnabled) {
- rec = _.set(['email', 'subject'])(alertSubject(alertRec, notifications.email))(rec)
- rec = _.set(['email', 'body'])(printEmailAlerts(alertRec, notifications.email))(rec)
- }
-
- alertFingerprint = currentAlertFingerprint
- lastAlertTime = Date.now()
-
- return plugins.sendMessage(rec)
- })
- .then(results => {
- if (results && results.length > 0) logger.debug('Successfully sent alerts')
- })
- .catch(logger.error)
-}
-
-const getDeviceTime = _.flow(_.get('device_time'), Date.parse)
-
-function dropRepeatsWith (comparator, arr) {
- const iteratee = (acc, val) => val === acc.last
- ? acc
- : { arr: _.concat(acc.arr, val), last: val }
-
- return _.reduce(iteratee, { arr: [] }, arr).arr
-}
-
-function checkStuckScreen (deviceEvents, machineName) {
- const sortedEvents = _.sortBy(getDeviceTime, _.map(jsonParse, deviceEvents))
- const noRepeatEvents = dropRepeatsWith(sameState, sortedEvents)
- const lastEvent = _.last(noRepeatEvents)
-
- if (!lastEvent) {
- return []
- }
-
- const state = lastEvent.note.state
- const isIdle = lastEvent.note.isIdle
-
- if (isIdle) {
- return []
- }
-
- const age = Math.floor(lastEvent.age)
- if (age > STALE_STATE) {
- return [{ code: STALE, state, age, machineName }]
- }
-
- return []
-}
-
-function checkPing (device) {
- const sql = `select (EXTRACT(EPOCH FROM (now() - updated))) * 1000 AS age from machine_pings
- where device_id=$1`
- const deviceId = device.deviceId
- return db.oneOrNone(sql, [deviceId])
- .then(row => {
- if (!row) return [{ code: PING }]
- if (row.age > NETWORK_DOWN_TIME) return [{ code: PING, age: row.age, machineName: device.name }]
- return []
- })
-}
-
-function checkPings (devices) {
- const deviceIds = _.map('deviceId', devices)
- const promises = _.map(checkPing, devices)
-
- return Promise.all(promises)
- .then(_.zipObject(deviceIds))
-}
-
-function checkStatus (plugins) {
- const alerts = { devices: {}, deviceNames: {} }
-
- return Promise.all([plugins.checkBalances(), dbm.machineEvents(), plugins.getMachineNames()])
- .then(([balances, events, devices]) => {
- return checkPings(devices)
- .then(pings => {
- alerts.general = _.filter(r => !r.deviceId, balances)
- devices.forEach(function (device) {
- const deviceId = device.deviceId
- const deviceName = device.name
- const deviceEvents = events.filter(function (eventRow) {
- return eventRow.device_id === deviceId
- })
-
- const ping = pings[deviceId] || []
- const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
-
- if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {}
- alerts.devices[deviceId].balanceAlerts = _.filter(['deviceId', deviceId], balances)
- alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
-
- alerts.deviceNames[deviceId] = deviceName
- })
-
- return alerts
- })
- })
-}
-
-function formatCurrency (num, code) {
- return numeral(num).format('0,0.00') + ' ' + code
-}
-
-function emailAlert (alert) {
- switch (alert.code) {
- case PING:
- if (alert.age) {
- const pingAge = prettyMs(alert.age, { compact: true, verbose: true })
- return `Machine down for ${pingAge}`
- }
- return 'Machine down for a while.'
- case STALE:
- const stuckAge = prettyMs(alert.age, { compact: true, verbose: true })
- return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
- case LOW_CRYPTO_BALANCE:
- const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
- return `Low balance in ${alert.cryptoCode} [${balance}]`
- case HIGH_CRYPTO_BALANCE:
- const highBalance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
- return `High balance in ${alert.cryptoCode} [${highBalance}]`
- case CASH_BOX_FULL:
- return `Cash box full on ${alert.machineName} [${alert.notes} banknotes]`
- case LOW_CASH_OUT:
- return `Cassette for ${alert.denomination} ${alert.fiatCode} low [${alert.notes} banknotes]`
- }
-}
-
-function emailAlerts (alerts) {
- return alerts.map(emailAlert).join('\n') + '\n'
-}
-
-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'
- }
-
- _.keys(alertRec.devices).forEach(function (device) {
- const deviceName = alertRec.deviceNames[device]
- body = 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)
- })
-
- return body
-}
-
-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)
- }
- })
-
- if (alerts.length === 0) return null
-
- const alertTypes = _.map(codeDisplay, _.uniq(_.map('code', alerts))).sort()
- return '[Lamassu] Errors reported: ' + alertTypes.join(', ')
-}
-
-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)
- }
- })
-
- if (alerts.length === 0) return null
-
- const alertsMap = _.groupBy('code', alerts)
-
- const alertTypes = _.map(entry => {
- const code = entry[0]
- const machineNames = _.filter(_.negate(_.isEmpty), _.map('machineName', entry[1]))
-
- return {
- codeDisplay: codeDisplay(code),
- machineNames
- }
- }, _.toPairs(alertsMap))
-
- const mapByCodeDisplay = _.map(it => _.isEmpty(it.machineNames)
- ? it.codeDisplay : `${it.codeDisplay} (${it.machineNames.join(', ')})`
- )
-
- const displayAlertTypes = _.compose(_.uniq, mapByCodeDisplay, _.sortBy('codeDisplay'))(alertTypes)
-
- return '[Lamassu] Errors reported: ' + displayAlertTypes.join(', ')
-}
-
-function getAlertTypes (alertRec, config) {
- let alerts = []
-
- if (!config.active || (!config.balance && !config.errors)) 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) {
- const sms = getAlertTypes(alertRec, notifications.sms)
- const email = getAlertTypes(alertRec, notifications.email)
-
- if (sms.length === 0 && email.length === 0) return null
-
- const smsTypes = _.map(codeDisplay, _.uniq(_.map('code', sms))).sort()
- const emailTypes = _.map(codeDisplay, _.uniq(_.map('code', email))).sort()
-
- const subject = _.concat(smsTypes, emailTypes).join(', ')
-
- return crypto.createHash('sha256').update(subject).digest('hex')
-}
-
-module.exports = {
- checkNotification,
- checkPings,
- checkStuckScreen
-}
diff --git a/lib/notifier/codes.js b/lib/notifier/codes.js
index 7211b802..8e8a7cc4 100644
--- a/lib/notifier/codes.js
+++ b/lib/notifier/codes.js
@@ -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
}
diff --git a/lib/notifier/email.js b/lib/notifier/email.js
index 7790127b..289180c0 100644
--- a/lib/notifier/email.js
+++ b/lib/notifier/email.js
@@ -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 }
diff --git a/lib/notifier/index.js b/lib/notifier/index.js
index 4484d29f..0466b4dd 100644
--- a/lib/notifier/index.js
+++ b/lib/notifier/index.js
@@ -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
}
diff --git a/lib/notifier/queries.js b/lib/notifier/queries.js
index 3b1fb1b6..76a4eeeb 100644
--- a/lib/notifier/queries.js
+++ b/lib/notifier/queries.js
@@ -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
}
diff --git a/lib/notifier/sms.js b/lib/notifier/sms.js
index 5c1e440b..65a64251 100644
--- a/lib/notifier/sms.js
+++ b/lib/notifier/sms.js
@@ -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,
diff --git a/lib/notifier/test/notifier.test.js b/lib/notifier/test/notifier.test.js
index f7314af8..77d973d1 100644
--- a/lib/notifier/test/notifier.test.js
+++ b/lib/notifier/test/notifier.test.js
@@ -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'}])
-})
\ No newline at end of file
+ expect(response).toEqual([{ prop: 'mock message' }])
+})
diff --git a/lib/notifier/test/utils.test.js b/lib/notifier/test/utils.test.js
index 785d76ef..aee67776 100644
--- a/lib/notifier/test/utils.test.js
+++ b/lib/notifier/test/utils.test.js
@@ -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'
- }
})
})
diff --git a/lib/notifier/utils.js b/lib/notifier/utils.js
index 3f2fc866..15465d7a 100644
--- a/lib/notifier/utils.js
+++ b/lib/notifier/utils.js
@@ -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
}
diff --git a/lib/plugins.js b/lib/plugins.js
index a8e3af0d..bacff63d 100644
--- a/lib/plugins.js
+++ b/lib/plugins.js
@@ -23,7 +23,7 @@ const coinUtils = require('./coin-utils')
const commissionMath = require('./commission-math')
const promoCodes = require('./promo-codes')
-const notifier = require('./notifier/index')
+const notifier = require('./notifier')
const mapValuesWithKey = _.mapValues.convert({
cap: false
@@ -55,8 +55,7 @@ function plugins (settings, deviceId) {
? undefined
: BN(1).add(BN(commissions.cashOut).div(100))
- if (Date.now() - rateRec.timestamp > STALE_TICKER)
- return logger.warn('Stale rate for ' + cryptoCode)
+ if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode)
const rate = rateRec.rates
withCommission ? rates[cryptoCode] = {
@@ -90,10 +89,8 @@ function plugins (settings, deviceId) {
cryptoCodes.forEach((cryptoCode, i) => {
const balanceRec = balanceRecs[i]
- if (!balanceRec)
- return logger.warn('No balance for ' + cryptoCode + ' yet')
- if (Date.now() - balanceRec.timestamp > STALE_BALANCE)
- return logger.warn('Stale balance for ' + cryptoCode)
+ if (!balanceRec) return logger.warn('No balance for ' + cryptoCode + ' yet')
+ if (Date.now() - balanceRec.timestamp > STALE_BALANCE) return logger.warn('Stale balance for ' + cryptoCode)
balances[cryptoCode] = balanceRec.balance
})
@@ -113,13 +110,10 @@ function plugins (settings, deviceId) {
const sumTxs = (sum, tx) => {
const bills = tx.bills
const sameDenominations = a => a[0].denomination === a[1].denomination
- const doDenominationsMatch = _.every(
- sameDenominations,
- _.zip(cassettes, bills)
- )
+ const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
if (!doDenominationsMatch) {
- throw new Error("Denominations don't add up, cassettes were changed.")
+ throw new Error('Denominations don\'t add up, cassettes were changed.')
}
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
@@ -162,41 +156,30 @@ function plugins (settings, deviceId) {
? argv.cassettes.split(',')
: rec.counts
- return Promise.all([
- dbm.cassetteCounts(deviceId),
- cashOutHelper.redeemableTxs(deviceId, excludeTxId)
- ]).then(([rec, _redeemableTxs]) => {
- const redeemableTxs = _.reject(
- _.matchesProperty('id', excludeTxId),
- _redeemableTxs
- )
+ const cassettes = [
+ {
+ denomination: parseInt(denominations[0], 10),
+ count: parseInt(counts[0], 10)
+ },
+ {
+ denomination: parseInt(denominations[1], 10),
+ count: parseInt(counts[1], 10)
+ }
+ ]
- const counts = argv.cassettes ? argv.cassettes.split(',') : rec.counts
-
- const cassettes = [
- {
- denomination: parseInt(denominations[0], 10),
- count: parseInt(counts[0], 10)
- },
- {
- denomination: parseInt(denominations[1], 10),
- count: parseInt(counts[1], 10)
+ try {
+ return {
+ cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
+ virtualCassettes
+ }
+ } catch (err) {
+ logger.error(err)
+ return {
+ cassettes,
+ virtualCassettes
+ }
}
- ]
-
- try {
- return {
- cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
- virtualCassettes
- }
- } catch (err) {
- logger.error(err)
- return {
- cassettes,
- virtualCassettes
- }
- }
- })
+ })
}
function fetchCurrentConfigVersion () {
@@ -206,23 +189,18 @@ function plugins (settings, deviceId) {
order by id desc
limit 1`
- return db.one(sql, ['config']).then(row => row.id)
+ return db.one(sql, ['config'])
+ .then(row => row.id)
}
function mapCoinSettings (coinParams) {
const cryptoCode = coinParams[0]
const cryptoNetwork = coinParams[1]
- const commissions = configManager.getCommissions(
- cryptoCode,
- deviceId,
- settings.config
- )
+ const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
const minimumTx = BN(commissions.minimumTx)
const cashInFee = BN(commissions.fixedFee)
const cashInCommission = BN(commissions.cashIn)
- const cashOutCommission = _.isNumber(commissions.cashOut)
- ? BN(commissions.cashOut)
- : null
+ const cashOutCommission = _.isNumber(commissions.cashOut) ? BN(commissions.cashOut) : null
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
return {
@@ -236,25 +214,15 @@ function plugins (settings, deviceId) {
}
}
- function pollQueries (
- serialNumber,
- deviceTime,
- deviceRec,
- machineVersion,
- machineModel
- ) {
+ function pollQueries (serialNumber, deviceTime, deviceRec, machineVersion, machineModel) {
const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies
- const tickerPromises = cryptoCodes.map(c =>
- ticker.getRates(settings, fiatCode, c)
- )
+ const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
- const testnetPromises = cryptoCodes.map(c =>
- wallet.cryptoNetwork(settings, c)
- )
+ const testnetPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c))
const pingPromise = recordPing(deviceTime, machineVersion, machineModel)
const currentConfigVersionPromise = fetchCurrentConfigVersion()
const currentAvailablePromoCodes = promoCodes.getNumberOfAvailablePromoCodes()
@@ -289,12 +257,7 @@ function plugins (settings, deviceId) {
}
function sendCoins (tx) {
- return wallet.sendCoins(
- settings,
- tx.toAddress,
- tx.cryptoAtoms,
- tx.cryptoCode
- )
+ return wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
}
function recordPing (deviceTime, version, model) {
@@ -305,18 +268,11 @@ function plugins (settings, deviceId) {
}
return Promise.all([
- db.none(
- `insert into machine_pings(device_id, device_time) values($1, $2)
- ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`,
- [deviceId, deviceTime]
- ),
- db.none(
- pgp.helpers.update(devices, null, 'devices') +
- 'WHERE device_id = ${deviceId}',
- {
- deviceId
- }
- )
+ db.none(`insert into machine_pings(device_id, device_time) values($1, $2)
+ ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`, [deviceId, deviceTime]),
+ db.none(pgp.helpers.update(devices, null, 'devices') + 'WHERE device_id = ${deviceId}', {
+ deviceId
+ })
])
}
@@ -348,37 +304,34 @@ function plugins (settings, deviceId) {
}
function fiatBalance (fiatCode, cryptoCode) {
- const commissions = configManager.getCommissions(
- cryptoCode,
- deviceId,
- settings.config
- )
+ const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
return Promise.all([
ticker.getRates(settings, fiatCode, cryptoCode),
wallet.balance(settings, cryptoCode)
- ]).then(([rates, balanceRec]) => {
- if (!rates || !balanceRec) return null
+ ])
+ .then(([rates, balanceRec]) => {
+ if (!rates || !balanceRec) return null
- const rawRate = rates.rates.ask
- const cashInCommission = BN(1).minus(BN(commissions.cashIn).div(100))
- const balance = balanceRec.balance
+ const rawRate = rates.rates.ask
+ const cashInCommission = BN(1).minus(BN(commissions.cashIn).div(100))
+ const balance = balanceRec.balance
- if (!rawRate || !balance) return null
+ if (!rawRate || !balance) return null
- const rate = rawRate.div(cashInCommission)
+ const rate = rawRate.div(cashInCommission)
- const lowBalanceMargin = BN(1.03)
+ const lowBalanceMargin = BN(1.03)
- const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
- const unitScale = cryptoRec.unitScale
- const shiftedRate = rate.shift(-unitScale)
- const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin)
+ const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
+ const unitScale = cryptoRec.unitScale
+ const shiftedRate = rate.shift(-unitScale)
+ const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin)
- return {
- timestamp: balanceRec.timestamp,
- balance: fiatTransferBalance.truncated().toString()
- }
- })
+ return {
+ timestamp: balanceRec.timestamp,
+ balance: fiatTransferBalance.truncated().toString()
+ }
+ })
}
function notifyConfirmation (tx) {
@@ -393,12 +346,13 @@ function plugins (settings, deviceId) {
}
}
- return sms.sendMessage(settings, rec).then(() => {
- const sql = 'update cash_out_txs set notified=$1 where id=$2'
- const values = [true, tx.id]
+ return sms.sendMessage(settings, rec)
+ .then(() => {
+ const sql = 'update cash_out_txs set notified=$1 where id=$2'
+ const values = [true, tx.id]
- return db.none(sql, values)
- })
+ return db.none(sql, values)
+ })
}
function notifyOperator (tx, rec) {
@@ -407,17 +361,14 @@ function plugins (settings, deviceId) {
}
function clearOldLogs () {
- return logs.clearOldLogs().catch(logger.error)
+ return logs.clearOldLogs()
+ .catch(logger.error)
}
function pong () {
- return db
- .none(
- `UPDATE server_events SET created=now() WHERE event_type=$1;
+ return db.none(`UPDATE server_events SET created=now() WHERE event_type=$1;
INSERT INTO server_events (event_type) SELECT $1
- WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`,
- ['ping']
- )
+ WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`, ['ping'])
.catch(logger.error)
}
@@ -458,18 +409,15 @@ function plugins (settings, deviceId) {
const marketTradesQueues = tradesQueues[market]
if (!marketTradesQueues || marketTradesQueues.length === 0) return null
- logger.debug(
- '[%s] tradesQueues size: %d',
- market,
- marketTradesQueues.length
- )
+ logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length)
logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0])
const t1 = Date.now()
- const filtered = marketTradesQueues.filter(tradeEntry => {
- return t1 - tradeEntry.timestamp < TRADE_TTL
- })
+ const filtered = marketTradesQueues
+ .filter(tradeEntry => {
+ return t1 - tradeEntry.timestamp < TRADE_TTL
+ })
const filteredCount = marketTradesQueues.length - filtered.length
@@ -480,14 +428,10 @@ function plugins (settings, deviceId) {
if (filtered.length === 0) return null
- const cryptoAtoms = filtered.reduce(
- (prev, current) => prev.plus(current.cryptoAtoms),
- BN(0)
- )
+ const cryptoAtoms = filtered
+ .reduce((prev, current) => prev.plus(current.cryptoAtoms), BN(0))
- const timestamp = filtered
- .map(r => r.timestamp)
- .reduce((acc, r) => Math.max(acc, r), 0)
+ const timestamp = filtered.map(r => r.timestamp).reduce((acc, r) => Math.max(acc, r), 0)
const consolidatedTrade = {
fiatCode,
@@ -503,15 +447,11 @@ function plugins (settings, deviceId) {
}
function executeTrades () {
- return machineLoader
- .getMachines()
+ return machineLoader.getMachines()
.then(devices => {
const deviceIds = devices.map(device => device.deviceId)
const lists = deviceIds.map(deviceId => {
- const localeConfig = configManager.getLocale(
- deviceId,
- settings.config
- )
+ const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies
@@ -521,9 +461,8 @@ function plugins (settings, deviceId) {
}))
})
- const tradesPromises = _.uniq(_.flatten(lists)).map(r =>
- executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)
- )
+ const tradesPromises = _.uniq(_.flatten(lists))
+ .map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))
return Promise.all(tradesPromises)
})
@@ -538,43 +477,41 @@ function plugins (settings, deviceId) {
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
- return executeTradeForType(tradeEntry).catch(err => {
- tradesQueues[market].push(tradeEntry)
- if (err.name === 'orderTooSmall') return logger.debug(err.message)
- logger.error(err)
- })
+ return executeTradeForType(tradeEntry)
+ .catch(err => {
+ tradesQueues[market].push(tradeEntry)
+ if (err.name === 'orderTooSmall') return logger.debug(err.message)
+ logger.error(err)
+ })
}
function executeTradeForType (_tradeEntry) {
- const expand = te =>
- _.assign(te, {
- cryptoAtoms: te.cryptoAtoms.abs(),
- type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell'
- })
+ const expand = te => _.assign(te, {
+ cryptoAtoms: te.cryptoAtoms.abs(),
+ type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell'
+ })
const tradeEntry = expand(_tradeEntry)
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
- return execute(
- settings,
- tradeEntry.cryptoAtoms,
- tradeEntry.fiatCode,
- tradeEntry.cryptoCode
- )
+ return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
.then(() => recordTrade(tradeEntry))
.catch(err => {
- return recordTrade(tradeEntry, err).then(() => {
- throw err
- })
+ return recordTrade(tradeEntry, err)
+ .then(() => {
+ throw err
+ })
})
}
function convertBigNumFields (obj) {
- const convert = (value, key) =>
- _.includes(key, ['cryptoAtoms', 'fiat']) ? value.toString() : value
+ const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
+ ? value.toString()
+ : value
- const convertKey = key =>
- _.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key
+ const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
+ ? key + '#'
+ : key
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
}
@@ -604,10 +541,8 @@ function plugins (settings, deviceId) {
const notifications = configManager.getGlobalNotifications(settings.config)
let promises = []
- if (notifications.email.active && rec.email)
- promises.push(email.sendMessage(settings, rec))
- if (notifications.sms.active && rec.sms)
- promises.push(sms.sendMessage(settings, rec))
+ if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec))
+ if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec))
return Promise.all(promises)
}
@@ -617,64 +552,53 @@ function plugins (settings, deviceId) {
}
function checkDeviceCashBalances (fiatCode, device) {
- const cashOutConfig = configManager.getCashOut(
- device.deviceId,
- settings.config
- )
+ const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config)
const denomination1 = cashOutConfig.top
const denomination2 = cashOutConfig.bottom
const cashOutEnabled = cashOutConfig.active
- const notifications = configManager.getNotifications(
- null,
- device.deviceId,
- settings.config
- )
+ const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
const machineName = device.name
- const cashInAlert =
- device.cashbox > notifications.cashInAlertThreshold
- ? {
- code: 'CASH_BOX_FULL',
- machineName,
- deviceId: device.deviceId,
- notes: device.cashbox
- }
- : null
+ const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
+ ? {
+ code: 'CASH_BOX_FULL',
+ machineName,
+ deviceId: device.deviceId,
+ notes: device.cashbox
+ }
+ : null
- const cassette1Alert =
- cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1
- ? {
- code: 'LOW_CASH_OUT',
- cassette: 1,
- machineName,
- deviceId: device.deviceId,
- notes: device.cassette1,
- denomination: denomination1,
- fiatCode
- }
- : null
+ const cassette1Alert = cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1
+ ? {
+ code: 'LOW_CASH_OUT',
+ cassette: 1,
+ machineName,
+ deviceId: device.deviceId,
+ notes: device.cassette1,
+ denomination: denomination1,
+ fiatCode
+ }
+ : null
- const cassette2Alert =
- cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2
- ? {
- code: 'LOW_CASH_OUT',
- cassette: 2,
- machineName,
- deviceId: device.deviceId,
- notes: device.cassette2,
- denomination: denomination2,
- fiatCode
- }
- : null
+ const cassette2Alert = cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2
+ ? {
+ code: 'LOW_CASH_OUT',
+ cassette: 2,
+ machineName,
+ deviceId: device.deviceId,
+ notes: device.cassette2,
+ denomination: denomination2,
+ fiatCode
+ }
+ : null
return _.compact([cashInAlert, cassette1Alert, cassette2Alert])
}
function checkCryptoBalances (fiatCode, devices) {
- const fiatBalancePromises = cryptoCodes =>
- _.map(c => fiatBalance(fiatCode, c), cryptoCodes)
+ const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes)
const fetchCryptoCodes = _deviceId => {
const localeConfig = configManager.getLocale(_deviceId, settings.config)
@@ -685,23 +609,18 @@ function plugins (settings, deviceId) {
const cryptoCodes = union(devices)
const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode])
- return Promise.all(fiatBalancePromises(cryptoCodes)).then(balances =>
- _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances))
- )
+ return Promise.all(fiatBalancePromises(cryptoCodes))
+ .then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)))
}
function checkCryptoBalance (fiatCode, rec) {
const [cryptoCode, fiatBalance] = rec
-
if (!fiatBalance) return null
- const notifications = configManager.getNotifications(
- cryptoCode,
- null,
- settings.config
- )
- const lowAlertThreshold = notifications.cryptoLowBalance
- const highAlertThreshold = notifications.cryptoHighBalance
+ const notifications = configManager.getNotifications(cryptoCode, null, settings.config)
+ const override = _.find(override => override.cryptoCurrency === cryptoCode, settings.config.notifications_cryptoBalanceOverrides)
+ const lowAlertThreshold = override ? override.lowBalance : notifications.cryptoLowBalance
+ const highAlertThreshold = override ? override.highBalance : notifications.cryptoHighBalance
const req = {
cryptoCode,
@@ -709,17 +628,13 @@ function plugins (settings, deviceId) {
fiatCode
}
- if (
- _.isFinite(lowAlertThreshold) &&
- BN(fiatBalance.balance).lt(lowAlertThreshold)
- )
+ if (_.isFinite(lowAlertThreshold) && BN(fiatBalance.balance).lt(lowAlertThreshold)) {
return _.set('code')('LOW_CRYPTO_BALANCE')(req)
+ }
- if (
- _.isFinite(highAlertThreshold) &&
- BN(fiatBalance.balance).gt(highAlertThreshold)
- )
+ if (_.isFinite(highAlertThreshold) && BN(fiatBalance.balance).gt(highAlertThreshold)) {
return _.set('code')('HIGH_CRYPTO_BALANCE')(req)
+ }
return null
}
@@ -728,23 +643,24 @@ function plugins (settings, deviceId) {
const localeConfig = configManager.getGlobalLocale(settings.config)
const fiatCode = localeConfig.fiatCurrency
- return machineLoader.getMachines().then(devices => {
- return Promise.all([
- checkCryptoBalances(fiatCode, devices),
- checkDevicesCashBalances(fiatCode, devices)
- ]).then(_.flow(_.flattenDeep, _.compact))
- })
+ return machineLoader.getMachines()
+ .then(devices => {
+ return Promise.all([
+ checkCryptoBalances(fiatCode, devices),
+ checkDevicesCashBalances(fiatCode, devices)
+ ])
+ .then(_.flow(_.flattenDeep, _.compact))
+ })
}
function randomCode () {
- return BN(crypto.randomBytes(3).toString('hex'), 16)
- .shift(-6)
- .toFixed(6)
- .slice(-6)
+ return BN(crypto.randomBytes(3).toString('hex'), 16).shift(-6).toFixed(6).slice(-6)
}
function getPhoneCode (phone) {
- const code = argv.mockSms ? '123' : randomCode()
+ const code = argv.mockSms
+ ? '123'
+ : randomCode()
const rec = {
sms: {
@@ -753,14 +669,14 @@ function plugins (settings, deviceId) {
}
}
- return sms.sendMessage(settings, rec).then(() => code)
+ return sms.sendMessage(settings, rec)
+ .then(() => code)
}
function sweepHdRow (row) {
const cryptoCode = row.crypto_code
- return wallet
- .sweep(settings, cryptoCode, row.hd_index)
+ return wallet.sweep(settings, cryptoCode, row.hd_index)
.then(txHash => {
if (txHash) {
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
@@ -771,17 +687,14 @@ function plugins (settings, deviceId) {
return db.none(sql, row.id)
}
})
- .catch(err =>
- logger.error('[%s] Sweep error: %s', cryptoCode, err.message)
- )
+ .catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message))
}
function sweepHd () {
const sql = `select id, crypto_code, hd_index from cash_out_txs
where hd_index is not null and not swept and status in ('confirmed', 'instant')`
- return db
- .any(sql)
+ return db.any(sql)
.then(rows => Promise.all(rows.map(sweepHdRow)))
.catch(err => logger.error(err))
}
@@ -795,15 +708,14 @@ function plugins (settings, deviceId) {
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config)
- const tickerPromises = cryptoCodes.map(c =>
- ticker.getRates(settings, fiatCode, c)
- )
+ const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
return Promise.all(tickerPromises)
}
function getRates () {
- return getRawRates().then(buildRates)
+ return getRawRates()
+ .then(buildRates)
}
return {
diff --git a/lib/plugins/sms/mock-sms/mock-sms.js b/lib/plugins/sms/mock-sms/mock-sms.js
index 19c43d07..952fca3c 100644
--- a/lib/plugins/sms/mock-sms/mock-sms.js
+++ b/lib/plugins/sms/mock-sms/mock-sms.js
@@ -2,7 +2,7 @@ const _ = require('lodash/fp')
exports.NAME = 'MockSMS'
-exports.sendMessage = function sendMessage(account, rec) {
+exports.sendMessage = function sendMessage (account, rec) {
console.log('Sending SMS: %j', rec)
return new Promise((resolve, reject) => {
if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) {
diff --git a/lib/poller.js b/lib/poller.js
index 7c90871b..e03bd689 100644
--- a/lib/poller.js
+++ b/lib/poller.js
@@ -1,7 +1,7 @@
const _ = require('lodash/fp')
const plugins = require('./plugins')
-const notifier = require('./notifier/index')
+const notifier = require('./notifier')
const T = require('./time')
const logger = require('./logger')
const cashOutTx = require('./cash-out/cash-out-tx')
diff --git a/lib/routes.js b/lib/routes.js
index f604473f..a2125197 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -28,7 +28,7 @@ const compliance = require('./compliance')
const promoCodes = require('./promo-codes')
const BN = require('./bn')
const commissionMath = require('./commission-math')
-const notifier = require('./notifier/index')
+const notifier = require('./notifier')
const version = require('../package.json').version
@@ -324,7 +324,6 @@ function updateCustomer (req, res, next) {
}
function triggerSanctions (req, res, next) {
- console.log("SANCTIONS TRIGGERED")
const id = req.params.id
customers.getById(id)
diff --git a/migrations/1607009558538-create-notifications-table.js b/migrations/1607009558538-create-notifications-table.js
index fda66b84..9dd89aa8 100644
--- a/migrations/1607009558538-create-notifications-table.js
+++ b/migrations/1607009558538-create-notifications-table.js
@@ -1,8 +1,6 @@
var db = require('./db')
-function singleQuotify(item) {
- return "'" + item + "'"
-}
+const singleQuotify = (item) => `'${item}'`
var types = [
'highValueTransaction',
@@ -18,20 +16,17 @@ exports.up = function (next) {
const sql = [
`
CREATE TYPE notification_type AS ENUM ${'(' + types + ')'};
- CREATE TABLE IF NOT EXISTS "notifications" (
+ CREATE TABLE "notifications" (
"id" uuid NOT NULL PRIMARY KEY,
"type" notification_type NOT NULL,
- "detail" TEXT,
- "device_id" TEXT,
+ "detail" JSONB,
"message" TEXT NOT NULL,
- "created" TIMESTAMP WITH TIME ZONE NOT NULL,
+ "created" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"read" BOOLEAN NOT NULL DEFAULT 'false',
"valid" BOOLEAN NOT NULL DEFAULT 'true',
- CONSTRAINT fk_devices
- FOREIGN KEY(device_id)
- REFERENCES devices(device_id)
- ON DELETE CASCADE
+ "modified" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
+ CREATE INDEX ON notifications (valid);
CREATE INDEX ON notifications (read);`
]
diff --git a/new-lamassu-admin/src/components/NotificationCenter/NotificationCenter.js b/new-lamassu-admin/src/components/NotificationCenter/NotificationCenter.js
new file mode 100644
index 00000000..5795e06d
--- /dev/null
+++ b/new-lamassu-admin/src/components/NotificationCenter/NotificationCenter.js
@@ -0,0 +1,144 @@
+import { useQuery, useMutation } from '@apollo/react-hooks'
+import { makeStyles } from '@material-ui/core/styles'
+import gql from 'graphql-tag'
+import * as R from 'ramda'
+import React, { useState } from 'react'
+
+import ActionButton from 'src/components/buttons/ActionButton'
+import { H5 } from 'src/components/typography'
+import { ReactComponent as NotificationIconZodiac } from 'src/styling/icons/menu/notification-zodiac.svg'
+import { ReactComponent as ClearAllIconInverse } from 'src/styling/icons/stage/spring/empty.svg'
+import { ReactComponent as ClearAllIcon } from 'src/styling/icons/stage/zodiac/empty.svg'
+import { ReactComponent as ShowUnreadIcon } from 'src/styling/icons/stage/zodiac/full.svg'
+
+import styles from './NotificationCenter.styles'
+import NotificationRow from './NotificationRow'
+
+const useStyles = makeStyles(styles)
+
+const GET_NOTIFICATIONS = gql`
+ query getNotifications {
+ notifications {
+ id
+ deviceName
+ type
+ detail
+ message
+ created
+ read
+ valid
+ }
+ hasUnreadNotifications
+ machines {
+ deviceId
+ name
+ }
+ }
+`
+
+const CLEAR_NOTIFICATION = gql`
+ mutation clearNotification($id: ID!) {
+ clearNotification(id: $id) {
+ id
+ }
+ }
+`
+
+const CLEAR_ALL_NOTIFICATIONS = gql`
+ mutation clearAllNotifications {
+ clearAllNotifications {
+ id
+ }
+ }
+`
+
+const NotificationCenter = ({ close, notifyUnread }) => {
+ const classes = useStyles()
+ const { data, loading } = useQuery(GET_NOTIFICATIONS)
+ const [showingUnread, setShowingUnread] = useState(false)
+ const machines = R.compose(
+ R.map(R.prop('name')),
+ R.indexBy(R.prop('deviceId'))
+ )(data?.machines ?? [])
+
+ const notifications = R.path(['notifications'])(data) ?? []
+ const hasUnread = data?.hasUnreadNotifications ?? false
+ if (!hasUnread) {
+ notifyUnread && notifyUnread()
+ }
+ const [clearNotification] = useMutation(CLEAR_NOTIFICATION, {
+ onError: () => console.error('Error while clearing notification'),
+ refetchQueries: () => ['getNotifications']
+ })
+ const [clearAllNotifications] = useMutation(CLEAR_ALL_NOTIFICATIONS, {
+ onError: () => console.error('Error while clearing all notifications'),
+ refetchQueries: () => ['getNotifications']
+ })
+
+ const handleClearNotification = id => {
+ clearNotification({ variables: { id } })
+ }
+
+ const buildNotifications = () => {
+ const notificationsToShow =
+ !showingUnread || !hasUnread
+ ? notifications
+ : R.filter(R.propEq('read', false))(notifications)
+ return notificationsToShow.map(n => {
+ return (
+