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:
parent
272127518d
commit
809bf5a2a9
7 changed files with 155 additions and 5 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -103,4 +103,3 @@ function toUnit (cryptoAtoms, cryptoCode) {
|
||||||
const unitScale = cryptoRec.unitScale
|
const unitScale = cryptoRec.unitScale
|
||||||
return cryptoAtoms.shift(-unitScale)
|
return cryptoAtoms.shift(-unitScale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,7 @@ function computeStatus (customer) {
|
||||||
}])
|
}])
|
||||||
|
|
||||||
return _.assign(customer, {
|
return _.assign(customer, {
|
||||||
status: status.label
|
status: _.get('label', status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue