From bf8f1d991c6b27869e0bac82f717b6325c5a0012 Mon Sep 17 00:00:00 2001 From: Taranto Date: Wed, 6 May 2020 22:11:55 +0100 Subject: [PATCH] chore: integrate new admin with l-s --- bin/lamassu-display-config.js | 4 +- bin/lamassu-send-coins | 7 +- dev/coinatmradar.js | 5 +- dev/config.js | 6 +- dev/plugins.js | 2 +- dev/send-message.js | 2 +- lib/app.js | 10 +- lib/cash-in/cash-in-tx.js | 13 +- lib/coinatmradar/coinatmradar.js | 95 ++++--- lib/coinatmradar/new-coinatmradar.js | 178 ------------- lib/compliance-triggers.js | 9 + lib/email.js | 7 +- lib/exchange.js | 4 +- lib/layer2.js | 7 +- lib/machine-loader.js | 29 +- lib/new-admin/admin-server.js | 6 +- lib/new-admin/config/accounts.js | 5 +- lib/new-admin/funding.js | 25 +- lib/new-admin/graphql/schema.js | 6 +- lib/new-admin/machines.js | 4 +- lib/new-config-manager.js | 93 +++++++ lib/new-settings-loader.js | 26 +- lib/notifier.js | 145 +++++++--- lib/pairing.js | 29 +- lib/plugins.js | 162 ++++++------ lib/poller.js | 12 +- lib/route-helpers.js | 27 +- lib/routes.js | 114 ++++---- lib/sms.js | 6 +- lib/ticker.js | 5 +- lib/wallet.js | 13 +- new-lamassu-admin/src/components/Modal.js | 44 +-- .../src/components/editableTable/Row.js | 2 + .../src/components/editableTable/Table.js | 2 + .../src/components/fake-table/Table.js | 15 +- .../src/components/fake-table/Table.styles.js | 12 +- .../src/components/inputs/base/TextInput.js | 2 +- .../components/inputs/formik/RadioGroup.js | 3 +- .../src/components/inputs/formik/index.js | 4 +- .../src/pages/Cashout/Cashout.js | 4 +- new-lamassu-admin/src/pages/Cashout/Wizard.js | 24 +- .../src/pages/Cashout/WizardStep.js | 102 +++---- new-lamassu-admin/src/pages/Cashout/helper.js | 13 +- new-lamassu-admin/src/pages/Cashout/index.js | 3 + new-lamassu-admin/src/pages/Commissions.js | 179 ------------- .../src/pages/Commissions.module.scss | 30 --- .../src/pages/Commissions/Commissions.js | 96 +++++++ .../src/pages/Commissions/helper.js | 156 +++++++++++ .../src/pages/Commissions/index.js | 3 + .../src/pages/Customers/Customers.js | 4 +- .../src/pages/Maintenance/Cashboxes.js | 117 ++++++++ .../MachineDetailsCard.js | 10 +- .../MachineStatus.js | 0 .../src/pages/OperatorInfo/ContactInfo.js | 36 +-- .../ReceiptPrinting/ReceiptPrinting.js | 28 +- .../src/pages/OperatorInfo/TermsConditions.js | 20 +- .../src/pages/Services/Services.js | 2 +- .../src/pages/Triggers/NewTriggerWizard.js | 85 ------ .../pages/Triggers/SelectTriggerDirection.js | 77 ------ .../Triggers/SelectTriggerRequirements.js | 141 ---------- .../src/pages/Triggers/SelectTriggerType.js | 104 -------- .../src/pages/Triggers/Triggers.js | 105 ++++---- .../src/pages/Triggers/Triggers.styles.js | 21 +- .../src/pages/Triggers/Wizard.js | 103 ++++++++ .../src/pages/Triggers/helper.js | 250 ++++++++++++++++++ .../src/pages/Wallet/WizardStep.js | 2 +- new-lamassu-admin/src/pages/common.styles.js | 17 -- .../src/pages/maintenance/Cashboxes.js | 210 --------------- new-lamassu-admin/src/routing/routes.js | 9 +- new-lamassu-admin/src/utils/config.js | 4 +- package-lock.json | 3 +- package.json | 6 +- 72 files changed, 1493 insertions(+), 1611 deletions(-) delete mode 100644 lib/coinatmradar/new-coinatmradar.js create mode 100644 lib/compliance-triggers.js create mode 100644 lib/new-config-manager.js create mode 100644 new-lamassu-admin/src/pages/Cashout/index.js delete mode 100644 new-lamassu-admin/src/pages/Commissions.js delete mode 100644 new-lamassu-admin/src/pages/Commissions.module.scss create mode 100644 new-lamassu-admin/src/pages/Commissions/Commissions.js create mode 100644 new-lamassu-admin/src/pages/Commissions/helper.js create mode 100644 new-lamassu-admin/src/pages/Commissions/index.js create mode 100644 new-lamassu-admin/src/pages/Maintenance/Cashboxes.js rename new-lamassu-admin/src/pages/{maintenance => Maintenance}/MachineDetailsCard.js (100%) rename new-lamassu-admin/src/pages/{maintenance => Maintenance}/MachineStatus.js (100%) delete mode 100644 new-lamassu-admin/src/pages/Triggers/NewTriggerWizard.js delete mode 100644 new-lamassu-admin/src/pages/Triggers/SelectTriggerDirection.js delete mode 100644 new-lamassu-admin/src/pages/Triggers/SelectTriggerRequirements.js delete mode 100644 new-lamassu-admin/src/pages/Triggers/SelectTriggerType.js create mode 100644 new-lamassu-admin/src/pages/Triggers/Wizard.js create mode 100644 new-lamassu-admin/src/pages/Triggers/helper.js delete mode 100644 new-lamassu-admin/src/pages/common.styles.js delete mode 100644 new-lamassu-admin/src/pages/maintenance/Cashboxes.js diff --git a/bin/lamassu-display-config.js b/bin/lamassu-display-config.js index ed29d4b7..2b4cb00b 100644 --- a/bin/lamassu-display-config.js +++ b/bin/lamassu-display-config.js @@ -1,4 +1,4 @@ -const settingsLoader = require('../lib/settings-loader') +const settingsLoader = require('../lib/new-settings-loader') const pp = require('../lib/pp') settingsLoader.loadLatest() @@ -9,4 +9,4 @@ settingsLoader.loadLatest() .catch(e => { console.log(e.stack) process.exit(1) - }) + }) \ No newline at end of file diff --git a/bin/lamassu-send-coins b/bin/lamassu-send-coins index 51ac67b3..9d829002 100755 --- a/bin/lamassu-send-coins +++ b/bin/lamassu-send-coins @@ -1,7 +1,7 @@ #!/usr/bin/env node -const settingsLoader = require('../lib/settings-loader') -const configManager = require('../lib/config-manager') +const settingsLoader = require('../lib/new-settings-loader') +const configManager = require('../lib/new-config-manager') const wallet = require('../lib/wallet') const coinUtils = require('../lib/coin-utils') const BN = require('../lib/bn') @@ -40,8 +40,7 @@ console.log('Loading ticker...') settingsLoader.loadLatest() .then(settings => { - const config = configManager.unscoped(settings.config) - const fiatCode = config.fiatCurrency + const fiatCode = configManager.getGlobalLocale(settings.config).fiatCurrency return wallet.isStrictAddress(settings, cryptoCode, toAddress) .then(isValid => { diff --git a/dev/coinatmradar.js b/dev/coinatmradar.js index 62161f25..4f4cf1be 100644 --- a/dev/coinatmradar.js +++ b/dev/coinatmradar.js @@ -1,14 +1,13 @@ const car = require('../lib/coinatmradar/coinatmradar') const plugins = require('../lib/plugins') -require('../lib/settings-loader').loadLatest() +require('../lib/new-settings-loader').loadLatest() .then(settings => { const pi = plugins(settings) - const config = settings.config return pi.getRawRates() .then(rates => { - return car.update({rates, config}, settings) + return car.update(rates, settings) .then(require('../lib/pp')('DEBUG100')) .catch(console.log) .then(() => process.exit()) diff --git a/dev/config.js b/dev/config.js index 51dc2c3e..97a2d33a 100644 --- a/dev/config.js +++ b/dev/config.js @@ -1,8 +1,8 @@ -const settingsLoader = require('../lib/settings-loader') -const configManager = require('../lib/config-manager') +const settingsLoader = require('../lib/new-settings-loader') +const configManager = require('../lib/new-config-manager') settingsLoader.loadLatest() .then(settings => { const config = settings.config - require('../lib/pp')('config')(configManager.all('cryptoCurrencies', config)) + require('../lib/pp')('config')(configManager.getAllCryptoCurrencies(config)) }) diff --git a/dev/plugins.js b/dev/plugins.js index aea224a3..30488199 100644 --- a/dev/plugins.js +++ b/dev/plugins.js @@ -1,5 +1,5 @@ const plugins = require('../lib/plugins') -const settingsLoader = require('../lib/settings-loader') +const settingsLoader = require('../lib/new-settings-loader') const pp = require('../lib/pp') settingsLoader.loadLatest() diff --git a/dev/send-message.js b/dev/send-message.js index f72f6faf..dcf8acd2 100644 --- a/dev/send-message.js +++ b/dev/send-message.js @@ -1,6 +1,6 @@ require('es6-promise').polyfill() -var config = require('../lib/settings-loader') +var config = require('../lib/new-settings-loader') var sms = require('../lib/sms') var rand = Math.floor(Math.random() * 1e6) diff --git a/lib/app.js b/lib/app.js index a5661ebf..293f5b74 100644 --- a/lib/app.js +++ b/lib/app.js @@ -6,8 +6,9 @@ const argv = require('minimist')(process.argv.slice(2)) const routes = require('./routes') const logger = require('./logger') const poller = require('./poller') -const settingsLoader = require('./settings-loader') -const configManager = require('./config-manager') +const settingsLoader = require('./new-settings-loader') +const configManager = require('./new-config-manager') +const complianceTriggers = require('./compliance-triggers') const options = require('./options') const ofac = require('./ofac/index') const ofacUpdate = require('./ofac/update') @@ -43,9 +44,10 @@ function run () { function loadSanctions (settings) { return Promise.resolve() .then(() => { - const config = configManager.unscoped(settings.config) + const triggers = configManager.getTriggers(settings.config) + const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) - if (!config.sanctionsVerificationActive) return + if (!compatTriggers.sanctions) return logger.info('Loading sanctions DB...') return ofacUpdate.update() diff --git a/lib/cash-in/cash-in-tx.js b/lib/cash-in/cash-in-tx.js index 0ef93717..2c8da04f 100644 --- a/lib/cash-in/cash-in-tx.js +++ b/lib/cash-in/cash-in-tx.js @@ -6,8 +6,8 @@ const blacklist = require('../blacklist') const db = require('../db') const plugins = require('../plugins') const logger = require('../logger') -const settingsLoader = require('../settings-loader') -const configManager = require('../config-manager') +const settingsLoader = require('../new-settings-loader') +// const configManager = require('../new-config-manager') const cashInAtomic = require('./cash-in-atomic') const cashInLow = require('./cash-in-low') @@ -26,7 +26,9 @@ function post (machineTx, pi) { return Promise.all([settingsLoader.loadLatest(), checkForBlacklisted(updatedTx)]) .then(([{ config }, blacklistItems]) => { - const rejectAddressReuseActive = configManager.unscoped(config).rejectAddressReuseActive + // TODO new-admin: addressReuse doesnt exist + // const rejectAddressReuseActive = configManager.unscoped(config).rejectAddressReuseActive + const rejectAddressReuseActive = true if (_.some(it => it.created_by_operator === true)(blacklistItems)) { blacklisted = true @@ -123,8 +125,9 @@ function postProcess (r, pi, isBlacklisted, addressReuse) { }) .then(sendRec => { settingsLoader.loadLatest().then(it => { - const config = configManager.unscoped(it.config) - if (config.rejectAddressReuseActive) { + // TODO new-admin: addressReuse doesnt exist + // const config = configManager.unscoped(it.config) + if (true) { blacklist.addToUsedAddresses(r.tx.toAddress, r.tx.cryptoCode) .catch(err => logger.error('Failure adding to addressReuse', err)) } diff --git a/lib/coinatmradar/coinatmradar.js b/lib/coinatmradar/coinatmradar.js index 78acf983..f774a4c4 100644 --- a/lib/coinatmradar/coinatmradar.js +++ b/lib/coinatmradar/coinatmradar.js @@ -7,7 +7,8 @@ const fs = pify(require('fs')) const db = require('../db') const mnemonicHelpers = require('../mnemonic-helpers') -const configManager = require('../config-manager') +const configManager = require('../new-config-manager') +const complianceTriggers = require('../compliance-triggers') const options = require('../options') const logger = require('../logger') const plugins = require('../plugins') @@ -18,19 +19,21 @@ const MAX_CONTENT_LENGTH = 2000 // How long a machine can be down before it's considered offline const STALE_INTERVAL = '2 minutes' -module.exports = { update, mapRecord } +module.exports = { update } -function mapCoin (info, deviceId, settings, cryptoCode) { - const config = info.config - const rates = plugins(settings, deviceId).buildRates(info.rates)[cryptoCode] || { cashIn: null, cashOut: null } - const cryptoConfig = configManager.scoped(cryptoCode, deviceId, config) - const unscoped = configManager.unscoped(config) - const showRates = unscoped.coinAtmRadarShowRates +function mapCoin (rates, deviceId, settings, cryptoCode) { + const config = settings.config + const buildedRates = plugins(settings, deviceId).buildRates(rates)[cryptoCode] || { cashIn: null, cashOut: null } - const cashInFee = showRates ? cryptoConfig.cashInCommission / 100 : null - const cashOutFee = showRates ? cryptoConfig.cashOutCommission / 100 : null - const cashInRate = showRates ? _.invoke('cashIn.toNumber', rates) : null - const cashOutRate = showRates ? _.invoke('cashOut.toNumber', rates) : null + const commissions = configManager.getCommissions(cryptoCode, deviceId, config) + const coinAtmRadar = configManager.getCoinAtmRadar(config) + + const showCommissions = coinAtmRadar.commissions + + const cashInFee = showCommissions ? commissions.cashIn / 100 : null + const cashOutFee = showCommissions ? commissions.cashOut / 100 : null + const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null + const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null return { cryptoCode, @@ -41,33 +44,51 @@ function mapCoin (info, deviceId, settings, cryptoCode) { } } -function mapIdentification (info, deviceId) { - const machineConfig = configManager.machineScoped(deviceId, info.config) +function mapIdentification (config, deviceId) { + const triggers = configManager.getTriggers(deviceId, config) + const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) return { - isPhone: machineConfig.smsVerificationActive, + isPhone: !!compatTriggers.sms, isPalmVein: false, - isPhoto: false, - isIdDocScan: machineConfig.idCardDataVerificationActive, + isPhoto: !!compatTriggers.facephoto, + isIdDocScan: !!compatTriggers.idData, isFingerprint: false } } -function mapMachine (info, settings, machineRow) { +function mapMachine (rates, settings, machineRow) { const deviceId = machineRow.device_id - const config = info.config - const machineConfig = configManager.machineScoped(deviceId, config) + const config = settings.config + + const coinAtmRadar = configManager.getCoinAtmRadar(config) + const triggers = configManager.getTriggers(deviceId, config) + const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) + const locale = configManager.getLocale(deviceId, config) + const cashOutConfig = configManager.getCashOut(deviceId, config) const lastOnline = machineRow.last_online.toISOString() const status = machineRow.stale ? 'online' : 'offline' + const showSupportedCryptocurrencies = coinAtmRadar.supportedCryptocurrencies + const showSupportedFiat = coinAtmRadar.supportedFiat + const showSupportedBuySellDirection = coinAtmRadar.supportedBuySellDirection + const showLimitsAndVerification = coinAtmRadar.limitsAndVerification - const cashLimit = machineConfig.hardLimitVerificationActive - ? machineConfig.hardLimitVerificationThreshold - : Infinity + // TODO new-admin: this is relaying info with backwards compatible triggers + // need to get in touch with coinatmradar before updating this + const cashLimit = showLimitsAndVerification ? ( + !!compatTriggers.block + ? compatTriggers.block + : Infinity ) : null - const cryptoCurrencies = machineConfig.cryptoCurrencies - const identification = mapIdentification(info, deviceId) - const coins = _.map(_.partial(mapCoin, [info, deviceId, settings]), cryptoCurrencies) + const cryptoCurrencies = locale.cryptoCurrencies + const cashInEnabled = showSupportedBuySellDirection ? true : null + const cashOutEnabled = showSupportedBuySellDirection ? cashOutConfig.active : null + const fiat = showSupportedFiat ? locale.fiatCurrency : null + const identification = mapIdentification(config, deviceId) + const coins = showSupportedCryptocurrencies ? + _.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies) + : null return { machineId: deviceId, @@ -85,27 +106,27 @@ function mapMachine (info, settings, machineRow) { }, status, lastOnline, - cashIn: true, - cashOut: machineConfig.cashOutEnabled, + cashIn: cashInEnabled, + cashOut: cashOutEnabled, manufacturer: 'lamassu', cashInTxLimit: cashLimit, cashOutTxLimit: cashLimit, cashInDailyLimit: cashLimit, cashOutDailyLimit: cashLimit, - fiatCurrency: machineConfig.fiatCurrency, + fiatCurrency: fiat, identification, coins } } -function getMachines (info, settings) { +function getMachines (rates, settings) { const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices where display=TRUE and paired=TRUE order by created` return db.any(sql, [STALE_INTERVAL]) - .then(_.map(_.partial(mapMachine, [info, settings]))) + .then(_.map(_.partial(mapMachine, [rates, settings]))) } function sendRadar (data) { @@ -129,9 +150,9 @@ function sendRadar (data) { .then(r => console.log(r.status)) } -function mapRecord (info, settings) { +function mapRecord (rates, settings) { const timestamp = new Date().toISOString() - return Promise.all([getMachines(info, settings), fs.readFile(options.mnemonicPath, 'utf8')]) + return Promise.all([getMachines(rates, settings), fs.readFile(options.mnemonicPath, 'utf8')]) .then(([machines, mnemonic]) => { return { operatorId: computeOperatorId(mnemonicHelpers.toEntropyBuffer(mnemonic)), @@ -146,12 +167,12 @@ function mapRecord (info, settings) { }) } -function update (info, settings) { - const config = configManager.unscoped(info.config) +function update (rates, settings) { + const coinAtmRadar = configManager.getCoinAtmRadar(settings.config) - if (!config.coinAtmRadarActive) return Promise.resolve() + if (!coinAtmRadar.active) return Promise.resolve() - return mapRecord(info, settings) + return mapRecord(rates, settings) .then(sendRadar) .catch(err => logger.error(`Failure to update CoinATMRadar`, err)) } diff --git a/lib/coinatmradar/new-coinatmradar.js b/lib/coinatmradar/new-coinatmradar.js deleted file mode 100644 index 5c590d11..00000000 --- a/lib/coinatmradar/new-coinatmradar.js +++ /dev/null @@ -1,178 +0,0 @@ -const axios = require('axios') -const _ = require('lodash/fp') -const hkdf = require('futoin-hkdf') - -const pify = require('pify') -const fs = pify(require('fs')) - -const db = require('../db') -const mnemonicHelpers = require('../mnemonic-helpers') -const configManager = require('../config-manager') -const options = require('../options') -const logger = require('../logger') -const plugins = require('../plugins') - -const TIMEOUT = 10000 -const MAX_CONTENT_LENGTH = 2000 - -// How long a machine can be down before it's considered offline -const STALE_INTERVAL = '2 minutes' - -module.exports = { update, mapRecord } - -function mapCoin (info, deviceId, settings, cryptoCode) { - const config = info.config - const rates = plugins(settings, deviceId).buildRates(info.rates)[cryptoCode] || { cashIn: null, cashOut: null } - const cryptoConfig = configManager.scoped(cryptoCode, deviceId, config) - const unscoped = configManager.unscoped(config) - const showCommissions = unscoped.coinAtmRadar.sendCommissions - - const cashInFee = showCommissions ? cryptoConfig.cashInCommission / 100 : null - const cashOutFee = showCommissions ? cryptoConfig.cashOutCommission / 100 : null - const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', rates) : null - const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', rates) : null - - return { - cryptoCode, - cashInFee, - cashOutFee, - cashInRate, - cashOutRate - } -} - -function mapIdentification (info, deviceId) { - const machineConfig = configManager.machineScoped(deviceId, info.config) - - return { - isPhone: machineConfig.smsVerificationActive, - isPalmVein: false, - isPhoto: false, - isIdDocScan: machineConfig.idCardDataVerificationActive, - isFingerprint: false - } -} - -function mapMachine (info, settings, machineRow) { - const deviceId = machineRow.device_id - const config = info.config - const unscoped = configManager.unscoped(config) - const machineConfig = configManager.machineScoped(deviceId, config) - - const lastOnline = machineRow.last_online.toISOString() - const status = machineRow.stale ? 'online' : 'offline' - const showSupportedCryptocurrencies = - unscoped.coinAtmRadar.sendSupportedCryptocurrencies - const showSupportedFiat = - unscoped.coinAtmRadar.sendSupportedFiat - const showSupportedBuySellDirection = - unscoped.coinAtmRadar.sendSupportedBuySellDirection - const showLimitsAndVerification = - unscoped.coinAtmRadar.sendLimitsAndVerification - - const cashLimit = showLimitsAndVerification ? ( - machineConfig.hardLimitVerificationActive - ? machineConfig.hardLimitVerificationThreshold - : Infinity ) : null - - const cryptoCurrencies = machineConfig.cryptoCurrencies - const cashInEnabled = showSupportedBuySellDirection ? true : null - const cashOutEnabled = showSupportedBuySellDirection - ? machineConfig.cashOutEnabled - : null - const fiat = showSupportedFiat ? machineConfig.fiatCurrency : null - const identification = mapIdentification(info, deviceId) - const coins = showSupportedCryptocurrencies ? - _.map(_.partial(mapCoin, [info, deviceId, settings]), cryptoCurrencies) - : null - - return { - machineId: deviceId, - address: { - streetAddress: null, - city: null, - region: null, - postalCode: null, - country: null - }, - location: { - name: null, - url: null, - phone: null - }, - status, - lastOnline, - cashIn: cashInEnabled, - cashOut: cashOutEnabled, - manufacturer: 'lamassu', - cashInTxLimit: cashLimit, - cashOutTxLimit: cashLimit, - cashInDailyLimit: cashLimit, - cashOutDailyLimit: cashLimit, - fiatCurrency: fiat, - identification, - coins - } -} - -function getMachines (info, settings) { - const sql = `select device_id, last_online, now() - last_online < $1 as stale from devices - where display=TRUE and - paired=TRUE - order by created` - - return db.any(sql, [STALE_INTERVAL]) - .then(_.map(_.partial(mapMachine, [info, settings]))) -} - -function sendRadar (data) { - const url = _.get(['coinAtmRadar', 'url'], options) - - if (_.isEmpty(url)) { - return Promise.reject(new Error('Missing coinAtmRadar url!')) - } - - const config = { - url, - method: 'post', - data, - timeout: TIMEOUT, - maxContentLength: MAX_CONTENT_LENGTH - } - - console.log('%j', data) - - return axios(config) - .then(r => console.log(r.status)) -} - -function mapRecord (info, settings) { - const timestamp = new Date().toISOString() - return Promise.all([getMachines(info, settings), fs.readFile(options.mnemonicPath, 'utf8')]) - .then(([machines, mnemonic]) => { - return { - operatorId: computeOperatorId(mnemonicHelpers.toEntropyBuffer(mnemonic)), - operator: { - name: null, - phone: null, - email: null - }, - timestamp, - machines - } - }) -} - -function update (info, settings) { - const config = configManager.unscoped(info.config) - - if (!config.coinAtmRadar.active) return Promise.resolve() - - return mapRecord(info, settings) - .then(sendRadar) - .catch(err => logger.error(`Failure to update CoinATMRadar`, err)) -} - -function computeOperatorId (masterSeed) { - return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex') -} diff --git a/lib/compliance-triggers.js b/lib/compliance-triggers.js new file mode 100644 index 00000000..d08cfdb7 --- /dev/null +++ b/lib/compliance-triggers.js @@ -0,0 +1,9 @@ +const _ = require('lodash/fp') + +function getBackwardsCompatibleTriggers (triggers) { + const filtered = _.filter(_.matches({ triggerType: 'volume', cashDirection: 'both' }))(triggers) + const grouped = _.groupBy(_.prop('requirement'))(filtered) + return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped) +} + +module.exports = { getBackwardsCompatibleTriggers} \ No newline at end of file diff --git a/lib/email.js b/lib/email.js index 73c46b04..b69b2124 100644 --- a/lib/email.js +++ b/lib/email.js @@ -1,10 +1,13 @@ -const configManager = require('./config-manager') +// const configManager = require('./new-config-manager') +const logger = require('./logger') const ph = require('./plugin-helper') function sendMessage (settings, rec) { return Promise.resolve() .then(() => { - const pluginCode = configManager.unscoped(settings.config).email + // TODO new-admin + // const pluginCode = configManager.unscoped(settings.config).email + const pluginCode = 'mailgun' const plugin = ph.load(ph.EMAIL, pluginCode) const account = settings.accounts[pluginCode] diff --git a/lib/exchange.js b/lib/exchange.js index a15a4b5b..10070c2b 100644 --- a/lib/exchange.js +++ b/lib/exchange.js @@ -1,8 +1,8 @@ -const configManager = require('./config-manager') +const configManager = require('./new-config-manager') const ph = require('./plugin-helper') function lookupExchange (settings, cryptoCode) { - const exchange = configManager.cryptoScoped(cryptoCode, settings.config).exchange + const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange if (exchange === 'no-exchange') return null return exchange } diff --git a/lib/layer2.js b/lib/layer2.js index 3376eb53..b77403da 100644 --- a/lib/layer2.js +++ b/lib/layer2.js @@ -1,10 +1,9 @@ -const configManager = require('./config-manager') +const configManager = require('./new-config-manager') const ph = require('./plugin-helper') const _ = require('lodash/fp') -const logger = require('./logger') function fetch (settings, cryptoCode) { - const plugin = configManager.cryptoScoped(cryptoCode, settings.config).layer2 + const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2 if (_.isEmpty(plugin) || plugin === 'no-layer2') return Promise.resolve() @@ -34,7 +33,7 @@ function getStatus (settings, tx) { } function cryptoNetwork (settings, cryptoCode) { - const plugin = configManager.cryptoScoped(cryptoCode, settings.config).layer2 + const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2 const layer2 = ph.load(ph.LAYER2, plugin) const account = settings.accounts[plugin] diff --git a/lib/machine-loader.js b/lib/machine-loader.js index 4bce608b..37e5f11f 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -3,8 +3,8 @@ const axios = require('axios') const db = require('./db') const pairing = require('./pairing') -const configManager = require('./config-manager') -const settingsLoader = require('./settings-loader') +const configManager = require('./new-config-manager') +const settingsLoader = require('./new-settings-loader') module.exports = {getMachineName, getMachines, getMachineNames, setMachine} @@ -17,6 +17,7 @@ function getMachines () { cassette2: r.cassette2, pairedAt: new Date(r.created).valueOf(), lastPing: new Date(r.last_online).valueOf(), + name: r.name, // TODO: we shall start using this JSON field at some point // location: r.location, paired: r.paired @@ -26,18 +27,20 @@ function getMachines () { function getConfig (defaultConfig) { if (defaultConfig) return Promise.resolve(defaultConfig) - return settingsLoader.loadRecentConfig() + return settingsLoader.loadLatest().config } function getMachineNames (config) { return Promise.all([getMachines(), getConfig(config)]) .then(([machines, config]) => { const addName = r => { - const machineScoped = configManager.machineScoped(r.deviceId, config) - const name = _.defaultTo('', machineScoped.machineName) - const cashOut = machineScoped.cashOutEnabled - const machineModel = _.defaultTo('', machineScoped.machineModel) - const machineLocation = _.defaultTo('', machineScoped.machineLocation) + const cashOutConfig = configManager.getCashOut(r.deviceId, config) + + const cashOut = cashOutConfig.active + + // TODO new-admin: these two fields were not ever working + const machineModel = '' + const machineLocation = '' // TODO: obtain next fields from somewhere const printer = null @@ -45,7 +48,7 @@ function getMachineNames (config) { const statuses = [{label: 'Unknown detailed status', type: 'warning'}] const softwareVersion = '' - return _.assign(r, {name, cashOut, machineModel, machineLocation, printer, pingTime, statuses, softwareVersion}) + return _.assign(r, {cashOut, machineModel, machineLocation, printer, pingTime, statuses, softwareVersion}) } return _.map(addName, machines) @@ -63,11 +66,9 @@ function getMachineNames (config) { * @returns {string} machine name */ function getMachineName (machineId) { - return settingsLoader.loadRecentConfig() - .then(config => { - const machineScoped = configManager.machineScoped(machineId, config) - return machineScoped.machineName - }) + const sql = 'select * from devices where device_id=$1' + return db.oneOrNone(sql, [machineId]) + .then(it => it.name) } function resetCashOutBills (rec) { diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index 9c4e3751..0f416fff 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -59,8 +59,8 @@ apolloServer.applyMiddleware({ // cors on app for /api/register endpoint. app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3000' })) -app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, {index: false})) -app.use('/front-camera-photo', serveStatic(frontCameraBasedir, {index: false})) +app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false })) +app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false })) app.get('/api/register', (req, res, next) => { const otp = req.query.otp @@ -90,7 +90,7 @@ app.get('/api/register', (req, res, next) => { }) // Everything not on graphql or api/register is redirected to the front-end -app.get('*', (req, res) => res.sendFile(path.resolve('client', 'build', 'index.html'))) +app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html'))) const certOptions = { key: fs.readFileSync(options.keyPath), diff --git a/lib/new-admin/config/accounts.js b/lib/new-admin/config/accounts.js index a972a4e3..4d0a9ac4 100644 --- a/lib/new-admin/config/accounts.js +++ b/lib/new-admin/config/accounts.js @@ -11,7 +11,7 @@ const ID_VERIFIER = 'idVerifier' const EMAIL = 'email' const ZERO_CONF = 'zeroConf' -const ACCOUNT_LIST = [ +const ALL_ACCOUNTS = [ { code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: [BTC, BCH] }, { code: 'kraken', display: 'Kraken', class: TICKER, cryptos: [BTC, ETH, LTC, DASH, ZEC, BCH] }, { code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: [BTC, ETH, LTC, BCH] }, @@ -43,4 +43,7 @@ const ACCOUNT_LIST = [ { code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: [BTC, ZEC, LTC, DASH, BCH, ETH], dev: true } ] +const devMode = require('minimist')(process.argv.slice(2)).dev +const ACCOUNT_LIST = devMode ? ALL_ACCOUNTS : _.filter(it => !it.dev)(ALL_ACCOUNTS) + module.exports = { ACCOUNT_LIST } diff --git a/lib/new-admin/funding.js b/lib/new-admin/funding.js index c896a4bb..cbb32b37 100644 --- a/lib/new-admin/funding.js +++ b/lib/new-admin/funding.js @@ -1,11 +1,10 @@ const _ = require('lodash/fp') const BN = require('../bn') -const settingsLoader = require('../settings-loader') -const configManager = require('../config-manager') +const settingsLoader = require('../new-settings-loader') +const configManager = require('../new-config-manager') const wallet = require('../wallet') const ticker = require('../ticker') const coinUtils = require('../coin-utils') -const machineLoader = require('../machine-loader') function allScopes (cryptoScopes, machineScopes) { const scopes = [] @@ -25,18 +24,6 @@ function allMachineScopes (machineList, machineScope) { return machineScopes } -function getCryptos (config, machineList) { - const scopes = allScopes(['global'], allMachineScopes(machineList, 'both')) - const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config) - - return _.uniq(_.flatten(_.map(scoped, scopes))) -} - -function fetchMachines () { - return machineLoader.getMachines() - .then(machineList => machineList.map(r => r.deviceId)) -} - function computeCrypto (cryptoCode, _balance) { const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const unitScale = cryptoRec.unitScale @@ -82,11 +69,9 @@ function getSingleCoinFunding (settings, fiatCode, cryptoCode) { } function getFunding () { - return Promise.all([settingsLoader.loadLatest(), fetchMachines()]) - .then(([settings, machineList]) => { - const config = configManager.unscoped(settings.config) - const cryptoCodes = getCryptos(settings.config, machineList) - const fiatCode = config.fiatCurrency + return settingsLoader.loadLatest().then(settings => { + const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config) + const fiatCode = configManager.getGlobalLocale(settings.config).fiatCurrency const pareCoins = c => _.includes(c.cryptoCode, cryptoCodes) const cryptoCurrencies = coinUtils.cryptoCurrencies() const cryptoDisplays = _.filter(pareCoins, cryptoCurrencies) diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js index 08e0a4f4..07bedf12 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -62,7 +62,7 @@ const typeDefs = gql` type Customer { id: ID! - name: String! + name: String authorizedOverride: String frontCameraPath: String phone: String @@ -215,7 +215,7 @@ const typeDefs = gql` } type Mutation { - machineAction(deviceId:ID!, action: MachineAction!, cassettes: [Int]): Machine + machineAction(deviceId:ID!, action: MachineAction!, cassette1: Int, cassette2: Int): Machine machineSupportLogs(deviceId: ID!): SupportLogsResponse serverSupportLogs: SupportLogsResponse setCustomer(customerId: ID!, customerInput: CustomerInput): Customer @@ -254,7 +254,7 @@ const resolvers = { accounts: () => settingsLoader.getAccounts() }, Mutation: { - machineAction: (...[, { deviceId, action, cassettes }]) => machineAction({ deviceId, action, cassettes }), + machineAction: (...[, { deviceId, action, cassette1, cassette2 }]) => machineAction({ deviceId, action, cassette1, cassette2 }), machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId), createPairingTotem: (...[, { name }]) => pairing.totem(name), serverSupportLogs: () => serverLogs.insert(), diff --git a/lib/new-admin/machines.js b/lib/new-admin/machines.js index cb0924ff..9fd779ef 100644 --- a/lib/new-admin/machines.js +++ b/lib/new-admin/machines.js @@ -6,13 +6,13 @@ function getMachine (machineId) { .then(machines => machines.find(({ deviceId }) => deviceId === machineId)) } -function machineAction ({ deviceId, action, cassettes }) { +function machineAction ({ deviceId, action, cassette1, cassette2 }) { return getMachine(deviceId) .then(machine => { if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId }) return machine }) - .then(machineLoader.setMachine({ deviceId, action, cassettes })) + .then(machineLoader.setMachine({ deviceId, action, cassettes: [cassette1, cassette2] })) .then(getMachine(deviceId)) } diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js new file mode 100644 index 00000000..caddcbb4 --- /dev/null +++ b/lib/new-config-manager.js @@ -0,0 +1,93 @@ +const _ = require('lodash/fp') +const logger = require('./logger') + +const namespaces = { + WALLETS: 'wallets', + OPERATOR_INFO: 'operatorInfo', + NOTIFICATIONS: 'notifications', + LOCALE: 'locale', + COMMISSIONS: 'commissions', + RECEIPT: 'receipt', + COIN_ATM_RADAR: 'coinAtmRadar', + TERMS_CONDITIONS: 'termsConditions', + CASH_OUT: 'cashOut' +} + +const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str) +const filter = namespace => _.pickBy((value, key) => _.startsWith(`${namespace}_`)(key)) +const strip = key => _.mapKeys(stripl(`${key}_`)) + +const fromNamespace = _.curry((key, config) => _.compose(strip(key), filter(key))(config)) +const toNamespace = (key, config) => _.mapKeys(it => `${key}_${it}`)(config) + +const resolveOverrides = (original, filter, overrides, overridesPath = 'overrides') => { + if (_.isEmpty(overrides)) return original + + return _.omit(overridesPath, _.mergeAll([original, ..._.filter(filter)(overrides)])) +} + +const getCommissions = (cryptoCode, deviceId, config) => { + const commissions = fromNamespace(namespaces.COMMISSIONS)(config) + + const filter = it => it.machine === deviceId && _.includes(cryptoCode)(it.cryptoCurrencies) + return resolveOverrides(commissions, filter, commissions.overrides) +} + +const getLocale = (deviceId, it) => { + const locale = fromNamespace(namespaces.LOCALE)(it) + + const filter = _.matches({ machine: deviceId }) + return resolveOverrides(locale, filter, locale.overrides) +} + +const getGlobalLocale = it => getLocale(null, it) + +const getWalletSettings = (key, it) => _.compose(fromNamespace(key), fromNamespace(namespaces.WALLETS))(it) +const getCashOut = (key, it) => _.compose(fromNamespace(key), fromNamespace(namespaces.CASH_OUT))(it) +const getOperatorInfo = fromNamespace(namespaces.OPERATOR_INFO) +const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR) +const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS) +const getReceipt = fromNamespace(namespaces.RECEIPT) + +const getAllCryptoCurrencies = (config) => { + const locale = fromNamespace(namespaces.LOCALE)(config) + const cryptos = locale.cryptoCurrencies + const overridesCryptos = _.map(_.get('cryptoCurrencies'))(locale.overrides) + return _.uniq(_.flatten([cryptos, ...overridesCryptos])) +} + +const getNotifications = (cryptoCurrency, machine, config) => { + const notifications = fromNamespace(namespaces.NOTIFICATIONS)(config) + + const cryptoFilter = _.matches({ cryptoCurrency }) + const withCryptoBalance = resolveOverrides(notifications, cryptoFilter, notifications.cryptoBalanceOverrides, 'cryptoBalanceOverrides') + + const fiatFilter = _.matches({ machine }) + const withFiatBalance = resolveOverrides(withCryptoBalance, fiatFilter, withCryptoBalance.fiatBalanceOverrides, 'fiatBalanceOverrides') + + const withSms = fromNamespace('sms', withFiatBalance) + const withEmail = fromNamespace('email', withFiatBalance) + + const final = { ...withFiatBalance, sms: withSms, email: withEmail } + return final +} + +const getGlobalNotifications = config => getNotifications(null, null, config) + +const getTriggers = _.get('triggers') + +module.exports = { + getWalletSettings, + getOperatorInfo, + getNotifications, + getGlobalNotifications, + getLocale, + getGlobalLocale, + getCommissions, + getReceipt, + getCoinAtmRadar, + getTermsConditions, + getAllCryptoCurrencies, + getTriggers, + getCashOut +} diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js index e789a436..3c53bd08 100644 --- a/lib/new-settings-loader.js +++ b/lib/new-settings-loader.js @@ -42,4 +42,28 @@ function getConfig () { return (state && state.config) || {} } -module.exports = { getConfig, saveConfig, saveAccounts, getAccounts } +function loadLatest () { + return new Promise((resolve) => { + if (!db) { + setTimeout(() => { + return resolve(db.getState()) + }, 1000) + } else { + return resolve(db.getState()) + } + }) +} + +function load (versionId) { + return new Promise((resolve) => { + if (!db) { + setTimeout(() => { + return resolve(db.getState()) + }, 1000) + } else { + return resolve(db.getState()) + } + }) +} + +module.exports = { getConfig, saveConfig, saveAccounts, getAccounts, loadLatest, load } diff --git a/lib/notifier.js b/lib/notifier.js index 0885fd12..5413062f 100644 --- a/lib/notifier.js +++ b/lib/notifier.js @@ -15,6 +15,7 @@ const ALERT_SEND_INTERVAL = T.hour const PING = 'PING' const STALE = 'STALE' const LOW_CRYPTO_BALANCE = 'LOW_CRYPTO_BALANCE' +const HIGH_CRYPTO_BALANCE = 'HIGH_CRYPTO_BALANCE' const CASH_BOX_FULL = 'CASH_BOX_FULL' const LOW_CASH_OUT = 'LOW_CASH_OUT' @@ -22,6 +23,7 @@ const CODES_DISPLAY = { PING: 'Machine Down', STALE: 'Machine Stuck', LOW_CRYPTO_BALANCE: 'Low Crypto Balance', + HIGH_CRYPTO_BALANCE: 'High Crypto Balance', CASH_BOX_FULL: 'Cash box full', LOW_CASH_OUT: 'Low Cash-out' } @@ -41,47 +43,54 @@ function sameState (a, b) { return a.note.txId === b.note.txId && a.note.state === b.note.state } -function sendNoAlerts (plugins) { +function sendNoAlerts (plugins, smsEnabled, emailEnabled) { const subject = '[Lamassu] All clear' - const rec = { - sms: { - body: subject - }, - email: { - subject, - body: 'No errors are reported for your machines.' - } + + let rec = {} + if (smsEnabled) { + rec = _.set(['sms', 'body'])(subject)(rec) + } + + if (emailEnabled) { + rec = _.set(['email', 'subject'])(subject)(rec) + rec = _.set(['email', 'body'])('No errors are reported for your machines.')(rec) } return plugins.sendMessage(rec) } function checkNotification (plugins) { - if (!plugins.notificationsEnabled()) return Promise.resolve() + const notifications = plugins.getNotificationConfig() + const isActive = it => it.active && (it.balance || it.errors) + const smsEnabled = isActive(notifications.sms) + const emailEnabled = isActive(notifications.email) + + if (!smsEnabled && !emailEnabled) return Promise.resolve() return checkStatus(plugins) .then(alertRec => { - const currentAlertFingerprint = buildAlertFingerprint(alertRec) + const currentAlertFingerprint = buildAlertFingerprint(alertRec, notifications) if (!currentAlertFingerprint) { const inAlert = !!alertFingerprint alertFingerprint = null lastAlertTime = null - if (inAlert) return sendNoAlerts(plugins) + if (inAlert) return sendNoAlerts(plugins, smsEnabled, emailEnabled) } const alertChanged = currentAlertFingerprint === alertFingerprint && lastAlertTime - Date.now() < ALERT_SEND_INTERVAL if (alertChanged) return - const rec = { - sms: { - body: printSmsAlerts(alertRec) - }, - email: { - subject: alertSubject(alertRec), - body: printEmailAlerts(alertRec) - } + let rec = {} + if (smsEnabled) { + rec = _.set(['sms', 'body'])(printSmsAlerts(alertRec, notifications.sms))(rec) } + + if (emailEnabled) { + rec = _.set(['email', 'subject'])(alertSubject(alertRec, notifications.email))(rec) + rec = _.set(['email', 'body'])(printEmailAlerts(alertRec, notifications.email))(rec) + } + alertFingerprint = currentAlertFingerprint lastAlertTime = Date.now() @@ -162,12 +171,13 @@ function checkStatus (plugins) { return eventRow.device_id === deviceId }) - const balanceAlerts = _.filter(['deviceId', deviceId], balances) const ping = pings[deviceId] || [] const stuckScreen = checkStuckScreen(deviceEvents, deviceName) - const deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping - alerts.devices[deviceId] = _.concat(deviceAlerts, balanceAlerts) + if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {} + alerts.devices[deviceId].balanceAlerts = _.filter(['deviceId', deviceId], balances) + alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping + alerts.deviceNames[deviceId] = deviceName }) @@ -194,6 +204,9 @@ function emailAlert (alert) { case LOW_CRYPTO_BALANCE: const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode) return `Low balance in ${alert.cryptoCode} [${balance}]` + case HIGH_CRYPTO_BALANCE: + const highBalance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode) + return `High balance in ${alert.cryptoCode} [${highBalance}]` case CASH_BOX_FULL: return `Cash box full on ${alert.machineName} [${alert.notes} banknotes]` case LOW_CASH_OUT: @@ -205,28 +218,48 @@ function emailAlerts (alerts) { return alerts.map(emailAlert).join('\n') + '\n' } -function printEmailAlerts (alertRec) { +function printEmailAlerts (alertRec, config) { let body = 'Errors were reported by your Lamassu Machines.\n' - if (alertRec.general.length !== 0) { + if (config.balance && alertRec.general.length !== 0) { body = body + '\nGeneral errors:\n' - body = body + emailAlerts(alertRec.general) + body = body + emailAlerts(alertRec.general) + '\n' } _.keys(alertRec.devices).forEach(function (device) { const deviceName = alertRec.deviceNames[device] body = body + '\nErrors for ' + deviceName + ':\n' - body = body + emailAlerts(alertRec.devices[device]) + + let alerts = [] + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } + + body = body + emailAlerts(alerts) }) return body } -function alertSubject (alertRec) { - let alerts = alertRec.general +function alertSubject (alertRec, config) { + let alerts = [] + + if (config.balance) { + alerts = _.concat(alerts, alertRec.general) + } _.keys(alertRec.devices).forEach(function (device) { - alerts = _.concat(alerts, alertRec.devices[device]) + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } }) if (alerts.length === 0) return null @@ -235,11 +268,21 @@ function alertSubject (alertRec) { return '[Lamassu] Errors reported: ' + alertTypes.join(', ') } -function printSmsAlerts (alertRec) { - let alerts = alertRec.general +function printSmsAlerts (alertRec, config) { + let alerts = [] + + if (config.balance) { + alerts = _.concat(alerts, alertRec.general) + } _.keys(alertRec.devices).forEach(function (device) { - alerts = _.concat(alerts, alertRec.devices[device]) + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } }) if (alerts.length === 0) return null @@ -265,9 +308,39 @@ function printSmsAlerts (alertRec) { return '[Lamassu] Errors reported: ' + displayAlertTypes.join(', ') } -function buildAlertFingerprint (alertRec) { - const subject = alertSubject(alertRec) - if (!subject) return null +function getAlertTypes (alertRec, config) { + let alerts = [] + + if (!config.active || (!config.balance && !config.errors)) return alerts + + if (config.balance) { + alerts = _.concat(alerts, alertRec.general) + } + + _.keys(alertRec.devices).forEach(function (device) { + if (config.balance) { + alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts) + } + + if (config.errors) { + alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts) + } + }) + + return alerts +} + +function buildAlertFingerprint (alertRec, notifications) { + const sms = getAlertTypes(alertRec, notifications.sms) + const email = getAlertTypes(alertRec, notifications.email) + + if (sms.length === 0 && email.length === 0) return null + + const smsTypes = _.map(codeDisplay, _.uniq(_.map('code', sms))).sort() + const emailTypes = _.map(codeDisplay, _.uniq(_.map('code', email))).sort() + + const subject = _.concat(smsTypes, emailTypes).join(', ') + return crypto.createHash('sha256').update(subject).digest('hex') } diff --git a/lib/pairing.js b/lib/pairing.js index 180aae6b..5932878c 100644 --- a/lib/pairing.js +++ b/lib/pairing.js @@ -4,7 +4,6 @@ const readFile = pify(fs.readFile) const db = require('./db') const options = require('./options') const logger = require('./logger') -const settingsLoader = require('./settings-loader') function pullToken (token) { const sql = `delete from pairing_tokens @@ -13,33 +12,12 @@ function pullToken (token) { return db.one(sql, [token]) } -function configureNewDevice (deviceId, machineName, machineModel) { - const scope = {crypto: 'global', machine: deviceId} - const newFields = [ - settingsLoader.configAddField(scope, 'cashOutEnabled', 'onOff', null, false), - settingsLoader.configAddField(scope, 'machineName', 'string', null, machineName), - settingsLoader.configAddField(scope, 'machineModel', 'string', null, machineModel) - ] - - return settingsLoader.modifyConfig(newFields) -} - -function removeDeviceConfig (deviceId) { - const scope = {crypto: 'global', machine: deviceId} - const newFields = [ - settingsLoader.configDeleteField(scope, 'cashOutEnabled'), - settingsLoader.configDeleteField(scope, 'machineName'), - settingsLoader.configDeleteField(scope, 'machineModel') - ] - - return settingsLoader.modifyConfig(newFields) -} - function unpair (deviceId) { const sql = 'delete from devices where device_id=$1' const deleteMachinePings = 'delete from machine_pings where device_id=$1' + + // TODO new-admin: We should remove all configs related to that device. This can get tricky. return Promise.all([db.none(sql, [deviceId]), db.none(deleteMachinePings, [deviceId])]) - .then(() => removeDeviceConfig(deviceId)) } function pair (token, deviceId, machineModel) { @@ -51,8 +29,7 @@ function pair (token, deviceId, machineModel) { on conflict (device_id) do update set paired=TRUE, display=TRUE` - return configureNewDevice(deviceId, r.name, machineModel) - .then(() => db.none(insertSql, [deviceId, r.name])) + return db.none(insertSql, [deviceId, r.name]) .then(() => true) }) .catch(err => { diff --git a/lib/plugins.js b/lib/plugins.js index 0b55a815..f128f2a2 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -11,7 +11,7 @@ const db = require('./db') const logger = require('./logger') const logs = require('./logs') const T = require('./time') -const configManager = require('./config-manager') +const configManager = require('./new-config-manager') const ticker = require('./ticker') const wallet = require('./wallet') const exchange = require('./exchange') @@ -34,22 +34,22 @@ const tradesQueues = {} function plugins (settings, deviceId) { function buildRates (tickers) { - const config = configManager.machineScoped(deviceId, settings.config) - const cryptoCodes = config.cryptoCurrencies + const localeConfig = configManager.getLocale(deviceId, settings.config) + const cryptoCodes = localeConfig.cryptoCurrencies const rates = {} cryptoCodes.forEach((cryptoCode, i) => { - const cryptoConfig = configManager.scoped(cryptoCode, deviceId, settings.config) + const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) const rateRec = tickers[i] if (!rateRec) return - const cashInCommission = BN(1).add(BN(cryptoConfig.cashInCommission).div(100)) + const cashInCommission = BN(1).add(BN(commissions.cashIn).div(100)) - const cashOutCommission = _.isNil(cryptoConfig.cashOutCommission) + const cashOutCommission = _.isNil(commissions.cashOut) ? undefined - : BN(1).add(BN(cryptoConfig.cashOutCommission).div(100)) + : BN(1).add(BN(commissions.cashOut).div(100)) if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode) const rate = rateRec.rates @@ -62,19 +62,13 @@ 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 + function getNotificationConfig () { + return configManager.getGlobalNotifications(settings.config) } function buildBalances (balanceRecs) { - const config = configManager.machineScoped(deviceId, settings.config) - const cryptoCodes = config.cryptoCurrencies + const localeConfig = configManager.getLocale(deviceId, settings.config) + const cryptoCodes = localeConfig.cryptoCurrencies const balances = {} @@ -90,8 +84,8 @@ function plugins (settings, deviceId) { } function isZeroConf (tx) { - const config = configManager.scoped(tx.cryptoCode, deviceId, settings.config) - const zeroConfLimit = config.zeroConfLimit + const cashOutConfig = configManager.getCashOut(deviceId, settings.config) + const zeroConfLimit = cashOutConfig.zeroConfLimit return tx.fiat.lte(zeroConfLimit) } @@ -131,14 +125,14 @@ function plugins (settings, deviceId) { } function buildAvailableCassettes (excludeTxId) { - const config = configManager.machineScoped(deviceId, settings.config) + const cashOutConfig = configManager.getCashOut(deviceId, settings.config) - if (!config.cashOutEnabled) return Promise.resolve() + if (!cashOutConfig.active) return Promise.resolve() - const denominations = [config.topCashOutDenomination, - config.bottomCashOutDenomination - ] - const virtualCassettes = [config.virtualCashOutDenomination] + const denominations = [cashOutConfig.top, cashOutConfig.bottom] + + // TODO new-admin: will this actually be calculated? + const virtualCassettes = [cashOutConfig.top + cashOutConfig.bottom] return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)]) .then(([rec, _redeemableTxs]) => { @@ -188,11 +182,11 @@ function plugins (settings, deviceId) { function mapCoinSettings (coinParams) { const cryptoCode = coinParams[0] const cryptoNetwork = coinParams[1] - const config = configManager.scoped(cryptoCode, deviceId, settings.config) - const minimumTx = BN(config.minimumTx) - const cashInFee = BN(config.cashInFee) - const cashInCommission = BN(config.cashInCommission) - const cashOutCommission = _.isNumber(config.cashOutCommission) ? BN(config.cashOutCommission) : null + const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) + const minimumTx = BN(commissions.minimumTx) + const cashInFee = BN(commissions.fixedFee) + const cashInCommission = BN(commissions.cashIn) + const cashOutCommission = _.isNumber(commissions.cashOut) ? BN(commissions.cashOut) : null const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) return { @@ -207,9 +201,10 @@ function plugins (settings, deviceId) { } function pollQueries (serialNumber, deviceTime, deviceRec) { - const config = configManager.machineScoped(deviceId, settings.config) - const fiatCode = config.fiatCurrency - const cryptoCodes = config.cryptoCurrencies + const localeConfig = configManager.getLocale(deviceId, settings.config) + + const fiatCode = localeConfig.fiatCurrency + const cryptoCodes = localeConfig.cryptoCurrencies const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) @@ -283,16 +278,14 @@ function plugins (settings, deviceId) { } function dispenseAck (tx) { - const config = configManager.machineScoped(deviceId, settings.config) - const cassettes = [config.topCashOutDenomination, - config.bottomCashOutDenomination - ] + const cashOutConfig = configManager.getCashOut(deviceId, settings.config) + const cassettes = [cashOutConfig.top, cashOutConfig.bottom] return dbm.addDispense(deviceId, tx, cassettes) } function fiatBalance (fiatCode, cryptoCode) { - const config = configManager.scoped(cryptoCode, deviceId, settings.config) + const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) return Promise.all([ ticker.getRates(settings, fiatCode, cryptoCode), wallet.balance(settings, cryptoCode) @@ -301,7 +294,7 @@ function plugins (settings, deviceId) { if (!rates || !balanceRec) return null const rawRate = rates.rates.ask - const cashInCommission = BN(1).minus(BN(config.cashInCommission).div(100)) + const cashInCommission = BN(1).minus(BN(commissions.cashIn).div(100)) const balance = balanceRec.balance if (!rawRate || !balance) return null @@ -344,7 +337,12 @@ function plugins (settings, deviceId) { } function notifyOperator (tx, rec) { - if (!transactionNotificationsEnabled()) return Promise.resolve() + const notifications = configManager.getGlobalNotifications(settings.config) + + const notificationsEnabled = notifications.sms.transactions || notifications.email.transactions + const highValueTx = tx.fiat.gt(notifications.highValueTransaction) + + if (!notificationsEnabled || !highValueTx) return Promise.resolve() const isCashOut = tx.direction === 'cashOut' const zeroConf = isCashOut && isZeroConf(tx) @@ -504,9 +502,9 @@ function plugins (settings, deviceId) { .then(devices => { const deviceIds = devices.map(device => device.deviceId) const lists = deviceIds.map(deviceId => { - const config = configManager.machineScoped(deviceId, settings.config) - const fiatCode = config.fiatCurrency - const cryptoCodes = config.cryptoCurrencies + const localeConfig = configManager.getLocale(deviceId, settings.config) + const fiatCode = localeConfig.fiatCurrency + const cryptoCodes = localeConfig.cryptoCurrencies return cryptoCodes.map(cryptoCode => ({ fiatCode, @@ -591,21 +589,25 @@ function plugins (settings, deviceId) { } function sendMessage (rec) { - const config = configManager.unscoped(settings.config) + const notifications = configManager.getGlobalNotifications(settings.config) let promises = [] - if (config.notificationsEmailEnabled) promises.push(email.sendMessage(settings, rec)) - if (config.notificationsSMSEnabled) promises.push(sms.sendMessage(settings, rec)) + if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec)) + if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec)) return Promise.all(promises) } function sendTransactionMessage (rec) { - const config = configManager.unscoped(settings.config) + const notifications = configManager.getGlobalNotifications(settings.config) let promises = [] - if (config.transactionNotificationsEmailEnabled) promises.push(email.sendMessage(settings, rec)) - if (config.transactionNotificationsSMSEnabled) promises.push(sms.sendMessage(settings, rec)) + + const emailActive = notifications.email.active && notifications.email.transactions + if (emailActive) promises.push(email.sendMessage(settings, rec)) + + const smsActive = notifications.sms.active && notifications.sms.transactions + if (smsActive) promises.push(sms.sendMessage(settings, rec)) return Promise.all(promises) } @@ -615,13 +617,16 @@ function plugins (settings, deviceId) { } function checkDeviceCashBalances (fiatCode, device) { - const config = configManager.machineScoped(device.deviceId, settings.config) - const denomination1 = config.topCashOutDenomination - const denomination2 = config.bottomCashOutDenomination - const machineName = config.machineName - const cashOutEnabled = config.cashOutEnabled + const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config) + const denomination1 = cashOutConfig.top + const denomination2 = cashOutConfig.bottom + const cashOutEnabled = cashOutConfig.active - const cashInAlert = device.cashbox > config.cashInAlertThreshold + const notifications = configManager.getNotifications(null, device.deviceId, settings.config) + + const machineName = device.machineName + + const cashInAlert = device.cashbox > notifications.cashInAlertThreshold ? { code: 'CASH_BOX_FULL', machineName, @@ -630,7 +635,7 @@ function plugins (settings, deviceId) { } : null - const cassette1Alert = cashOutEnabled && device.cassette1 < config.cashOutCassette1AlertThreshold + const cassette1Alert = cashOutEnabled && device.cassette1 < notifications.fiatBalanceCassette1 ? { code: 'LOW_CASH_OUT', cassette: 1, @@ -642,7 +647,7 @@ function plugins (settings, deviceId) { } : null - const cassette2Alert = cashOutEnabled && device.cassette2 < config.cashOutCassette2AlertThreshold + const cassette2Alert = cashOutEnabled && device.cassette2 < notifications.fiatBalanceCassette2 ? { code: 'LOW_CASH_OUT', cassette: 2, @@ -661,8 +666,8 @@ function plugins (settings, deviceId) { const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes) const fetchCryptoCodes = _deviceId => { - const config = configManager.machineScoped(_deviceId, settings.config) - return config.cryptoCurrencies + const localeConfig = configManager.getLocale(_deviceId, settings.config) + return localeConfig.cryptoCurrencies } const union = _.flow(_.map(fetchCryptoCodes), _.flatten, _.uniq) @@ -678,22 +683,28 @@ function plugins (settings, deviceId) { if (!fiatBalance) return null - const config = configManager.cryptoScoped(cryptoCode, settings.config) - const cryptoAlertThreshold = config.cryptoAlertThreshold + const notifications = configManager.getNotifications(cryptoCode, null, settings.config) + const lowAlertThreshold = notifications.cryptoLowBalance + const highAlertThreshold = notifications.cryptoHighBalance - return BN(fiatBalance.balance).lt(cryptoAlertThreshold) - ? { - code: 'LOW_CRYPTO_BALANCE', - cryptoCode, - fiatBalance, - fiatCode - } - : null + const req = { + cryptoCode, + fiatBalance, + fiatCode, + } + + if (BN(fiatBalance.balance).lt(lowAlertThreshold)) + return _.set('code')('LOW_CRYPTO_BALANCE')(req) + + if (BN(fiatBalance.balance).gt(highAlertThreshold)) + return _.set('code')('HIGH_CRYPTO_BALANCE')(req) + + return null } function checkBalances () { - const globalConfig = configManager.unscoped(settings.config) - const fiatCode = globalConfig.fiatCurrency + const localeConfig = configManager.getGlobalLocale(settings.config) + const fiatCode = localeConfig.fiatCurrency return machineLoader.getMachines() .then(devices => { @@ -756,9 +767,10 @@ function plugins (settings, deviceId) { } function getRawRates () { - const config = configManager.unscoped(settings.config) - const cryptoCodes = _.flatten(configManager.all('cryptoCurrencies', settings.config)) - const fiatCode = config.fiatCurrency + const localeConfig = configManager.getGlobalLocale(settings.config) + const fiatCode = localeConfig.fiatCurrency + + const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config) const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c)) return Promise.all(tickerPromises) @@ -792,7 +804,7 @@ function plugins (settings, deviceId) { buildAvailableCassettes, buy, sell, - notificationsEnabled, + getNotificationConfig, notifyOperator, fetchCurrentConfigVersion } diff --git a/lib/poller.js b/lib/poller.js index 22b15b27..bbd3f708 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -9,7 +9,8 @@ const cashInTx = require('./cash-in/cash-in-tx') const sanctionsUpdater = require('./ofac/update') const sanctions = require('./ofac/index') const coinAtmRadar = require('./coinatmradar/coinatmradar') -const configManager = require('./config-manager') +const configManager = require('./new-config-manager') +const complianceTriggers = require('./compliance-triggers') const INCOMING_TX_INTERVAL = 30 * T.seconds const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds @@ -48,9 +49,10 @@ function initialSanctionsDownload () { } function updateAndLoadSanctions () { - const config = configManager.unscoped(settings().config) + const triggers = configManager.getTriggers(settings().config) + const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) - if (!config.sanctionsVerificationActive) return Promise.resolve() + if (!compatTriggers.sanctions) return Promise.resolve() logger.info('Updating sanctions database...') return sanctionsUpdater.update() @@ -59,10 +61,8 @@ function updateAndLoadSanctions () { } function updateCoinAtmRadar () { - const config = settings().config - return pi().getRawRates() - .then(rates => coinAtmRadar.update({ rates, config }, settings())) + .then(rates => coinAtmRadar.update(rates, settings())) } function start (__settings) { diff --git a/lib/route-helpers.js b/lib/route-helpers.js index 857dded8..4351f7d9 100644 --- a/lib/route-helpers.js +++ b/lib/route-helpers.js @@ -4,7 +4,6 @@ const db = require('./db') const dbm = require('./postgresql_interface') const T = require('./time') const BN = require('./bn') -const settingsLoader = require('./settings-loader') const TRANSACTION_EXPIRATION = T.day @@ -90,33 +89,9 @@ function updateDeviceConfigVersion (versionId) { return db.none('update devices set user_config_id=$1', [versionId]) } -function updateMachineDefaults (deviceId) { - const newFields = [{ - fieldLocator: { - fieldScope: { - crypto: 'global', - machine: deviceId - }, - code: 'cashOutEnabled', - fieldType: 'onOff', - fieldClass: null - }, - fieldValue: { - fieldType: 'onOff', - value: false - } - }] - - return settingsLoader.loadLatest() - .then(settings => { - return settingsLoader.save(settingsLoader.mergeValues(settings.config, newFields)) - }) -} - module.exports = { stateChange, fetchPhoneTx, fetchStatusTx, - updateDeviceConfigVersion, - updateMachineDefaults + updateDeviceConfigVersion } diff --git a/lib/routes.js b/lib/routes.js index 52ba93fb..8a6490d4 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -12,9 +12,11 @@ const semver = require('semver') const dbErrorCodes = require('./db-error-codes') const options = require('./options') const logger = require('./logger') -const configManager = require('./config-manager') +const configManager = require('./new-config-manager') +const complianceTriggers = require('./compliance-triggers') const pairing = require('./pairing') -const settingsLoader = require('./settings-loader') +// TODO new-admin: remove old settings loader from here. +const newSettingsLoader = require('./new-settings-loader') const plugins = require('./plugins') const helpers = require('./route-helpers') const poller = require('./poller') @@ -44,7 +46,7 @@ const settingsCache = {} const devMode = argv.dev || options.http function checkHasLightning (settings) { - return configManager.cryptoScoped('BTC', settings.config).layer2 !== 'no-layer2' + return configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2' } function poll (req, res, next) { @@ -54,10 +56,18 @@ function poll (req, res, next) { const serialNumber = req.query.sn const pid = req.query.pid const settings = req.settings - const config = configManager.machineScoped(deviceId, settings.config) + const localeConfig = configManager.getLocale(deviceId, settings.config) const pi = plugins(settings, deviceId) const hasLightning = checkHasLightning(settings) + const triggers = configManager.getTriggers(settings.config) + const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) + + const operatorInfo = configManager.getOperatorInfo(settings.config) + const terms = configManager.getTermsConditions(settings.config) + const cashOutConfig = configManager.getCashOut(deviceId, settings.config) + const receipt = configManager.getReceipt(settings.config) + pids[deviceId] = { pid, ts: Date.now() } return pi.pollQueries(serialNumber, deviceTime, req.query) @@ -66,14 +76,14 @@ function poll (req, res, next) { const reboot = pid && reboots[deviceId] && reboots[deviceId] === pid const restartServices = pid && restartServicesMap[deviceId] && restartServicesMap[deviceId] === pid - const langs = config.machineLanguages + const langs = localeConfig.languages const locale = { - fiatCode: config.fiatCurrency, + fiatCode: localeConfig.fiatCurrency, localeInfo: { primaryLocale: langs[0], primaryLocales: langs, - country: config.country + country: localeConfig.country } } @@ -81,47 +91,33 @@ function poll (req, res, next) { error: null, locale, version, - txLimit: config.cashInTransactionLimit, - idVerificationEnabled: config.idVerificationEnabled, - smsVerificationActive: config.smsVerificationActive, - smsVerificationThreshold: config.smsVerificationThreshold, - hardLimitVerificationActive: config.hardLimitVerificationActive, - hardLimitVerificationThreshold: config.hardLimitVerificationThreshold, - idCardDataVerificationActive: config.idCardDataVerificationActive, - idCardDataVerificationThreshold: config.idCardDataVerificationThreshold, - idCardPhotoVerificationActive: config.idCardPhotoVerificationActive, - idCardPhotoVerificationThreshold: config.idCardPhotoVerificationThreshold, - sanctionsVerificationActive: config.sanctionsVerificationActive, - sanctionsVerificationThreshold: config.sanctionsVerificationThreshold, - crossRefVerificationActive: config.crossRefVerificationActive, - crossRefVerificationThreshold: config.crossRefVerificationThreshold, - frontCameraVerificationActive: config.frontCameraVerificationActive, - frontCameraVerificationThreshold: config.frontCameraVerificationThreshold, - receiptPrintingActive: config.receiptPrintingActive, + smsVerificationActive: !!compatTriggers.sms, + smsVerificationThreshold: compatTriggers.sms, + hardLimitVerificationActive: !!compatTriggers.block, + hardLimitVerificationThreshold: compatTriggers.block, + idCardDataVerificationActive: !!compatTriggers.idData, + idCardDataVerificationThreshold: compatTriggers.idData, + idCardPhotoVerificationActive: !!compatTriggers.idPhoto, + idCardPhotoVerificationThreshold: compatTriggers.idPhoto, + sanctionsVerificationActive: !!compatTriggers.sancations, + sanctionsVerificationThreshold: compatTriggers.sancations, + frontCameraVerificationActive: !!compatTriggers.facephoto, + frontCameraVerificationThreshold: compatTriggers.facephoto, + receiptPrintingActive: receipt.active, cassettes, - twoWayMode: config.cashOutEnabled, - zeroConfLimit: config.zeroConfLimit, + twoWayMode: cashOutConfig.active, + zeroConfLimit: cashOutConfig.zeroConfLimit, reboot, restartServices, hasLightning, - operatorInfo: { - active: config.operatorInfoActive, - name: config.operatorInfoName, - phone: config.operatorInfoPhone, - email: config.operatorInfoEmail, - website: config.operatorInfoWebsite, - companyNumber: config.operatorInfoCompanyNumber - } + receipt, + operatorInfo } // BACKWARDS_COMPATIBILITY 7.5 // machines before 7.5 expect t&c on poll if (!machineVersion || semver.lt(machineVersion, '7.5.0-beta')) { - response.terms = config.termsScreenActive && config.termsScreenText ? createTerms(config) : null - } - - if (response.idVerificationEnabled) { - response.idVerificationLimit = config.idVerificationLimit + response.terms = createTerms(terms) } return res.json(_.assign(response, results)) @@ -133,13 +129,12 @@ function getTermsConditions (req, res, next) { const deviceId = req.deviceId const settings = req.settings - const config = configManager.unscoped(req.settings.config) + const terms = configManager.getTermsConditions(settings.config) + const pi = plugins(settings, deviceId) - const terms = config.termsScreenActive && config.termsScreenText ? createTerms(config) : null - return pi.fetchCurrentConfigVersion().then(version => { - return res.json({ terms, version }) + return res.json({ terms: createTerms(terms), version }) }) .catch(next) } @@ -213,7 +208,8 @@ function verifyTx (req, res, next) { function addOrUpdateCustomer (req) { const customerData = req.body - const config = configManager.unscoped(req.settings.config) + const triggers = configManager.getTriggers(req.settings.config) + const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) return customers.get(customerData.phone) .then(customer => { @@ -222,7 +218,7 @@ function addOrUpdateCustomer (req) { return customers.add(req.body) }) .then(customer => { - return compliance.validationPatch(req.deviceId, config, customer) + return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, customer) .then(patch => { if (_.isEmpty(patch)) return customer return customers.update(customer.id, patch) @@ -250,14 +246,15 @@ function updateCustomer (req, res, next) { const id = req.params.id const txId = req.query.txId const patch = req.body - const config = configManager.unscoped(req.settings.config) + const triggers = configManager.getTriggers(req.settings.config) + const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) customers.getById(id) .then(customer => { if (!customer) { throw httpError('Not Found', 404) } const mergedCustomer = _.merge(customer, patch) - return compliance.validationPatch(req.deviceId, config, mergedCustomer) + return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, mergedCustomer) .then(_.merge(patch)) .then(newPatch => customers.updatePhotoCard(id, newPatch)) .then(newPatch => customers.updateFrontCamera(id, newPatch)) @@ -305,8 +302,7 @@ function pair (req, res, next) { return pairing.pair(token, deviceId, model) .then(valid => { if (valid) { - return helpers.updateMachineDefaults(deviceId) - .then(() => res.json({ status: 'paired' })) + return res.json({ status: 'paired' }) } throw httpError('Pairing failed') @@ -457,7 +453,7 @@ localApp.post('/restartServices', (req, res) => { localApp.post('/dbChange', (req, res, next) => { settingsCache.cache = null - return settingsLoader.loadLatest() + return newSettingsLoader.loadLatest() .then(poller.reload) .then(() => logger.info('Config reloaded')) .catch(err => { @@ -504,7 +500,7 @@ function populateSettings (req, res, next) { } if (!versionId && !settingsCache.cache) { - return settingsLoader.loadLatest() + return newSettingsLoader.loadLatest() .then(settings => { settingsCache.cache = settings settingsCache.timestamp = Date.now() @@ -514,20 +510,22 @@ function populateSettings (req, res, next) { .catch(next) } - settingsLoader.load(versionId) + newSettingsLoader.load(versionId) .then(settings => { req.settings = settings }) .then(() => helpers.updateDeviceConfigVersion(versionId)) .then(() => next()) .catch(next) } -function createTerms (config) { +function createTerms (terms) { + if (!terms.active || !terms.text) return null + return { - active: config.termsScreenActive, - title: config.termsScreenTitle, - text: nmd(config.termsScreenText), - accept: config.termsAcceptButtonText, - cancel: config.termsCancelButtonText + active: terms.active, + title: terms.title, + text: nmd(terms.text), + accept: terms.acceptButtonText, + cancel: terms.cancelButtonText } } diff --git a/lib/sms.js b/lib/sms.js index b6b0a82e..c338113b 100644 --- a/lib/sms.js +++ b/lib/sms.js @@ -1,10 +1,12 @@ -const configManager = require('./config-manager') +// const configManager = require('./config-manager') const ph = require('./plugin-helper') function sendMessage (settings, rec) { return Promise.resolve() .then(() => { - const pluginCode = configManager.unscoped(settings.config).sms + // TODO new-admin: how to load mock here? Only on dev? + // const pluginCode = configManager.unscoped(settings.config).sms + const pluginCode = 'twilio' const plugin = ph.load(ph.SMS, pluginCode) const account = settings.accounts[pluginCode] diff --git a/lib/ticker.js b/lib/ticker.js index ed42c7af..5ea068ec 100644 --- a/lib/ticker.js +++ b/lib/ticker.js @@ -1,5 +1,5 @@ const mem = require('mem') -const configManager = require('./config-manager') +const configManager = require('./new-config-manager') const ph = require('./plugin-helper') const logger = require('./logger') @@ -11,8 +11,9 @@ function _getRates (settings, fiatCode, cryptoCode) { return Promise.resolve() .then(() => { const config = settings.config - const plugin = configManager.cryptoScoped(cryptoCode, config).ticker + const plugin = configManager.getWalletSettings(cryptoCode, config).ticker + logger.info(plugin) const account = settings.accounts[plugin] const ticker = ph.load(ph.TICKER, plugin) diff --git a/lib/wallet.js b/lib/wallet.js index 0e069f8c..086d9a4f 100644 --- a/lib/wallet.js +++ b/lib/wallet.js @@ -2,7 +2,7 @@ const _ = require('lodash/fp') const mem = require('mem') const hkdf = require('futoin-hkdf') -const configManager = require('./config-manager') +const configManager = require('./new-config-manager') const pify = require('pify') const fs = pify(require('fs')) @@ -32,7 +32,7 @@ function fetchWallet (settings, cryptoCode) { return fs.readFile(options.mnemonicPath, 'utf8') .then(mnemonic => { const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic) - const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet + const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet const wallet = ph.load(ph.WALLET, plugin) const rawAccount = settings.accounts[plugin] const account = _.set('seed', computeSeed(masterSeed), rawAccount) @@ -135,10 +135,9 @@ function getWalletStatus (settings, tx) { } function authorizeZeroConf (settings, tx, machineId) { - const cryptoConfig = configManager.cryptoScoped(tx.cryptoCode, settings.config) - const machineConfig = configManager.machineScoped(machineId, settings.config) - const plugin = cryptoConfig.zeroConf - const zeroConfLimit = machineConfig.zeroConfLimit + const plugin = configManager.getWalletSettings(tx.cryptoCode, settings.config).zeroConf + const cashOutConfig = configManager.cashOutConfig(machineId, settings.config) + const zeroConfLimit = cashOutConfig.zeroConfLimit if (!_.isObject(tx.fiat)) { return Promise.reject(new Error('tx.fiat is undefined!')) @@ -189,7 +188,7 @@ function isHd (settings, cryptoCode) { } function cryptoNetwork (settings, cryptoCode) { - const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet + const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet const wallet = ph.load(ph.WALLET, plugin) const account = settings.accounts[plugin] diff --git a/new-lamassu-admin/src/components/Modal.js b/new-lamassu-admin/src/components/Modal.js index 7651be26..fc84d0a8 100644 --- a/new-lamassu-admin/src/components/Modal.js +++ b/new-lamassu-admin/src/components/Modal.js @@ -3,17 +3,19 @@ import classnames from 'classnames' import React from 'react' import { IconButton } from 'src/components/buttons' -import { H1 } from 'src/components/typography' +import { H1, H2 } from 'src/components/typography' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' const styles = { modal: { display: 'flex', justifyContent: 'center', + flexDirection: 'column', alignItems: 'center' }, - wrapper: ({ width }) => ({ + wrapper: ({ width, height }) => ({ width, + height, display: 'flex', flexDirection: 'column', minHeight: 400, @@ -45,7 +47,10 @@ const useStyles = makeStyles(styles) const Modal = ({ width, + height, title, + titleSmall, + infoPanel, handleClose, children, className, @@ -53,7 +58,9 @@ const Modal = ({ closeOnBackdropClick, ...props }) => { - const classes = useStyles({ width }) + const classes = useStyles({ width, height }) + const TitleCase = titleSmall ? H2 : H1 + const closeSize = titleSmall ? 16 : 20 const innerClose = (evt, reason) => { if (!closeOnBackdropClick && reason === 'backdropClick') return @@ -63,18 +70,25 @@ const Modal = ({ return ( - -
- {title &&

{title}

} - handleClose()}> - - -
-
{children}
-
+ <> + +
+ {title && {title}} + handleClose()}> + + +
+
{children}
+
+ {infoPanel && ( + + {infoPanel} + + )} +
) } diff --git a/new-lamassu-admin/src/components/editableTable/Row.js b/new-lamassu-admin/src/components/editableTable/Row.js index 0a6b91dc..ab95f363 100644 --- a/new-lamassu-admin/src/components/editableTable/Row.js +++ b/new-lamassu-admin/src/components/editableTable/Row.js @@ -154,6 +154,7 @@ const ERow = ({ editing, disabled }) => { enableEdit, enableDelete, enableToggle, + rowSize, stripeWhen } = useContext(TableCtx) @@ -163,6 +164,7 @@ const ERow = ({ editing, disabled }) => { const innerElements = shouldStripe ? groupStriped(elements) : elements return ( {innerElements.map((it, idx) => { diff --git a/new-lamassu-admin/src/components/editableTable/Table.js b/new-lamassu-admin/src/components/editableTable/Table.js index b6ce3345..7477bcca 100644 --- a/new-lamassu-admin/src/components/editableTable/Table.js +++ b/new-lamassu-admin/src/components/editableTable/Table.js @@ -31,6 +31,7 @@ const ETable = ({ elements = [], data = [], save, + rowSize = 'md', validationSchema, enableCreate, enableEdit, @@ -108,6 +109,7 @@ const ETable = ({ onDelete, deleteWidth, enableToggle, + rowSize, onToggle, toggleWidth, actionColSize, diff --git a/new-lamassu-admin/src/components/fake-table/Table.js b/new-lamassu-admin/src/components/fake-table/Table.js index b5a84760..e7e2a368 100644 --- a/new-lamassu-admin/src/components/fake-table/Table.js +++ b/new-lamassu-admin/src/components/fake-table/Table.js @@ -76,18 +76,8 @@ const ThDoubleLevel = ({ title, children, className }) => { ) } -const CellDoubleLevel = ({ children, className }) => { - const classes = useStyles() - - return ( -
- {children} -
- ) -} - -const Tr = ({ error, errorMessage, children, className }) => { - const classes = useStyles() +const Tr = ({ error, errorMessage, children, className, size }) => { + const classes = useStyles({ size }) const cardClasses = { root: classes.cardContentRoot } const classNames = { [classes.tr]: true, @@ -128,6 +118,5 @@ export { Td, Th, ThDoubleLevel, - CellDoubleLevel, EditCell } diff --git a/new-lamassu-admin/src/components/fake-table/Table.styles.js b/new-lamassu-admin/src/components/fake-table/Table.styles.js index 84acf4a9..05498e4a 100644 --- a/new-lamassu-admin/src/components/fake-table/Table.styles.js +++ b/new-lamassu-admin/src/components/fake-table/Table.styles.js @@ -70,14 +70,16 @@ export default { trError: { backgroundColor: tableErrorColor }, - mainContent: { - display: 'flex', - alignItems: 'center', - minHeight: 48 + mainContent: ({ size }) => { + const minHeight = size === 'lg' ? 68 : 48 + return { + display: 'flex', + alignItems: 'center', + minHeight + } }, // mui-overrides cardContentRoot: { - // display: 'flex', margin: 0, padding: 0, '&:last-child': { diff --git a/new-lamassu-admin/src/components/inputs/base/TextInput.js b/new-lamassu-admin/src/components/inputs/base/TextInput.js index b4b41f3c..243cf829 100644 --- a/new-lamassu-admin/src/components/inputs/base/TextInput.js +++ b/new-lamassu-admin/src/components/inputs/base/TextInput.js @@ -26,7 +26,7 @@ const TextInput = memo( ...props }) => { const classes = useStyles({ textAlign, width, size }) - const filled = !error && value && !R.isEmpty(value) + const filled = !error && !R.isNil(value) && !R.isEmpty(value) const inputClasses = { [classes.bold]: bold diff --git a/new-lamassu-admin/src/components/inputs/formik/RadioGroup.js b/new-lamassu-admin/src/components/inputs/formik/RadioGroup.js index 3702101c..e7b94554 100644 --- a/new-lamassu-admin/src/components/inputs/formik/RadioGroup.js +++ b/new-lamassu-admin/src/components/inputs/formik/RadioGroup.js @@ -13,8 +13,9 @@ const RadioGroupFormik = memo(({ label, ...props }) => { options={props.options} ariaLabel={name} onChange={e => { + console.log(e) onChange(e) - props.resetError() + props.resetError && props.resetError() }} className={props.className} {...props} diff --git a/new-lamassu-admin/src/components/inputs/formik/index.js b/new-lamassu-admin/src/components/inputs/formik/index.js index 66e1e1e2..30937441 100644 --- a/new-lamassu-admin/src/components/inputs/formik/index.js +++ b/new-lamassu-admin/src/components/inputs/formik/index.js @@ -1,4 +1,6 @@ +import Autocomplete from './Autocomplete' import Checkbox from './Checkbox' +import RadioGroup from './RadioGroup' import TextInput from './TextInput' -export { Checkbox, TextInput } +export { Autocomplete, Checkbox, TextInput, RadioGroup } diff --git a/new-lamassu-admin/src/pages/Cashout/Cashout.js b/new-lamassu-admin/src/pages/Cashout/Cashout.js index 5a82d083..03dbd611 100644 --- a/new-lamassu-admin/src/pages/Cashout/Cashout.js +++ b/new-lamassu-admin/src/pages/Cashout/Cashout.js @@ -11,9 +11,8 @@ import Wizard from './Wizard' import { DenominationsSchema, getElements } from './helper' const SAVE_CONFIG = gql` - mutation Save($config: JSONObject, $accounts: [JSONObject]) { + mutation Save($config: JSONObject) { saveConfig(config: $config) - saveAccounts(accounts: $accounts) } ` @@ -44,7 +43,6 @@ const CashOut = ({ name: SCREEN_KEY }) => { const save = (rawConfig, accounts) => { const config = toNamespace(SCREEN_KEY)(rawConfig) setError(false) - return saveConfig({ variables: { config, accounts } }) } diff --git a/new-lamassu-admin/src/pages/Cashout/Wizard.js b/new-lamassu-admin/src/pages/Cashout/Wizard.js index a49576b3..2411f7f8 100644 --- a/new-lamassu-admin/src/pages/Cashout/Wizard.js +++ b/new-lamassu-admin/src/pages/Cashout/Wizard.js @@ -1,11 +1,13 @@ import * as R from 'ramda' import React, { useState } from 'react' +import * as Yup from 'yup' import Modal from 'src/components/Modal' import { toNamespace } from 'src/utils/config' import WizardSplash from './WizardSplash' import WizardStep from './WizardStep' +import { DenominationsSchema } from './helper' const LAST_STEP = 3 const MODAL_WIDTH = 554 @@ -20,12 +22,14 @@ const Wizard = ({ machine, onClose, save, error }) => { const isLastStep = step === LAST_STEP const onContinue = async it => { - const newConfig = R.merge(config, it) - if (isLastStep) { - return save(toNamespace(machine.deviceId, newConfig)) + return save( + toNamespace(machine.deviceId, DenominationsSchema.cast(config)) + ) } + const newConfig = R.merge(config, it) + setState({ step: step + 1, config: newConfig @@ -35,11 +39,17 @@ const Wizard = ({ machine, onClose, save, error }) => { const getStepData = () => { switch (step) { case 1: - return { type: 'top', display: 'Cassete 1 (Top)' } + return { + type: 'top', + display: 'Cassete 1 (Top)', + schema: Yup.object().shape({ top: Yup.number().required() }) + } case 2: - return { type: 'bottom', display: 'Cassete 2' } - case 3: - return { type: 'agreed' } + return { + type: 'bottom', + display: 'Cassete 2', + schema: Yup.object().shape({ bottom: Yup.number().required() }) + } default: return null } diff --git a/new-lamassu-admin/src/pages/Cashout/WizardStep.js b/new-lamassu-admin/src/pages/Cashout/WizardStep.js index e2fe99fa..b0a0a3e9 100644 --- a/new-lamassu-admin/src/pages/Cashout/WizardStep.js +++ b/new-lamassu-admin/src/pages/Cashout/WizardStep.js @@ -1,77 +1,33 @@ import { makeStyles } from '@material-ui/core' import classnames from 'classnames' -import * as R from 'ramda' -import React, { useReducer, useEffect } from 'react' +import { Formik, Form, Field } from 'formik' +import React from 'react' import ErrorMessage from 'src/components/ErrorMessage' import Stepper from 'src/components/Stepper' import { Button } from 'src/components/buttons' -import { TextInput } from 'src/components/inputs' +import { TextInput } from 'src/components/inputs/formik' import { Info2, H4, P } from 'src/components/typography' import styles from './WizardStep.styles' const useStyles = makeStyles(styles) -const initialState = { - selected: null, - iError: false -} - -const reducer = (state, action) => { - switch (action.type) { - case 'select': - return { - form: null, - selected: action.selected, - isNew: null, - iError: false - } - case 'form': - return { - form: action.form, - selected: action.form.code, - isNew: true, - iError: false - } - case 'error': - return R.merge(state, { iError: true }) - case 'reset': - return initialState - default: - throw new Error() - } -} - const WizardStep = ({ type, name, step, + schema, error, lastStep, onContinue, display }) => { const classes = useStyles() - const [{ iError, selected }, dispatch] = useReducer(reducer, initialState) - - useEffect(() => { - dispatch({ type: 'reset' }) - }, [step]) - - const iContinue = config => { - if (lastStep) config[type] = true - - if (!config || !config[type]) { - return dispatch({ type: 'error' }) - } - - onContinue(config) - } const label = lastStep ? 'Finish' : 'Next' const subtitleClass = { [classes.subtitle]: true, - [classes.error]: iError + [classes.error]: error } return ( @@ -81,19 +37,26 @@ const WizardStep = ({ {display &&

Edit {display}

} {!lastStep && ( - - dispatch({ type: 'select', selected: evt.target.value }) - } - autoFocus - id="confirm-input" - type="text" - size="lg" - touched={{}} - error={false} - InputLabelProps={{ shrink: true }} - /> + +
+ +
+ +
+ +
// TODO: there was a disabled link here showing the currency code; restore it )} @@ -112,17 +75,14 @@ const WizardStep = ({ Settings. where you can set exceptions for each of the available cryptocurrencies.

+
+ {error && Failed to save} + +
)} - -
- {error && Failed to save} - -
) } diff --git a/new-lamassu-admin/src/pages/Cashout/helper.js b/new-lamassu-admin/src/pages/Cashout/helper.js index 466da650..8eb9a786 100644 --- a/new-lamassu-admin/src/pages/Cashout/helper.js +++ b/new-lamassu-admin/src/pages/Cashout/helper.js @@ -4,7 +4,8 @@ import TextInput from 'src/components/inputs/formik/TextInput' const DenominationsSchema = Yup.object().shape({ top: Yup.number().required('Required'), - bottom: Yup.number().required('Required') + bottom: Yup.number().required('Required'), + zeroConfLimit: Yup.number().required('Required') }) const getElements = (machines, { fiatCurrency } = {}) => { @@ -28,12 +29,20 @@ const getElements = (machines, { fiatCurrency } = {}) => { }, { name: 'bottom', - header: 'Cassette 2', + header: 'Cassette 2 (Bottom)', view: it => `${it} ${fiatCurrency}`, size: 'sm', stripe: true, width: 265, input: TextInput + }, + { + name: 'zeroConfLimit', + header: '0-conf Limit', + size: 'sm', + stripe: true, + width: 200, + input: TextInput } ] } diff --git a/new-lamassu-admin/src/pages/Cashout/index.js b/new-lamassu-admin/src/pages/Cashout/index.js new file mode 100644 index 00000000..e625c749 --- /dev/null +++ b/new-lamassu-admin/src/pages/Cashout/index.js @@ -0,0 +1,3 @@ +import Cashout from './Cashout' + +export default Cashout diff --git a/new-lamassu-admin/src/pages/Commissions.js b/new-lamassu-admin/src/pages/Commissions.js deleted file mode 100644 index 60c440ba..00000000 --- a/new-lamassu-admin/src/pages/Commissions.js +++ /dev/null @@ -1,179 +0,0 @@ -import React, { useState } from 'react' - -import { Link } from 'src/components/buttons' -import { TextInput } from 'src/components/inputs' -import { - Table, - TableHead, - TableRow, - TableHeader, - TableBody, - TableCell -} from 'src/components/table' -import { H1, H3, Info1, TL2 } from 'src/components/typography' - -const styles = {} -const EditRow = ({ data = {}, commitValues, setEditing }) => { - const [values, setValues] = React.useState(data) - - const handleChange = name => event => { - setValues({ ...values, [name]: event.target.value }) - } - - return ( - <> - - - - - - - - - - - - - - { - setEditing(false) - }}> - Cancel - - { - commitValues(values) - setEditing(false) - }}> - Save - - - - ) -} - -const ViewRow = ({ data, setEditing }) => ( - <> - - - {data.cashInCommission} - - {data.cashInCommission && ( - - % - - )} - - - - {data.cashOutCommission} - - {data.cashOutCommission && ( - - % - - )} - - - - {data.cashInFee} - - {data.cashOutCommission && ( - - EUR - - )} - - - - {data.minimumTx} - - {data.cashOutCommission && ( - - EUR - - )} - - - setEditing(true)}> - Edit - - - -) - -const Commissions = () => { - const [dataset, setDataset] = useState([{}]) - - const commitValues = (values, idx) => { - const clonedDs = dataset.slice() - clonedDs[idx] = Object.assign({}, clonedDs[idx], values) - setDataset(clonedDs) - } - - const EditableRow = () => - - return ( - <> -

Commissions

-

Default Setup

-
- - - - Cash-in - Cash-out - - Cash-in only - - - Edit - - - - Fixed Fee - Minimum Tx - - - - commitValues(value)} - EditRow={EditRow} - ViewRow={ViewRow} - /> - -
-
- - ) -} - -export default Commissions diff --git a/new-lamassu-admin/src/pages/Commissions.module.scss b/new-lamassu-admin/src/pages/Commissions.module.scss deleted file mode 100644 index 5537de93..00000000 --- a/new-lamassu-admin/src/pages/Commissions.module.scss +++ /dev/null @@ -1,30 +0,0 @@ -.multiRowHeader { - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - text-align: center; - background-color: white; //$placeholder-color; -} - -.tableWrapper { - width: 855px; -} - -.centerAlign { - text-align: center; -} - -.firstLink { - margin-right: 32px; -} - -.noMargin { - margin: 0; -} - -.suffix { - margin-left: 8px; -} - -.numberSmallInput { - width: 85px; -} diff --git a/new-lamassu-admin/src/pages/Commissions/Commissions.js b/new-lamassu-admin/src/pages/Commissions/Commissions.js new file mode 100644 index 00000000..140199e7 --- /dev/null +++ b/new-lamassu-admin/src/pages/Commissions/Commissions.js @@ -0,0 +1,96 @@ +import { useQuery, useMutation } from '@apollo/react-hooks' +import { gql } from 'apollo-boost' +import * as R from 'ramda' +import React from 'react' + +import { Table as EditableTable } from 'src/components/editableTable' +import Section from 'src/components/layout/Section' +import TitleSection from 'src/components/layout/TitleSection' +import { fromNamespace, toNamespace } from 'src/utils/config' + +import { + mainFields, + overrides, + schema, + OverridesSchema, + defaults, + overridesDefaults +} from './helper' + +const GET_DATA = gql` + query getData { + config + cryptoCurrencies { + code + display + } + machines { + name + deviceId + } + } +` + +const SAVE_CONFIG = gql` + mutation Save($config: JSONObject) { + saveConfig(config: $config) + } +` + +const Commissions = ({ name: SCREEN_KEY }) => { + const { data } = useQuery(GET_DATA) + const [saveConfig] = useMutation(SAVE_CONFIG, { + refetchQueries: () => ['getData'] + }) + + const config = data?.config && fromNamespace(SCREEN_KEY)(data.config) + + const commission = config && !R.isEmpty(config) ? config : defaults + + const save = it => { + const config = toNamespace(SCREEN_KEY)(it.commissions[0]) + return saveConfig({ variables: { config } }) + } + + const saveOverrides = it => { + const config = toNamespace(SCREEN_KEY)(it) + return saveConfig({ variables: { config } }) + } + + return ( + <> + +
+ +
+
+ +
+ + ) +} + +export default Commissions diff --git a/new-lamassu-admin/src/pages/Commissions/helper.js b/new-lamassu-admin/src/pages/Commissions/helper.js new file mode 100644 index 00000000..5b2bb2e9 --- /dev/null +++ b/new-lamassu-admin/src/pages/Commissions/helper.js @@ -0,0 +1,156 @@ +import * as R from 'ramda' +import * as Yup from 'yup' + +import { TextInput } from 'src/components/inputs/formik' +import Autocomplete from 'src/components/inputs/formik/Autocomplete.js' + +const getOverridesFields = getData => { + const getView = (data, code, compare) => it => { + if (!data) return '' + + return R.compose( + R.prop(code), + R.find(R.propEq(compare ?? 'code', it)) + )(data) + } + + const displayCodeArray = data => it => { + if (!it) return it + + return R.compose(R.join(', '), R.map(getView(data, 'code')))(it) + } + + const machineData = getData(['machines']) + const cryptoData = getData(['cryptoCurrencies']) + + return [ + { + name: 'machine', + width: 196, + size: 'sm', + view: getView(machineData, 'name', 'deviceId'), + input: Autocomplete, + inputProps: { + options: machineData, + valueProp: 'deviceId', + getLabel: R.path(['name']), + limit: null + } + }, + { + name: 'cryptoCurrencies', + width: 270, + size: 'sm', + view: displayCodeArray(cryptoData), + input: Autocomplete, + inputProps: { + options: cryptoData, + valueProp: 'code', + getLabel: R.path(['code']), + multiple: true + } + }, + { + name: 'cashIn', + display: 'Cash-in', + width: 140, + input: TextInput + }, + { + name: 'cashOut', + display: 'Cash-out', + width: 140, + input: TextInput + }, + { + name: 'fixedFee', + display: 'Fixed fee', + width: 140, + input: TextInput + }, + { + name: 'minimumTx', + display: 'Minimun Tx', + width: 140, + input: TextInput + } + ] +} + +const mainFields = auxData => [ + { + name: 'cashIn', + display: 'Cash-in', + width: 169, + size: 'lg', + input: TextInput + }, + { + name: 'cashOut', + display: 'Cash-out', + width: 169, + size: 'lg', + input: TextInput + }, + { + name: 'fixedFee', + display: 'Fixed fee', + width: 169, + size: 'lg', + input: TextInput + }, + { + name: 'minimumTx', + display: 'Minimun Tx', + width: 169, + size: 'lg', + input: TextInput + } +] + +const overrides = auxData => { + const getData = R.path(R.__, auxData) + + return getOverridesFields(getData) +} + +const schema = Yup.object().shape({ + cashIn: Yup.number().required('Required'), + cashOut: Yup.number().required('Required'), + fixedFee: Yup.number().required('Required'), + minimumTx: Yup.number().required('Required') +}) + +const OverridesSchema = Yup.object().shape({ + machine: Yup.string().required('Required'), + cryptoCurrencies: Yup.array().required('Required'), + cashIn: Yup.number().required('Required'), + cashOut: Yup.number().required('Required'), + fixedFee: Yup.number().required('Required'), + minimumTx: Yup.number().required('Required') +}) + +const defaults = { + cashIn: '', + cashOut: '', + fixedFee: '', + minimumTx: '' +} + +const overridesDefaults = { + machine: '', + cryptoCurrencies: [], + cashIn: '', + cashOut: '', + fixedFee: '', + minimumTx: '' +} + +export { + mainFields, + overrides, + schema, + OverridesSchema, + defaults, + overridesDefaults +} diff --git a/new-lamassu-admin/src/pages/Commissions/index.js b/new-lamassu-admin/src/pages/Commissions/index.js new file mode 100644 index 00000000..3e3f2b53 --- /dev/null +++ b/new-lamassu-admin/src/pages/Commissions/index.js @@ -0,0 +1,3 @@ +import Commissions from './Commissions' + +export default Commissions diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index 9f421323..4e0402df 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -1,8 +1,8 @@ import { useQuery } from '@apollo/react-hooks' import { gql } from 'apollo-boost' -import { useHistory } from 'react-router-dom' import * as R from 'ramda' import React from 'react' +import { useHistory } from 'react-router-dom' import CustomersList from './CustomersList' @@ -29,7 +29,7 @@ const Customers = () => { const handleCustomerClicked = customer => history.push(`/compliance/customer/${customer.id}`) - const customersData = R.sortWith([R.descend('lastActive')])( + const customersData = R.sortWith([R.descend(R.prop('lastActive'))])( R.path(['customers'])(customersResponse) ?? [] ) diff --git a/new-lamassu-admin/src/pages/Maintenance/Cashboxes.js b/new-lamassu-admin/src/pages/Maintenance/Cashboxes.js new file mode 100644 index 00000000..adfeea98 --- /dev/null +++ b/new-lamassu-admin/src/pages/Maintenance/Cashboxes.js @@ -0,0 +1,117 @@ +import { useQuery, useMutation } from '@apollo/react-hooks' +import { gql } from 'apollo-boost' +import React from 'react' +import * as Yup from 'yup' + +import { Table as EditableTable } from 'src/components/editableTable' +import TextInputFormik from 'src/components/inputs/formik/TextInput' +import TitleSection from 'src/components/layout/TitleSection' + +const ValidationSchema = Yup.object().shape({ + name: Yup.string().required('Required'), + cassette1: Yup.number() + .required('Required') + .integer() + .min(0), + cassette2: Yup.number() + .required('Required') + .integer() + .min(0) +}) + +const GET_MACHINES_AND_CONFIG = gql` + { + machines { + name + id: deviceId + cassette1 + cassette2 + } + config + } +` + +const RESET_CASHOUT_BILLS = gql` + mutation MachineAction( + $deviceId: ID! + $action: MachineAction! + $cassette1: Int! + $cassette2: Int! + ) { + machineAction( + deviceId: $deviceId + action: $action + cassette1: $cassette1 + cassette2: $cassette2 + ) { + deviceId + cassette1 + cassette2 + } + } +` + +const Cashboxes = () => { + const { data } = useQuery(GET_MACHINES_AND_CONFIG) + + const [resetCashOut] = useMutation(RESET_CASHOUT_BILLS, { + onError: ({ graphQLErrors, message }) => { + const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message + // TODO: this should not be final + alert(JSON.stringify(errorMessage)) + } + }) + + const onSave = (...[, { id, cassette1, cassette2 }]) => { + return resetCashOut({ + variables: { + action: 'resetCashOutBills', + deviceId: id, + cassette1, + cassette2 + } + }) + } + + const elements = [ + { + name: 'name', + header: 'Machine', + width: 254, + textAlign: 'left', + view: name => <>{name}, + input: ({ field: { value: name } }) => <>{name} + }, + { + name: 'cassette1', + header: 'Cash-out 1', + width: 265, + textAlign: 'left', + input: TextInputFormik + }, + { + name: 'cassette2', + header: 'Cash-out 2', + width: 265, + textAlign: 'left', + input: TextInputFormik + } + ] + + return ( + <> + + + + + ) +} + +export default Cashboxes diff --git a/new-lamassu-admin/src/pages/maintenance/MachineDetailsCard.js b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.js similarity index 100% rename from new-lamassu-admin/src/pages/maintenance/MachineDetailsCard.js rename to new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.js index 510c33fe..1e39947e 100644 --- a/new-lamassu-admin/src/pages/maintenance/MachineDetailsCard.js +++ b/new-lamassu-admin/src/pages/Maintenance/MachineDetailsCard.js @@ -1,16 +1,16 @@ +import { useMutation } from '@apollo/react-hooks' +import { Dialog, DialogContent } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' +import { gql } from 'apollo-boost' import classnames from 'classnames' import moment from 'moment' import React, { useState } from 'react' -import { useMutation } from '@apollo/react-hooks' -import { gql } from 'apollo-boost' -import { Dialog, DialogContent } from '@material-ui/core' import { H4 } from 'src/components/typography' -import ActionButton from '../../components/buttons/ActionButton' import { DialogTitle, ConfirmDialog } from '../../components/ConfirmDialog' import { Status } from '../../components/Status' +import ActionButton from '../../components/buttons/ActionButton' import { ReactComponent as DownloadReversedIcon } from '../../styling/icons/button/download/white.svg' import { ReactComponent as DownloadIcon } from '../../styling/icons/button/download/zodiac.svg' import { ReactComponent as RebootReversedIcon } from '../../styling/icons/button/reboot/white.svg' @@ -19,11 +19,11 @@ import { ReactComponent as ShutdownReversedIcon } from '../../styling/icons/butt import { ReactComponent as ShutdownIcon } from '../../styling/icons/button/shut down/zodiac.svg' import { ReactComponent as UnpairReversedIcon } from '../../styling/icons/button/unpair/white.svg' import { ReactComponent as UnpairIcon } from '../../styling/icons/button/unpair/zodiac.svg' +import { zircon } from '../../styling/variables' import { detailsRowStyles, labelStyles } from '../Transactions/Transactions.styles' -import { zircon } from '../../styling/variables' const MACHINE_ACTION = gql` mutation MachineAction($deviceId: ID!, $action: MachineAction!) { diff --git a/new-lamassu-admin/src/pages/maintenance/MachineStatus.js b/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js similarity index 100% rename from new-lamassu-admin/src/pages/maintenance/MachineStatus.js rename to new-lamassu-admin/src/pages/Maintenance/MachineStatus.js diff --git a/new-lamassu-admin/src/pages/OperatorInfo/ContactInfo.js b/new-lamassu-admin/src/pages/OperatorInfo/ContactInfo.js index 702a45d9..8b1adfe3 100644 --- a/new-lamassu-admin/src/pages/OperatorInfo/ContactInfo.js +++ b/new-lamassu-admin/src/pages/OperatorInfo/ContactInfo.js @@ -22,9 +22,9 @@ import { } from './OperatorInfo.styles' const validationSchema = Yup.object().shape({ - infoCardEnabled: Yup.boolean().required(), - fullName: Yup.string().required(), - phoneNumber: Yup.string().required(), + active: Yup.boolean().required(), + name: Yup.string().required(), + phone: Yup.string().required(), email: Yup.string() .email('Please enter a valid email address') .required(), @@ -111,7 +111,7 @@ const ContactInfo = () => { const [error, setError] = useState(null) const [saveConfig] = useMutation(SAVE_CONFIG, { onCompleted: data => { - setInfo(fromNamespace(namespaces.CONTACT_INFO, data.saveConfig)) + setInfo(fromNamespace(namespaces.OPERATOR_INFO, data.saveConfig)) setEditing(false) }, onError: e => setError(e) @@ -119,13 +119,13 @@ const ContactInfo = () => { useQuery(GET_CONFIG, { onCompleted: data => { - setInfo(fromNamespace(namespaces.CONTACT_INFO, data.config)) + setInfo(fromNamespace(namespaces.OPERATOR_INFO, data.config)) } }) const save = it => { return saveConfig({ - variables: { config: toNamespace(namespaces.CONTACT_INFO, it) } + variables: { config: toNamespace(namespaces.OPERATOR_INFO, it) } }) } @@ -135,21 +135,21 @@ const ContactInfo = () => { const fields = [ { - name: 'infoCardEnabled', + name: 'active', label: 'Info Card Enabled', - value: String(info.infoCardEnabled), + value: String(info.active), component: RadioGroupFormik }, { - name: 'fullName', + name: 'name', label: 'Full name', - value: info.fullName ?? '', + value: info.name ?? '', component: TextInputFormik }, { - name: 'phoneNumber', + name: 'phone', label: 'Phone number', - value: info.phoneNumber ?? '', + value: info.phone ?? '', component: TextInputFormik }, { @@ -179,9 +179,9 @@ const ContactInfo = () => { const form = { initialValues: { - infoCardEnabled: findValue('infoCardEnabled'), - fullName: findValue('fullName'), - phoneNumber: info.phoneNumber ?? '', + active: findValue('active'), + name: findValue('name'), + phone: info.phone ?? '', email: findValue('email'), website: findValue('website'), companyNumber: findValue('companyNumber') @@ -213,7 +213,7 @@ const ContactInfo = () => {
(it === 'true' ? 'On' : 'Off')} options={[ @@ -226,13 +226,13 @@ const ContactInfo = () => {
setError(null)} /> setError(null)} diff --git a/new-lamassu-admin/src/pages/OperatorInfo/ReceiptPrinting/ReceiptPrinting.js b/new-lamassu-admin/src/pages/OperatorInfo/ReceiptPrinting/ReceiptPrinting.js index dc5e8565..556637ee 100644 --- a/new-lamassu-admin/src/pages/OperatorInfo/ReceiptPrinting/ReceiptPrinting.js +++ b/new-lamassu-admin/src/pages/OperatorInfo/ReceiptPrinting/ReceiptPrinting.js @@ -1,10 +1,12 @@ // import { makeStyles } from '@material-ui/core/styles' import { useQuery, useMutation } from '@apollo/react-hooks' import { gql } from 'apollo-boost' +import * as R from 'ramda' import React, { useState, memo } from 'react' import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable' import { EditableProperty } from 'src/components/editableProperty' +import { fromNamespace, toNamespace, namespaces } from 'src/utils/config' // import { ActionButton } from 'src/components/buttons' // import { ReactComponent as UploadIcon } from 'src/styling/icons/button/upload/zodiac.svg' // import { ReactComponent as UploadIconInverse } from 'src/styling/icons/button/upload/white.svg' @@ -64,21 +66,28 @@ const ReceiptPrinting = memo(() => { // TODO: treat errors on useMutation and useQuery const [saveConfig] = useMutation(SAVE_CONFIG, { - onCompleted: configResponse => - setReceiptPrintingConfig(configResponse.saveConfig.receiptPrinting) + onCompleted: configResponse => { + console.log(configResponse.saveConfig) + return setReceiptPrintingConfig( + fromNamespace(namespaces.RECEIPT, configResponse.saveConfig) + ) + } }) useQuery(GET_CONFIG, { onCompleted: configResponse => { - setReceiptPrintingConfig( - configResponse?.config?.receiptPrinting ?? initialValues - ) + const response = fromNamespace(namespaces.RECEIPT, configResponse.config) + const values = R.merge(initialValues, response) + setReceiptPrintingConfig(values) } }) const save = it => - saveConfig({ variables: { config: { receiptPrinting: it } } }) + saveConfig({ + variables: { config: toNamespace(namespaces.RECEIPT, it) } + }) if (!receiptPrintingConfig) return null + console.log(receiptPrintingConfig) return ( <> @@ -90,7 +99,12 @@ const ReceiptPrinting = memo(() => { code={receiptPrintingConfig.active} save={it => saveConfig({ - variables: { config: { receiptPrinting: { active: it } } } + variables: { + config: toNamespace( + namespaces.RECEIPT, + R.merge(receiptPrintingConfig, { active: it }) + ) + } }) } /> diff --git a/new-lamassu-admin/src/pages/OperatorInfo/TermsConditions.js b/new-lamassu-admin/src/pages/OperatorInfo/TermsConditions.js index 88a52837..ccfa0e52 100644 --- a/new-lamassu-admin/src/pages/OperatorInfo/TermsConditions.js +++ b/new-lamassu-admin/src/pages/OperatorInfo/TermsConditions.js @@ -46,7 +46,7 @@ const TermsConditions = () => { data.saveConfig ) setFormData(termsAndConditions) - setShowOnScreen(termsAndConditions.show) + setShowOnScreen(termsAndConditions.active) setError(null) }, onError: e => setError(e) @@ -61,7 +61,7 @@ const TermsConditions = () => { data.config ) setFormData(termsAndConditions ?? {}) - setShowOnScreen(termsAndConditions?.show ?? false) + setShowOnScreen(termsAndConditions?.active ?? false) } }) @@ -74,21 +74,21 @@ const TermsConditions = () => { const handleEnable = () => { const s = !showOnScreen - save({ show: s }) + save({ active: s }) } if (!formData) return null const fields = [ { - name: 'screenTitle', + name: 'title', label: 'Screen title', - value: formData.screenTitle ?? '' + value: formData.title ?? '' }, { - name: 'textContent', + name: 'text', label: 'Text content', - value: formData.textContent ?? '', + value: formData.text ?? '', multiline: true }, { @@ -109,14 +109,14 @@ const TermsConditions = () => { const findValue = name => findField(name).value const initialValues = { - screenTitle: findValue('screenTitle'), - textContent: findValue('textContent'), + title: findValue('title'), + text: findValue('text'), acceptButtonText: findValue('acceptButtonText'), cancelButtonText: findValue('cancelButtonText') } const validationSchema = Yup.object().shape({ - screenTitle: Yup.string().max(50, 'Too long'), + title: Yup.string().max(50, 'Too long'), acceptButtonText: Yup.string().max(15, 'Too long'), cancelButtonText: Yup.string().max(15, 'Too long') }) diff --git a/new-lamassu-admin/src/pages/Services/Services.js b/new-lamassu-admin/src/pages/Services/Services.js index 54103118..0bc63367 100644 --- a/new-lamassu-admin/src/pages/Services/Services.js +++ b/new-lamassu-admin/src/pages/Services/Services.js @@ -34,7 +34,7 @@ const styles = { const useStyles = makeStyles(styles) -const Services = ({ key: SCREEN_KEY }) => { +const Services = () => { const [editingSchema, setEditingSchema] = useState(null) const { data } = useQuery(GET_INFO) diff --git a/new-lamassu-admin/src/pages/Triggers/NewTriggerWizard.js b/new-lamassu-admin/src/pages/Triggers/NewTriggerWizard.js deleted file mode 100644 index e169f3bc..00000000 --- a/new-lamassu-admin/src/pages/Triggers/NewTriggerWizard.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState } from 'react' -import { useQuery } from '@apollo/react-hooks' -import { gql } from 'apollo-boost' -import { makeStyles } from '@material-ui/core' - -import { Wizard } from 'src/components/wizard' -import { H2, P } from 'src/components/typography' -import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg' -import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' -import Popper from 'src/components/Popper' - -import SelectTriggerDirection from './SelectTriggerDirection' -import SelectTriggerType from './SelectTriggerType' -import SelectTriggerRequirements from './SelectTriggerRequirements' -import { mainStyles } from './Triggers.styles' - -const useStyles = makeStyles(mainStyles) - -const GET_CONFIG = gql` - { - config - } -` - -const NewTriggerWizard = ({ close, finish }) => { - const { data: configResponse } = useQuery(GET_CONFIG) - const [helpPopperAnchorEl, setHelpPopperAnchorEl] = useState(null) - - const classes = useStyles() - - const fiatCurrencyCode = configResponse?.config?.['locale_fiatCurrency']?.code - - const handleOpenHelpPopper = event => { - setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget) - } - - const handleCloseHelpPopper = () => { - setHelpPopperAnchorEl(null) - } - - const helpPopperOpen = Boolean(helpPopperAnchorEl) - - const wizardHeader = ( -
-

New Compliance Trigger

-
- -
-
- -
-
- ) - - return ( - - - - - - ) -} - -export { NewTriggerWizard } diff --git a/new-lamassu-admin/src/pages/Triggers/SelectTriggerDirection.js b/new-lamassu-admin/src/pages/Triggers/SelectTriggerDirection.js deleted file mode 100644 index 8156543c..00000000 --- a/new-lamassu-admin/src/pages/Triggers/SelectTriggerDirection.js +++ /dev/null @@ -1,77 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import classnames from 'classnames' -import React, { useState } from 'react' - -import Popper from 'src/components/Popper' -import { RadioGroup } from 'src/components/inputs' -import { H4, P } from 'src/components/typography' -import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg' - -import { mainStyles } from './Triggers.styles' - -const useStyles = makeStyles(mainStyles) - -const SelectTriggerDirection = () => { - const [helpPopperAnchorEl, setHelpPopperAnchorEl] = useState(null) - const [radioGroupValue, setRadioGroupValue] = useState('both') - - const classes = useStyles() - - const handleOpenHelpPopper = event => { - setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget) - } - - const handleCloseHelpPopper = () => { - setHelpPopperAnchorEl(null) - } - - const handleRadioButtons = newValue => { - setRadioGroupValue(newValue) - } - - const helpPopperOpen = Boolean(helpPopperAnchorEl) - - const radioButtonOptions = [ - { display: 'Both', code: 'both' }, - { display: 'Only cash-in', code: 'cash-in' }, - { display: 'Only cash-out', code: 'cash-out' } - ] - - return ( -
-
-

In which type of transactions will it trigger?

-
- -
-
-
- handleRadioButtons(event.target.value)} - className={classnames( - classes.radioButtons, - classes.stepOneRadioButtons - )} - /> -
-
- ) -} -export default SelectTriggerDirection diff --git a/new-lamassu-admin/src/pages/Triggers/SelectTriggerRequirements.js b/new-lamassu-admin/src/pages/Triggers/SelectTriggerRequirements.js deleted file mode 100644 index f9d222b2..00000000 --- a/new-lamassu-admin/src/pages/Triggers/SelectTriggerRequirements.js +++ /dev/null @@ -1,141 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import classnames from 'classnames' -import React, { useState } from 'react' - -import Popper from 'src/components/Popper' -import { RadioGroup } from 'src/components/inputs' -import { H4, P } from 'src/components/typography' -import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg' - -import { mainStyles } from './Triggers.styles' - -const useStyles = makeStyles(mainStyles) - -const SelectTriggerRequirements = () => { - const [ - requirementHelpPopperAnchorEl, - setRequirementHelpPopperAnchorEl - ] = useState(null) - const [typeHelpPopperAnchorEl, setTypeHelpPopperAnchorEl] = useState(null) - const [requirementRadioGroupValue, setRequirementRadioGroupValue] = useState( - 'sms' - ) - const [typeRadioGroupValue, setTypeRadioGroupValue] = useState('automatic') - - const classes = useStyles() - - const handleOpenRequirementHelpPopper = event => { - setRequirementHelpPopperAnchorEl( - requirementHelpPopperAnchorEl ? null : event.currentTarget - ) - } - - const handleOpenTypeHelpPopper = event => { - setTypeHelpPopperAnchorEl( - typeHelpPopperAnchorEl ? null : event.currentTarget - ) - } - - const handleCloseRequirementHelpPopper = () => { - setRequirementHelpPopperAnchorEl(null) - } - - const handleCloseTypeHelpPopper = () => { - setTypeHelpPopperAnchorEl(null) - } - - const handleRequirementRadioButtons = newValue => { - setRequirementRadioGroupValue(newValue) - } - - const handleTypeRadioButtons = newValue => { - setTypeRadioGroupValue(newValue) - } - - const requirementHelpPopperOpen = Boolean(requirementHelpPopperAnchorEl) - const typeHelpPopperOpen = Boolean(typeHelpPopperAnchorEl) - - const requirementRadioButtonOptions = [ - { display: 'SMS verification', code: 'sms' }, - { display: 'ID card image', code: 'id-card' }, - { display: 'ID data', code: 'id-data' }, - { display: 'Customer camera', code: 'camera' }, - { display: 'Sanctions', code: 'sanctions' }, - { display: 'Super user', code: 'super-user' }, - { display: 'Suspend', code: 'suspend' }, - { display: 'Block', code: 'block' } - ] - - const typeRadioButtonOptions = [ - { display: 'Fully automatic', code: 'automatic' }, - { display: 'Semi automatic', code: 'semi-automatic' }, - { display: 'Manual', code: 'manual' } - ] - - return ( -
-
-

Choose a requirement

-
- -
-
- handleRequirementRadioButtons(event.target.value)} - className={classnames( - classes.radioButtons, - classes.stepThreeRadioButtons - )} - /> -
-

Choose trigger type

-
- -
-
- handleTypeRadioButtons(event.target.value)} - className={classnames( - classes.radioButtons, - classes.stepThreeRadioButtons - )} - /> -
- ) -} - -export default SelectTriggerRequirements diff --git a/new-lamassu-admin/src/pages/Triggers/SelectTriggerType.js b/new-lamassu-admin/src/pages/Triggers/SelectTriggerType.js deleted file mode 100644 index df5e30b4..00000000 --- a/new-lamassu-admin/src/pages/Triggers/SelectTriggerType.js +++ /dev/null @@ -1,104 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import classnames from 'classnames' -import React, { useState } from 'react' - -import Popper from 'src/components/Popper' -import { RadioGroup, TextInput } from 'src/components/inputs' -import { H4, TL1, P } from 'src/components/typography' -import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg' - -import { mainStyles } from './Triggers.styles' - -const useStyles = makeStyles(mainStyles) - -const SelectTriggerType = ({ fiatCurrencyCode }) => { - const [helpPopperAnchorEl, setHelpPopperAnchorEl] = useState(null) - const [radioGroupValue, setRadioGroupValue] = useState('amount') - const [thresholdValue, setThresholdValue] = useState('') - const [thresholdError, setThresholdError] = useState(false) - - const classes = useStyles() - - const handleOpenHelpPopper = event => { - setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget) - } - - const handleCloseHelpPopper = () => { - setHelpPopperAnchorEl(null) - } - - const handleRadioButtons = newValue => { - setRadioGroupValue(newValue) - } - - const validateThresholdInputIsPositiveInteger = value => { - if ( - (parseFloat(value) === value >>> 0 && !value.includes('.')) || - value === '' - ) { - setThresholdValue(value) - setThresholdError(value === '') - } - } - - const helpPopperOpen = Boolean(helpPopperAnchorEl) - - const radioButtonOptions = [ - { display: 'Transaction amount', code: 'amount' }, - { display: 'Transaction velocity', code: 'velocity' }, - { display: 'Transaction volume', code: 'volume' }, - { display: 'Consecutive days', code: 'days' } - ] - - return ( -
-
-

