lamassu-server/packages/server/lib/notifier/notificationCenter.js
2025-05-12 15:35:00 +01:00

298 lines
9.2 KiB
JavaScript

const _ = require('lodash/fp')
const queries = require('./queries')
const utils = require('./utils')
const customers = require('../customers')
const {
NOTIFICATION_TYPES: {
SECURITY,
COMPLIANCE,
CRYPTO_BALANCE,
FIAT_BALANCE,
ERROR,
HIGH_VALUE_TX,
NORMAL_VALUE_TX,
},
STALE,
PING,
HIGH_CRYPTO_BALANCE,
LOW_CRYPTO_BALANCE,
CASH_BOX_FULL,
LOW_CASH_OUT,
LOW_RECYCLER_STACKER,
} = require('./codes')
const sanctionsNotify = (customer, phone) => {
const code = 'SANCTIONS'
const detailB = utils.buildDetail({ customerId: customer.id, code })
const addNotif = phone =>
queries.addNotification(
COMPLIANCE,
`Blocked customer with phone ${phone} for being on the OFAC sanctions list`,
detailB,
)
// if it's a new customer then phone comes as undefined
return phone
? addNotif(phone)
: customers.getById(customer.id).then(c => addNotif(c.phone))
}
const clearOldCustomerSuspendedNotifications = (customerId, deviceId) => {
const detailB = utils.buildDetail({ code: 'SUSPENDED', customerId, deviceId })
return queries.invalidateNotification(detailB, 'compliance')
}
const customerComplianceNotify = (
customer,
deviceId,
code,
machineName,
days = null,
) => {
// code for now can be "BLOCKED", "SUSPENDED"
const detailB = utils.buildDetail({ customerId: customer.id, code, deviceId })
const date = new Date()
if (days) {
date.setDate(date.getDate() + days)
}
const message =
code === 'SUSPENDED'
? `Customer ${customer.phone} suspended until ${date.toLocaleString()}`
: code === 'BLOCKED'
? `Customer ${customer.phone} blocked`
: `Customer ${customer.phone} has pending compliance in machine ${machineName}`
return clearOldCustomerSuspendedNotifications(customer.id, deviceId)
.then(() => queries.getValidNotifications(COMPLIANCE, detailB))
.then(res => {
if (res.length > 0) return Promise.resolve()
return queries.addNotification(COMPLIANCE, message, detailB)
})
}
const clearOldFiatNotifications = balances => {
return queries.getAllValidNotifications(FIAT_BALANCE).then(notifications => {
const filterByBalance = _.filter(notification => {
const { cassette, deviceId } = notification.detail
return !_.find(
balance =>
balance.cassette === cassette && balance.deviceId === deviceId,
)(balances)
})
const indexesToInvalidate = _.compose(
_.map('id'),
filterByBalance,
)(notifications)
const notInvalidated = _.filter(notification => {
return !_.find(id => notification.id === id)(indexesToInvalidate)
}, notifications)
return (
indexesToInvalidate.length
? queries.batchInvalidate(indexesToInvalidate)
: Promise.resolve()
).then(() => notInvalidated)
})
}
const fiatBalancesNotify = fiatWarnings => {
return clearOldFiatNotifications(fiatWarnings).then(notInvalidated => {
return fiatWarnings.forEach(balance => {
if (
_.find(o => {
const { cassette, deviceId } = o.detail
return cassette === balance.cassette && deviceId === balance.deviceId
}, notInvalidated)
)
return
const message =
balance.code === LOW_CASH_OUT
? `Cash-out cassette ${balance.cassette} low or empty!`
: balance.code === LOW_RECYCLER_STACKER
? `Recycler ${balance.cassette} low or empty!`
: balance.code === CASH_BOX_FULL
? `Cash box full or almost full!`
: `Cash box full or almost full!` /* Shouldn't happen */
const detailB = utils.buildDetail({
deviceId: balance.deviceId,
cassette: balance.cassette,
})
return queries.addNotification(FIAT_BALANCE, message, detailB)
})
})
}
const clearOldCryptoNotifications = balances => {
return queries.getAllValidNotifications(CRYPTO_BALANCE).then(res => {
const filterByBalance = _.filter(notification => {
const { cryptoCode, code } = notification.detail
return !_.find(
balance => balance.cryptoCode === cryptoCode && balance.code === code,
)(balances)
})
const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(res)
const notInvalidated = _.filter(notification => {
return !_.find(id => notification.id === id)(indexesToInvalidate)
}, res)
return (
indexesToInvalidate.length
? queries.batchInvalidate(indexesToInvalidate)
: Promise.resolve()
).then(() => notInvalidated)
})
}
const cryptoBalancesNotify = cryptoWarnings => {
return clearOldCryptoNotifications(cryptoWarnings).then(notInvalidated => {
return cryptoWarnings.forEach(balance => {
// if notification exists in DB and wasnt invalidated then don't add a duplicate
if (
_.find(o => {
const { code, cryptoCode } = o.detail
return code === balance.code && cryptoCode === balance.cryptoCode
}, notInvalidated)
)
return
const fiat = utils.formatCurrency(
balance.fiatBalance.balance,
balance.fiatCode,
)
const message = `${balance.code === HIGH_CRYPTO_BALANCE ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]`
const detailB = utils.buildDetail({
cryptoCode: balance.cryptoCode,
code: balance.code,
})
return queries.addNotification(CRYPTO_BALANCE, message, detailB)
})
})
}
const balancesNotify = balances => {
const isCryptoCode = c =>
_.includes(c, [HIGH_CRYPTO_BALANCE, LOW_CRYPTO_BALANCE])
const isFiatCode = c =>
_.includes(c, [LOW_CASH_OUT, CASH_BOX_FULL, LOW_RECYCLER_STACKER])
const by = o =>
isCryptoCode(o) ? 'crypto' : isFiatCode(o) ? 'fiat' : undefined
const warnings = _.flow(
_.groupBy(_.flow(_.get(['code']), by)),
_.update('crypto', _.defaultTo([])),
_.update('fiat', _.defaultTo([])),
)(balances)
return Promise.all([
cryptoBalancesNotify(warnings.crypto),
fiatBalancesNotify(warnings.fiat),
])
}
const clearOldErrorNotifications = alerts => {
return queries.getAllValidNotifications(ERROR).then(res => {
// for each valid notification in DB see if it exists in alerts
// if the notification doesn't exist in alerts, it is not valid anymore
const filterByAlert = _.filter(notification => {
const { code, deviceId } = notification.detail
return !_.find(
alert => alert.code === code && alert.deviceId === deviceId,
)(alerts)
})
const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res)
if (!indexesToInvalidate.length) return Promise.resolve()
return queries.batchInvalidate(indexesToInvalidate)
})
}
const errorAlertsNotify = alertRec => {
const embedDeviceId = deviceId => _.assign({ deviceId })
const mapToAlerts = _.map(it =>
_.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts),
)
const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices)
return clearOldErrorNotifications(alerts).then(() => {
_.forEach(alert => {
switch (alert.code) {
case PING: {
const detailB = utils.buildDetail({
code: PING,
age: alert.age ? alert.age : -1,
deviceId: alert.deviceId,
})
return queries
.getValidNotifications(ERROR, _.omit(['age'], detailB))
.then(res => {
if (res.length > 0) return Promise.resolve()
const message = `Machine down`
return queries.addNotification(ERROR, message, detailB)
})
}
case STALE: {
const detailB = utils.buildDetail({
code: STALE,
deviceId: alert.deviceId,
})
return queries.getValidNotifications(ERROR, detailB).then(res => {
if (res.length > 0) return Promise.resolve()
const message = `Machine is stuck on ${alert.state} screen`
return queries.addNotification(ERROR, message, detailB)
})
}
}
}, alerts)
})
}
function notifCenterTransactionNotify(
isHighValue,
direction,
fiat,
fiatCode,
deviceId,
cryptoAddress,
) {
const messageSuffix = isHighValue ? 'High value' : ''
const message = `${messageSuffix} ${fiat} ${fiatCode} ${direction} transaction`
const detailB = utils.buildDetail({
deviceId: deviceId,
direction,
fiat,
fiatCode,
cryptoAddress,
})
return queries.addNotification(
isHighValue ? HIGH_VALUE_TX : NORMAL_VALUE_TX,
message,
detailB,
)
}
const blacklistNotify = (tx, isAddressReuse) => {
const code = isAddressReuse ? 'REUSED' : 'BLOCKED'
const name = isAddressReuse ? 'reused' : 'blacklisted'
const detailB = utils.buildDetail({
cryptoCode: tx.cryptoCode,
code,
cryptoAddress: tx.toAddress,
})
const message = `Blocked ${name} address: ${tx.cryptoCode} ${tx.toAddress.substr(0, 10)}...`
return queries.addNotification(COMPLIANCE, message, detailB)
}
const cashboxNotify = deviceId => {
const detailB = utils.buildDetail({ deviceId: deviceId })
const message = `Cashbox removed`
return queries.addNotification(SECURITY, message, detailB)
}
module.exports = {
sanctionsNotify,
customerComplianceNotify,
balancesNotify,
errorAlertsNotify,
notifCenterTransactionNotify,
blacklistNotify,
cashboxNotify,
}