diff --git a/lamassu-schema.json b/lamassu-schema.json index 9ccf41fe..ddac5ee9 100644 --- a/lamassu-schema.json +++ b/lamassu-schema.json @@ -114,6 +114,9 @@ "notificationsEnabled", "notificationsEmailEnabled", "notificationsSMSEnabled", + "transactionNotificationsEnabled", + "transactionNotificationsEmailEnabled", + "transactionNotificationsSMSEnabled", "sms", "email" ] @@ -769,6 +772,52 @@ ], "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", "displayTop": "Gateways", diff --git a/lib/cash-in/cash-in-tx.js b/lib/cash-in/cash-in-tx.js index 3126d8e3..47aad2ad 100644 --- a/lib/cash-in/cash-in-tx.js +++ b/lib/cash-in/cash-in-tx.js @@ -80,7 +80,11 @@ function postProcess (r, pi) { 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) { diff --git a/lib/cash-out/cash-out-atomic.js b/lib/cash-out/cash-out-atomic.js index 369ef42c..446315b4 100644 --- a/lib/cash-out/cash-out-atomic.js +++ b/lib/cash-out/cash-out-atomic.js @@ -3,6 +3,7 @@ const pgp = require('pg-promise')() const E = require('../error') const socket = require('../socket-client') +const logger = require('../logger') const helper = require('./cash-out-helper') const cashOutActions = require('./cash-out-actions') @@ -53,6 +54,8 @@ function preProcess (t, oldTx, newTx, pi) { return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx) }) .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) .then(() => { throw err }) }) @@ -62,7 +65,11 @@ function preProcess (t, oldTx, newTx, pi) { .then(updatedTx => { if (updatedTx.status !== oldTx.status) { 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 = { to_address: updatedTx.toAddress, @@ -78,6 +85,11 @@ function preProcess (t, oldTx, newTx, pi) { if (hasError || hasDispenseOccurred) { return cashOutActions.logDispense(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) { diff --git a/lib/cash-out/cash-out-tx.js b/lib/cash-out/cash-out-tx.js index 310e9581..41ee2261 100644 --- a/lib/cash-out/cash-out-tx.js +++ b/lib/cash-out/cash-out-tx.js @@ -79,6 +79,8 @@ function postProcess (txVector, pi) { .then(_.constant({bills})) }) .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) .then(() => { throw err }) }) diff --git a/lib/coin-utils.js b/lib/coin-utils.js index df3ebdca..eee6f306 100644 --- a/lib/coin-utils.js +++ b/lib/coin-utils.js @@ -103,4 +103,3 @@ function toUnit (cryptoAtoms, cryptoCode) { const unitScale = cryptoRec.unitScale return cryptoAtoms.shift(-unitScale) } - diff --git a/lib/customers.js b/lib/customers.js index f42eeac3..15307015 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -283,7 +283,7 @@ function computeStatus (customer) { }]) return _.assign(customer, { - status: status.label + status: _.get('label', status) }) } diff --git a/lib/plugins.js b/lib/plugins.js index 9a297a46..89b9a6da 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -18,6 +18,7 @@ const sms = require('./sms') const email = require('./email') const cashOutHelper = require('./cash-out/cash-out-helper') const machineLoader = require('./machine-loader') +const customers = require('./customers') const coinUtils = require('./coin-utils') const mapValuesWithKey = _.mapValues.convert({cap: false}) @@ -58,6 +59,11 @@ function plugins (settings, deviceId) { return rates } + function transactionNotificationsEnabled () { + const config = configManager.unscoped(settings.config) + return config.transactionNotificationsEnabled + } + function notificationsEnabled () { const config = configManager.unscoped(settings.config) 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 () { db.none('insert into server_events (event_type) values ($1)', ['ping']) .catch(logger.error) @@ -495,6 +568,16 @@ function plugins (settings, deviceId) { 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) { return _.map(device => checkDeviceCashBalances(fiatCode, device), devices) } @@ -657,7 +740,8 @@ function plugins (settings, deviceId) { buildAvailableCassettes, buy, sell, - notificationsEnabled + notificationsEnabled, + notifyOperator } }