Choose trigger type

-
- -
-
-
- handleRadioButtons(event.target.value)} - className={classnames( - classes.radioButtons, - classes.stepTwoRadioButtons - )} - /> -
-

Threshold

-
- - validateThresholdInputIsPositiveInteger(event.target.value) - } - error={thresholdError} - size="lg" - value={thresholdValue} - /> - {fiatCurrencyCode} -
-
- ) -} - -export default SelectTriggerType diff --git a/new-lamassu-admin/src/pages/Triggers/Triggers.js b/new-lamassu-admin/src/pages/Triggers/Triggers.js index a5067353..1ebfd830 100644 --- a/new-lamassu-admin/src/pages/Triggers/Triggers.js +++ b/new-lamassu-admin/src/pages/Triggers/Triggers.js @@ -1,5 +1,9 @@ +import { useQuery, useMutation } from '@apollo/react-hooks' +import { makeStyles } from '@material-ui/core' +import { gql } from 'apollo-boost' +import * as R from 'ramda' import React, { useState } from 'react' -import { makeStyles, Modal, Paper } from '@material-ui/core' +import { v4 } from 'uuid' import Title from 'src/components/Title' import { FeatureButton, Link } from 'src/components/buttons' @@ -7,35 +11,50 @@ import { Table as EditableTable } from 'src/components/editableTable' import { ReactComponent as ConfigureInverseIcon } from 'src/styling/icons/button/configure/white.svg' import { ReactComponent as Configure } from 'src/styling/icons/button/configure/zodiac.svg' -import { NewTriggerWizard } from './NewTriggerWizard' import { mainStyles } from './Triggers.styles' +import Wizard from './Wizard' +import { Schema, elements } from './helper' const useStyles = makeStyles(mainStyles) -const sizes = { - triggerType: 236, - requirement: 293, - threshold: 231, - cashDirection: 296 -} +const SAVE_CONFIG = gql` + mutation Save($config: JSONObject) { + saveConfig(config: $config) + } +` + +const GET_INFO = gql` + query getData { + config + } +` const Triggers = () => { - const [wizardModalOpen, setWizardModalOpen] = useState(false) + const [wizard, setWizard] = useState(false) + const [error, setError] = useState(false) + + const { data } = useQuery(GET_INFO) + const triggers = data?.config?.triggers ?? [] + + const [saveConfig] = useMutation(SAVE_CONFIG, { + onCompleted: () => setWizard(false), + onError: () => setError(true), + refetchQueries: () => ['getData'] + }) + + const add = rawConfig => { + const toSave = R.concat([{ id: v4(), ...rawConfig }])(triggers) + setError(false) + return saveConfig({ variables: { config: { triggers: toSave } } }) + } + + const save = config => { + setError(false) + return saveConfig({ variables: { config } }) + } const classes = useStyles() - const handleOpenWizard = () => { - setWizardModalOpen(true) - } - - const handleCloseWizard = () => { - handleFinishWizard() - } - - const handleFinishWizard = () => { - setWizardModalOpen(false) - } - return ( <>
@@ -50,46 +69,22 @@ const Triggers = () => {
- + setWizard(true)}> + Add new trigger
- {wizardModalOpen && ( - - - - - + {wizard && ( + setWizard(null)} /> )} ) diff --git a/new-lamassu-admin/src/pages/Triggers/Triggers.styles.js b/new-lamassu-admin/src/pages/Triggers/Triggers.styles.js index 61584b81..eeae173a 100644 --- a/new-lamassu-admin/src/pages/Triggers/Triggers.styles.js +++ b/new-lamassu-admin/src/pages/Triggers/Triggers.styles.js @@ -1,5 +1,5 @@ -import baseStyles from 'src/pages/Logs.styles' import { booleanPropertiesTableStyles } from 'src/components/booleanPropertiesTable/BooleanPropertiesTable.styles' +import baseStyles from 'src/pages/Logs.styles' const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles const { rowWrapper, radioButtons } = booleanPropertiesTableStyles @@ -10,6 +10,17 @@ const mainStyles = { buttonsWrapper, rowWrapper, radioButtons, + radioGroup: { + flexDirection: 'row' + }, + radioLabel: { + width: 150, + height: 40 + }, + radio: { + padding: 4, + margin: 4 + }, closeButton: { position: 'absolute', width: 16, @@ -34,14 +45,6 @@ const mainStyles = { marginRight: 12 } }, - modal: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - '& > div': { - outline: 'none' - } - }, wizardHeaderText: { display: 'flex', margin: [[24, 0]] diff --git a/new-lamassu-admin/src/pages/Triggers/Wizard.js b/new-lamassu-admin/src/pages/Triggers/Wizard.js new file mode 100644 index 00000000..ff76c550 --- /dev/null +++ b/new-lamassu-admin/src/pages/Triggers/Wizard.js @@ -0,0 +1,103 @@ +import { makeStyles } from '@material-ui/core' +import { Form, Formik } from 'formik' +import * as R from 'ramda' +import React, { useState, Fragment } from 'react' + +import ErrorMessage from 'src/components/ErrorMessage' +import Modal from 'src/components/Modal' +import Stepper from 'src/components/Stepper' +import { Button } from 'src/components/buttons' + +import { direction, type, requirements } from './helper' + +const LAST_STEP = 3 + +const styles = { + stepper: { + margin: [[16, 0, 14, 0]] + }, + submit: { + display: 'flex', + flexDirection: 'row', + margin: [['auto', 0, 24]] + }, + button: { + marginLeft: 'auto' + }, + form: { + height: '100%', + display: 'flex', + flexDirection: 'column' + } +} + +const useStyles = makeStyles(styles) + +const getStep = step => { + switch (step) { + case 1: + return direction + case 2: + return type + case 3: + return requirements + default: + return Fragment + } +} + +const Wizard = ({ machine, onClose, save, error }) => { + const classes = useStyles() + + const [{ step, config }, setState] = useState({ + step: 1 + }) + + const isLastStep = step === LAST_STEP + const stepOptions = getStep(step) + + const onContinue = async it => { + const newConfig = R.merge(config, stepOptions.schema.cast(it)) + + if (isLastStep) { + return save(newConfig) + } + + setState({ + step: step + 1, + config: newConfig + }) + } + + return ( + + + + + +
+ {error && Failed to save} + +
+ +
+
+ ) +} + +export default Wizard diff --git a/new-lamassu-admin/src/pages/Triggers/helper.js b/new-lamassu-admin/src/pages/Triggers/helper.js new file mode 100644 index 00000000..bebc7dc4 --- /dev/null +++ b/new-lamassu-admin/src/pages/Triggers/helper.js @@ -0,0 +1,250 @@ +import { makeStyles, Box } from '@material-ui/core' +import classnames from 'classnames' +import { Field, useFormikContext } from 'formik' +import * as R from 'ramda' +import React from 'react' +import * as Yup from 'yup' + +import { TextInput, RadioGroup } from 'src/components/inputs/formik' +import Autocomplete from 'src/components/inputs/formik/Autocomplete' +import { H4 } from 'src/components/typography' +import { errorColor } from 'src/styling/variables' + +const useStyles = makeStyles({ + radioLabel: { + height: 40, + padding: [[0, 10]] + }, + radio: { + padding: 4, + margin: 4 + }, + radioGroup: { + flexDirection: 'row' + }, + error: { + color: errorColor + }, + specialLabel: { + height: 40, + padding: 0 + }, + specialGrid: { + display: 'grid', + gridTemplateColumns: [[182, 162, 141]] + } +}) + +const cashDirection = Yup.string().required('Required') +const triggerType = Yup.string().required('Required') +const threshold = Yup.number().required('Required') +const requirement = Yup.string().required('Required') + +const Schema = Yup.object().shape({ + triggerType, + requirement, + threshold, + cashDirection +}) + +// Direction +const directionSchema = Yup.object().shape({ cashDirection }) + +const directionOptions = [ + { display: 'Both', code: 'both' }, + { display: 'Only cash-in', code: 'cashIn' }, + { display: 'Only cash-out', code: 'cashOut' } +] + +const Direction = () => { + const classes = useStyles() + const { errors } = useFormikContext() + + const titleClass = { + [classes.error]: errors.cashDirection + } + + return ( + <> + +

+ In which type of transactions will it trigger? +

+
+ + + ) +} + +const direction = { + schema: directionSchema, + options: directionOptions, + Component: Direction, + initialValues: { cashDirection: '' } +} + +// TYPE +const typeSchema = Yup.object().shape({ + triggerType, + threshold +}) + +const typeOptions = [ + { display: 'Transaction amount', code: 'txAmount' }, + { display: 'Transaction velocity', code: 'txVelocity' }, + { display: 'Transaction volume', code: 'txVolume' }, + { display: 'Consecutive days', code: 'consecutiveDays' } +] + +const Type = () => { + const classes = useStyles() + const { errors, touched } = useFormikContext() + + const typeClass = { + [classes.error]: errors.triggerType && touched.triggerType + } + + return ( + <> + +

Choose trigger type

+
+ + + + + ) +} + +const type = { + schema: typeSchema, + options: typeOptions, + Component: Type, + initialValues: { triggerType: '', threshold: '' } +} + +const requirementSchema = Yup.object().shape({ + requirement +}) + +const requirementOptions = [ + { display: 'SMS verification', code: 'sms' }, + { display: 'ID card image', code: 'idPhoto' }, + { display: 'ID data', code: 'idData' }, + { display: 'Customer camera', code: 'facephoto' }, + { display: 'Sanctions', code: 'sanctions' }, + { display: 'Super user', code: 'superuser' }, + { display: 'Suspend', code: 'suspend' }, + { display: 'Block', code: 'block' } +] + +const Requirement = () => { + const classes = useStyles() + const { errors } = useFormikContext() + + const titleClass = { + [classes.error]: errors.requirement + } + + return ( + <> + +

Choose a requirement

+
+ + + ) +} + +const requirements = { + schema: requirementSchema, + options: requirementOptions, + Component: Requirement, + initialValues: { requirement: '' } +} + +const getView = (data, code, compare) => it => { + if (!data) return '' + + return R.compose(R.prop(code), R.find(R.propEq(compare ?? 'code', it)))(data) +} + +const elements = [ + { + name: 'triggerType', + size: 'sm', + width: 271, + input: Autocomplete, + view: getView(typeOptions, 'display'), + inputProps: { + options: typeOptions, + valueProp: 'code', + getLabel: R.path(['display']), + limit: null + } + }, + { + name: 'requirement', + size: 'sm', + width: 271, + input: Autocomplete, + view: getView(requirementOptions, 'display'), + inputProps: { + options: requirementOptions, + valueProp: 'code', + getLabel: R.path(['display']), + limit: null + } + }, + { + name: 'threshold', + size: 'sm', + width: 271, + input: TextInput + }, + { + name: 'cashDirection', + size: 'sm', + width: 200, + view: getView(directionOptions, 'display'), + input: Autocomplete, + inputProps: { + options: directionOptions, + valueProp: 'code', + getLabel: R.path(['display']), + limit: null + } + } +] + +export { Schema, elements, direction, type, requirements } diff --git a/new-lamassu-admin/src/pages/Wallet/WizardStep.js b/new-lamassu-admin/src/pages/Wallet/WizardStep.js index ed8dd072..752e57b4 100644 --- a/new-lamassu-admin/src/pages/Wallet/WizardStep.js +++ b/new-lamassu-admin/src/pages/Wallet/WizardStep.js @@ -7,7 +7,7 @@ import ErrorMessage from 'src/components/ErrorMessage' import Stepper from 'src/components/Stepper' import { Button } from 'src/components/buttons' import { RadioGroup, Autocomplete } from 'src/components/inputs' -import { Info2, H4 } from 'src/components/typography' +import { H4, Info2 } from 'src/components/typography' import FormRenderer from 'src/pages/Services/FormRenderer' import schema from 'src/pages/Services/schemas' import { startCase } from 'src/utils/string' diff --git a/new-lamassu-admin/src/pages/common.styles.js b/new-lamassu-admin/src/pages/common.styles.js deleted file mode 100644 index db9958b5..00000000 --- a/new-lamassu-admin/src/pages/common.styles.js +++ /dev/null @@ -1,17 +0,0 @@ -export default { - titleWrapper: { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - flexDirection: 'row' - }, - titleAndButtonsContainer: { - display: 'flex' - }, - iconButton: { - border: 'none', - outline: 0, - backgroundColor: 'transparent', - cursor: 'pointer' - } -} diff --git a/new-lamassu-admin/src/pages/maintenance/Cashboxes.js b/new-lamassu-admin/src/pages/maintenance/Cashboxes.js deleted file mode 100644 index 214f2ab2..00000000 --- a/new-lamassu-admin/src/pages/maintenance/Cashboxes.js +++ /dev/null @@ -1,210 +0,0 @@ -import { useQuery, useMutation } from '@apollo/react-hooks' -import { gql } from 'apollo-boost' -import React, { useState } from 'react' -import * as Yup from 'yup' - -import { Table as EditableTable } from 'src/components/editableTable' -import { - CashIn, - CashOut, - CashOutFormik, - CashInFormik -} from 'src/components/inputs/cashbox/Cashbox' -import TitleSection from 'src/components/layout/TitleSection' -import { ReactComponent as ErrorIcon } from 'src/styling/icons/status/tomato.svg' - -const ValidationSchema = Yup.object().shape({ - name: Yup.string().required('Required'), - cashin: Yup.object() - .required('Required') - .shape({ - notes: Yup.number() - .required('Required') - .integer() - .min(0) - }), - cashout1: Yup.object() - .required('Required') - .shape({ - notes: Yup.number() - .required('Required') - .integer() - .min(0), - denomination: Yup.number() - .required('Required') - .integer() - .default(0) - }), - cashout2: Yup.object() - .required('Required') - .shape({ - notes: Yup.number() - .required('Required') - .integer() - .min(0), - denomination: Yup.number() - .required('Required') - .integer() - .default(0) - }) -}) - -const GET_MACHINES_AND_CONFIG = gql` - { - machines { - name - deviceId - cashbox - cassette1 - cassette2 - } - config - } -` - -const EMPTY_CASHIN_BILLS = gql` - mutation MachineAction($deviceId: ID!, $action: MachineAction!) { - machineAction(deviceId: $deviceId, action: $action) { - deviceId - cashbox - cassette1 - cassette2 - } - } -` - -const RESET_CASHOUT_BILLS = gql` - mutation MachineAction( - $deviceId: ID! - $action: MachineAction! - $cassettes: [Int]! - ) { - machineAction(deviceId: $deviceId, action: $action, cassettes: $cassettes) { - deviceId - cashbox - cassette1 - cassette2 - } - } -` - -const Cashboxes = () => { - const [machines, setMachines] = useState([]) - - useQuery(GET_MACHINES_AND_CONFIG, { - onCompleted: ({ machines, config }) => - setMachines( - machines.map(m => ({ - ...m, - currency: { code: config.locale_fiatCurrency ?? '' }, - denominations: { - top: config[`denominations_${m.deviceId}_top`], - bottom: config[`denominations_${m.deviceId}_bottom`] - } - })) - ) - }) - - const [resetCashOut] = useMutation(RESET_CASHOUT_BILLS, { - onError: ({ graphQLErrors, message }) => { - const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message - // TODO: this should not be final - alert(JSON.stringify(errorMessage)) - } - }) - - const [onEmpty] = useMutation(EMPTY_CASHIN_BILLS, { - onError: ({ graphQLErrors, message }) => { - const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message - // TODO: this should not be final - alert(JSON.stringify(errorMessage)) - } - }) - - const onSave = (_, { cashin, cashout1, cashout2 }) => - resetCashOut({ - variables: { - deviceId: cashin.deviceId, - action: 'resetCashOutBills', - cassettes: [Number(cashout1.notes), Number(cashout2.notes)] - } - }) - - const elements = [ - { - name: 'name', - header: 'Machine', - width: 254, - textAlign: 'left', - view: name => <>{name}, - input: ({ field: { value: name } }) => <>{name} - }, - { - name: 'cashin', - header: 'Cash-in', - width: 265, - textAlign: 'left', - view: props => , - input: props => - }, - { - name: 'cashout1', - header: 'Cash-out 1', - width: 265, - textAlign: 'left', - view: props => , - input: CashOutFormik - }, - { - name: 'cashout2', - header: 'Cash-out 2', - width: 265, - textAlign: 'left', - view: props => , - input: CashOutFormik - } - ] - - const data = machines.map( - ({ - name, - cassette1, - cassette2, - currency, - denominations: { top, bottom }, - cashbox, - deviceId - }) => ({ - id: deviceId, - name, - cashin: { notes: cashbox, deviceId }, - cashout1: { notes: cassette1, denomination: top, currency }, - cashout2: { notes: cassette2, denomination: bottom, currency } - }) - ) - - return ( - <> - - - Action required - - } - /> - - - - ) -} - -export default Cashboxes diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 7266898e..d65af0ce 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -3,12 +3,14 @@ import React from 'react' import { Route, Redirect, Switch } from 'react-router-dom' import AuthRegister from 'src/pages/AuthRegister' -import Cashout from 'src/pages/Cashout/Cashout' +import Cashout from 'src/pages/Cashout' import Commissions from 'src/pages/Commissions' import { Customers, CustomerProfile } from 'src/pages/Customers' import Funding from 'src/pages/Funding' import Locales from 'src/pages/Locales' import MachineLogs from 'src/pages/MachineLogs' +import Cashboxes from 'src/pages/Maintenance/Cashboxes' +import MachineStatus from 'src/pages/Maintenance/MachineStatus' import Notifications from 'src/pages/Notifications/Notifications' import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo' import ServerLogs from 'src/pages/ServerLogs' @@ -16,11 +18,8 @@ import Services from 'src/pages/Services/Services' import Transactions from 'src/pages/Transactions/Transactions' import Triggers from 'src/pages/Triggers' import WalletSettings from 'src/pages/Wallet/Wallet' -import MachineStatus from 'src/pages/maintenance/MachineStatus' import { namespaces } from 'src/utils/config' -import Cashboxes from '../pages/maintenance/Cashboxes' - const tree = [ { key: 'transactions', @@ -95,7 +94,7 @@ const tree = [ component: Cashout }, { - key: namespaces.SERVICES, + key: 'services', label: '3rd party services', route: '/settings/3rd-party-services', component: Services diff --git a/new-lamassu-admin/src/utils/config.js b/new-lamassu-admin/src/utils/config.js index f6c8efe8..12f4ba7c 100644 --- a/new-lamassu-admin/src/utils/config.js +++ b/new-lamassu-admin/src/utils/config.js @@ -1,14 +1,12 @@ import * as R from 'ramda' const namespaces = { - CASH_OUT: 'denominations', + CASH_OUT: 'cashOut', WALLETS: 'wallets', OPERATOR_INFO: 'operatorInfo', NOTIFICATIONS: 'notifications', - SERVICES: 'services', LOCALE: 'locale', COMMISSIONS: 'commissions', - CONTACT_INFO: 'operatorInfo', RECEIPT: 'receipt', COIN_ATM_RADAR: 'coinAtmRadar', TERMS_CONDITIONS: 'termsConditions' diff --git a/package-lock.json b/package-lock.json index 9457b220..121e11af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12752,7 +12752,6 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.20.6.tgz", "integrity": "sha1-PpcwauAk+yThCj11yIQwJWIhUSA=", "requires": { - "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xhr2": "*", @@ -12761,7 +12760,7 @@ "dependencies": { "bignumber.js": { "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", - "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git" + "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" } } }, diff --git a/package.json b/package.json index 8c5e38e2..32f479d8 100644 --- a/package.json +++ b/package.json @@ -112,8 +112,7 @@ "build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu" }, "devDependencies": { - "ava": "^0.19.1", - "eslint-plugin-import": "^2.19.1", + "ava": "3.8.2", "mocha": "^5.0.1", "rewire": "^4.0.1", "standard": "^12.0.1" @@ -121,7 +120,8 @@ "standard": { "ignore": [ "/lamassu-admin-elm", - "/public" + "/public", + "/new-lamassu-admin" ] } }