update cryptoBalance notification

This commit is contained in:
Josh Harvey 2017-05-29 17:02:29 +01:00
parent 6759bec826
commit e8356c1041
8 changed files with 599 additions and 378 deletions

View file

@ -48,6 +48,18 @@
"minimumTx" "minimumTx"
] ]
}, },
{
"code": "balanceAlerts",
"display": "Balance Alerts",
"cryptoScope": "both",
"machineScope": "both",
"fields": [
"cryptoAlertThreshold",
"cashInAlertThreshold",
"cashOutCassette1AlertThreshold",
"cashOutCassette2AlertThreshold"
]
},
{ {
"code": "compliance", "code": "compliance",
"display": "Compliance", "display": "Compliance",
@ -84,8 +96,7 @@
"notificationsEmailEnabled", "notificationsEmailEnabled",
"notificationsSMSEnabled", "notificationsSMSEnabled",
"sms", "sms",
"email", "email"
"balancesThreshold"
] ]
} }
], ],
@ -171,6 +182,93 @@
} }
] ]
}, },
{
"code": "cryptoAlertThreshold",
"displayTop": "Crypto",
"displayBottom": "Threshold",
"fieldType": "integer",
"fieldClass": "fiat",
"cryptoScope": "both",
"machineScope": "global",
"enabledIf": [
"notificationsEnabled"
],
"fieldValidation": [
{
"code": "required"
},
{
"code": "min",
"min": 0
}
]
},
{
"code": "cashInAlertThreshold",
"displayTop": "Cash-in",
"displayBottom": "Threshold",
"fieldType": "integer",
"fieldClass": "banknotes",
"cryptoScope": "global",
"machineScope": "both",
"enabledIf": [
"notificationsEnabled"
],
"fieldValidation": [
{
"code": "required"
},
{
"code": "min",
"min": 0
}
]
},
{
"code": "cashOutCassette1AlertThreshold",
"displayTop": "Cash-out Thresholds",
"displayTopCount": 2,
"displayBottom": "Top",
"fieldType": "integer",
"fieldClass": "banknotes",
"cryptoScope": "global",
"machineScope": "both",
"default": 10,
"enabledIf": [
"notificationsEnabled"
],
"fieldValidation": [
{
"code": "required"
},
{
"code": "min",
"min": 0
}
]
},
{
"code": "cashOutCassette2AlertThreshold",
"displayTopCount": 0,
"displayBottom": "Bottom",
"fieldType": "integer",
"fieldClass": "banknotes",
"cryptoScope": "global",
"machineScope": "both",
"default": 10,
"enabledIf": [
"notificationsEnabled"
],
"fieldValidation": [
{
"code": "required"
},
{
"code": "min",
"min": 0
}
]
},
{ {
"code": "zeroConfLimit", "code": "zeroConfLimit",
"displayTop": "0-conf", "displayTop": "0-conf",
@ -497,17 +595,6 @@
"notificationsEmailEnabled" "notificationsEmailEnabled"
], ],
"fieldValidation": [{"code": "required"}] "fieldValidation": [{"code": "required"}]
},
{
"code": "balancesThreshold",
"displayTop": "Balances",
"displayBottom": "Threshold",
"fieldType": "percentage",
"fieldClass": null,
"enabledIf": [
"notificationsEnabled"
],
"fieldValidation": [{"code": "required"}]
} }
] ]
} }

View file

