Add sms/email notifications on transactions (#198)

* Add sms/email notifications on transactions

* Add configuration for transaction notification

* Add notification on provision address error

* Remove unneeded noop from promises
This commit is contained in:
Rafael Taranto 2018-11-06 10:49:12 -02:00 committed by Josh Harvey
parent 272127518d
commit 809bf5a2a9
7 changed files with 155 additions and 5 deletions

View file

@ -114,6 +114,9 @@
"notificationsEnabled", "notificationsEnabled",
"notificationsEmailEnabled", "notificationsEmailEnabled",
"notificationsSMSEnabled", "notificationsSMSEnabled",
"transactionNotificationsEnabled",
"transactionNotificationsEmailEnabled",
"transactionNotificationsSMSEnabled",
"sms", "sms",
"email" "email"
] ]
@ -769,6 +772,52 @@
], ],
"default": false "default": false
}, },
{
"code": "transactionNotificationsEnabled",
"displayTop": "Transaction Notifications enabled",
"displayBottom": "General",
"displayTopCount": 3,
"fieldType": "onOff",
"fieldClass": null,
"fieldValidation": [
{
"code": "required"
}
],
"default": false
},
{
"code": "transactionNotificationsEmailEnabled",
"displayBottom": "Email",
"displayTopCount": 0,
"fieldType": "onOff",
"fieldClass": null,
"enabledIfAny": [
"transactionNotificationsEnabled"
],
"fieldValidation": [
{
"code": "required"
}
],
"default": false
},
{
"code": "transactionNotificationsSMSEnabled",
"displayBottom": "SMS",
"displayTopCount": 0,
"fieldType": "onOff",
"fieldClass": null,
"enabledIfAny": [
"transactionNotificationsEnabled"
],
"fieldValidation": [
{
"code": "required"
}
],
"default": false
},
{ {
"code": "sms", "code": "sms",
"displayTop": "Gateways", "displayTop": "Gateways",

View file

@ -80,7 +80,11 @@ function postProcess (r, pi) {
sendPending sendPending
} }
}) })
.then(sendRec => logAction(sendRec, r.tx)) .then(sendRec => {
pi.notifyOperator(r.tx, sendRec)
.catch((err) => logger.error('Failure sending transaction notification', err))
return logAction(sendRec, r.tx)
})
} }
function monitorPending (settings) { function monitorPending (settings) {

View file

@ -3,6 +3,7 @@ const pgp = require('pg-promise')()
const E = require('../error') const E = require('../error')
const socket = require('../socket-client') const socket = require('../socket-client')
const logger = require('../logger')
const helper = require('./cash-out-helper') const helper = require('./cash-out-helper')
const cashOutActions = require('./cash-out-actions') const cashOutActions = require('./cash-out-actions')
@ -53,6 +54,8 @@ function preProcess (t, oldTx, newTx, pi) {
return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx) return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx)
}) })
.catch(err => { .catch(err => {
pi.notifyOperator(newTx, { isRedemption: false, error: 'Error while provisioning address' })
.catch((err) => logger.error('Failure sending transaction notification', err))
return cashOutActions.logError(t, 'provisionAddress', err, newTx) return cashOutActions.logError(t, 'provisionAddress', err, newTx)
.then(() => { throw err }) .then(() => { throw err })
}) })
@ -62,7 +65,11 @@ function preProcess (t, oldTx, newTx, pi) {
.then(updatedTx => { .then(updatedTx => {
if (updatedTx.status !== oldTx.status) { if (updatedTx.status !== oldTx.status) {
const isZeroConf = pi.isZeroConf(updatedTx) const isZeroConf = pi.isZeroConf(updatedTx)
if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) pi.sell(updatedTx) if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) {
pi.sell(updatedTx)
pi.notifyOperator(updatedTx, { isRedemption: false })
.catch((err) => logger.error('Failure sending transaction notification', err))
}
const rec = { const rec = {
to_address: updatedTx.toAddress, to_address: updatedTx.toAddress,
@ -78,6 +85,11 @@ function preProcess (t, oldTx, newTx, pi) {
if (hasError || hasDispenseOccurred) { if (hasError || hasDispenseOccurred) {
return cashOutActions.logDispense(t, updatedTx) return cashOutActions.logDispense(t, updatedTx)
.then(updateCassettes(t, updatedTx)) .then(updateCassettes(t, updatedTx))
.then((t) => {
pi.notifyOperator(updatedTx, { isRedemption: true })
.catch((err) => logger.error('Failure sending transaction notification', err))
return t
})
} }
if (!oldTx.phone && newTx.phone) { if (!oldTx.phone && newTx.phone) {

View file

@ -79,6 +79,8 @@ function postProcess (txVector, pi) {
.then(_.constant({bills})) .then(_.constant({bills}))
}) })
.catch(err => { .catch(err => {
pi.notifyOperator(newTx, { error: err.message, isRedemption: true })
.catch((err) => logger.error('Failure sending transaction notification', err))
return cashOutActions.logError(db, 'provisionNotesError', err, newTx) return cashOutActions.logError(db, 'provisionNotesError', err, newTx)
.then(() => { throw err }) .then(() => { throw err })
}) })

View file

@ -103,4 +103,3 @@ function toUnit (cryptoAtoms, cryptoCode) {
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
return cryptoAtoms.shift(-unitScale) return cryptoAtoms.shift(-unitScale)
} }

View file

@ -283,7 +283,7 @@ function computeStatus (customer) {
}]) }])
return _.assign(customer, { return _.assign(customer, {
status: status.label status: _.get('label', status)
}) })
} }

View file

@ -18,6 +18,7 @@ const sms = require('./sms')
const email = require('./email') const email = require('./email')
const cashOutHelper = require('./cash-out/cash-out-helper') const cashOutHelper = require('./cash-out/cash-out-helper')
const machineLoader = require('./machine-loader') const machineLoader = require('./machine-loader')
const customers = require('./customers')
const coinUtils = require('./coin-utils') const coinUtils = require('./coin-utils')
const mapValuesWithKey = _.mapValues.convert({cap: false}) const mapValuesWithKey = _.mapValues.convert({cap: false})
@ -58,6 +59,11 @@ function plugins (settings, deviceId) {
return rates return rates
} }
function transactionNotificationsEnabled () {
const config = configManager.unscoped(settings.config)
return config.transactionNotificationsEnabled
}
function notificationsEnabled () { function notificationsEnabled () {
const config = configManager.unscoped(settings.config) const config = configManager.unscoped(settings.config)
return config.notificationsEnabled return config.notificationsEnabled
@ -324,6 +330,73 @@ function plugins (settings, deviceId) {
}) })
} }
function notifyOperator (tx, rec) {
if (!transactionNotificationsEnabled()) return Promise.resolve()
const isCashOut = tx.direction === 'cashOut'
const zeroConf = isZeroConf(tx)
// 0-conf cash-out should only send notification on redemption
if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) return Promise.resolve()
if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error)
const customerPromise = tx.customerId ? customers.getById(tx.customerId) : Promise.resolve({})
return Promise.all([machineLoader.getMachineName(tx.deviceId), customerPromise])
.then(([machineName, customer]) => {
const direction = isCashOut ? 'Cash Out' : 'Cash In'
const crypto = `${coinUtils.toUnit(tx.cryptoAtoms, tx.cryptoCode)} ${tx.cryptoCode}`
const fiat = `${tx.fiat} ${tx.fiatCode}`
const customerName = customer.name || customer.id
const phone = customer.phone ? `- Phone: ${customer.phone}` : ''
let status
if (rec.error) {
status = `Error - ${rec.error}`
} else {
status = !isCashOut ? 'Successful' : !rec.isRedemption
? 'Successful & awaiting redemption' : 'Successful & dispensed'
}
const body = `
- Transaction ID: ${tx.id}
- Status: ${status}
- Machine name: ${machineName}
- ${direction}
- ${fiat}
- ${crypto}
- Customer: ${customerName}
${phone}
`
const subject = `A transaction just happened`
return {
sms: {
body: `${subject} - ${status}`
},
email: {
subject,
body
}
}
})
.then(sendTransactionMessage)
}
function sendRedemptionMessage (txId, error) {
const subject = `Here's an update on transaction ${txId}`
const body = error ? `Error: ${error}` : 'It was just dispensed successfully'
const rec = {
sms: {
body: `${subject} - ${body}`
},
email: { subject, body }
}
return sendTransactionMessage(rec)
}
function pong () { function pong () {
db.none('insert into server_events (event_type) values ($1)', ['ping']) db.none('insert into server_events (event_type) values ($1)', ['ping'])
.catch(logger.error) .catch(logger.error)
@ -495,6 +568,16 @@ function plugins (settings, deviceId) {
return Promise.all(promises) return Promise.all(promises)
} }
function sendTransactionMessage (rec) {
const config = configManager.unscoped(settings.config)
let promises = []
if (config.transactionNotificationsEmailEnabled) promises.push(email.sendMessage(settings, rec))
if (config.transactionNotificationsSMSEnabled) promises.push(sms.sendMessage(settings, rec))
return Promise.all(promises)
}
function checkDevicesCashBalances (fiatCode, devices) { function checkDevicesCashBalances (fiatCode, devices) {
return _.map(device => checkDeviceCashBalances(fiatCode, device), devices) return _.map(device => checkDeviceCashBalances(fiatCode, device), devices)
} }
@ -657,7 +740,8 @@ function plugins (settings, deviceId) {
buildAvailableCassettes, buildAvailableCassettes,
buy, buy,
sell, sell,
notificationsEnabled notificationsEnabled,
notifyOperator
} }
} }