chore: integrate new admin with l-s
This commit is contained in:
parent
6b3db134e7
commit
bf8f1d991c
72 changed files with 1493 additions and 1611 deletions
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
10
lib/app.js
10
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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
9
lib/compliance-triggers.js
Normal file
9
lib/compliance-triggers.js
Normal file
|
|
@ -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}
|
||||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
93
lib/new-config-manager.js
Normal file
93
lib/new-config-manager.js
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
145
lib/notifier.js
145
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')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
162
lib/plugins.js
162
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
114
lib/routes.js
114
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<MaterialModal onClose={innerClose} className={classes.modal} {...props}>
|
||||
<Paper className={classnames(classes.wrapper, className)}>
|
||||
<div className={classes.header}>
|
||||
{title && <H1 className={classes.title}>{title}</H1>}
|
||||
<IconButton
|
||||
size={20}
|
||||
className={classes.button}
|
||||
onClick={() => handleClose()}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={classes.content}>{children}</div>
|
||||
</Paper>
|
||||
<>
|
||||
<Paper className={classnames(classes.wrapper, className)}>
|
||||
<div className={classes.header}>
|
||||
{title && <TitleCase className={classes.title}>{title}</TitleCase>}
|
||||
<IconButton
|
||||
size={closeSize}
|
||||
className={classes.button}
|
||||
onClick={() => handleClose()}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={classes.content}>{children}</div>
|
||||
</Paper>
|
||||
{infoPanel && (
|
||||
<Paper className={classnames(classes.wrapper, className)}>
|
||||
{infoPanel}
|
||||
</Paper>
|
||||
)}
|
||||
</>
|
||||
</MaterialModal>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Tr
|
||||
size={rowSize}
|
||||
error={errors && errors.length}
|
||||
errorMessage={errors && errors.toString()}>
|
||||
{innerElements.map((it, idx) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -76,18 +76,8 @@ const ThDoubleLevel = ({ title, children, className }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const CellDoubleLevel = ({ children, className }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div className={classnames(className, classes.cellDoubleLevel)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 } })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 && <H4 className={classnames(subtitleClass)}>Edit {display}</H4>}
|
||||
|
||||
{!lastStep && (
|
||||
<TextInput
|
||||
label={'Choose bill denomination'}
|
||||
onChange={evt =>
|
||||
dispatch({ type: 'select', selected: evt.target.value })
|
||||
}
|
||||
autoFocus
|
||||
id="confirm-input"
|
||||
type="text"
|
||||
size="lg"
|
||||
touched={{}}
|
||||
error={false}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Formik
|
||||
onSubmit={onContinue}
|
||||
initialValues={{ [type]: '' }}
|
||||
enableReinitialize
|
||||
validationSchema={schema}>
|
||||
<Form>
|
||||
<Field
|
||||
name={type}
|
||||
component={TextInput}
|
||||
label={'Choose bill denomination'}
|
||||
autoFocus
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<div className={classes.submit}>
|
||||
<Button className={classes.button} type="submit">
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Formik>
|
||||
// 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.
|
||||
</P>
|
||||
<div className={classes.submit}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button className={classes.button} onClick={() => onContinue()}>
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={classes.submit}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button
|
||||
className={classes.button}
|
||||
onClick={() => iContinue({ [type]: selected })}>
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
3
new-lamassu-admin/src/pages/Cashout/index.js
Normal file
3
new-lamassu-admin/src/pages/Cashout/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Cashout from './Cashout'
|
||||
|
||||
export default Cashout
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<TableCell>
|
||||
<TextInput
|
||||
large
|
||||
required
|
||||
className={styles.numberSmallInput}
|
||||
value={values.cashInCommission || ''}
|
||||
onChange={handleChange('cashInCommission')}
|
||||
suffix="%"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInput
|
||||
large
|
||||
className={styles.numberSmallInput}
|
||||
value={values.cashOutCommission || ''}
|
||||
onChange={handleChange('cashOutCommission')}
|
||||
suffix="%"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInput
|
||||
large
|
||||
value={values.cashInFee || ''}
|
||||
onChange={handleChange('cashInFee')}
|
||||
suffix="EUR"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInput
|
||||
large
|
||||
value={values.minimumTx || ''}
|
||||
onChange={handleChange('minimumTx')}
|
||||
suffix="EUR"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
color="secondary"
|
||||
className={styles.firstLink}
|
||||
onClick={() => {
|
||||
setEditing(false)
|
||||
}}>
|
||||
Cancel
|
||||
</Link>
|
||||
<Link
|
||||
color="primary"
|
||||
submit
|
||||
onClick={() => {
|
||||
commitValues(values)
|
||||
setEditing(false)
|
||||
}}>
|
||||
Save
|
||||
</Link>
|
||||
</TableCell>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ViewRow = ({ data, setEditing }) => (
|
||||
<>
|
||||
<TableCell>
|
||||
<Info1 inline className={styles.noMargin}>
|
||||
{data.cashInCommission}
|
||||
</Info1>
|
||||
{data.cashInCommission && (
|
||||
<TL2 inline className={styles.suffix}>
|
||||
%
|
||||
</TL2>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Info1 inline className={styles.noMargin}>
|
||||
{data.cashOutCommission}
|
||||
</Info1>
|
||||
{data.cashOutCommission && (
|
||||
<TL2 inline className={styles.suffix}>
|
||||
%
|
||||
</TL2>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Info1 inline className={styles.noMargin}>
|
||||
{data.cashInFee}
|
||||
</Info1>
|
||||
{data.cashOutCommission && (
|
||||
<TL2 inline className={styles.suffix}>
|
||||
EUR
|
||||
</TL2>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Info1 inline className={styles.noMargin}>
|
||||
{data.minimumTx}
|
||||
</Info1>
|
||||
{data.cashOutCommission && (
|
||||
<TL2 inline className={styles.suffix}>
|
||||
EUR
|
||||
</TL2>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={styles.centerAlign}>
|
||||
<Link color="primary" onClick={() => setEditing(true)}>
|
||||
Edit
|
||||
</Link>
|
||||
</TableCell>
|
||||
</>
|
||||
)
|
||||
|
||||
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 = () => <td />
|
||||
|
||||
return (
|
||||
<>
|
||||
<H1>Commissions</H1>
|
||||
<H3>Default Setup</H3>
|
||||
<form className={styles.tableWrapper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow header>
|
||||
<TableHeader rowSpan="2">Cash-in</TableHeader>
|
||||
<TableHeader rowSpan="2">Cash-out</TableHeader>
|
||||
<TableHeader colSpan="2" className={styles.multiRowHeader}>
|
||||
Cash-in only
|
||||
</TableHeader>
|
||||
<TableHeader className={styles.centerAlign} rowSpan="2">
|
||||
Edit
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
<TableRow header>
|
||||
<TableHeader>Fixed Fee</TableHeader>
|
||||
<TableHeader>Minimum Tx</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<EditableRow
|
||||
commitValues={value => commitValues(value)}
|
||||
EditRow={EditRow}
|
||||
ViewRow={ViewRow}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Commissions
|
||||
|
|
@ -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;
|
||||
}
|
||||
96
new-lamassu-admin/src/pages/Commissions/Commissions.js
Normal file
96
new-lamassu-admin/src/pages/Commissions/Commissions.js
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<TitleSection title="Commissions" />
|
||||
<Section>
|
||||
<EditableTable
|
||||
title="Default setup"
|
||||
rowSize="lg"
|
||||
titleLg
|
||||
name="commissions"
|
||||
enableEdit
|
||||
initialValues={commission}
|
||||
save={save}
|
||||
validationSchema={schema}
|
||||
data={R.of(commission)}
|
||||
elements={mainFields(data)}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<EditableTable
|
||||
title="Overrides"
|
||||
titleLg
|
||||
name="overrides"
|
||||
enableDelete
|
||||
enableEdit
|
||||
enableCreate
|
||||
initialValues={overridesDefaults}
|
||||
save={saveOverrides}
|
||||
validationSchema={OverridesSchema}
|
||||
data={commission.overrides ?? []}
|
||||
elements={overrides(data)}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Commissions
|
||||
156
new-lamassu-admin/src/pages/Commissions/helper.js
Normal file
156
new-lamassu-admin/src/pages/Commissions/helper.js
Normal file
|
|
@ -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
|
||||
}
|
||||
3
new-lamassu-admin/src/pages/Commissions/index.js
Normal file
3
new-lamassu-admin/src/pages/Commissions/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Commissions from './Commissions'
|
||||
|
||||
export default Commissions
|
||||
|
|
@ -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) ?? []
|
||||
)
|
||||
|
||||
|
|
|
|||
117
new-lamassu-admin/src/pages/Maintenance/Cashboxes.js
Normal file
117
new-lamassu-admin/src/pages/Maintenance/Cashboxes.js
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<TitleSection title="Cashboxes" />
|
||||
|
||||
<EditableTable
|
||||
name="cashboxes"
|
||||
enableEdit
|
||||
elements={elements}
|
||||
data={data && data.machines}
|
||||
save={onSave}
|
||||
validationSchema={ValidationSchema}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cashboxes
|
||||
|
|
@ -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!) {
|
||||
|
|
@ -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 = () => {
|
|||
<Form>
|
||||
<div className={classnames(classes.row, classes.radioButtonsRow)}>
|
||||
<Field
|
||||
field={findField('infoCardEnabled')}
|
||||
field={findField('active')}
|
||||
editing={editing}
|
||||
displayValue={it => (it === 'true' ? 'On' : 'Off')}
|
||||
options={[
|
||||
|
|
@ -226,13 +226,13 @@ const ContactInfo = () => {
|
|||
</div>
|
||||
<div className={classes.row}>
|
||||
<Field
|
||||
field={findField('fullName')}
|
||||
field={findField('name')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
<Field
|
||||
field={findField('phoneNumber')}
|
||||
field={findField('phone')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
onFocus={() => setError(null)}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<div className={classes.rowWrapper}>
|
||||
<H2 className={classes.wizardHeaderText}>New Compliance Trigger</H2>
|
||||
<div className={classes.transparentButton}>
|
||||
<button onClick={handleOpenHelpPopper}>
|
||||
<HelpIcon />
|
||||
<Popper
|
||||
open={helpPopperOpen}
|
||||
anchorEl={helpPopperAnchorEl}
|
||||
placement="bottom"
|
||||
onClose={handleCloseHelpPopper}>
|
||||
<div className={classes.popoverContent}>
|
||||
<P>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi
|
||||
tempor velit a dolor ultricies posuere. Proin massa sapien,
|
||||
euismod quis auctor vel, blandit in enim.
|
||||
</P>
|
||||
</div>
|
||||
</Popper>
|
||||
</button>
|
||||
</div>
|
||||
<div className={classes.transparentButton}>
|
||||
<button onClick={close}>
|
||||
<CloseIcon className={classes.closeButton} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
header={wizardHeader}
|
||||
nextStepText={'Next'}
|
||||
finalStepText={'Add Trigger'}
|
||||
finish={finish}>
|
||||
<SelectTriggerDirection />
|
||||
<SelectTriggerType fiatCurrencyCode={fiatCurrencyCode} />
|
||||
<SelectTriggerRequirements />
|
||||
</Wizard>
|
||||
)
|
||||
}
|
||||
|
||||
export { NewTriggerWizard }
|
||||
|
|
@ -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 (
|
||||
<div className={classes.columnWrapper}>
|
||||
<div className={classes.rowWrapper}>
|
||||
<H4>In which type of transactions will it trigger?</H4>
|
||||
<div className={classes.transparentButton}>
|
||||
<button onClick={handleOpenHelpPopper}>
|
||||
<HelpIcon />
|
||||
<Popper
|
||||
open={helpPopperOpen}
|
||||
anchorEl={helpPopperAnchorEl}
|
||||
placement="bottom"
|
||||
onClose={handleCloseHelpPopper}>
|
||||
<div className={classes.popoverContent}>
|
||||
<P>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi
|
||||
tempor velit a dolor ultricies posuere. Proin massa sapien,
|
||||
euismod quis auctor vel, blandit in enim.
|
||||
</P>
|
||||
</div>
|
||||
</Popper>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.radioGroupWrapper}>
|
||||
<RadioGroup
|
||||
options={radioButtonOptions}
|
||||
value={radioGroupValue}
|
||||
onChange={event => handleRadioButtons(event.target.value)}
|
||||
className={classnames(
|
||||
classes.radioButtons,
|
||||
classes.stepOneRadioButtons
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default SelectTriggerDirection
|
||||
|
|
@ -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 (
|
||||
<div className={classes.columnWrapper}>
|
||||
<div className={classes.rowWrapper}>
|
||||
<H4>Choose a requirement</H4>
|
||||
<div className={classes.transparentButton}>
|
||||
<button onClick={handleOpenRequirementHelpPopper}>
|
||||
<HelpIcon />
|
||||
<Popper
|
||||
open={requirementHelpPopperOpen}
|
||||
anchorEl={requirementHelpPopperAnchorEl}
|
||||
placement="bottom"
|
||||
onClose={handleCloseRequirementHelpPopper}>
|
||||
<div className={classes.popoverContent}>
|
||||
<P>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi
|
||||
tempor velit a dolor ultricies posuere. Proin massa sapien,
|
||||
euismod quis auctor vel, blandit in enim.
|
||||
</P>
|
||||
</div>
|
||||
</Popper>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<RadioGroup
|
||||
options={requirementRadioButtonOptions}
|
||||
value={requirementRadioGroupValue}
|
||||
onChange={event => handleRequirementRadioButtons(event.target.value)}
|
||||
className={classnames(
|
||||
classes.radioButtons,
|
||||
classes.stepThreeRadioButtons
|
||||
)}
|
||||
/>
|
||||
<div className={classes.rowWrapper}>
|
||||
<H4>Choose trigger type</H4>
|
||||
<div className={classes.transparentButton}>
|
||||
<button onClick={handleOpenTypeHelpPopper}>
|
||||
<HelpIcon />
|
||||
<Popper
|
||||
open={typeHelpPopperOpen}
|
||||
anchorEl={typeHelpPopperAnchorEl}
|
||||
placement="bottom"
|
||||
onClose={handleCloseTypeHelpPopper}>
|
||||
<div className={classes.popoverContent}>
|
||||
<P>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi
|
||||
tempor velit a dolor ultricies posuere. Proin massa sapien,
|
||||
euismod quis auctor vel, blandit in enim.
|
||||
</P>
|
||||
</div>
|
||||
</Popper>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<RadioGroup
|
||||
options={typeRadioButtonOptions}
|
||||
value={typeRadioGroupValue}
|
||||
onChange={event => handleTypeRadioButtons(event.target.value)}
|
||||
className={classnames(
|
||||
classes.radioButtons,
|
||||
classes.stepThreeRadioButtons
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectTriggerRequirements
|
||||
|
|
@ -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 (
|
||||
<div className={classes.columnWrapper}>
|
||||
<div className={classes.rowWrapper}>
|
||||
<H4>Choose trigger type</H4>
|
||||
<div className={classes.transparentButton}>
|
||||
<button onClick={handleOpenHelpPopper}>
|
||||
<HelpIcon />
|
||||
<Popper
|
||||
open={helpPopperOpen}
|
||||
anchorEl={helpPopperAnchorEl}
|
||||
placement="bottom"
|
||||
onClose={handleCloseHelpPopper}>
|
||||
<div className={classes.popoverContent}>
|
||||
<P>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi
|
||||
tempor velit a dolor ultricies posuere. Proin massa sapien,
|
||||
euismod quis auctor vel, blandit in enim.
|
||||
</P>
|
||||
</div>
|
||||
</Popper>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.radioGroupWrapper}>
|
||||
<RadioGroup
|
||||
options={radioButtonOptions}
|
||||
value={radioGroupValue}
|
||||
onChange={event => handleRadioButtons(event.target.value)}
|
||||
className={classnames(
|
||||
classes.radioButtons,
|
||||
classes.stepTwoRadioButtons
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<H4>Threshold</H4>
|
||||
<div className={classes.rowWrapper}>
|
||||
<TextInput
|
||||
className={classes.textInput}
|
||||
onChange={event =>
|
||||
validateThresholdInputIsPositiveInteger(event.target.value)
|
||||
}
|
||||
error={thresholdError}
|
||||
size="lg"
|
||||
value={thresholdValue}
|
||||
/>
|
||||
<TL1>{fiatCurrencyCode}</TL1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectTriggerType
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
|
|
@ -50,46 +69,22 @@ const Triggers = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className={classes.headerLabels}>
|
||||
<Link color="primary" onClick={handleOpenWizard}>
|
||||
<Link color="primary" onClick={() => setWizard(true)}>
|
||||
+ Add new trigger
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<EditableTable
|
||||
data={[]}
|
||||
elements={[
|
||||
{
|
||||
name: 'triggerType',
|
||||
size: sizes.triggerType
|
||||
},
|
||||
{
|
||||
name: 'requirement',
|
||||
size: sizes.requirement
|
||||
},
|
||||
{
|
||||
name: 'threshold',
|
||||
size: sizes.threshold
|
||||
},
|
||||
{
|
||||
name: 'cashDirection',
|
||||
size: sizes.cashDirection
|
||||
}
|
||||
]}
|
||||
data={triggers}
|
||||
name="triggers"
|
||||
enableEdit
|
||||
enableDelete
|
||||
save={save}
|
||||
validationSchema={Schema}
|
||||
elements={elements}
|
||||
/>
|
||||
{wizardModalOpen && (
|
||||
<Modal
|
||||
aria-labelledby="simple-modal-title"
|
||||
aria-describedby="simple-modal-description"
|
||||
open={wizardModalOpen}
|
||||
onClose={handleCloseWizard}
|
||||
className={classes.modal}>
|
||||
<Paper className={classes.paper}>
|
||||
<NewTriggerWizard
|
||||
close={handleCloseWizard}
|
||||
finish={handleFinishWizard}
|
||||
/>
|
||||
</Paper>
|
||||
</Modal>
|
||||
{wizard && (
|
||||
<Wizard error={error} save={add} onClose={() => setWizard(null)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
103
new-lamassu-admin/src/pages/Triggers/Wizard.js
Normal file
103
new-lamassu-admin/src/pages/Triggers/Wizard.js
Normal file
|
|
@ -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 (
|
||||
<Modal
|
||||
title="New compliance trigger"
|
||||
handleClose={onClose}
|
||||
width={520}
|
||||
height={480}
|
||||
open={true}>
|
||||
<Stepper
|
||||
className={classes.stepper}
|
||||
steps={LAST_STEP}
|
||||
currentStep={step}
|
||||
/>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
onSubmit={onContinue}
|
||||
initialValues={stepOptions.initialValues}
|
||||
validationSchema={stepOptions.schema}>
|
||||
<Form className={classes.form}>
|
||||
<stepOptions.Component />
|
||||
<div className={classes.submit}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button className={classes.button} type="submit">
|
||||
{isLastStep ? 'Finish' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default Wizard
|
||||
250
new-lamassu-admin/src/pages/Triggers/helper.js
Normal file
250
new-lamassu-admin/src/pages/Triggers/helper.js
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<Box display="flex" alignItems="center">
|
||||
<H4 className={classnames(titleClass)}>
|
||||
In which type of transactions will it trigger?
|
||||
</H4>
|
||||
</Box>
|
||||
<Field
|
||||
component={RadioGroup}
|
||||
name="cashDirection"
|
||||
options={directionOptions}
|
||||
labelClassName={classes.radioLabel}
|
||||
radioClassName={classes.radio}
|
||||
className={classes.radioGroup}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Box display="flex" alignItems="center">
|
||||
<H4 className={classnames(typeClass)}>Choose trigger type</H4>
|
||||
</Box>
|
||||
<Field
|
||||
component={RadioGroup}
|
||||
name="triggerType"
|
||||
options={typeOptions}
|
||||
labelClassName={classes.radioLabel}
|
||||
radioClassName={classes.radio}
|
||||
className={classes.radioGroup}
|
||||
/>
|
||||
|
||||
<Field
|
||||
component={TextInput}
|
||||
label="Threshold"
|
||||
size="lg"
|
||||
name="threshold"
|
||||
options={typeOptions}
|
||||
labelClassName={classes.radioLabel}
|
||||
radioClassName={classes.radio}
|
||||
className={classes.radioGroup}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Box display="flex" alignItems="center">
|
||||
<H4 className={classnames(titleClass)}>Choose a requirement</H4>
|
||||
</Box>
|
||||
<Field
|
||||
component={RadioGroup}
|
||||
name="requirement"
|
||||
options={requirementOptions}
|
||||
labelClassName={classes.specialLabel}
|
||||
radioClassName={classes.radio}
|
||||
className={classnames(classes.radioGroup, classes.specialGrid)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 }
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
@ -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 => <CashIn {...props} />,
|
||||
input: props => <CashInFormik onEmpty={onEmpty} {...props} />
|
||||
},
|
||||
{
|
||||
name: 'cashout1',
|
||||
header: 'Cash-out 1',
|
||||
width: 265,
|
||||
textAlign: 'left',
|
||||
view: props => <CashOut {...props} />,
|
||||
input: CashOutFormik
|
||||
},
|
||||
{
|
||||
name: 'cashout2',
|
||||
header: 'Cash-out 2',
|
||||
width: 265,
|
||||
textAlign: 'left',
|
||||
view: props => <CashOut {...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 (
|
||||
<>
|
||||
<TitleSection
|
||||
title="Cashboxes"
|
||||
labels={
|
||||
<>
|
||||
<ErrorIcon />
|
||||
<span>Action required</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<EditableTable
|
||||
name="cashboxes"
|
||||
enableEdit
|
||||
elements={elements}
|
||||
data={data}
|
||||
save={onSave}
|
||||
validationSchema={ValidationSchema}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cashboxes
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
3
package-lock.json
generated
3
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue