Chore: make notification center UI
Chore: fiatBalancesNotify refactor Chore: removed now-unused code in some files Feat: change column "detail" in database to use jsonb Chore: add notification center background and button Chore: notifications screen scaffolding Fix: change position of notification UI Feat: join backend and frontend Feat: notification icons and machine names Feat: add clear all button, stripe overlay on invalid notification Fix: rework notification styles Feat: use popper to render notifications Feat: make notification center UI Fix: fix css on notification center Fix: fix invalidateNotification Chore: apply PR requested changes Fix: PR fixes Fix: make toggleable body/root styles be handled by react Chore: delete old notifier file Fix: undo variable name changes for cryptobalance notifs
This commit is contained in:
parent
2a9e8dadba
commit
c457faab40
37 changed files with 1337 additions and 1332 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
const notifier = require('./notifier')
|
||||||
|
|
||||||
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
|
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
|
||||||
const getBlacklist = () => {
|
const getBlacklist = () => {
|
||||||
|
|
@ -13,11 +14,9 @@ const getBlacklist = () => {
|
||||||
|
|
||||||
// Delete row from blacklist table by crypto code and address
|
// Delete row from blacklist table by crypto code and address
|
||||||
const deleteFromBlacklist = (cryptoCode, address) => {
|
const deleteFromBlacklist = (cryptoCode, address) => {
|
||||||
const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2;
|
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^)`
|
notifier.clearBlacklistNotification(cryptoCode, address)
|
||||||
|
return db.none(sql, [cryptoCode, address])
|
||||||
const detail = `'${cryptoCode}_BLOCKED_${address}', '${cryptoCode}_REUSED_${address}'`
|
|
||||||
return db.none(sql, [cryptoCode, address, detail])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertIntoBlacklist = (cryptoCode, address) => {
|
const insertIntoBlacklist = (cryptoCode, address) => {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const plugins = require('../plugins')
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
const settingsLoader = require('../new-settings-loader')
|
const settingsLoader = require('../new-settings-loader')
|
||||||
const configManager = require('../new-config-manager')
|
const configManager = require('../new-config-manager')
|
||||||
const notifier = require("../notifier/index")
|
const notifier = require('../notifier')
|
||||||
|
|
||||||
const cashInAtomic = require('./cash-in-atomic')
|
const cashInAtomic = require('./cash-in-atomic')
|
||||||
const cashInLow = require('./cash-in-low')
|
const cashInLow = require('./cash-in-low')
|
||||||
|
|
@ -16,7 +16,7 @@ const cashInLow = require('./cash-in-low')
|
||||||
const PENDING_INTERVAL = '60 minutes'
|
const PENDING_INTERVAL = '60 minutes'
|
||||||
const MAX_PENDING = 10
|
const MAX_PENDING = 10
|
||||||
|
|
||||||
module.exports = {post, monitorPending, cancel, PENDING_INTERVAL}
|
module.exports = { post, monitorPending, cancel, PENDING_INTERVAL }
|
||||||
|
|
||||||
function post (machineTx, pi) {
|
function post (machineTx, pi) {
|
||||||
return db.tx(cashInAtomic.atomic(machineTx, pi))
|
return db.tx(cashInAtomic.atomic(machineTx, pi))
|
||||||
|
|
@ -29,10 +29,10 @@ function post (machineTx, pi) {
|
||||||
.then(([{ config }, blacklistItems]) => {
|
.then(([{ config }, blacklistItems]) => {
|
||||||
const rejectAddressReuseActive = configManager.getCompliance(config).rejectAddressReuse
|
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
|
blacklisted = true
|
||||||
notifier.blacklistNotify(r.tx, false)
|
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)
|
notifier.blacklistNotify(r.tx, true)
|
||||||
addressReuse = true
|
addressReuse = true
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ function logAction (rec, tx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function logActionById (action, _rec, txId) {
|
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')
|
const sql = pgp.helpers.insert(rec, null, 'cash_in_actions')
|
||||||
|
|
||||||
return db.none(sql)
|
return db.none(sql)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ const helper = require('./cash-out-helper')
|
||||||
const cashOutActions = require('./cash-out-actions')
|
const cashOutActions = require('./cash-out-actions')
|
||||||
const cashOutLow = require('./cash-out-low')
|
const cashOutLow = require('./cash-out-low')
|
||||||
|
|
||||||
const notifier = require("../notifier/index")
|
|
||||||
|
|
||||||
const toObj = helper.toObj
|
const toObj = helper.toObj
|
||||||
|
|
||||||
module.exports = {atomic}
|
module.exports = {atomic}
|
||||||
|
|
@ -124,10 +122,8 @@ function updateCassettes (t, tx) {
|
||||||
tx.deviceId
|
tx.deviceId
|
||||||
]
|
]
|
||||||
|
|
||||||
return t.one(sql, values).then(r => {
|
return t.one(sql, values)
|
||||||
notifier.cashCassettesNotify(r, tx.deviceId)
|
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
|
||||||
return socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId}))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
|
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ const complianceOverrides = require('./compliance_overrides')
|
||||||
const users = require('./users')
|
const users = require('./users')
|
||||||
const options = require('./options')
|
const options = require('./options')
|
||||||
const writeFile = util.promisify(fs.writeFile)
|
const writeFile = util.promisify(fs.writeFile)
|
||||||
|
const notifierQueries = require('./notifier/queries')
|
||||||
|
const notifierUtils = require('./notifier/utils')
|
||||||
const NUM_RESULTS = 1000
|
const NUM_RESULTS = 1000
|
||||||
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
|
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
|
||||||
const frontCameraBaseDir = _.get('frontCameraDir', 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') +
|
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
||||||
' where id=$1'
|
' where id=$1'
|
||||||
invalidateCustomerNotifications(id, formattedData)
|
invalidateCustomerNotifications(id, formattedData).catch(console.error)
|
||||||
|
|
||||||
await db.none(sql, [id])
|
await db.none(sql, [id])
|
||||||
|
|
||||||
|
|
@ -123,12 +124,10 @@ async function updateCustomer (id, data, userToken) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const invalidateCustomerNotifications = (id, data) => {
|
const invalidateCustomerNotifications = (id, data) => {
|
||||||
let detail = '';
|
if (data.authorized_override !== 'verified') return Promise.resolve()
|
||||||
if(data.authorized_override === 'verified') {
|
|
||||||
detail = `BLOCKED_${id}`
|
const detailB = notifierUtils.buildDetail({ code: 'BLOCKED', customerId: id })
|
||||||
}
|
return notifierQueries.invalidateNotification(detailB, 'compliance').catch(console.error)
|
||||||
const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE valid = 't' AND detail = $1`
|
|
||||||
return db.none(sql, [detail])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -409,7 +408,7 @@ function populateOverrideUsernames (customer) {
|
||||||
return users.getByIds(queryTokens)
|
return users.getByIds(queryTokens)
|
||||||
.then(usersList => {
|
.then(usersList => {
|
||||||
return _.map(userField => {
|
return _.map(userField => {
|
||||||
const user = _.find({token: userField.token}, usersList)
|
const user = _.find({ token: userField.token }, usersList)
|
||||||
return {
|
return {
|
||||||
[userField.field]: user ? user.name : null
|
[userField.field]: user ? user.name : null
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +442,7 @@ function batch () {
|
||||||
// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored
|
// 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
|
* and with aggregate columns based on their
|
||||||
* transactions
|
* 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
|
* and with aggregate columns based on their
|
||||||
* transactions
|
* transactions
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
const logger = require('./logger')
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const pairing = require('./pairing')
|
const pairing = require('./pairing')
|
||||||
const notifier = require('./notifier')
|
const notifier = require('./notifier')
|
||||||
const dbm = require('./postgresql_interface')
|
const dbm = require('./postgresql_interface')
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const settingsLoader = require('./new-settings-loader')
|
const settingsLoader = require('./new-settings-loader')
|
||||||
|
const notifierUtils = require('./notifier/utils')
|
||||||
module.exports = {getMachineName, getMachines, getMachine, getMachineNames, setMachine}
|
const notifierQueries = require('./notifier/queries')
|
||||||
|
|
||||||
function getMachines () {
|
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 => ({
|
.then(rr => rr.map(r => ({
|
||||||
deviceId: r.device_id,
|
deviceId: r.device_id,
|
||||||
cashbox: r.cashbox,
|
cashbox: r.cashbox,
|
||||||
|
|
@ -36,10 +35,10 @@ function getConfig (defaultConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachineNames (config) {
|
function getMachineNames (config) {
|
||||||
const fullyFunctionalStatus = {label: 'Fully functional', type: 'success'}
|
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||||
const unresponsiveStatus = {label: 'Unresponsive', type: 'error'}
|
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||||
const stuckStatus = {label: 'Stuck', type: 'error'}
|
const stuckStatus = { label: 'Stuck', type: 'error' }
|
||||||
|
|
||||||
return Promise.all([getMachines(), getConfig(config)])
|
return Promise.all([getMachines(), getConfig(config)])
|
||||||
.then(([machines, config]) => Promise.all(
|
.then(([machines, config]) => Promise.all(
|
||||||
[machines, notifier.checkPings(machines), dbm.machineEvents(), config]
|
[machines, notifier.checkPings(machines), dbm.machineEvents(), config]
|
||||||
|
|
@ -49,7 +48,7 @@ function getMachineNames (config) {
|
||||||
if (ping && ping.age) return unresponsiveStatus
|
if (ping && ping.age) return unresponsiveStatus
|
||||||
|
|
||||||
if (stuck && stuck.age) return stuckStatus
|
if (stuck && stuck.age) return stuckStatus
|
||||||
|
|
||||||
return fullyFunctionalStatus
|
return fullyFunctionalStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +56,7 @@ function getMachineNames (config) {
|
||||||
const cashOutConfig = configManager.getCashOut(r.deviceId, config)
|
const cashOutConfig = configManager.getCashOut(r.deviceId, config)
|
||||||
|
|
||||||
const cashOut = !!cashOutConfig.active
|
const cashOut = !!cashOutConfig.active
|
||||||
|
|
||||||
const statuses = [
|
const statuses = [
|
||||||
getStatus(
|
getStatus(
|
||||||
_.first(pings[r.deviceId]),
|
_.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)
|
return _.map(addName, machines)
|
||||||
|
|
@ -83,31 +82,29 @@ function getMachineNames (config) {
|
||||||
* @returns {string} machine name
|
* @returns {string} machine name
|
||||||
*/
|
*/
|
||||||
function getMachineName (machineId) {
|
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])
|
return db.oneOrNone(sql, [machineId])
|
||||||
.then(it => it.name)
|
.then(it => it.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMachine (machineId) {
|
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))
|
return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameMachine (rec) {
|
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])
|
return db.none(sql, [rec.newName, rec.deviceId])
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCashOutBills (rec) {
|
function resetCashOutBills (rec) {
|
||||||
const sql = `
|
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
|
||||||
update devices set cassette1=$1, cassette2=$2 where device_id=$3;
|
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]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
|
||||||
`
|
|
||||||
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyCashInBills (rec) {
|
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])
|
return db.none(sql, [rec.deviceId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,3 +142,5 @@ function setMachine (rec) {
|
||||||
default: throw new Error('No such action: ' + rec.action)
|
default: throw new Error('No such action: ' + rec.action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = { getMachineName, getMachines, getMachine, getMachineNames, setMachine }
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ const logs = require('../../logs')
|
||||||
const settingsLoader = require('../../new-settings-loader')
|
const settingsLoader = require('../../new-settings-loader')
|
||||||
// const tokenManager = require('../../token-manager')
|
// const tokenManager = require('../../token-manager')
|
||||||
const blacklist = require('../../blacklist')
|
const blacklist = require('../../blacklist')
|
||||||
const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch
|
const machineEventsByIdBatch = require('../../postgresql_interface').machineEventsByIdBatch
|
||||||
const promoCodeManager = require('../../promo-codes')
|
const promoCodeManager = require('../../promo-codes')
|
||||||
|
const notifierQueries = require('../../notifier/queries')
|
||||||
|
|
||||||
const serverVersion = require('../../../package.json').version
|
const serverVersion = require('../../../package.json').version
|
||||||
const transactions = require('../transactions')
|
const transactions = require('../transactions')
|
||||||
|
|
@ -247,6 +248,17 @@ const typeDefs = gql`
|
||||||
rate: Float
|
rate: Float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Notification {
|
||||||
|
id: ID!
|
||||||
|
deviceName: String
|
||||||
|
type: String
|
||||||
|
detail: JSON
|
||||||
|
message: String
|
||||||
|
created: Date
|
||||||
|
read: Boolean
|
||||||
|
valid: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
countries: [Country]
|
countries: [Country]
|
||||||
currencies: [Currency]
|
currencies: [Currency]
|
||||||
|
|
@ -279,6 +291,8 @@ const typeDefs = gql`
|
||||||
promoCodes: [PromoCode]
|
promoCodes: [PromoCode]
|
||||||
cryptoRates: JSONObject
|
cryptoRates: JSONObject
|
||||||
fiatRates: [Rate]
|
fiatRates: [Rate]
|
||||||
|
notifications: [Notification]
|
||||||
|
hasUnreadNotifications: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportLogsResponse {
|
type SupportLogsResponse {
|
||||||
|
|
@ -312,6 +326,8 @@ const typeDefs = gql`
|
||||||
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
|
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
|
||||||
createPromoCode(code: String!, discount: Int!): PromoCode
|
createPromoCode(code: String!, discount: Int!): PromoCode
|
||||||
deletePromoCode(codeId: ID!): PromoCode
|
deletePromoCode(codeId: ID!): PromoCode
|
||||||
|
clearNotification(id: ID!): Notification
|
||||||
|
clearAllNotifications: Notification
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -344,9 +360,9 @@ const resolvers = {
|
||||||
customers: () => customers.getCustomersList(),
|
customers: () => customers.getCustomersList(),
|
||||||
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
|
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
|
||||||
funding: () => funding.getFunding(),
|
funding: () => funding.getFunding(),
|
||||||
machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
|
machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
|
||||||
logs.simpleGetMachineLogs(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),
|
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset).then(parseAsync),
|
||||||
serverVersion: () => serverVersion,
|
serverVersion: () => serverVersion,
|
||||||
uptime: () => supervisor.getAllProcessInfo(),
|
uptime: () => supervisor.getAllProcessInfo(),
|
||||||
|
|
@ -373,7 +389,9 @@ const resolvers = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
fiatRates: () => forex.getFiatRates()
|
fiatRates: () => forex.getFiatRates(),
|
||||||
|
notifications: () => notifierQueries.getNotifications(),
|
||||||
|
hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications()
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }),
|
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }),
|
||||||
|
|
@ -397,7 +415,9 @@ const resolvers = {
|
||||||
blacklist.insertIntoBlacklist(cryptoCode, address),
|
blacklist.insertIntoBlacklist(cryptoCode, address),
|
||||||
// revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
|
// revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
|
||||||
createPromoCode: (...[, { code, discount }]) => promoCodeManager.createPromoCode(code, discount),
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,18 +44,6 @@ const configSql = 'insert into user_config (type, data, valid, schema_version) v
|
||||||
function saveConfig (config) {
|
function saveConfig (config) {
|
||||||
return loadLatestConfigOrNone()
|
return loadLatestConfigOrNone()
|
||||||
.then(currentConfig => {
|
.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)
|
const newConfig = _.assign(currentConfig, config)
|
||||||
return db.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
|
return db.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
|
||||||
})
|
})
|
||||||
|
|
|
||||||
351
lib/notifier.js
351
lib/notifier.js
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -20,6 +20,14 @@ const NETWORK_DOWN_TIME = 1 * T.minute
|
||||||
const STALE_STATE = 7 * T.minute
|
const STALE_STATE = 7 * T.minute
|
||||||
const ALERT_SEND_INTERVAL = T.hour
|
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 = {
|
module.exports = {
|
||||||
PING,
|
PING,
|
||||||
STALE,
|
STALE,
|
||||||
|
|
@ -30,5 +38,6 @@ module.exports = {
|
||||||
CODES_DISPLAY,
|
CODES_DISPLAY,
|
||||||
NETWORK_DOWN_TIME,
|
NETWORK_DOWN_TIME,
|
||||||
STALE_STATE,
|
STALE_STATE,
|
||||||
ALERT_SEND_INTERVAL
|
ALERT_SEND_INTERVAL,
|
||||||
|
NOTIFICATION_TYPES
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const utils = require("./utils")
|
const utils = require('./utils')
|
||||||
|
|
||||||
const email = require('../email')
|
const email = require('../email')
|
||||||
|
|
||||||
|
|
@ -12,61 +12,47 @@ const {
|
||||||
LOW_CASH_OUT
|
LOW_CASH_OUT
|
||||||
} = require('./codes')
|
} = require('./codes')
|
||||||
|
|
||||||
function alertSubject(alertRec, config) {
|
function alertSubject (alertRec, config) {
|
||||||
let alerts = []
|
let alerts = []
|
||||||
|
|
||||||
if (config.balance) {
|
if (config.balance) {
|
||||||
alerts = _.concat(alerts, alertRec.general)
|
alerts = _.concat(alerts, alertRec.general)
|
||||||
}
|
}
|
||||||
|
|
||||||
_.keys(alertRec.devices).forEach(function (device) {
|
_.forEach(device => {
|
||||||
if (config.balance) {
|
alerts = _.concat(alerts, utils.deviceAlerts(config, alertRec, device))
|
||||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
}, _.keys(alertRec.devices))
|
||||||
}
|
|
||||||
|
|
||||||
if (config.errors) {
|
|
||||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alerts.length === 0) return null
|
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(', ')
|
return '[Lamassu] Errors reported: ' + alertTypes.join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function printEmailAlerts(alertRec, config) {
|
function printEmailAlerts (alertRec, config) {
|
||||||
let body = 'Errors were reported by your Lamassu Machines.\n'
|
let body = 'Errors were reported by your Lamassu Machines.\n'
|
||||||
|
|
||||||
if (config.balance && alertRec.general.length !== 0) {
|
if (config.balance && alertRec.general.length !== 0) {
|
||||||
body = body + '\nGeneral errors:\n'
|
body += '\nGeneral errors:\n'
|
||||||
body = body + emailAlerts(alertRec.general) + '\n'
|
body += emailAlerts(alertRec.general) + '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
_.keys(alertRec.devices).forEach(function (device) {
|
_.forEach(device => {
|
||||||
const deviceName = alertRec.deviceNames[device]
|
const deviceName = alertRec.deviceNames[device]
|
||||||
body = body + '\nErrors for ' + deviceName + ':\n'
|
body += '\nErrors for ' + deviceName + ':\n'
|
||||||
|
|
||||||
let alerts = []
|
const alerts = utils.deviceAlerts(config, alertRec, device)
|
||||||
if (config.balance) {
|
|
||||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.errors) {
|
|
||||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
|
||||||
}
|
|
||||||
|
|
||||||
body = body + emailAlerts(alerts)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
body += emailAlerts(alerts)
|
||||||
|
}, _.keys(alertRec.devices))
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
function emailAlerts(alerts) {
|
function emailAlerts (alerts) {
|
||||||
return alerts.map(emailAlert).join('\n') + '\n'
|
return _.join('\n', _.map(emailAlert, alerts)) + '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
function emailAlert(alert) {
|
function emailAlert (alert) {
|
||||||
switch (alert.code) {
|
switch (alert.code) {
|
||||||
case PING:
|
case PING:
|
||||||
if (alert.age) {
|
if (alert.age) {
|
||||||
|
|
@ -98,5 +84,4 @@ function emailAlert(alert) {
|
||||||
|
|
||||||
const sendMessage = email.sendMessage
|
const sendMessage = email.sendMessage
|
||||||
|
|
||||||
|
|
||||||
module.exports = { alertSubject, printEmailAlerts, sendMessage }
|
module.exports = { alertSubject, printEmailAlerts, sendMessage }
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,15 @@ const utils = require('./utils')
|
||||||
const emailFuncs = require('./email')
|
const emailFuncs = require('./email')
|
||||||
const smsFuncs = require('./sms')
|
const smsFuncs = require('./sms')
|
||||||
const { STALE, STALE_STATE, PING } = require('./codes')
|
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 smsEnabled = utils.isActive(notifications.sms)
|
||||||
const emailEnabled = utils.isActive(notifications.email)
|
const emailEnabled = utils.isActive(notifications.email)
|
||||||
|
|
||||||
|
|
@ -34,7 +41,7 @@ function buildMessage(alerts, notifications) {
|
||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkNotification(plugins) {
|
function checkNotification (plugins) {
|
||||||
const notifications = plugins.getNotificationConfig()
|
const notifications = plugins.getNotificationConfig()
|
||||||
const smsEnabled = utils.isActive(notifications.sms)
|
const smsEnabled = utils.isActive(notifications.sms)
|
||||||
const emailEnabled = utils.isActive(notifications.email)
|
const emailEnabled = utils.isActive(notifications.email)
|
||||||
|
|
@ -50,28 +57,25 @@ function checkNotification(plugins) {
|
||||||
)
|
)
|
||||||
if (!currentAlertFingerprint) {
|
if (!currentAlertFingerprint) {
|
||||||
const inAlert = !!utils.getAlertFingerprint()
|
const inAlert = !!utils.getAlertFingerprint()
|
||||||
// (fingerprint = null, lastAlertTime = null)
|
// variables for setAlertFingerprint: (fingerprint = null, lastAlertTime = null)
|
||||||
utils.setAlertFingerprint(null, null)
|
utils.setAlertFingerprint(null, null)
|
||||||
if (inAlert) {
|
if (inAlert) return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
|
||||||
return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (utils.shouldNotAlert(currentAlertFingerprint)) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if (utils.shouldNotAlert(currentAlertFingerprint)) return
|
||||||
|
|
||||||
const message = buildMessage(alerts, notifications)
|
const message = buildMessage(alerts, notifications)
|
||||||
utils.setAlertFingerprint(currentAlertFingerprint, Date.now())
|
utils.setAlertFingerprint(currentAlertFingerprint, Date.now())
|
||||||
return plugins.sendMessage(message)
|
return plugins.sendMessage(message)
|
||||||
})
|
})
|
||||||
.then(results => {
|
.then(results => {
|
||||||
if (results && results.length > 0)
|
if (results && results.length > 0) {
|
||||||
logger.debug('Successfully sent alerts')
|
logger.debug('Successfully sent alerts')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(logger.error)
|
.catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAlerts(plugins) {
|
function getAlerts (plugins) {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
plugins.checkBalances(),
|
plugins.checkBalances(),
|
||||||
queries.machineEvents(),
|
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: {} }
|
const alerts = { devices: {}, deviceNames: {} }
|
||||||
alerts.general = _.filter(r => !r.deviceId, balances)
|
alerts.general = _.filter(r => !r.deviceId, balances)
|
||||||
devices.forEach(function (device) {
|
_.forEach(device => {
|
||||||
const deviceId = device.deviceId
|
const deviceId = device.deviceId
|
||||||
const deviceName = device.name
|
const deviceName = device.name
|
||||||
const deviceEvents = events.filter(function (eventRow) {
|
const deviceEvents = events.filter(function (eventRow) {
|
||||||
|
|
@ -94,83 +98,78 @@ function buildAlerts(pings, balances, events, devices) {
|
||||||
const ping = pings[deviceId] || []
|
const ping = pings[deviceId] || []
|
||||||
const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
|
const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
|
||||||
|
|
||||||
if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {}
|
alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter(
|
||||||
alerts.devices[deviceId].balanceAlerts = _.filter(
|
|
||||||
['deviceId', deviceId],
|
['deviceId', deviceId],
|
||||||
balances
|
balances
|
||||||
)
|
), alerts.devices)
|
||||||
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
|
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
|
||||||
|
|
||||||
alerts.deviceNames[deviceId] = deviceName
|
alerts.deviceNames[deviceId] = deviceName
|
||||||
})
|
}, devices)
|
||||||
|
|
||||||
return alerts
|
return alerts
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPings(devices) {
|
function checkPings (devices) {
|
||||||
const deviceIds = _.map('deviceId', devices)
|
const deviceIds = _.map('deviceId', devices)
|
||||||
const pings = _.map(utils.checkPing, devices)
|
const pings = _.map(utils.checkPing, devices)
|
||||||
return _.zipObject(deviceIds)(pings)
|
return _.zipObject(deviceIds)(pings)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkStuckScreen(deviceEvents, machineName) {
|
function checkStuckScreen (deviceEvents, machineName) {
|
||||||
const sortedEvents = _.sortBy(
|
const sortedEvents = _.sortBy(
|
||||||
utils.getDeviceTime,
|
utils.getDeviceTime,
|
||||||
_.map(utils.parseEventNote, deviceEvents)
|
_.map(utils.parseEventNote, deviceEvents)
|
||||||
)
|
)
|
||||||
const lastEvent = _.last(sortedEvents)
|
const lastEvent = _.last(sortedEvents)
|
||||||
|
|
||||||
if (!lastEvent) {
|
if (!lastEvent) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = lastEvent.note.state
|
const state = lastEvent.note.state
|
||||||
const isIdle = lastEvent.note.isIdle
|
const isIdle = lastEvent.note.isIdle
|
||||||
|
|
||||||
if (isIdle) {
|
if (isIdle) return []
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const age = Math.floor(lastEvent.age)
|
const age = Math.floor(lastEvent.age)
|
||||||
if (age > STALE_STATE) {
|
if (age > STALE_STATE) return [{ code: STALE, state, age, machineName }]
|
||||||
return [{ code: STALE, state, age, machineName }]
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transactionNotify (tx, rec) {
|
function transactionNotify (tx, rec) {
|
||||||
const settings = await settingsLoader.loadLatest()
|
return settingsLoader.loadLatest().then(settings => {
|
||||||
const notifSettings = configManager.getGlobalNotifications(settings.config)
|
const notifSettings = configManager.getGlobalNotifications(settings.config)
|
||||||
const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity)
|
const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity)
|
||||||
const isCashOut = tx.direction === 'cashOut'
|
const isCashOut = tx.direction === 'cashOut'
|
||||||
|
// high value tx on database
|
||||||
// high value tx on database
|
if (highValueTx && (tx.direction === 'cashIn' || (tx.direction === 'cashOut' && rec.isRedemption))) {
|
||||||
if(highValueTx && tx.direction === 'cashIn' || highValueTx && tx.direction === 'cashOut' && rec.isRedemption) {
|
const direction = tx.direction === 'cashOut' ? 'cash-out' : 'cash-in'
|
||||||
queries.addHighValueTx(tx)
|
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)
|
||||||
// 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([
|
// alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled
|
||||||
machineLoader.getMachineName(tx.deviceId),
|
const cashOutConfig = configManager.getCashOut(tx.deviceId, settings.config)
|
||||||
customerPromise
|
const zeroConfLimit = cashOutConfig.zeroConfLimit
|
||||||
])
|
const zeroConf = isCashOut && tx.fiat.lte(zeroConfLimit)
|
||||||
.then(([machineName, customer]) => {
|
const notificationsEnabled = notifSettings.sms.transactions || notifSettings.email.transactions
|
||||||
return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer)
|
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 subject = `Here's an update on transaction ${txId}`
|
||||||
const body = error
|
const body = error
|
||||||
? `Error: ${error}`
|
? `Error: ${error}`
|
||||||
|
|
@ -188,218 +187,173 @@ function sendRedemptionMessage(txId, error) {
|
||||||
return sendTransactionMessage(rec)
|
return sendTransactionMessage(rec)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendTransactionMessage(rec, isHighValueTx) {
|
function sendTransactionMessage (rec, isHighValueTx) {
|
||||||
const settings = await settingsLoader.loadLatest()
|
return settingsLoader.loadLatest().then(settings => {
|
||||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||||
|
|
||||||
let promises = []
|
const promises = []
|
||||||
|
|
||||||
const emailActive =
|
const emailActive =
|
||||||
notifications.email.active &&
|
notifications.email.active &&
|
||||||
(notifications.email.transactions || isHighValueTx)
|
(notifications.email.transactions || isHighValueTx)
|
||||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||||
|
|
||||||
const smsActive =
|
const smsActive =
|
||||||
notifications.sms.active &&
|
notifications.sms.active &&
|
||||||
(notifications.sms.transactions || isHighValueTx)
|
(notifications.sms.transactions || isHighValueTx)
|
||||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||||
|
|
||||||
return Promise.all(promises)
|
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearOldCryptoNotifications = balances => {
|
||||||
|
return queries.getAllValidNotifications(CRYPTO_BALANCE).then(res => {
|
||||||
|
const filterByBalance = _.filter(notification => {
|
||||||
|
const { cryptoCode, code } = notification.detail
|
||||||
|
return !_.find(balance => balance.cryptoCode === cryptoCode && balance.code === code)(balances)
|
||||||
|
})
|
||||||
|
const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(res)
|
||||||
|
|
||||||
/*
|
const notInvalidated = _.filter(notification => {
|
||||||
Notes for new "valid" column on notifications table:
|
return !_.find(id => notification.id === id)(indexesToInvalidate)
|
||||||
|
|
||||||
- We only want to add a new notification if it is present in the high or low warning consts.
|
|
||||||
- Before we add the notification we need to see if there is no "valid" notification in the database.
|
|
||||||
- Since the poller runs every few seconds, if the user marks it as read, this code would add a new notification
|
|
||||||
immediately. This new column helps us decide if a new notification should be added.
|
|
||||||
- "Valid" is defaulted to "true". When the cryptobalance goes over the low threshold or under the high threshold,
|
|
||||||
the notification will be marked as invalid. This will allow a new one to be sent. If the cryptobalance never goes
|
|
||||||
into the middle of the high and low thresholds, the old, "read" notification will still be relevant so we won't add
|
|
||||||
a new one.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const clearOldCryptoNotifications = (balances) => {
|
|
||||||
// get valid crypto notifications from DB
|
|
||||||
// if that notification doesn't exist in balances, then make it invalid on the DB
|
|
||||||
queries.getAllValidNotifications('cryptoBalance').then(res => {
|
|
||||||
const notifications = _.map(it => {
|
|
||||||
return {
|
|
||||||
cryptoCode: it.detail.split('_')[0],
|
|
||||||
code: it.detail.split('_').splice(1).join('_')
|
|
||||||
}
|
|
||||||
}, res)
|
}, res)
|
||||||
_.forEach(notification => {
|
return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated)
|
||||||
const idx = _.findIndex(balance => {
|
})
|
||||||
return balance.code === notification.code && balance.cryptoCode === notification.cryptoCode
|
}
|
||||||
}, balances)
|
|
||||||
|
|
||||||
if(idx !== -1) {
|
const cryptoBalancesNotify = (cryptoWarnings) => {
|
||||||
return
|
return clearOldCryptoNotifications(cryptoWarnings).then(notInvalidated => {
|
||||||
}
|
return cryptoWarnings.forEach(balance => {
|
||||||
// if the notification doesn't exist in the new balances object, then it is outdated and is not valid anymore
|
// if notification exists in DB and wasnt invalidated then don't add a duplicate
|
||||||
return queries.invalidateNotification(notification.id)
|
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)
|
}, 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 balancesNotify = (balances) => {
|
||||||
const highFilter = o => o.code === 'HIGH_CRYPTO_BALANCE'
|
const cryptoFilter = o => o.code === 'HIGH_CRYPTO_BALANCE' || o.code === 'LOW_CRYPTO_BALANCE'
|
||||||
const lowFilter = o => o.code === 'LOW_CRYPTO_BALANCE'
|
const fiatFilter = o => o.code === 'LOW_CASH_OUT'
|
||||||
const highWarnings = _.filter(highFilter, balances)
|
const cryptoWarnings = _.filter(cryptoFilter, balances)
|
||||||
const lowWarnings = _.filter(lowFilter, balances)
|
const fiatWarnings = _.filter(fiatFilter, balances)
|
||||||
|
return Promise.all([cryptoBalancesNotify(cryptoWarnings), fiatBalancesNotify(fiatWarnings)]).catch(console.error)
|
||||||
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 clearOldErrorNotifications = (alerts) => {
|
const clearOldErrorNotifications = alerts => {
|
||||||
queries.getAllValidNotifications('error').then(res => {
|
return queries.getAllValidNotifications(ERROR)
|
||||||
_.forEach(notification => {
|
.then(res => {
|
||||||
const idx = _.findIndex(alert => {
|
// for each valid notification in DB see if it exists in alerts
|
||||||
return alert.code === notification.detail.split('_')[0] && alert.deviceId === notification.device_id
|
// if the notification doesn't exist in alerts, it is not valid anymore
|
||||||
}, alerts)
|
const filterByAlert = _.filter(notification => {
|
||||||
if(idx !== -1) {
|
const { code, deviceId } = notification.detail
|
||||||
return
|
return !_.find(alert => alert.code === code && alert.deviceId === deviceId)(alerts)
|
||||||
}
|
})
|
||||||
// if the notification doesn't exist, then it is outdated and is not valid anymore
|
const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res)
|
||||||
return queries.invalidateNotification(notification.id)
|
if (!indexesToInvalidate.length) return Promise.resolve()
|
||||||
}, res)
|
return queries.batchInvalidate(indexesToInvalidate)
|
||||||
})
|
})
|
||||||
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorAlertsNotify = (alertRec) => {
|
const errorAlertsNotify = (alertRec) => {
|
||||||
let alerts = []
|
const embedDeviceId = deviceId => _.assign({ deviceId })
|
||||||
_.keys(alertRec.devices).forEach(function (device) {
|
const mapToAlerts = _.map(it => _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts))
|
||||||
// embed device ID in each alert object inside the deviceAlerts array
|
const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices)
|
||||||
alertRec.devices[device].deviceAlerts = _.map(alert => {
|
|
||||||
return {...alert, deviceId: device}
|
|
||||||
}, alertRec.devices[device].deviceAlerts)
|
|
||||||
// concat every array into one
|
|
||||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
|
||||||
})
|
|
||||||
|
|
||||||
// now that we have all the alerts, we want to add PING and STALE alerts to the DB
|
|
||||||
// if there is a valid alert on the DB that doesn't exist on the new alerts array,
|
|
||||||
// that alert should be considered invalid
|
|
||||||
// after that, for the alerts array, we have to see if there is a valid alert of
|
|
||||||
// the sorts already on the DB
|
|
||||||
clearOldErrorNotifications(alerts)
|
|
||||||
|
|
||||||
_.forEach(alert => {
|
return clearOldErrorNotifications(alerts).then(() => {
|
||||||
switch(alert.code) {
|
_.forEach(alert => {
|
||||||
case PING:
|
switch (alert.code) {
|
||||||
return queries.getValidNotifications('error', PING, alert.deviceId).then(res => {
|
case PING: {
|
||||||
if(res.length > 0) {
|
const detailB = utils.buildDetail({ code: PING, age: alert.age ? alert.age : -1, deviceId: alert.deviceId })
|
||||||
return Promise.resolve()
|
return queries.getValidNotifications(ERROR, _.omit(['age'], detailB)).then(res => {
|
||||||
}
|
if (res.length > 0) return Promise.resolve()
|
||||||
console.log("Adding PING alert on database for " + alert.machineName)
|
const message = `Machine down`
|
||||||
const message = `Machine down`
|
return queries.addNotification(ERROR, message, detailB)
|
||||||
return queries.addErrorNotification(`${PING}_${alert.age ? alert.age : '-1'}`, message, alert.deviceId)
|
})
|
||||||
})
|
}
|
||||||
case STALE:
|
case STALE: {
|
||||||
return queries.getValidNotifications('error', STALE, alert.deviceId).then(res => {
|
const detailB = utils.buildDetail({ code: STALE, deviceId: alert.deviceId })
|
||||||
if(res.length > 0) {
|
return queries.getValidNotifications(ERROR, detailB).then(res => {
|
||||||
return Promise.resolve()
|
if (res.length > 0) return Promise.resolve()
|
||||||
}
|
const message = `Machine is stuck on ${alert.state} screen`
|
||||||
console.log("Adding STALE alert on database for " + alert.machineName)
|
return queries.addNotification(ERROR, message, detailB)
|
||||||
const message = `Machine is stuck on ${alert.state} screen`
|
})
|
||||||
return queries.addErrorNotification(STALE, message, alert.deviceId)
|
}
|
||||||
})
|
}
|
||||||
default:
|
}, alerts)
|
||||||
return
|
}).catch(console.error)
|
||||||
}
|
|
||||||
}, alerts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const blacklistNotify = (tx, isAddressReuse) => {
|
const blacklistNotify = (tx, isAddressReuse) => {
|
||||||
let detail = ''
|
const code = isAddressReuse ? 'REUSED' : 'BLOCKED'
|
||||||
let message = ''
|
const name = isAddressReuse ? 'reused' : 'blacklisted'
|
||||||
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)}...`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 clearOldCustomerSuspendedNotifications = (customerId, deviceId) => {
|
||||||
const detail = `SUSPENDED_${customerId}`
|
const detailB = utils.buildDetail({ code: 'SUSPENDED', customerId, deviceId })
|
||||||
return queries.invalidateNotification(null, detail, deviceId)
|
return queries.invalidateNotification(detailB, 'compliance')
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerComplianceNotify = (customer, deviceId, prefix, days = null) => {
|
const customerComplianceNotify = (customer, deviceId, code, days = null) => {
|
||||||
// prefix can be "BLOCKED", "SUSPENDED", etc
|
// code for now can be "BLOCKED", "SUSPENDED"
|
||||||
const detail = `${prefix}_${customer.id}`
|
const detailB = utils.buildDetail({ customerId: customer.id, code, deviceId })
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
if (days) {
|
if (days) {
|
||||||
date.setDate(date.getDate() + 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
|
return clearOldCustomerSuspendedNotifications(customer.id, deviceId)
|
||||||
clearOldCustomerSuspendedNotifications(customer.id, deviceId).then(() => {
|
.then(() => queries.getValidNotifications(COMPLIANCE, detailB))
|
||||||
return queries.getValidNotifications('compliance', detail, deviceId)
|
.then(res => {
|
||||||
}).then(res => {
|
if (res.length > 0) return Promise.resolve()
|
||||||
if (res.length > 0) {
|
return queries.addNotification(COMPLIANCE, message, detailB)
|
||||||
return Promise.resolve()
|
})
|
||||||
}
|
.catch(console.error)
|
||||||
return queries.addComplianceNotification(deviceId, detail, message)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -408,7 +362,7 @@ module.exports = {
|
||||||
checkPings,
|
checkPings,
|
||||||
checkStuckScreen,
|
checkStuckScreen,
|
||||||
sendRedemptionMessage,
|
sendRedemptionMessage,
|
||||||
cashCassettesNotify,
|
|
||||||
blacklistNotify,
|
blacklistNotify,
|
||||||
customerComplianceNotify,
|
customerComplianceNotify,
|
||||||
|
clearBlacklistNotification
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
const { v4: uuidv4 } = require('uuid')
|
||||||
|
const pgp = require('pg-promise')()
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
const dbm = require('../postgresql_interface')
|
const dbm = require('../postgresql_interface')
|
||||||
const db = require('../db')
|
const db = require('../db')
|
||||||
const { v4: uuidv4 } = require('uuid')
|
|
||||||
|
|
||||||
// types of notifications able to be inserted into db:
|
// 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
|
error - notifications related to errors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const addHighValueTx = (tx) => {
|
const addNotification = (type, message, detail) => {
|
||||||
const sql = `INSERT INTO notifications (id, type, device_id, message, created) values ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
const sql = `INSERT INTO notifications (id, type, message, detail) VALUES ($1, $2, $3, $4)`
|
||||||
const direction = tx.direction === "cashOut" ? 'cash-out' : 'cash-in'
|
return db.oneOrNone(sql, [uuidv4(), type, message, detail])
|
||||||
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 getAllValidNotifications = (type) => {
|
const getAllValidNotifications = (type) => {
|
||||||
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'`
|
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'`
|
||||||
return db.any(sql, [type])
|
return db.any(sql, [type])
|
||||||
}
|
}
|
||||||
|
|
||||||
const addErrorNotification = (detail, message, deviceId) => {
|
const invalidateNotification = (detail, type) => {
|
||||||
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)`
|
detail = _.omitBy(_.isEmpty, detail)
|
||||||
return db.oneOrNone(sql, [uuidv4(), 'error', detail, deviceId, message])
|
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) => {
|
const batchInvalidate = (ids) => {
|
||||||
let sql;
|
const formattedIds = _.map(pgp.as.text, ids).join(',')
|
||||||
if(!deviceId) {
|
const sql = `UPDATE notifications SET valid = 'f', read = 't', modified = CURRENT_TIMESTAMP WHERE id IN ($1^)`
|
||||||
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2`
|
return db.none(sql, [formattedIds])
|
||||||
}
|
|
||||||
else {
|
|
||||||
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2 AND device_id = $3`
|
|
||||||
}
|
|
||||||
return db.any(sql, [type, `%${detail}%`, deviceId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const invalidateNotification = (id, detail = null, deviceId = null) => {
|
const clearBlacklistNotification = (cryptoCode, cryptoAddress) => {
|
||||||
let sql = ''
|
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')`
|
||||||
if(id) {
|
return db.none(sql, [cryptoCode, cryptoAddress])
|
||||||
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 addComplianceNotification = (deviceId, detail, message) => {
|
const getValidNotifications = (type, detail) => {
|
||||||
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, 'compliance', $2, $3, $4, CURRENT_TIMESTAMP)`
|
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail @> $2`
|
||||||
return db.oneOrNone(sql, [uuidv4(), detail, deviceId, message])
|
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 = {
|
module.exports = {
|
||||||
machineEvents: dbm.machineEvents,
|
machineEvents: dbm.machineEvents,
|
||||||
addHighValueTx,
|
addNotification,
|
||||||
addCashCassetteWarning,
|
getAllValidNotifications,
|
||||||
addCryptoBalanceWarning,
|
invalidateNotification,
|
||||||
addErrorNotification,
|
batchInvalidate,
|
||||||
getUnreadCassetteNotifications,
|
clearBlacklistNotification,
|
||||||
getAllValidNotifications,
|
getValidNotifications,
|
||||||
getValidNotifications,
|
getNotifications,
|
||||||
invalidateNotification,
|
markAsRead,
|
||||||
addComplianceNotification
|
markAllAsRead,
|
||||||
|
hasUnreadNotifications
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,16 @@ const _ = require('lodash/fp')
|
||||||
const utils = require('./utils')
|
const utils = require('./utils')
|
||||||
const sms = require('../sms')
|
const sms = require('../sms')
|
||||||
|
|
||||||
function printSmsAlerts(alertRec, config) {
|
function printSmsAlerts (alertRec, config) {
|
||||||
let alerts = []
|
let alerts = []
|
||||||
|
|
||||||
if (config.balance) {
|
if (config.balance) {
|
||||||
alerts = _.concat(alerts, alertRec.general)
|
alerts = _.concat(alerts, alertRec.general)
|
||||||
}
|
}
|
||||||
|
|
||||||
_.keys(alertRec.devices).forEach(function (device) {
|
_.forEach(device => {
|
||||||
if (config.balance) {
|
alerts = _.concat(alerts, utils.deviceAlerts(config, alertRec, device))
|
||||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
}, _.keys(alertRec.devices))
|
||||||
}
|
|
||||||
|
|
||||||
if (config.errors) {
|
|
||||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alerts.length === 0) return null
|
if (alerts.length === 0) return null
|
||||||
|
|
||||||
|
|
@ -27,12 +21,12 @@ function printSmsAlerts(alertRec, config) {
|
||||||
const code = entry[0]
|
const code = entry[0]
|
||||||
const machineNames = _.filter(
|
const machineNames = _.filter(
|
||||||
_.negate(_.isEmpty),
|
_.negate(_.isEmpty),
|
||||||
_.map('machineName', entry[1]),
|
_.map('machineName', entry[1])
|
||||||
)
|
)
|
||||||
|
|
||||||
const cryptoCodes = _.filter(
|
const cryptoCodes = _.filter(
|
||||||
_.negate(_.isEmpty),
|
_.negate(_.isEmpty),
|
||||||
_.map('cryptoCode', entry[1]),
|
_.map('cryptoCode', entry[1])
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -43,16 +37,11 @@ function printSmsAlerts(alertRec, config) {
|
||||||
}, _.toPairs(alertsMap))
|
}, _.toPairs(alertsMap))
|
||||||
|
|
||||||
const mapByCodeDisplay = _.map(it => {
|
const mapByCodeDisplay = _.map(it => {
|
||||||
if(_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) {
|
if (_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) return it.codeDisplay
|
||||||
return it.codeDisplay
|
if (_.isEmpty(it.machineNames)) return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
|
||||||
}
|
return `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
||||||
if(_.isEmpty(it.machineNames)) {
|
|
||||||
return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
|
|
||||||
}
|
|
||||||
else return `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const displayAlertTypes = _.compose(
|
const displayAlertTypes = _.compose(
|
||||||
_.uniq,
|
_.uniq,
|
||||||
mapByCodeDisplay,
|
mapByCodeDisplay,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ const BigNumber = require('../../../lib/bn')
|
||||||
|
|
||||||
const notifier = require('..')
|
const notifier = require('..')
|
||||||
const utils = require('../utils')
|
const utils = require('../utils')
|
||||||
const queries = require("../queries")
|
|
||||||
const emailFuncs = require('../email')
|
|
||||||
const smsFuncs = require('../sms')
|
const smsFuncs = require('../sms')
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -83,76 +81,79 @@ const notifSettings = {
|
||||||
email_errors: false,
|
email_errors: false,
|
||||||
sms_errors: true,
|
sms_errors: true,
|
||||||
sms_transactions: true,
|
sms_transactions: true,
|
||||||
highValueTransaction: Infinity, //this will make highValueTx always false
|
highValueTransaction: Infinity, // this will make highValueTx always false
|
||||||
sms: {
|
sms: {
|
||||||
active: true,
|
active: true,
|
||||||
errors: true,
|
errors: true,
|
||||||
transactions: false // force early return
|
transactions: false // force early return
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
active: false,
|
active: false,
|
||||||
errors: 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 () => {
|
describe('checkNotifications', () => {
|
||||||
expect.assertions(1)
|
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => {
|
||||||
await expect(
|
expect.assertions(1)
|
||||||
notifier.checkNotification({
|
await expect(
|
||||||
getNotificationConfig: () => ({
|
notifier.checkNotification({
|
||||||
sms: { active: false, errors: false },
|
getNotificationConfig: () => ({
|
||||||
email: { active: false, errors: false }
|
sms: { active: false, errors: false },
|
||||||
|
email: { active: false, errors: false }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
).resolves.toBe(undefined)
|
||||||
).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 () => {
|
||||||
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)
|
||||||
expect.assertions(1)
|
await expect(
|
||||||
await expect(
|
notifier.checkNotification({
|
||||||
notifier.checkNotification({
|
getNotificationConfig: () => ({
|
||||||
getNotificationConfig: () => ({
|
sms: { active: false, errors: true, balance: true },
|
||||||
sms: { active: false, errors: true, balance: true },
|
email: { active: false, errors: true, balance: true }
|
||||||
email: { active: false, errors: true, balance: true }
|
})
|
||||||
})
|
})
|
||||||
})
|
).resolves.toBe(undefined)
|
||||||
).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' }
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Checkpings returns empty array as the value for the id prop, if the lastPing is more recent than 60 seconds', () => {
|
describe('checkPings', () => {
|
||||||
expect(
|
test("Check Pings should return code PING for devices that haven't been pinged recently", () => {
|
||||||
notifier.checkPings([
|
expect(
|
||||||
{
|
notifier.checkPings([
|
||||||
deviceId:
|
{
|
||||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
deviceId:
|
||||||
lastPing: new Date(),
|
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||||
name: 'Abc123'
|
lastPing: '2020-11-16T13:11:03.169Z',
|
||||||
}
|
name: 'Abc123'
|
||||||
])
|
}
|
||||||
).toMatchObject({
|
])
|
||||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': []
|
).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 () => {
|
test('Check notification resolves to undefined if shouldNotAlert is called and is true', async () => {
|
||||||
const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert')
|
const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert')
|
||||||
mockShouldNotAlert.mockReturnValue(true)
|
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)
|
expect(mockSendNoAlerts).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
// vvv tests for checkstuckscreen...
|
describe('checkStuckScreen', () => {
|
||||||
test('checkStuckScreen returns [] when no events are found', () => {
|
test('checkStuckScreen returns [] when no events are found', () => {
|
||||||
expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([])
|
expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('checkStuckScreen returns [] if most recent event is idle', () => {
|
test('checkStuckScreen returns [] if most recent event is idle', () => {
|
||||||
// device_time is what matters for the sorting of the events by recency
|
// device_time is what matters for the sorting of the events by recency
|
||||||
expect(
|
expect(
|
||||||
notifier.checkStuckScreen([
|
notifier.checkStuckScreen([
|
||||||
{
|
{
|
||||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||||
device_id:
|
device_id:
|
||||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||||
event_type: 'stateChange',
|
event_type: 'stateChange',
|
||||||
note: '{"state":"chooseCoin","isIdle":false}',
|
note: '{"state":"chooseCoin","isIdle":false}',
|
||||||
created: '2020-11-23T19:30:29.209Z',
|
created: '2020-11-23T19:30:29.209Z',
|
||||||
device_time: '1999-11-23T19:30:29.177Z',
|
device_time: '1999-11-23T19:30:29.177Z',
|
||||||
age: 157352628.123
|
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',
|
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||||
device_id:
|
device_id:
|
||||||
|
|
@ -216,122 +234,106 @@ test('checkStuckScreen returns [] if most recent event is idle', () => {
|
||||||
event_type: 'stateChange',
|
event_type: 'stateChange',
|
||||||
note: '{"state":"chooseCoin","isIdle":true}',
|
note: '{"state":"chooseCoin","isIdle":true}',
|
||||||
created: '2020-11-23T19:30:29.209Z',
|
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',
|
device_time: '2020-11-23T19:30:29.177Z',
|
||||||
age: 157352628.123
|
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', () => {
|
test('calls sendRedemptionMessage if !zeroConf and rec.isRedemption', () => {
|
||||||
// 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 () => {
|
|
||||||
const configManager = require('../../new-config-manager')
|
const configManager = require('../../new-config-manager')
|
||||||
const settingsLoader = require('../../new-settings-loader')
|
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 getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications')
|
||||||
const getCashOut = jest.spyOn(configManager, 'getCashOut')
|
const getCashOut = jest.spyOn(configManager, 'getCashOut')
|
||||||
|
|
||||||
// sendRedemptionMessage will cause this func to be called
|
// sendRedemptionMessage will cause this func to be called
|
||||||
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => rec)
|
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => rec)
|
||||||
|
|
||||||
getCashOut.mockReturnValue({zeroConfLimit: -Infinity})
|
getCashOut.mockReturnValue({ zeroConfLimit: -Infinity })
|
||||||
loadLatest.mockReturnValue({})
|
loadLatest.mockReturnValue(Promise.resolve({}))
|
||||||
getGlobalNotifications.mockReturnValue({... notifSettings, sms: { active: true, errors: true, transactions: true }})
|
getGlobalNotifications.mockReturnValue({ ...notifSettings, sms: { active: true, errors: true, transactions: true } })
|
||||||
|
|
||||||
const response = await notifier.transactionNotify(tx, {isRedemption: true})
|
return notifier.transactionNotify(tx, { isRedemption: true }).then(response => {
|
||||||
|
// this type of response implies sendRedemptionMessage was called
|
||||||
// this type of response implies sendRedemptionMessage was called
|
expect(response[0]).toMatchObject({
|
||||||
expect(response[0]).toMatchObject({
|
sms: {
|
||||||
sms: {
|
body: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00 - It was just dispensed successfully"
|
||||||
body: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00 - It was just dispensed successfully"
|
},
|
||||||
},
|
email: {
|
||||||
email: {
|
subject: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00",
|
||||||
subject: "Here's an update on transaction bec8d452-9ea2-4846-841b-55a9df8bbd00",
|
body: 'It was just dispensed successfully'
|
||||||
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 configManager = require('../../new-config-manager')
|
||||||
const settingsLoader = require('../../new-settings-loader')
|
const settingsLoader = require('../../new-settings-loader')
|
||||||
const machineLoader = require('../../machine-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 getGlobalNotifications = jest.spyOn(configManager, 'getGlobalNotifications')
|
||||||
const getCashOut = jest.spyOn(configManager, 'getCashOut')
|
const getCashOut = jest.spyOn(configManager, 'getCashOut')
|
||||||
const getMachineName = jest.spyOn(machineLoader, 'getMachineName')
|
const getMachineName = jest.spyOn(machineLoader, 'getMachineName')
|
||||||
const buildTransactionMessage = jest.spyOn(utils, 'buildTransactionMessage')
|
const buildTransactionMessage = jest.spyOn(utils, 'buildTransactionMessage')
|
||||||
|
|
||||||
// sendMessage on emailFuncs isn't called because it is disabled in getGlobalNotifications.mockReturnValue
|
// sendMessage on emailFuncs isn't called because it is disabled in getGlobalNotifications.mockReturnValue
|
||||||
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => ({prop: rec}))
|
jest.spyOn(smsFuncs, 'sendMessage').mockImplementation((_, rec) => ({ prop: rec }))
|
||||||
buildTransactionMessage.mockImplementation(() => ["mock message", false])
|
buildTransactionMessage.mockImplementation(() => ['mock message', false])
|
||||||
|
|
||||||
getMachineName.mockReturnValue("mockMachineName")
|
getMachineName.mockReturnValue('mockMachineName')
|
||||||
getCashOut.mockReturnValue({zeroConfLimit: -Infinity})
|
getCashOut.mockReturnValue({ zeroConfLimit: -Infinity })
|
||||||
loadLatest.mockReturnValue({})
|
loadLatest.mockReturnValue(Promise.resolve({}))
|
||||||
getGlobalNotifications.mockReturnValue({... notifSettings, sms: { active: true, errors: true, transactions: true }})
|
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
|
// getMachineName, buildTransactionMessage and sendTransactionMessage were called, in this order
|
||||||
expect(response).toEqual([{prop: 'mock message'}])
|
expect(response).toEqual([{ prop: 'mock message' }])
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -26,75 +26,79 @@ const notifications = {
|
||||||
email: { active: false, errors: false }
|
email: { active: false, errors: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Build alert fingerprint returns null if no sms or email alerts', () => {
|
describe('buildAlertFingerprint', () => {
|
||||||
expect(
|
test('Build alert fingerprint returns null if no sms or email alerts', () => {
|
||||||
utils.buildAlertFingerprint(
|
expect(
|
||||||
{
|
utils.buildAlertFingerprint(
|
||||||
devices: {},
|
{
|
||||||
deviceNames: {},
|
devices: {},
|
||||||
general: []
|
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
|
sms: {
|
||||||
)
|
body: '[Lamassu] All clear'
|
||||||
).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', () => {
|
test('Send no alerts returns object with email prop if only email is enabled', () => {
|
||||||
expect(
|
expect(utils.sendNoAlerts(plugins, false, true)).toEqual({
|
||||||
typeof utils.buildAlertFingerprint(alertRec, {
|
email: {
|
||||||
sms: { active: true, errors: true },
|
body: 'No errors are reported for your machines.',
|
||||||
email: { active: false, errors: false }
|
subject: '[Lamassu] All clear'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
).toBe('string')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Build alert fingerprint returns hash if [email] or sms are enabled and there are alerts in alertrec', () => {
|
|
||||||
expect(
|
|
||||||
typeof utils.buildAlertFingerprint(alertRec, {
|
|
||||||
sms: { active: false, errors: false },
|
|
||||||
email: { active: true, errors: true }
|
|
||||||
})
|
|
||||||
).toBe('string')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Send no alerts returns empty object with sms and email disabled', () => {
|
|
||||||
expect(utils.sendNoAlerts(plugins, false, false)).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Send no alerts returns object with sms prop with sms only enabled', () => {
|
|
||||||
expect(utils.sendNoAlerts(plugins, true, false)).toEqual({
|
|
||||||
sms: {
|
|
||||||
body: '[Lamassu] All clear'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Send no alerts returns object with sms and email prop with both enabled', () => {
|
|
||||||
expect(utils.sendNoAlerts(plugins, true, true)).toEqual({
|
|
||||||
email: {
|
|
||||||
body: 'No errors are reported for your machines.',
|
|
||||||
subject: '[Lamassu] All clear'
|
|
||||||
},
|
|
||||||
sms: {
|
|
||||||
body: '[Lamassu] All clear'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Send no alerts returns object with email prop if only email is enabled', () => {
|
|
||||||
expect(utils.sendNoAlerts(plugins, false, true)).toEqual({
|
|
||||||
email: {
|
|
||||||
body: 'No errors are reported for your machines.',
|
|
||||||
subject: '[Lamassu] All clear'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,26 @@ const {
|
||||||
ALERT_SEND_INTERVAL
|
ALERT_SEND_INTERVAL
|
||||||
} = require('./codes')
|
} = 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)
|
return _.set('note', JSON.parse(event.note), event)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPing(device) {
|
function checkPing (device) {
|
||||||
const age = +Date.now() - +new Date(device.lastPing)
|
const age = Date.now() - (new Date(device.lastPing).getTime())
|
||||||
if (age > NETWORK_DOWN_TIME)
|
if (age > NETWORK_DOWN_TIME) return [{ code: PING, age, machineName: device.name }]
|
||||||
return [{ code: PING, age, machineName: device.name }]
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,28 +61,7 @@ const shouldNotAlert = currentAlertFingerprint => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAlertTypes(alertRec, config) {
|
function buildAlertFingerprint (alertRec, notifications) {
|
||||||
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) {
|
|
||||||
const sms = getAlertTypes(alertRec, notifications.sms)
|
const sms = getAlertTypes(alertRec, notifications.sms)
|
||||||
const email = getAlertTypes(alertRec, notifications.email)
|
const email = getAlertTypes(alertRec, notifications.email)
|
||||||
if (sms.length === 0 && email.length === 0) return null
|
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')
|
return crypto.createHash('sha256').update(subject).digest('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendNoAlerts(plugins, smsEnabled, emailEnabled) {
|
function sendNoAlerts (plugins, smsEnabled, emailEnabled) {
|
||||||
const subject = '[Lamassu] All clear'
|
const subject = '[Lamassu] All clear'
|
||||||
|
|
||||||
let rec = {}
|
let rec = {}
|
||||||
|
|
@ -117,8 +108,8 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
||||||
status = !isCashOut
|
status = !isCashOut
|
||||||
? 'Successful'
|
? 'Successful'
|
||||||
: !rec.isRedemption
|
: !rec.isRedemption
|
||||||
? 'Successful & awaiting redemption'
|
? 'Successful & awaiting redemption'
|
||||||
: 'Successful & dispensed'
|
: 'Successful & dispensed'
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = `
|
const body = `
|
||||||
|
|
@ -145,7 +136,7 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
||||||
}, highValueTx]
|
}, highValueTx]
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(num, code) {
|
function formatCurrency (num, code) {
|
||||||
return numeral(num).format('0,0.00') + ' ' + code
|
return numeral(num).format('0,0.00') + ' ' + code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +144,42 @@ function formatAge (age, settings) {
|
||||||
return prettyMs(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 = {
|
module.exports = {
|
||||||
codeDisplay,
|
codeDisplay,
|
||||||
parseEventNote,
|
parseEventNote,
|
||||||
|
|
@ -167,5 +194,7 @@ module.exports = {
|
||||||
sendNoAlerts,
|
sendNoAlerts,
|
||||||
buildTransactionMessage,
|
buildTransactionMessage,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatAge
|
formatAge,
|
||||||
|
buildDetail,
|
||||||
|
deviceAlerts
|
||||||
}
|
}
|
||||||
|
|
|
||||||
424
lib/plugins.js
424
lib/plugins.js
|
|
@ -23,7 +23,7 @@ const coinUtils = require('./coin-utils')
|
||||||
const commissionMath = require('./commission-math')
|
const commissionMath = require('./commission-math')
|
||||||
const promoCodes = require('./promo-codes')
|
const promoCodes = require('./promo-codes')
|
||||||
|
|
||||||
const notifier = require('./notifier/index')
|
const notifier = require('./notifier')
|
||||||
|
|
||||||
const mapValuesWithKey = _.mapValues.convert({
|
const mapValuesWithKey = _.mapValues.convert({
|
||||||
cap: false
|
cap: false
|
||||||
|
|
@ -55,8 +55,7 @@ function plugins (settings, deviceId) {
|
||||||
? undefined
|
? undefined
|
||||||
: BN(1).add(BN(commissions.cashOut).div(100))
|
: BN(1).add(BN(commissions.cashOut).div(100))
|
||||||
|
|
||||||
if (Date.now() - rateRec.timestamp > STALE_TICKER)
|
if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode)
|
||||||
return logger.warn('Stale rate for ' + cryptoCode)
|
|
||||||
const rate = rateRec.rates
|
const rate = rateRec.rates
|
||||||
|
|
||||||
withCommission ? rates[cryptoCode] = {
|
withCommission ? rates[cryptoCode] = {
|
||||||
|
|
@ -90,10 +89,8 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
cryptoCodes.forEach((cryptoCode, i) => {
|
cryptoCodes.forEach((cryptoCode, i) => {
|
||||||
const balanceRec = balanceRecs[i]
|
const balanceRec = balanceRecs[i]
|
||||||
if (!balanceRec)
|
if (!balanceRec) return logger.warn('No balance for ' + cryptoCode + ' yet')
|
||||||
return logger.warn('No balance for ' + cryptoCode + ' yet')
|
if (Date.now() - balanceRec.timestamp > STALE_BALANCE) return logger.warn('Stale balance for ' + cryptoCode)
|
||||||
if (Date.now() - balanceRec.timestamp > STALE_BALANCE)
|
|
||||||
return logger.warn('Stale balance for ' + cryptoCode)
|
|
||||||
|
|
||||||
balances[cryptoCode] = balanceRec.balance
|
balances[cryptoCode] = balanceRec.balance
|
||||||
})
|
})
|
||||||
|
|
@ -113,13 +110,10 @@ function plugins (settings, deviceId) {
|
||||||
const sumTxs = (sum, tx) => {
|
const sumTxs = (sum, tx) => {
|
||||||
const bills = tx.bills
|
const bills = tx.bills
|
||||||
const sameDenominations = a => a[0].denomination === a[1].denomination
|
const sameDenominations = a => a[0].denomination === a[1].denomination
|
||||||
const doDenominationsMatch = _.every(
|
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
||||||
sameDenominations,
|
|
||||||
_.zip(cassettes, bills)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!doDenominationsMatch) {
|
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))
|
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
|
||||||
|
|
@ -162,41 +156,30 @@ function plugins (settings, deviceId) {
|
||||||
? argv.cassettes.split(',')
|
? argv.cassettes.split(',')
|
||||||
: rec.counts
|
: rec.counts
|
||||||
|
|
||||||
return Promise.all([
|
const cassettes = [
|
||||||
dbm.cassetteCounts(deviceId),
|
{
|
||||||
cashOutHelper.redeemableTxs(deviceId, excludeTxId)
|
denomination: parseInt(denominations[0], 10),
|
||||||
]).then(([rec, _redeemableTxs]) => {
|
count: parseInt(counts[0], 10)
|
||||||
const redeemableTxs = _.reject(
|
},
|
||||||
_.matchesProperty('id', excludeTxId),
|
{
|
||||||
_redeemableTxs
|
denomination: parseInt(denominations[1], 10),
|
||||||
)
|
count: parseInt(counts[1], 10)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const counts = argv.cassettes ? argv.cassettes.split(',') : rec.counts
|
try {
|
||||||
|
return {
|
||||||
const cassettes = [
|
cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
|
||||||
{
|
virtualCassettes
|
||||||
denomination: parseInt(denominations[0], 10),
|
}
|
||||||
count: parseInt(counts[0], 10)
|
} catch (err) {
|
||||||
},
|
logger.error(err)
|
||||||
{
|
return {
|
||||||
denomination: parseInt(denominations[1], 10),
|
cassettes,
|
||||||
count: parseInt(counts[1], 10)
|
virtualCassettes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
})
|
||||||
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
|
|
||||||
virtualCassettes
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err)
|
|
||||||
return {
|
|
||||||
cassettes,
|
|
||||||
virtualCassettes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchCurrentConfigVersion () {
|
function fetchCurrentConfigVersion () {
|
||||||
|
|
@ -206,23 +189,18 @@ function plugins (settings, deviceId) {
|
||||||
order by id desc
|
order by id desc
|
||||||
limit 1`
|
limit 1`
|
||||||
|
|
||||||
return db.one(sql, ['config']).then(row => row.id)
|
return db.one(sql, ['config'])
|
||||||
|
.then(row => row.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCoinSettings (coinParams) {
|
function mapCoinSettings (coinParams) {
|
||||||
const cryptoCode = coinParams[0]
|
const cryptoCode = coinParams[0]
|
||||||
const cryptoNetwork = coinParams[1]
|
const cryptoNetwork = coinParams[1]
|
||||||
const commissions = configManager.getCommissions(
|
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||||
cryptoCode,
|
|
||||||
deviceId,
|
|
||||||
settings.config
|
|
||||||
)
|
|
||||||
const minimumTx = BN(commissions.minimumTx)
|
const minimumTx = BN(commissions.minimumTx)
|
||||||
const cashInFee = BN(commissions.fixedFee)
|
const cashInFee = BN(commissions.fixedFee)
|
||||||
const cashInCommission = BN(commissions.cashIn)
|
const cashInCommission = BN(commissions.cashIn)
|
||||||
const cashOutCommission = _.isNumber(commissions.cashOut)
|
const cashOutCommission = _.isNumber(commissions.cashOut) ? BN(commissions.cashOut) : null
|
||||||
? BN(commissions.cashOut)
|
|
||||||
: null
|
|
||||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -236,25 +214,15 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollQueries (
|
function pollQueries (serialNumber, deviceTime, deviceRec, machineVersion, machineModel) {
|
||||||
serialNumber,
|
|
||||||
deviceTime,
|
|
||||||
deviceRec,
|
|
||||||
machineVersion,
|
|
||||||
machineModel
|
|
||||||
) {
|
|
||||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||||
|
|
||||||
const fiatCode = localeConfig.fiatCurrency
|
const fiatCode = localeConfig.fiatCurrency
|
||||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||||
|
|
||||||
const tickerPromises = cryptoCodes.map(c =>
|
const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
|
||||||
ticker.getRates(settings, fiatCode, c)
|
|
||||||
)
|
|
||||||
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
|
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
|
||||||
const testnetPromises = cryptoCodes.map(c =>
|
const testnetPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c))
|
||||||
wallet.cryptoNetwork(settings, c)
|
|
||||||
)
|
|
||||||
const pingPromise = recordPing(deviceTime, machineVersion, machineModel)
|
const pingPromise = recordPing(deviceTime, machineVersion, machineModel)
|
||||||
const currentConfigVersionPromise = fetchCurrentConfigVersion()
|
const currentConfigVersionPromise = fetchCurrentConfigVersion()
|
||||||
const currentAvailablePromoCodes = promoCodes.getNumberOfAvailablePromoCodes()
|
const currentAvailablePromoCodes = promoCodes.getNumberOfAvailablePromoCodes()
|
||||||
|
|
@ -289,12 +257,7 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCoins (tx) {
|
function sendCoins (tx) {
|
||||||
return wallet.sendCoins(
|
return wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
|
||||||
settings,
|
|
||||||
tx.toAddress,
|
|
||||||
tx.cryptoAtoms,
|
|
||||||
tx.cryptoCode
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordPing (deviceTime, version, model) {
|
function recordPing (deviceTime, version, model) {
|
||||||
|
|
@ -305,18 +268,11 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
db.none(
|
db.none(`insert into machine_pings(device_id, device_time) values($1, $2)
|
||||||
`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]),
|
||||||
ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`,
|
db.none(pgp.helpers.update(devices, null, 'devices') + 'WHERE device_id = ${deviceId}', {
|
||||||
[deviceId, deviceTime]
|
deviceId
|
||||||
),
|
})
|
||||||
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) {
|
function fiatBalance (fiatCode, cryptoCode) {
|
||||||
const commissions = configManager.getCommissions(
|
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||||
cryptoCode,
|
|
||||||
deviceId,
|
|
||||||
settings.config
|
|
||||||
)
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
ticker.getRates(settings, fiatCode, cryptoCode),
|
ticker.getRates(settings, fiatCode, cryptoCode),
|
||||||
wallet.balance(settings, 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 rawRate = rates.rates.ask
|
||||||
const cashInCommission = BN(1).minus(BN(commissions.cashIn).div(100))
|
const cashInCommission = BN(1).minus(BN(commissions.cashIn).div(100))
|
||||||
const balance = balanceRec.balance
|
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 cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||||
const unitScale = cryptoRec.unitScale
|
const unitScale = cryptoRec.unitScale
|
||||||
const shiftedRate = rate.shift(-unitScale)
|
const shiftedRate = rate.shift(-unitScale)
|
||||||
const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin)
|
const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timestamp: balanceRec.timestamp,
|
timestamp: balanceRec.timestamp,
|
||||||
balance: fiatTransferBalance.truncated().toString()
|
balance: fiatTransferBalance.truncated().toString()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifyConfirmation (tx) {
|
function notifyConfirmation (tx) {
|
||||||
|
|
@ -393,12 +346,13 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sms.sendMessage(settings, rec).then(() => {
|
return sms.sendMessage(settings, rec)
|
||||||
const sql = 'update cash_out_txs set notified=$1 where id=$2'
|
.then(() => {
|
||||||
const values = [true, tx.id]
|
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) {
|
function notifyOperator (tx, rec) {
|
||||||
|
|
@ -407,17 +361,14 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearOldLogs () {
|
function clearOldLogs () {
|
||||||
return logs.clearOldLogs().catch(logger.error)
|
return logs.clearOldLogs()
|
||||||
|
.catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pong () {
|
function pong () {
|
||||||
return db
|
return db.none(`UPDATE server_events SET created=now() WHERE event_type=$1;
|
||||||
.none(
|
|
||||||
`UPDATE server_events SET created=now() WHERE event_type=$1;
|
|
||||||
INSERT INTO server_events (event_type) SELECT $1
|
INSERT INTO server_events (event_type) SELECT $1
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`,
|
WHERE NOT EXISTS (SELECT 1 FROM server_events WHERE event_type=$1);`, ['ping'])
|
||||||
['ping']
|
|
||||||
)
|
|
||||||
.catch(logger.error)
|
.catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -458,18 +409,15 @@ function plugins (settings, deviceId) {
|
||||||
const marketTradesQueues = tradesQueues[market]
|
const marketTradesQueues = tradesQueues[market]
|
||||||
if (!marketTradesQueues || marketTradesQueues.length === 0) return null
|
if (!marketTradesQueues || marketTradesQueues.length === 0) return null
|
||||||
|
|
||||||
logger.debug(
|
logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length)
|
||||||
'[%s] tradesQueues size: %d',
|
|
||||||
market,
|
|
||||||
marketTradesQueues.length
|
|
||||||
)
|
|
||||||
logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0])
|
logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0])
|
||||||
|
|
||||||
const t1 = Date.now()
|
const t1 = Date.now()
|
||||||
|
|
||||||
const filtered = marketTradesQueues.filter(tradeEntry => {
|
const filtered = marketTradesQueues
|
||||||
return t1 - tradeEntry.timestamp < TRADE_TTL
|
.filter(tradeEntry => {
|
||||||
})
|
return t1 - tradeEntry.timestamp < TRADE_TTL
|
||||||
|
})
|
||||||
|
|
||||||
const filteredCount = marketTradesQueues.length - filtered.length
|
const filteredCount = marketTradesQueues.length - filtered.length
|
||||||
|
|
||||||
|
|
@ -480,14 +428,10 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
if (filtered.length === 0) return null
|
if (filtered.length === 0) return null
|
||||||
|
|
||||||
const cryptoAtoms = filtered.reduce(
|
const cryptoAtoms = filtered
|
||||||
(prev, current) => prev.plus(current.cryptoAtoms),
|
.reduce((prev, current) => prev.plus(current.cryptoAtoms), BN(0))
|
||||||
BN(0)
|
|
||||||
)
|
|
||||||
|
|
||||||
const timestamp = filtered
|
const timestamp = filtered.map(r => r.timestamp).reduce((acc, r) => Math.max(acc, r), 0)
|
||||||
.map(r => r.timestamp)
|
|
||||||
.reduce((acc, r) => Math.max(acc, r), 0)
|
|
||||||
|
|
||||||
const consolidatedTrade = {
|
const consolidatedTrade = {
|
||||||
fiatCode,
|
fiatCode,
|
||||||
|
|
@ -503,15 +447,11 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeTrades () {
|
function executeTrades () {
|
||||||
return machineLoader
|
return machineLoader.getMachines()
|
||||||
.getMachines()
|
|
||||||
.then(devices => {
|
.then(devices => {
|
||||||
const deviceIds = devices.map(device => device.deviceId)
|
const deviceIds = devices.map(device => device.deviceId)
|
||||||
const lists = deviceIds.map(deviceId => {
|
const lists = deviceIds.map(deviceId => {
|
||||||
const localeConfig = configManager.getLocale(
|
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||||
deviceId,
|
|
||||||
settings.config
|
|
||||||
)
|
|
||||||
const fiatCode = localeConfig.fiatCurrency
|
const fiatCode = localeConfig.fiatCurrency
|
||||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||||
|
|
||||||
|
|
@ -521,9 +461,8 @@ function plugins (settings, deviceId) {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
const tradesPromises = _.uniq(_.flatten(lists)).map(r =>
|
const tradesPromises = _.uniq(_.flatten(lists))
|
||||||
executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)
|
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.all(tradesPromises)
|
return Promise.all(tradesPromises)
|
||||||
})
|
})
|
||||||
|
|
@ -538,43 +477,41 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
|
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
|
||||||
|
|
||||||
return executeTradeForType(tradeEntry).catch(err => {
|
return executeTradeForType(tradeEntry)
|
||||||
tradesQueues[market].push(tradeEntry)
|
.catch(err => {
|
||||||
if (err.name === 'orderTooSmall') return logger.debug(err.message)
|
tradesQueues[market].push(tradeEntry)
|
||||||
logger.error(err)
|
if (err.name === 'orderTooSmall') return logger.debug(err.message)
|
||||||
})
|
logger.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeTradeForType (_tradeEntry) {
|
function executeTradeForType (_tradeEntry) {
|
||||||
const expand = te =>
|
const expand = te => _.assign(te, {
|
||||||
_.assign(te, {
|
cryptoAtoms: te.cryptoAtoms.abs(),
|
||||||
cryptoAtoms: te.cryptoAtoms.abs(),
|
type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell'
|
||||||
type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell'
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const tradeEntry = expand(_tradeEntry)
|
const tradeEntry = expand(_tradeEntry)
|
||||||
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
|
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
|
||||||
|
|
||||||
return execute(
|
return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
|
||||||
settings,
|
|
||||||
tradeEntry.cryptoAtoms,
|
|
||||||
tradeEntry.fiatCode,
|
|
||||||
tradeEntry.cryptoCode
|
|
||||||
)
|
|
||||||
.then(() => recordTrade(tradeEntry))
|
.then(() => recordTrade(tradeEntry))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return recordTrade(tradeEntry, err).then(() => {
|
return recordTrade(tradeEntry, err)
|
||||||
throw err
|
.then(() => {
|
||||||
})
|
throw err
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertBigNumFields (obj) {
|
function convertBigNumFields (obj) {
|
||||||
const convert = (value, key) =>
|
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
|
||||||
_.includes(key, ['cryptoAtoms', 'fiat']) ? value.toString() : value
|
? value.toString()
|
||||||
|
: value
|
||||||
|
|
||||||
const convertKey = key =>
|
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
|
||||||
_.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key
|
? key + '#'
|
||||||
|
: key
|
||||||
|
|
||||||
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
||||||
}
|
}
|
||||||
|
|
@ -604,10 +541,8 @@ function plugins (settings, deviceId) {
|
||||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||||
|
|
||||||
let promises = []
|
let promises = []
|
||||||
if (notifications.email.active && rec.email)
|
if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec))
|
||||||
promises.push(email.sendMessage(settings, rec))
|
if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec))
|
||||||
if (notifications.sms.active && rec.sms)
|
|
||||||
promises.push(sms.sendMessage(settings, rec))
|
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
@ -617,64 +552,53 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDeviceCashBalances (fiatCode, device) {
|
function checkDeviceCashBalances (fiatCode, device) {
|
||||||
const cashOutConfig = configManager.getCashOut(
|
const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config)
|
||||||
device.deviceId,
|
|
||||||
settings.config
|
|
||||||
)
|
|
||||||
const denomination1 = cashOutConfig.top
|
const denomination1 = cashOutConfig.top
|
||||||
const denomination2 = cashOutConfig.bottom
|
const denomination2 = cashOutConfig.bottom
|
||||||
const cashOutEnabled = cashOutConfig.active
|
const cashOutEnabled = cashOutConfig.active
|
||||||
|
|
||||||
const notifications = configManager.getNotifications(
|
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
||||||
null,
|
|
||||||
device.deviceId,
|
|
||||||
settings.config
|
|
||||||
)
|
|
||||||
|
|
||||||
const machineName = device.name
|
const machineName = device.name
|
||||||
|
|
||||||
const cashInAlert =
|
const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
|
||||||
device.cashbox > notifications.cashInAlertThreshold
|
? {
|
||||||
? {
|
code: 'CASH_BOX_FULL',
|
||||||
code: 'CASH_BOX_FULL',
|
machineName,
|
||||||
machineName,
|
deviceId: device.deviceId,
|
||||||
deviceId: device.deviceId,
|
notes: device.cashbox
|
||||||
notes: device.cashbox
|
}
|
||||||
}
|
: null
|
||||||
: null
|
|
||||||
|
|
||||||
const cassette1Alert =
|
const cassette1Alert = cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1
|
||||||
cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1
|
? {
|
||||||
? {
|
code: 'LOW_CASH_OUT',
|
||||||
code: 'LOW_CASH_OUT',
|
cassette: 1,
|
||||||
cassette: 1,
|
machineName,
|
||||||
machineName,
|
deviceId: device.deviceId,
|
||||||
deviceId: device.deviceId,
|
notes: device.cassette1,
|
||||||
notes: device.cassette1,
|
denomination: denomination1,
|
||||||
denomination: denomination1,
|
fiatCode
|
||||||
fiatCode
|
}
|
||||||
}
|
: null
|
||||||
: null
|
|
||||||
|
|
||||||
const cassette2Alert =
|
const cassette2Alert = cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2
|
||||||
cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2
|
? {
|
||||||
? {
|
code: 'LOW_CASH_OUT',
|
||||||
code: 'LOW_CASH_OUT',
|
cassette: 2,
|
||||||
cassette: 2,
|
machineName,
|
||||||
machineName,
|
deviceId: device.deviceId,
|
||||||
deviceId: device.deviceId,
|
notes: device.cassette2,
|
||||||
notes: device.cassette2,
|
denomination: denomination2,
|
||||||
denomination: denomination2,
|
fiatCode
|
||||||
fiatCode
|
}
|
||||||
}
|
: null
|
||||||
: null
|
|
||||||
|
|
||||||
return _.compact([cashInAlert, cassette1Alert, cassette2Alert])
|
return _.compact([cashInAlert, cassette1Alert, cassette2Alert])
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkCryptoBalances (fiatCode, devices) {
|
function checkCryptoBalances (fiatCode, devices) {
|
||||||
const fiatBalancePromises = cryptoCodes =>
|
const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes)
|
||||||
_.map(c => fiatBalance(fiatCode, c), cryptoCodes)
|
|
||||||
|
|
||||||
const fetchCryptoCodes = _deviceId => {
|
const fetchCryptoCodes = _deviceId => {
|
||||||
const localeConfig = configManager.getLocale(_deviceId, settings.config)
|
const localeConfig = configManager.getLocale(_deviceId, settings.config)
|
||||||
|
|
@ -685,23 +609,18 @@ function plugins (settings, deviceId) {
|
||||||
const cryptoCodes = union(devices)
|
const cryptoCodes = union(devices)
|
||||||
const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode])
|
const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode])
|
||||||
|
|
||||||
return Promise.all(fiatBalancePromises(cryptoCodes)).then(balances =>
|
return Promise.all(fiatBalancePromises(cryptoCodes))
|
||||||
_.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances))
|
.then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkCryptoBalance (fiatCode, rec) {
|
function checkCryptoBalance (fiatCode, rec) {
|
||||||
const [cryptoCode, fiatBalance] = rec
|
const [cryptoCode, fiatBalance] = rec
|
||||||
|
|
||||||
if (!fiatBalance) return null
|
if (!fiatBalance) return null
|
||||||
|
|
||||||
const notifications = configManager.getNotifications(
|
const notifications = configManager.getNotifications(cryptoCode, null, settings.config)
|
||||||
cryptoCode,
|
const override = _.find(override => override.cryptoCurrency === cryptoCode, settings.config.notifications_cryptoBalanceOverrides)
|
||||||
null,
|
const lowAlertThreshold = override ? override.lowBalance : notifications.cryptoLowBalance
|
||||||
settings.config
|
const highAlertThreshold = override ? override.highBalance : notifications.cryptoHighBalance
|
||||||
)
|
|
||||||
const lowAlertThreshold = notifications.cryptoLowBalance
|
|
||||||
const highAlertThreshold = notifications.cryptoHighBalance
|
|
||||||
|
|
||||||
const req = {
|
const req = {
|
||||||
cryptoCode,
|
cryptoCode,
|
||||||
|
|
@ -709,17 +628,13 @@ function plugins (settings, deviceId) {
|
||||||
fiatCode
|
fiatCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (_.isFinite(lowAlertThreshold) && BN(fiatBalance.balance).lt(lowAlertThreshold)) {
|
||||||
_.isFinite(lowAlertThreshold) &&
|
|
||||||
BN(fiatBalance.balance).lt(lowAlertThreshold)
|
|
||||||
)
|
|
||||||
return _.set('code')('LOW_CRYPTO_BALANCE')(req)
|
return _.set('code')('LOW_CRYPTO_BALANCE')(req)
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (_.isFinite(highAlertThreshold) && BN(fiatBalance.balance).gt(highAlertThreshold)) {
|
||||||
_.isFinite(highAlertThreshold) &&
|
|
||||||
BN(fiatBalance.balance).gt(highAlertThreshold)
|
|
||||||
)
|
|
||||||
return _.set('code')('HIGH_CRYPTO_BALANCE')(req)
|
return _.set('code')('HIGH_CRYPTO_BALANCE')(req)
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -728,23 +643,24 @@ function plugins (settings, deviceId) {
|
||||||
const localeConfig = configManager.getGlobalLocale(settings.config)
|
const localeConfig = configManager.getGlobalLocale(settings.config)
|
||||||
const fiatCode = localeConfig.fiatCurrency
|
const fiatCode = localeConfig.fiatCurrency
|
||||||
|
|
||||||
return machineLoader.getMachines().then(devices => {
|
return machineLoader.getMachines()
|
||||||
return Promise.all([
|
.then(devices => {
|
||||||
checkCryptoBalances(fiatCode, devices),
|
return Promise.all([
|
||||||
checkDevicesCashBalances(fiatCode, devices)
|
checkCryptoBalances(fiatCode, devices),
|
||||||
]).then(_.flow(_.flattenDeep, _.compact))
|
checkDevicesCashBalances(fiatCode, devices)
|
||||||
})
|
])
|
||||||
|
.then(_.flow(_.flattenDeep, _.compact))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomCode () {
|
function randomCode () {
|
||||||
return BN(crypto.randomBytes(3).toString('hex'), 16)
|
return BN(crypto.randomBytes(3).toString('hex'), 16).shift(-6).toFixed(6).slice(-6)
|
||||||
.shift(-6)
|
|
||||||
.toFixed(6)
|
|
||||||
.slice(-6)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPhoneCode (phone) {
|
function getPhoneCode (phone) {
|
||||||
const code = argv.mockSms ? '123' : randomCode()
|
const code = argv.mockSms
|
||||||
|
? '123'
|
||||||
|
: randomCode()
|
||||||
|
|
||||||
const rec = {
|
const rec = {
|
||||||
sms: {
|
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) {
|
function sweepHdRow (row) {
|
||||||
const cryptoCode = row.crypto_code
|
const cryptoCode = row.crypto_code
|
||||||
|
|
||||||
return wallet
|
return wallet.sweep(settings, cryptoCode, row.hd_index)
|
||||||
.sweep(settings, cryptoCode, row.hd_index)
|
|
||||||
.then(txHash => {
|
.then(txHash => {
|
||||||
if (txHash) {
|
if (txHash) {
|
||||||
logger.debug('[%s] Swept address with tx: %s', cryptoCode, 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)
|
return db.none(sql, row.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message))
|
||||||
logger.error('[%s] Sweep error: %s', cryptoCode, err.message)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sweepHd () {
|
function sweepHd () {
|
||||||
const sql = `select id, crypto_code, hd_index from cash_out_txs
|
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')`
|
where hd_index is not null and not swept and status in ('confirmed', 'instant')`
|
||||||
|
|
||||||
return db
|
return db.any(sql)
|
||||||
.any(sql)
|
|
||||||
.then(rows => Promise.all(rows.map(sweepHdRow)))
|
.then(rows => Promise.all(rows.map(sweepHdRow)))
|
||||||
.catch(err => logger.error(err))
|
.catch(err => logger.error(err))
|
||||||
}
|
}
|
||||||
|
|
@ -795,15 +708,14 @@ function plugins (settings, deviceId) {
|
||||||
const fiatCode = localeConfig.fiatCurrency
|
const fiatCode = localeConfig.fiatCurrency
|
||||||
|
|
||||||
const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config)
|
const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config)
|
||||||
const tickerPromises = cryptoCodes.map(c =>
|
const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
|
||||||
ticker.getRates(settings, fiatCode, c)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.all(tickerPromises)
|
return Promise.all(tickerPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRates () {
|
function getRates () {
|
||||||
return getRawRates().then(buildRates)
|
return getRawRates()
|
||||||
|
.then(buildRates)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ const _ = require('lodash/fp')
|
||||||
|
|
||||||
exports.NAME = 'MockSMS'
|
exports.NAME = 'MockSMS'
|
||||||
|
|
||||||
exports.sendMessage = function sendMessage(account, rec) {
|
exports.sendMessage = function sendMessage (account, rec) {
|
||||||
console.log('Sending SMS: %j', rec)
|
console.log('Sending SMS: %j', rec)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) {
|
if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
const plugins = require('./plugins')
|
const plugins = require('./plugins')
|
||||||
const notifier = require('./notifier/index')
|
const notifier = require('./notifier')
|
||||||
const T = require('./time')
|
const T = require('./time')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const cashOutTx = require('./cash-out/cash-out-tx')
|
const cashOutTx = require('./cash-out/cash-out-tx')
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const compliance = require('./compliance')
|
||||||
const promoCodes = require('./promo-codes')
|
const promoCodes = require('./promo-codes')
|
||||||
const BN = require('./bn')
|
const BN = require('./bn')
|
||||||
const commissionMath = require('./commission-math')
|
const commissionMath = require('./commission-math')
|
||||||
const notifier = require('./notifier/index')
|
const notifier = require('./notifier')
|
||||||
|
|
||||||
const version = require('../package.json').version
|
const version = require('../package.json').version
|
||||||
|
|
||||||
|
|
@ -324,7 +324,6 @@ function updateCustomer (req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerSanctions (req, res, next) {
|
function triggerSanctions (req, res, next) {
|
||||||
console.log("SANCTIONS TRIGGERED")
|
|
||||||
const id = req.params.id
|
const id = req.params.id
|
||||||
|
|
||||||
customers.getById(id)
|
customers.getById(id)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
var db = require('./db')
|
var db = require('./db')
|
||||||
|
|
||||||
function singleQuotify(item) {
|
const singleQuotify = (item) => `'${item}'`
|
||||||
return "'" + item + "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
var types = [
|
var types = [
|
||||||
'highValueTransaction',
|
'highValueTransaction',
|
||||||
|
|
@ -18,20 +16,17 @@ exports.up = function (next) {
|
||||||
const sql = [
|
const sql = [
|
||||||
`
|
`
|
||||||
CREATE TYPE notification_type AS ENUM ${'(' + types + ')'};
|
CREATE TYPE notification_type AS ENUM ${'(' + types + ')'};
|
||||||
CREATE TABLE IF NOT EXISTS "notifications" (
|
CREATE TABLE "notifications" (
|
||||||
"id" uuid NOT NULL PRIMARY KEY,
|
"id" uuid NOT NULL PRIMARY KEY,
|
||||||
"type" notification_type NOT NULL,
|
"type" notification_type NOT NULL,
|
||||||
"detail" TEXT,
|
"detail" JSONB,
|
||||||
"device_id" TEXT,
|
|
||||||
"message" TEXT NOT NULL,
|
"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',
|
"read" BOOLEAN NOT NULL DEFAULT 'false',
|
||||||
"valid" BOOLEAN NOT NULL DEFAULT 'true',
|
"valid" BOOLEAN NOT NULL DEFAULT 'true',
|
||||||
CONSTRAINT fk_devices
|
"modified" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
FOREIGN KEY(device_id)
|
|
||||||
REFERENCES devices(device_id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
);
|
);
|
||||||
|
CREATE INDEX ON notifications (valid);
|
||||||
CREATE INDEX ON notifications (read);`
|
CREATE INDEX ON notifications (read);`
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<NotificationRow
|
||||||
|
key={n.id}
|
||||||
|
id={n.id}
|
||||||
|
type={n.type}
|
||||||
|
detail={n.detail}
|
||||||
|
message={n.message}
|
||||||
|
deviceName={machines[n.detail.deviceId]}
|
||||||
|
created={n.created}
|
||||||
|
read={n.read}
|
||||||
|
valid={n.valid}
|
||||||
|
onClear={handleClearNotification}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.container}>
|
||||||
|
<div className={classes.header}>
|
||||||
|
<H5 className={classes.headerText}>Notifications</H5>
|
||||||
|
<button onClick={close} className={classes.notificationIcon}>
|
||||||
|
<NotificationIconZodiac />
|
||||||
|
{hasUnread && <div className={classes.hasUnread} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={classes.actionButtons}>
|
||||||
|
{hasUnread && (
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
Icon={ShowUnreadIcon}
|
||||||
|
InverseIcon={ClearAllIconInverse}
|
||||||
|
className={classes.clearAllButton}
|
||||||
|
onClick={() => setShowingUnread(!showingUnread)}>
|
||||||
|
{showingUnread ? 'Show all' : 'Show unread'}
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
Icon={ClearAllIcon}
|
||||||
|
InverseIcon={ClearAllIconInverse}
|
||||||
|
className={classes.clearAllButton}
|
||||||
|
onClick={clearAllNotifications}>
|
||||||
|
Mark all as read
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<div className={classes.notificationsList}>
|
||||||
|
{!loading && buildNotifications()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.background} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationCenter
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import {
|
||||||
|
spacer,
|
||||||
|
white,
|
||||||
|
zircon,
|
||||||
|
secondaryColor,
|
||||||
|
spring3,
|
||||||
|
comet
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
background: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: -1,
|
||||||
|
backgroundColor: white,
|
||||||
|
boxShadow: '0 0 14px 0 rgba(0, 0, 0, 0.24)'
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
left: -200,
|
||||||
|
top: -42,
|
||||||
|
backgroundColor: white,
|
||||||
|
height: '110vh'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
headerText: {
|
||||||
|
marginTop: spacer * 2.5,
|
||||||
|
marginLeft: spacer * 3
|
||||||
|
},
|
||||||
|
actionButtons: {
|
||||||
|
display: 'flex',
|
||||||
|
marginLeft: spacer * 2
|
||||||
|
},
|
||||||
|
notificationIcon: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: spacer * 33,
|
||||||
|
top: spacer * 2 + 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'transparent',
|
||||||
|
boxShadow: '0px 0px 0px transparent',
|
||||||
|
border: '0px solid transparent',
|
||||||
|
textShadow: '0px 0px 0px transparent',
|
||||||
|
outline: 'none'
|
||||||
|
},
|
||||||
|
clearAllButton: {
|
||||||
|
marginTop: -spacer * 2,
|
||||||
|
marginLeft: spacer,
|
||||||
|
backgroundColor: zircon
|
||||||
|
},
|
||||||
|
notificationsList: {
|
||||||
|
width: 440,
|
||||||
|
height: '90vh',
|
||||||
|
maxHeight: '100vh',
|
||||||
|
marginTop: 8,
|
||||||
|
marginLeft: 0,
|
||||||
|
marginRight: 10,
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
backgroundColor: white,
|
||||||
|
zIndex: 10
|
||||||
|
},
|
||||||
|
notificationRow: {
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: spacer / 2,
|
||||||
|
paddingTop: spacer * 1.5
|
||||||
|
},
|
||||||
|
unread: {
|
||||||
|
backgroundColor: spring3
|
||||||
|
},
|
||||||
|
notificationRowIcon: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
'& > *': {
|
||||||
|
marginLeft: spacer * 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unreadIcon: {
|
||||||
|
marginLeft: spacer,
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
backgroundColor: secondaryColor,
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
zIndex: 1
|
||||||
|
},
|
||||||
|
notificationTitle: {
|
||||||
|
margin: 0,
|
||||||
|
color: comet
|
||||||
|
},
|
||||||
|
notificationBody: {
|
||||||
|
margin: 0
|
||||||
|
},
|
||||||
|
notificationSubtitle: {
|
||||||
|
margin: 0,
|
||||||
|
marginBottom: spacer,
|
||||||
|
color: comet
|
||||||
|
},
|
||||||
|
stripes: {
|
||||||
|
position: 'absolute',
|
||||||
|
height: '100%',
|
||||||
|
top: '0px',
|
||||||
|
opacity: '60%'
|
||||||
|
},
|
||||||
|
hasUnread: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 16,
|
||||||
|
width: '9px',
|
||||||
|
height: '9px',
|
||||||
|
backgroundColor: secondaryColor,
|
||||||
|
borderRadius: '50%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styles
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import Grid from '@material-ui/core/Grid'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import prettyMs from 'pretty-ms'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Label1, Label2, TL2 } from 'src/components/typography'
|
||||||
|
import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg'
|
||||||
|
import { ReactComponent as Transaction } from 'src/styling/icons/arrow/transaction.svg'
|
||||||
|
import { ReactComponent as StripesSvg } from 'src/styling/icons/stripes.svg'
|
||||||
|
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
|
||||||
|
|
||||||
|
import styles from './NotificationCenter.styles'
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
highValueTransaction: { display: 'Transactions', icon: <Transaction /> },
|
||||||
|
fiatBalance: { display: 'Maintenance', icon: <Wrench /> },
|
||||||
|
cryptoBalance: { display: 'Maintenance', icon: <Wrench /> },
|
||||||
|
compliance: { display: 'Compliance', icon: <WarningIcon /> },
|
||||||
|
error: { display: 'Error', icon: <WarningIcon /> }
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationRow = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
detail,
|
||||||
|
message,
|
||||||
|
deviceName,
|
||||||
|
created,
|
||||||
|
read,
|
||||||
|
valid,
|
||||||
|
onClear
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const buildType = () => {
|
||||||
|
return types[type].display
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildAge = () => {
|
||||||
|
const createdDate = new Date(created)
|
||||||
|
const interval = +new Date() - createdDate
|
||||||
|
return prettyMs(interval, { compact: true, verbose: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
className={classnames(
|
||||||
|
classes.notificationRow,
|
||||||
|
!read && valid ? classes.unread : ''
|
||||||
|
)}>
|
||||||
|
<Grid item xs={2} className={classes.notificationRowIcon}>
|
||||||
|
{types[type].icon}
|
||||||
|
</Grid>
|
||||||
|
<Grid item container xs={7} direction="row">
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Label2 className={classes.notificationTitle}>
|
||||||
|
{`${buildType()} ${deviceName ? '- ' + deviceName : ''}`}
|
||||||
|
</Label2>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TL2 className={classes.notificationBody}>{message}</TL2>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Label1 className={classes.notificationSubtitle}>
|
||||||
|
{buildAge(created)}
|
||||||
|
</Label1>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={3} style={{ zIndex: 1 }}>
|
||||||
|
{!read && (
|
||||||
|
<div onClick={() => onClear(id)} className={classes.unreadIcon} />
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
{!valid && <StripesSvg className={classes.stripes} />}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationRow
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
import NotificationCenter from './NotificationCenter'
|
||||||
|
export default NotificationCenter
|
||||||
|
|
@ -1,19 +1,31 @@
|
||||||
|
import { useQuery } from '@apollo/react-hooks'
|
||||||
|
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
|
||||||
|
import Popper from '@material-ui/core/Popper'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
import React, { memo, useState } from 'react'
|
import React, { memo, useState } from 'react'
|
||||||
import { NavLink, useHistory } from 'react-router-dom'
|
import { NavLink, useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
import NotificationCenter from 'src/components/NotificationCenter'
|
||||||
import ActionButton from 'src/components/buttons/ActionButton'
|
import ActionButton from 'src/components/buttons/ActionButton'
|
||||||
import { H4 } from 'src/components/typography'
|
import { H4 } from 'src/components/typography'
|
||||||
import AddMachine from 'src/pages/AddMachine'
|
import AddMachine from 'src/pages/AddMachine'
|
||||||
import { ReactComponent as AddIconReverse } from 'src/styling/icons/button/add/white.svg'
|
import { ReactComponent as AddIconReverse } from 'src/styling/icons/button/add/white.svg'
|
||||||
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
|
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
|
||||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
import { ReactComponent as NotificationIcon } from 'src/styling/icons/menu/notification.svg'
|
||||||
|
|
||||||
import styles from './Header.styles'
|
import styles from './Header.styles'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const HAS_UNREAD = gql`
|
||||||
|
query getUnread {
|
||||||
|
hasUnreadNotifications
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const Subheader = ({ item, classes }) => {
|
const Subheader = ({ item, classes }) => {
|
||||||
const [prev, setPrev] = useState(null)
|
const [prev, setPrev] = useState(null)
|
||||||
|
|
||||||
|
|
@ -46,8 +58,10 @@ const Subheader = ({ item, classes }) => {
|
||||||
|
|
||||||
const Header = memo(({ tree }) => {
|
const Header = memo(({ tree }) => {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState(null)
|
||||||
const [active, setActive] = useState()
|
const [active, setActive] = useState()
|
||||||
|
const { data, refetch } = useQuery(HAS_UNREAD)
|
||||||
|
const hasUnread = data?.hasUnreadNotifications ?? false
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
|
@ -56,8 +70,26 @@ const Header = memo(({ tree }) => {
|
||||||
history.push('/maintenance/machine-status', { id: machine.deviceId })
|
history.push('/maintenance/machine-status', { id: machine.deviceId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// these inline styles prevent scroll bubbling: when the user reaches the bottom of the notifications list and keeps scrolling,
|
||||||
|
// the body scrolls, stealing the focus from the notification center, preventing the admin from scrolling the notifications back up
|
||||||
|
// on the first scroll, needing to move the mouse to recapture the focus on the notification center
|
||||||
|
// it also disables the scrollbars caused by the notification center's background to the right of the page, but keeps the scrolling on the body enabled
|
||||||
|
const onClickAway = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
document.querySelector('#root').classList.remove('root-notifcenter-open')
|
||||||
|
document.querySelector('body').classList.remove('body-notifcenter-open')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = event => {
|
||||||
|
setAnchorEl(anchorEl ? null : event.currentTarget)
|
||||||
|
document.querySelector('#root').classList.add('root-notifcenter-open')
|
||||||
|
document.querySelector('body').classList.add('body-notifcenter-open')
|
||||||
|
}
|
||||||
|
|
||||||
|
const popperOpen = Boolean(anchorEl)
|
||||||
|
const id = popperOpen ? 'notifications-popper' : undefined
|
||||||
return (
|
return (
|
||||||
<header>
|
<header className={classes.headerContainer}>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -87,6 +119,8 @@ const Header = memo(({ tree }) => {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div className={classes.actionButtonsContainer}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
Icon={AddIcon}
|
Icon={AddIcon}
|
||||||
|
|
@ -94,7 +128,34 @@ const Header = memo(({ tree }) => {
|
||||||
onClick={() => setOpen(true)}>
|
onClick={() => setOpen(true)}>
|
||||||
Add machine
|
Add machine
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</nav>
|
<ClickAwayListener onClickAway={onClickAway}>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
className={classes.notificationIcon}>
|
||||||
|
<NotificationIcon />
|
||||||
|
{hasUnread && <div className={classes.hasUnread} />}
|
||||||
|
</button>
|
||||||
|
<Popper
|
||||||
|
id={id}
|
||||||
|
open={popperOpen}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
className={classes.popper}
|
||||||
|
disablePortal={false}
|
||||||
|
modifiers={{
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true,
|
||||||
|
boundariesElement: 'viewport'
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<NotificationCenter
|
||||||
|
close={onClickAway}
|
||||||
|
notifyUnread={refetch}
|
||||||
|
/>
|
||||||
|
</Popper>
|
||||||
|
</div>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{active && active.children && (
|
{active && active.children && (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
spacer,
|
spacer,
|
||||||
white,
|
white,
|
||||||
primaryColor,
|
primaryColor,
|
||||||
|
secondaryColor,
|
||||||
placeholderColor,
|
placeholderColor,
|
||||||
subheaderColor,
|
subheaderColor,
|
||||||
fontColor
|
fontColor
|
||||||
|
|
@ -21,6 +22,9 @@ if (version === 8) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
|
headerContainer: {
|
||||||
|
position: 'relative'
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
backgroundColor: primaryColor,
|
backgroundColor: primaryColor,
|
||||||
color: white,
|
color: white,
|
||||||
|
|
@ -80,27 +84,6 @@ const styles = {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: white,
|
color: white,
|
||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent'
|
||||||
// '&:hover': {
|
|
||||||
// color: white
|
|
||||||
// },
|
|
||||||
// '&:hover::after': {
|
|
||||||
// width: '50%',
|
|
||||||
// marginLeft: '-25%'
|
|
||||||
// },
|
|
||||||
// position: 'relative',
|
|
||||||
// '&:after': {
|
|
||||||
// content: '""',
|
|
||||||
// display: 'block',
|
|
||||||
// background: white,
|
|
||||||
// width: 0,
|
|
||||||
// height: 4,
|
|
||||||
// left: '50%',
|
|
||||||
// marginLeft: 0,
|
|
||||||
// bottom: -8,
|
|
||||||
// position: 'absolute',
|
|
||||||
// borderRadius: 1000,
|
|
||||||
// transition: [['all', '0.2s', 'cubic-bezier(0.95, 0.1, 0.45, 0.94)']]
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
forceSize: {
|
forceSize: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
|
|
@ -167,6 +150,35 @@ const styles = {
|
||||||
},
|
},
|
||||||
logoLink: {
|
logoLink: {
|
||||||
cursor: 'pointer'
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
|
actionButtonsContainer: {
|
||||||
|
zIndex: 1,
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
minWidth: 200,
|
||||||
|
transform: 'translateZ(0)'
|
||||||
|
},
|
||||||
|
notificationIcon: {
|
||||||
|
marginTop: spacer / 2,
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'transparent',
|
||||||
|
boxShadow: '0px 0px 0px transparent',
|
||||||
|
border: '0px solid transparent',
|
||||||
|
textShadow: '0px 0px 0px transparent',
|
||||||
|
outline: 'none'
|
||||||
|
},
|
||||||
|
hasUnread: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 5,
|
||||||
|
left: 185,
|
||||||
|
width: '9px',
|
||||||
|
height: '9px',
|
||||||
|
backgroundColor: secondaryColor,
|
||||||
|
borderRadius: '50%'
|
||||||
|
},
|
||||||
|
popper: {
|
||||||
|
zIndex: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
TransactionsList,
|
TransactionsList,
|
||||||
ComplianceDetails
|
ComplianceDetails
|
||||||
} from './components'
|
} from './components'
|
||||||
import { /* getFormattedPhone, */ getName } from './helper'
|
import { getFormattedPhone, getName } from './helper'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
|
@ -147,13 +147,12 @@ const CustomerProfile = memo(() => {
|
||||||
Customers
|
Customers
|
||||||
</Label1>
|
</Label1>
|
||||||
<Label2 noMargin className={classes.labelLink}>
|
<Label2 noMargin className={classes.labelLink}>
|
||||||
{name.length ? name : R.path(['phone'])(customerData)}
|
{name.length
|
||||||
{/* {name.length
|
|
||||||
? name
|
? name
|
||||||
: getFormattedPhone(
|
: getFormattedPhone(
|
||||||
R.path(['phone'])(customerData),
|
R.path(['phone'])(customerData),
|
||||||
locale.country
|
locale.country
|
||||||
)} */}
|
)}
|
||||||
</Label2>
|
</Label2>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,7 @@ import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-ou
|
||||||
import { ifNotNull } from 'src/utils/nullCheck'
|
import { ifNotNull } from 'src/utils/nullCheck'
|
||||||
|
|
||||||
import styles from './CustomersList.styles'
|
import styles from './CustomersList.styles'
|
||||||
import {
|
import { getAuthorizedStatus, getFormattedPhone, getName } from './helper'
|
||||||
getAuthorizedStatus,
|
|
||||||
getName
|
|
||||||
/* getFormattedPhone */
|
|
||||||
} from './helper'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
|
@ -26,7 +22,7 @@ const CustomersList = ({ data, locale, onClick, loading }) => {
|
||||||
{
|
{
|
||||||
header: 'Phone',
|
header: 'Phone',
|
||||||
width: 172,
|
width: 172,
|
||||||
view: it => it.phone // getFormattedPhone(it.phone, locale.country)
|
view: it => getFormattedPhone(it.phone, locale.country)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { ReactComponent as LawIconInverse } from 'src/styling/icons/circle butto
|
||||||
import { ReactComponent as LawIcon } from 'src/styling/icons/circle buttons/law/zodiac.svg'
|
import { ReactComponent as LawIcon } from 'src/styling/icons/circle buttons/law/zodiac.svg'
|
||||||
|
|
||||||
import mainStyles from '../CustomersList.styles'
|
import mainStyles from '../CustomersList.styles'
|
||||||
import { /* getFormattedPhone, */ getName } from '../helper'
|
import { getFormattedPhone, getName } from '../helper'
|
||||||
|
|
||||||
import FrontCameraPhoto from './FrontCameraPhoto'
|
import FrontCameraPhoto from './FrontCameraPhoto'
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ const CustomerDetails = memo(({ customer, locale, setShowCompliance }) => {
|
||||||
{
|
{
|
||||||
header: 'Phone number',
|
header: 'Phone number',
|
||||||
size: 172,
|
size: 172,
|
||||||
value: customer.phone // getFormattedPhone(customer.phone, locale.country)
|
value: getFormattedPhone(customer.phone, locale.country)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'ID number',
|
header: 'ID number',
|
||||||
|
|
@ -47,8 +47,9 @@ const CustomerDetails = memo(({ customer, locale, setShowCompliance }) => {
|
||||||
<div className={classes.name}>
|
<div className={classes.name}>
|
||||||
<IdIcon className={classes.idIcon} />
|
<IdIcon className={classes.idIcon} />
|
||||||
<H2 noMargin>
|
<H2 noMargin>
|
||||||
{name.length ? name : R.path(['phone'])(customer)}
|
{name.length
|
||||||
{/* getFormattedPhone(R.path(['phone'])(customer), locale.country)} */}
|
? name
|
||||||
|
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
|
||||||
</H2>
|
</H2>
|
||||||
<SubpageButton
|
<SubpageButton
|
||||||
className={classes.subpageButton}
|
className={classes.subpageButton}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ const getAuthorizedStatus = it =>
|
||||||
: { label: 'Authorized', type: 'success' }
|
: { label: 'Authorized', type: 'success' }
|
||||||
|
|
||||||
const getFormattedPhone = (phone, country) => {
|
const getFormattedPhone = (phone, country) => {
|
||||||
return phone && country
|
const phoneNumber =
|
||||||
? parsePhoneNumberFromString(phone, country).formatInternational()
|
phone && country ? parsePhoneNumberFromString(phone, country) : null
|
||||||
: ''
|
|
||||||
|
return phoneNumber ? phoneNumber.formatInternational() : phone
|
||||||
}
|
}
|
||||||
|
|
||||||
const getName = it => {
|
const getName = it => {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import { transformNumber } from 'src/utils/number'
|
||||||
|
|
||||||
import NotificationsCtx from '../NotificationsContext'
|
import NotificationsCtx from '../NotificationsContext'
|
||||||
|
|
||||||
const HIGH_BALANCE_KEY = 'cryptoHighBalance'
|
const HIGH_BALANCE_KEY = 'highBalance'
|
||||||
const LOW_BALANCE_KEY = 'cryptoLowBalance'
|
const LOW_BALANCE_KEY = 'lowBalance'
|
||||||
const CRYPTOCURRENCY_KEY = 'cryptoCurrency'
|
const CRYPTOCURRENCY_KEY = 'cryptoCurrency'
|
||||||
const NAME = 'cryptoBalanceOverrides'
|
const NAME = 'cryptoBalanceOverrides'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,18 @@ export default {
|
||||||
width: fill,
|
width: fill,
|
||||||
minHeight: fill
|
minHeight: fill
|
||||||
},
|
},
|
||||||
|
'.root-notifcenter-open': {
|
||||||
|
// for when notification center is open
|
||||||
|
overflowY: 'scroll',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0
|
||||||
|
},
|
||||||
|
'.body-notifcenter-open': {
|
||||||
|
// for when notification center is open
|
||||||
|
overflow: 'hidden'
|
||||||
|
},
|
||||||
html: {
|
html: {
|
||||||
height: fill
|
height: fill
|
||||||
},
|
},
|
||||||
|
|
|
||||||
12
new-lamassu-admin/src/styling/icons/action/wrench/zodiac.svg
Normal file
12
new-lamassu-admin/src/styling/icons/action/wrench/zodiac.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="32px" height="32px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="↳-notification-center" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<g id="notification-center_v01a#2-(open)" transform="translate(-1023.000000, -459.000000)" stroke="#1B2559">
|
||||||
|
<g id="Group-5" transform="translate(1000.000000, 0.000000)">
|
||||||
|
<g id="icon/sf-small/wrench" transform="translate(24.000000, 460.000000)">
|
||||||
|
<path d="M15.7602493,3.10720971 L13.1962412,5.67121772 L10.3290323,5.67121772 L10.3290323,2.80400876 L12.8930403,0.24000075 C12.4378389,0.0872002725 11.9506373,0 11.4434358,0 C8.9282279,0 6.88822153,2.04000637 6.88822153,4.55681424 C6.88822153,5.08081588 6.98102182,5.58321745 7.14422233,6.05201891 L0.580201813,12.6168394 C-0.193400604,13.3904418 -0.193400604,14.6456458 0.580201813,15.4200482 C1.35460423,16.1936506 2.60980816,16.1936506 3.38341057,15.4200482 L9.94823109,8.85602767 C10.4170326,9.01922818 10.9186341,9.11202847 11.4434358,9.11202847 C13.9602436,9.11202847 16.00025,7.0720221 16.00025,4.55681424 C16.00025,4.04961265 15.9130497,3.56241113 15.7602493,3.10720971 Z" id="Stroke-1"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
19
new-lamassu-admin/src/styling/icons/arrow/transaction.svg
Normal file
19
new-lamassu-admin/src/styling/icons/arrow/transaction.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="↳-notification-center" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<g id="notification-center_v01a#1-(marked-one-as-read)" transform="translate(-1023.000000, -123.000000)" stroke="#1B2559">
|
||||||
|
<g id="Group-5" transform="translate(1000.000000, 0.000000)">
|
||||||
|
<g id="Group-4" transform="translate(24.000000, 124.000000)">
|
||||||
|
<g id="Group-3">
|
||||||
|
<line x1="0" y1="4" x2="16" y2="4" id="Path-2"></line>
|
||||||
|
<polyline id="Path-3" points="12 0 16 4 12 8"></polyline>
|
||||||
|
</g>
|
||||||
|
<g id="Group-2" transform="translate(8.000000, 12.000000) scale(-1, 1) translate(-8.000000, -12.000000) translate(0.000000, 8.000000)">
|
||||||
|
<line x1="0" y1="4" x2="16" y2="4" id="Path-2-Copy"></line>
|
||||||
|
<polyline id="Path-3-Copy" points="12 0 16 4 12 8"></polyline>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="nav-/-primary-/-1440" transform="translate(-1295.000000, -19.000000)" stroke="#1B2559" stroke-width="2">
|
||||||
|
<g id="icon/menu/notification" transform="translate(1296.000000, 20.000000)">
|
||||||
|
<path d="M10.1052632,14.3157895 C10.1052632,15.2454737 9.35073684,16 8.42105263,16 C7.49136842,16 6.73684211,15.2454737 6.73684211,14.3157895" id="Stroke-1"></path>
|
||||||
|
<path d="M1.6,14.3157895 C0.7168,14.3157895 0,13.6031813 0,12.7251462 C0,11.8471111 0.7168,11.1345029 1.6,11.1345029 L1.6,6.3625731 C1.6,2.84884211 4.4656,0 8,0 C11.5344,0 14.4,2.84884211 14.4,6.3625731 L14.4,11.1345029 C15.2832,11.1345029 16,11.8471111 16,12.7251462 C16,13.6031813 15.2832,14.3157895 14.4,14.3157895 L1.6,14.3157895 Z" id="Stroke-3" stroke-linejoin="round"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="icon/stage/zodiac/full" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<circle id="Oval-2-Copy" fill="#1B2559" cx="9" cy="9" r="8"></circle>
|
||||||
|
<circle id="Oval-Copy-5" stroke="#1B2559" stroke-width="2" transform="translate(9.000000, 9.000000) rotate(-270.000000) translate(-9.000000, -9.000000) " cx="9" cy="9" r="8"></circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 671 B |
Loading…
Add table
Add a link
Reference in a new issue