@ -147,6 +147,7 @@ function validateRequires (config) {
return schema.groups.filter(group => { return schema.groups.filter(group => {
return group.fields.some(fieldCode => { return group.fields.some(fieldCode => {
const field = getGroupField(group, fieldCode) const field = getGroupField(group, fieldCode)
if (!field.fieldValidation.find(r => r.code === 'required')) return false if (!field.fieldValidation.find(r => r.code === 'required')) return false
const refFields = _.map(_.partial(getField, group), field.enabledIf) const refFields = _.map(_.partial(getField, group), field.enabledIf)

View file

@ -12,14 +12,18 @@ const STALE_STATE = 2 * T.minute
const NETWORK_DOWN_TIME = 1 * T.minute const NETWORK_DOWN_TIME = 1 * T.minute
const ALERT_SEND_INTERVAL = T.hour const ALERT_SEND_INTERVAL = T.hour
const PING = Symbol('PING') const PING = 'PING'
const STALE = Symbol('STALE') const STALE = 'STALE'
const LOW_BALANCE = Symbol('LOW_BALANCE') const LOW_CRYPTO_BALANCE = 'LOW_CRYPTO_BALANCE'
const CASH_BOX_FULL = 'CASH_BOX_FULL'
const LOW_CASH_OUT = 'LOW_CASH_OUT'
const CODES_DISPLAY = { const CODES_DISPLAY = {
[PING]: 'Machine Down', PING: 'Machine Down',
[STALE]: 'Machine Stuck', STALE: 'Machine Stuck',
[LOW_BALANCE]: 'Low Balance' LOW_CRYPTO_BALANCE: 'Low Crypto Balance',
CASH_BOX_FULL: 'Cash box full',
LOW_CASH_OUT: 'Low Cash-out'
} }
let alertFingerprint let alertFingerprint
@ -151,7 +155,7 @@ function checkStatus (plugins) {
.then(([balances, events, devices]) => { .then(([balances, events, devices]) => {
return checkPings(devices) return checkPings(devices)
.then(pings => { .then(pings => {
alerts.general = balances alerts.general = _.filter(r => !r.deviceId, balances)
devices.forEach(function (device) { devices.forEach(function (device) {
const deviceId = device.deviceId const deviceId = device.deviceId
const deviceName = device.name const deviceName = device.name
@ -159,12 +163,13 @@ function checkStatus (plugins) {
return eventRow.device_id === deviceId return eventRow.device_id === deviceId
}) })
const balanceAlerts = _.filter(['deviceId', deviceId], balances)
const ping = pings[deviceId] || [] const ping = pings[deviceId] || []
const stuckScreen = checkStuckScreen(deviceEvents) const stuckScreen = checkStuckScreen(deviceEvents)
const deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping const deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
alerts.devices[deviceId] = deviceAlerts alerts.devices[deviceId] = _.concat(deviceAlerts, balanceAlerts)
alerts.deviceNames[deviceId] = deviceName alerts.deviceNames[deviceId] = deviceName
}) })
@ -188,9 +193,13 @@ function emailAlert (alert) {
case STALE: case STALE:
const stuckAge = prettyMs(alert.age, {compact: true, verbose: true}) const stuckAge = prettyMs(alert.age, {compact: true, verbose: true})
return `Machine is stuck on ${alert.state} screen for ${stuckAge}` return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
case LOW_BALANCE: case LOW_CRYPTO_BALANCE:
const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode) const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
return `Low balance of ${balance} in ${alert.cryptoCode} wallet` return `Low balance in ${alert.cryptoCode} [${balance}]`
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]`
} }
} }

View file

@ -470,48 +470,70 @@ function plugins (settings, deviceId) {
return Promise.all(promises) return Promise.all(promises)
} }
function checkDeviceBalances (_deviceId) { function checkDevicesCashBalances (fiatCode, devices) {
const config = configManager.machineScoped(_deviceId, settings.config) return _.map(device => checkDeviceCashBalances(fiatCode, device), devices)
const cryptoCodes = config.cryptoCurrencies
const fiatCode = config.fiatCurrency
const fiatBalancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
return Promise.all(fiatBalancePromises)
.then(arr => {
return arr.map((balance, i) => ({
fiatBalance: balance,
cryptoCode: cryptoCodes[i],
fiatCode,
_deviceId
}))
})
} }
function checkBalance (rec) { function checkDeviceCashBalances (fiatCode, device) {
const config = configManager.unscoped(settings.config) const config = configManager.machineScoped(device.deviceId, settings.config)
const lowBalanceThreshold = config.lowBalanceThreshold const denomination1 = config.topCashOutDenomination
const denomination2 = config.bottomCashOutDenomination
const machineName = config.machineName
return rec.fiatBalance.balance <= lowBalanceThreshold const cashInAlert = device.cashbox > config.cashInAlertThreshold
? {code: Symbol('LOW_BALANCE'), cryptoCode: rec.cryptoCode, fiatBalance: rec.fiatBalance, fiatCode: rec.fiatCode} ? {code: 'CASH_BOX_FULL', machineName, deviceId: device.deviceId, notes: device.cashbox}
: null
const cassette1Alert = device.cassette1 < config.cashOutCassette1AlertThreshold
? {code: 'LOW_CASH_OUT', cassette: 1, machineName, deviceId: device.deviceId,
notes: device.cassette1, denomination: denomination1, fiatCode}
: null
const cassette2Alert = device.cassette2 < config.cashOutCassette2AlertThreshold
? {code: 'LOW_CASH_OUT', cassette: 2, machineName, deviceId: device.deviceId,
notes: device.cassette2, denomination: denomination2, fiatCode}
: null
return _.compact([cashInAlert, cassette1Alert, cassette2Alert])
}
function checkCryptoBalances (fiatCode, devices) {
const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes)
const fetchCryptoCodes = _deviceId => {
const config = configManager.machineScoped(_deviceId, settings.config)
return config.cryptoCurrencies
}
const union = _.flow(_.map(fetchCryptoCodes), _.flatten, _.uniq)
const cryptoCodes = union(devices)
const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode])
return Promise.all(fiatBalancePromises(cryptoCodes))
.then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)))
}
function checkCryptoBalance (fiatCode, rec) {
const [cryptoCode, fiatBalance] = rec
const config = configManager.cryptoScoped(cryptoCode, settings.config)
const cryptoAlertThreshold = config.cryptoAlertThreshold
return BN(fiatBalance.balance).lt(cryptoAlertThreshold)
? {code: 'LOW_CRYPTO_BALANCE', cryptoCode, fiatBalance, fiatCode}
: null : null
} }
function checkBalances () { function checkBalances () {
const globalConfig = configManager.unscoped(settings.config)
const fiatCode = globalConfig.fiatCurrency
return machineLoader.getMachines() return machineLoader.getMachines()
.then(devices => { .then(devices => {
const deviceIds = devices.map(r => r.deviceId) return Promise.all([
const deviceBalancePromises = deviceIds.map(deviceId => checkDeviceBalances(deviceId)) checkCryptoBalances(fiatCode, devices),
checkDevicesCashBalances(fiatCode, devices)
return Promise.all(deviceBalancePromises) ])
.then(arr => { .then(_.flow(_.flattenDeep, _.compact))
const toMarket = r => [r.fiatCode, r.cryptoCode].join('')
const min = _.minBy(r => r.fiatBalance)
const byMarket = _.groupBy(toMarket, _.flatten(arr))
const minByMarket = _.flatMap(min, byMarket)
return _.reject(_.isNil, _.map(checkBalance, minByMarket))
})
}) })
} }

View file

@ -12,7 +12,9 @@ const SWEEP_HD_INTERVAL = T.minute
const TRADE_INTERVAL = 10 * T.seconds const TRADE_INTERVAL = 10 * T.seconds
const PONG_INTERVAL = 10 * T.seconds const PONG_INTERVAL = 10 * T.seconds
const PONG_CLEAR_INTERVAL = 1 * T.day const PONG_CLEAR_INTERVAL = 1 * T.day
const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds
const PENDING_INTERVAL = 10 * T.seconds const PENDING_INTERVAL = 10 * T.seconds
let _pi, _settings let _pi, _settings

View file

@ -0,0 +1,26 @@
var db = require('./db')
exports.up = function (next) {
var sql = [
`create table cash_out_refills (
id uuid PRIMARY KEY,
device_id text not null,
user_id integer not null,
cassette1 integer not null,
cassette2 integer not null,
denomination1 integer not null,
denomination2 integer not null,
created timestamptz not null default now())`,
`create table cash_in_refills (
id uuid PRIMARY KEY,
device_id text not null,
user_id integer not null,
cash_box_count integer not null,
created timestamptz not null default now())`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

File diff suppressed because one or more lines are too long

View file

@ -248,6 +248,8 @@ p {
border-radius: 3px; border-radius: 3px;
position: relative; position: relative;
margin: 0; margin: 0;
border: 2px solid #E6E6E3;
border-radius: 3px;
} }
.lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminNoOptions { .lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminNoOptions {
@ -279,7 +281,7 @@ p {
font-size: 80%; font-size: 80%;
border-radius: 3px; border-radius: 3px;
background-color: #ffffff; background-color: #ffffff;
border: 3px solid #f6f6f4; border: 2px solid #E6E6E3;
border-top: 0; border-top: 0;
color: #5f5f56; color: #5f5f56;
width: 15em; width: 15em;
@ -306,7 +308,7 @@ p {
.lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminMultiItemContainer .lamassuAdminSelectedItem { .lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminMultiItemContainer .lamassuAdminSelectedItem {
background-color: #004062; background-color: #004062;
color: #ffffff; color: #ffffff;
padding: 4px 4px 3px; padding: 2px;
margin: 0 1px; margin: 0 1px;
font-family: Inconsolata, monospace; font-family: Inconsolata, monospace;
font-size: 70%; font-size: 70%;
@ -315,7 +317,7 @@ p {
} }
.lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminMultiItemContainer .lamassuAdminFallbackItem { .lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminMultiItemContainer .lamassuAdminFallbackItem {
background-color: #5f5f56; background-color: #37e8d7;
} }
.lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminSingleItemContainer .lamassuAdminSelectedItem { .lamassuAdminConfigTable .lamassuAdminSelectizeContainer .lamassuAdminSingleItemContainer .lamassuAdminSelectedItem {
@ -340,16 +342,19 @@ p {
.lamassuAdminConfigTable .lamassuAdminInputContainer { .lamassuAdminConfigTable .lamassuAdminInputContainer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
border: 2px solid #E6E6E3;
border-radius: 3px;
} }
.lamassuAdminConfigTable .lamassuAdminUnitDisplay { .lamassuAdminConfigTable .lamassuAdminUnitDisplay {
border-radius: 0px 3px 3px 0px;
background-color: #E6E6E3; background-color: #E6E6E3;
color: #5f5f56; color: #5f5f56;
padding: 0 5px; padding: 0 5px;
font-weight: 700; font-weight: 700;
font-size: 80%; font-size: 80%;
line-height: 25px; line-height: 25px;
cursor: default;
font-family: Nunito, sans-serif;
} }
.lamassuAdminConfigTable input { .lamassuAdminConfigTable input {
@ -369,29 +374,35 @@ p {
background: repeating-linear-gradient(45deg,#dfdfdc,#dfdfdc 2px,#e6e6e3 5px); background: repeating-linear-gradient(45deg,#dfdfdc,#dfdfdc 2px,#e6e6e3 5px);
} }
.lamassuAdminConfigTable .lamassuAdminBasicInput::placeholder {
color: #37e8d7;
opacity: 1;
}
.lamassuAdminConfigTable .lamassuAdminBasicInputDisabled { .lamassuAdminConfigTable .lamassuAdminBasicInputDisabled {
height: 25px; height: 25px;
line-height: 25px; line-height: 25px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #5f5f56; color: #5f5f56;
opacity: 0.7;
text-align: left; text-align: left;
padding: 0 1em; padding: 0 1em;
cursor: default;
background: repeating-linear-gradient(45deg,#dfdfdc,#dfdfdc 2px,#e6e6e3 5px); background: repeating-linear-gradient(45deg,#dfdfdc,#dfdfdc 2px,#e6e6e3 5px);
} }
.lamassuAdminConfigTable .lamassuAdminBasicInputReadOnly { .lamassuAdminConfigTable .lamassuAdminReadOnly {
height: 25px;
line-height: 25px; line-height: 25px;
font-size: 14px;
font-weight: 500;
color: #5f5f56;
text-align: left;
padding: 0 1em;
cursor: default;
background-color: #f6f6f4; background-color: #f6f6f4;
border: 2px solid #E6E6E3; font-family: Inconsolata, monospace;
font-size: 14px;
font-weight: 600;
color: #5f5f56;
cursor: default;
}
.lamassuAdminConfigTable .lamassuAdminReadOnly > .lamassuAdminBasicInputReadOnly {
padding: 0 5px;
} }
.lamassuAdminConfigTable td { .lamassuAdminConfigTable td {
@ -407,16 +418,16 @@ p {
background-color: #ffffff; background-color: #ffffff;
} }
.lamassuAdminConfigTable .lamassuAdminInvalidComponent { .lamassuAdminConfigTable .lamassuAdminFocusedComponent > .lamassuAdminInputContainer {
border-top-color: #eb6b6e; border-color: #37e8d7;
} }
.lamassuAdminConfigTable .lamassuAdminInvalidComponent input { .lamassuAdminConfigTable .lamassuAdminInvalidComponent input {
color: #eb6b6e; color: #eb6b6e;
} }
.lamassuAdminConfigTable .lamassuAdminFocusedComponent { .lamassuAdminConfigTable .lamassuAdminInvalidComponent > .lamassuAdminInputContainer {
border-top-color: #37e8d7; border-color: #eb6b6e;
} }
.lamassuAdminConfigTable tbody td { .lamassuAdminConfigTable tbody td {