Merge pull request #400 from lamassu/fix/flat-schema
WIP: New admin integration
This commit is contained in:
commit
1bcc87757b
113 changed files with 4378 additions and 4934 deletions
46
TODO.json
Normal file
46
TODO.json
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"groups": [
|
||||
{
|
||||
"code": "balanceAlerts",
|
||||
"fields": [
|
||||
"cashInAlertThreshold"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "compliance",
|
||||
"fields": [
|
||||
"smsVerificationActive",
|
||||
"smsVerificationThreshold",
|
||||
"idCardDataVerificationActive",
|
||||
"idCardDataVerificationThreshold",
|
||||
"idCardPhotoVerificationActive",
|
||||
"idCardPhotoVerificationThreshold",
|
||||
"sanctionsVerificationActive",
|
||||
"sanctionsVerificationThreshold",
|
||||
"frontCameraVerificationActive",
|
||||
"frontCameraVerificationThreshold",
|
||||
"hardLimitVerificationActive",
|
||||
"hardLimitVerificationThreshold",
|
||||
"receiptPrintingActive",
|
||||
"rejectAddressReuseActive"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "walletSettings",
|
||||
"fields": [
|
||||
"ticker",
|
||||
"wallet",
|
||||
"layer2",
|
||||
"exchange",
|
||||
"zeroConf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "notifications",
|
||||
"fields": [
|
||||
"sms",
|
||||
"email"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ determineConfigCategory : String -> Maybe Category
|
|||
determineConfigCategory configCode =
|
||||
if List.member configCode [ "definition", "setup", "cashOut", "commissions", "balanceAlerts" ] then
|
||||
Just MachineSettingsCat
|
||||
else if List.member configCode [ "walletSettings", "notifications", "compliance", "coinAtmRadar", "terms", "operatorInfo" ] then
|
||||
else if List.member configCode [ "walletSettings", "notifications", "compliance", "coinAtmRadar", "terms", "operatorInfo", "fudgeFactor" ] then
|
||||
Just GlobalSettingsCat
|
||||
else
|
||||
Nothing
|
||||
|
|
@ -253,6 +253,7 @@ view route invalidGroups =
|
|||
, configLink "coinAtmRadar" "Coin ATM Radar"
|
||||
, configLink "terms" "Terms and Conditions"
|
||||
, configLink "operatorInfo" "Operator Info"
|
||||
, configLink "fudgeFactor" "Fudge Factor"
|
||||
]
|
||||
, ll ( "Third Party Services", AccountCat, AccountRoute "bitgo", True )
|
||||
[ ( "BitGo", AccountRoute "bitgo", True )
|
||||
|
|
|
|||
|
|
@ -147,6 +147,15 @@
|
|||
"operatorInfoWebsite",
|
||||
"operatorInfoCompanyNumber"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "fudgeFactor",
|
||||
"display": "Fudge Factor",
|
||||
"cryptoScope": "global",
|
||||
"machineScope": "global",
|
||||
"fields": [
|
||||
"fudgeFactorActive"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -846,7 +855,8 @@
|
|||
"notificationsSMSEnabled",
|
||||
"cashOutEnabled"
|
||||
],
|
||||
"fieldValidation": [{"code": "required"}]
|
||||
"fieldValidation": [{"code": "required"}],
|
||||
"default": "twilio"
|
||||
},
|
||||
{
|
||||
"code": "email",
|
||||
|
|
@ -857,7 +867,8 @@
|
|||
"enabledIfAny": [
|
||||
"notificationsEmailEnabled"
|
||||
],
|
||||
"fieldValidation": [{"code": "required"}]
|
||||
"fieldValidation": [{"code": "required"}],
|
||||
"default": "mailgun"
|
||||
},
|
||||
{
|
||||
"code": "coinAtmRadarActive",
|
||||
|
|
@ -939,6 +950,14 @@
|
|||
],
|
||||
"fieldValidation": []
|
||||
},
|
||||
{
|
||||
"code": "fudgeFactorActive",
|
||||
"displayBottom": "Enabled",
|
||||
"fieldType": "onOff",
|
||||
"fieldClass": null,
|
||||
"fieldValidation": [],
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"code": "operatorInfoActive",
|
||||
"displayBottom": "Info card enabled",
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ function getFunding (_cryptoCode) {
|
|||
const rate = (rates.ask.add(rates.bid)).div(2)
|
||||
const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance
|
||||
const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance)
|
||||
const pending = fundingRec.fundingPendingBalance.sub(fundingConfirmedBalance)
|
||||
const pending = fundingRec.fundingPendingBalance
|
||||
const fiatPending = computeFiat(rate, cryptoCode, pending)
|
||||
const fundingAddress = fundingRec.fundingAddress
|
||||
const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress)
|
||||
|
|
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -21,28 +21,28 @@ module.exports = {
|
|||
|
||||
const BINARIES = {
|
||||
BTC: {
|
||||
url: 'https://bitcoin.org/bin/bitcoin-core-0.18.1/bitcoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-0.18.1/bin'
|
||||
url: 'https://bitcoincore.org/bin/bitcoin-core-0.19.1/bitcoin-0.19.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-0.19.1/bin'
|
||||
},
|
||||
ETH: {
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.7-a718daa6.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.9.7-a718daa6'
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.14-6d74d1e5.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.9.14-6d74d1e5'
|
||||
},
|
||||
ZEC: {
|
||||
url: 'https://z.cash/downloads/zcash-2.1.0-1-linux64-debian-jessie.tar.gz',
|
||||
dir: 'zcash-2.1.0-1/bin'
|
||||
url: 'https://z.cash/downloads/zcash-2.1.2-3-linux64-debian-jessie.tar.gz',
|
||||
dir: 'zcash-2.1.2-3/bin'
|
||||
},
|
||||
DASH: {
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.14.0.3/dashcore-0.14.0.3-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-0.14.0/bin'
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.15.0.0/dashcore-0.15.0.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-0.15.0/bin'
|
||||
},
|
||||
LTC: {
|
||||
url: 'https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'litecoin-0.17.1/bin'
|
||||
},
|
||||
BCH: {
|
||||
url: 'https://download.bitcoinabc.org/0.20.5/linux/bitcoin-abc-0.20.5-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-abc-0.20.5/bin',
|
||||
url: 'https://download.bitcoinabc.org/0.21.7/linux/bitcoin-abc-0.21.7-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-abc-0.21.7/bin',
|
||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,7 @@ dbcache=500
|
|||
keypool=10000
|
||||
litemode=1
|
||||
prune=4000
|
||||
txindex=0`
|
||||
txindex=0
|
||||
enableprivatesend=1
|
||||
privatesendautostart=1`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,11 +65,7 @@ function preProcess (t, oldTx, newTx, pi) {
|
|||
.then(updatedTx => {
|
||||
if (updatedTx.status !== oldTx.status) {
|
||||
const isZeroConf = pi.isZeroConf(updatedTx)
|
||||
if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) {
|
||||
pi.sell(updatedTx)
|
||||
pi.notifyOperator(updatedTx, { isRedemption: false })
|
||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
||||
}
|
||||
updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf)
|
||||
|
||||
const rec = {
|
||||
to_address: updatedTx.toAddress,
|
||||
|
|
|
|||
|
|
@ -11,14 +11,18 @@ module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE }
|
|||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||
|
||||
function convertBigNumFields (obj) {
|
||||
const convert = (value, key) => _.includes(key, [
|
||||
'cryptoAtoms',
|
||||
'fiat',
|
||||
'commissionPercentage',
|
||||
'rawTickerPrice'
|
||||
])
|
||||
? value.toString()
|
||||
: value
|
||||
const convert = (value, key) => {
|
||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat' ])) {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
// Only test isNil for these fields since the others should not be empty.
|
||||
if (_.includes(key, [ 'commissionPercentage', 'rawTickerPrice' ]) && !_.isNil(value)) {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
|
||||
? key + '#'
|
||||
|
|
@ -58,6 +62,10 @@ function toObj (row) {
|
|||
|
||||
keys.forEach(key => {
|
||||
const objKey = _.camelCase(key)
|
||||
if (key === 'received_crypto_atoms' && row[key]) {
|
||||
newObj[objKey] = BN(row[key])
|
||||
return
|
||||
}
|
||||
if (_.includes(key, ['crypto_atoms', 'fiat', 'commission_percentage', 'raw_ticker_price'])) {
|
||||
newObj[objKey] = BN(row[key])
|
||||
return
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ const toDb = helper.toDb
|
|||
const toObj = helper.toObj
|
||||
|
||||
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
|
||||
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode']
|
||||
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode',
|
||||
'receivedCryptoAtoms' ]
|
||||
|
||||
module.exports = {upsert, update, insert}
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ function upsert (t, oldTx, tx) {
|
|||
}
|
||||
|
||||
return update(t, tx, diff(oldTx, tx))
|
||||
.then(newTx => [oldTx, newTx])
|
||||
.then(newTx => [oldTx, newTx, tx.justAuthorized])
|
||||
}
|
||||
|
||||
function insert (t, tx) {
|
||||
|
|
|
|||
|
|
@ -45,15 +45,21 @@ function selfPost (tx, pi) {
|
|||
function post (tx, pi, fromClient = true) {
|
||||
return db.tx(cashOutAtomic.atomic(tx, pi, fromClient))
|
||||
.then(txVector => {
|
||||
const [, newTx] = txVector
|
||||
return postProcess(txVector, pi)
|
||||
const [, newTx, justAuthorized] = txVector
|
||||
return postProcess(txVector, justAuthorized, pi)
|
||||
.then(changes => cashOutLow.update(db, newTx, changes))
|
||||
})
|
||||
}
|
||||
|
||||
function postProcess (txVector, pi) {
|
||||
function postProcess (txVector, justAuthorized, pi) {
|
||||
const [oldTx, newTx] = txVector
|
||||
|
||||
if (justAuthorized) {
|
||||
pi.sell(newTx)
|
||||
pi.notifyOperator(newTx, { isRedemption: false })
|
||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
||||
}
|
||||
|
||||
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
||||
return pi.buildAvailableCassettes(newTx.id)
|
||||
.then(cassettes => {
|
||||
|
|
@ -106,7 +112,7 @@ function processTxStatus (tx, settings) {
|
|||
const pi = plugins(settings, tx.deviceId)
|
||||
|
||||
return pi.getStatus(tx)
|
||||
.then(res => _.assign(tx, {status: res.status}))
|
||||
.then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status }))
|
||||
.then(_tx => selfPost(_tx, pi))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,56 +19,77 @@ 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,
|
||||
cashInFee,
|
||||
cashOutFee,
|
||||
cashInFixedFee,
|
||||
cashInRate,
|
||||
cashOutRate
|
||||
}
|
||||
}
|
||||
|
||||
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 +107,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 +151,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 +168,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
|
||||
|
|
@ -196,7 +196,7 @@ const typeDefs = gql`
|
|||
uptime: [ProcessStatus]
|
||||
serverLogs: [ServerLog]
|
||||
transactions: [Transaction]
|
||||
accounts: [JSONObject]
|
||||
accounts: JSONObject
|
||||
config: JSONObject
|
||||
}
|
||||
|
||||
|
|
@ -215,14 +215,13 @@ 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
|
||||
saveConfig(config: JSONObject): JSONObject
|
||||
createPairingTotem(name: String!): String
|
||||
saveAccount(account: JSONObject): [JSONObject]
|
||||
saveAccounts(accounts: [JSONObject]): [JSONObject]
|
||||
saveAccounts(accounts: JSONObject): JSONObject
|
||||
}
|
||||
`
|
||||
|
||||
|
|
@ -255,11 +254,10 @@ 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(),
|
||||
saveAccount: (...[, { account }]) => settingsLoader.saveAccounts([account]),
|
||||
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
||||
setCustomer: (...[, { customerId, customerInput } ]) => customers.updateCustomer(customerId, customerInput),
|
||||
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ function batch () {
|
|||
}
|
||||
|
||||
function getCustomerTransactions (customerId) {
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']),
|
||||
const packager = _.flow(it => {
|
||||
console.log()
|
||||
return it
|
||||
}, _.flatten, _.orderBy(_.property('created'), ['desc']),
|
||||
_.take(NUM_RESULTS), _.map(camelize), addNames)
|
||||
|
||||
const cashInSql = `select 'cashIn' as tx_class, txs.*,
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
|
@ -9,20 +9,11 @@ low(adapter).then(it => {
|
|||
db = it
|
||||
})
|
||||
|
||||
function replace (array, index, value) {
|
||||
return array.slice(0, index).concat([value]).concat(array.slice(index + 1))
|
||||
}
|
||||
|
||||
function replaceOrAdd (accounts, account) {
|
||||
const index = _.findIndex(['code', account.code], accounts)
|
||||
return index !== -1 ? replace(accounts, index, account) : _.concat(accounts)(account)
|
||||
}
|
||||
|
||||
function saveAccounts (accountsToSave) {
|
||||
const currentState = db.getState() || {}
|
||||
const accounts = currentState.accounts || []
|
||||
const accounts = currentState.accounts || {}
|
||||
|
||||
const newAccounts = _.reduce(replaceOrAdd)(accounts)(accountsToSave)
|
||||
const newAccounts = _.assign(accounts)(accountsToSave)
|
||||
|
||||
const newState = _.set('accounts', newAccounts, currentState)
|
||||
db.setState(newState)
|
||||
|
|
@ -51,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 => {
|
||||
|
|
|
|||
165
lib/plugins.js
165
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,12 @@ 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)
|
||||
logger.info('FEE', cashInFee)
|
||||
const cashInCommission = BN(commissions.cashIn)
|
||||
const cashOutCommission = _.isNumber(commissions.cashOut) ? BN(commissions.cashOut) : null
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
|
||||
return {
|
||||
|
|
@ -207,9 +202,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 +279,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,14 +295,14 @@ 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
|
||||
|
||||
const rate = rawRate.div(cashInCommission)
|
||||
|
||||
const lowBalanceMargin = BN(1)
|
||||
const lowBalanceMargin = BN(1.03)
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
|
@ -344,7 +338,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 +503,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 +590,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 +618,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 +636,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 +648,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 +667,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 +684,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 +768,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 +805,7 @@ function plugins (settings, deviceId) {
|
|||
buildAvailableCassettes,
|
||||
buy,
|
||||
sell,
|
||||
notificationsEnabled,
|
||||
getNotificationConfig,
|
||||
notifyOperator,
|
||||
fetchCurrentConfigVersion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,16 +25,22 @@ function checkCryptoCode (cryptoCode) {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (account, cryptoCode, confirmations) {
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getbalance', ['', confirmations]))
|
||||
.then(r => BN(r).shift(unitScale).round())
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode) {
|
||||
return accountBalance(account, cryptoCode, 1)
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
|
||||
|
|
@ -80,13 +86,13 @@ function getStatus (account, toAddress, requested, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return {status: 'confirmed'}
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return {status: 'authorized'}
|
||||
if (pending.gt(0)) return {status: 'insufficientFunds'}
|
||||
return {status: 'notSeen'}
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -95,9 +101,9 @@ function newFunding (account, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountBalance(account, cryptoCode, 0),
|
||||
accountBalance(account, cryptoCode, 1),
|
||||
newAddress(account, {cryptoCode})
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -25,16 +25,22 @@ function checkCryptoCode (cryptoCode) {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (account, cryptoCode, confirmations) {
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getbalance', ['*', confirmations]))
|
||||
.then(r => BN(r).shift(unitScale).round())
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode) {
|
||||
return accountBalance(account, cryptoCode, 1)
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
|
||||
|
|
@ -80,13 +86,13 @@ function getStatus (account, toAddress, requested, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return {status: 'confirmed'}
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return {status: 'authorized'}
|
||||
if (pending.gt(0)) return {status: 'insufficientFunds'}
|
||||
return {status: 'notSeen'}
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -95,9 +101,9 @@ function newFunding (account, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountBalance(account, cryptoCode, 0),
|
||||
accountBalance(account, cryptoCode, 1),
|
||||
newAddress(account, {cryptoCode})
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -124,10 +124,10 @@ function getStatus (account, toAddress, requested, cryptoCode) {
|
|||
const confirmed = _.compose(sum, toBn, filterConfirmed)(transfers)
|
||||
const pending = _.compose(sum, toBn, filterPending)(transfers)
|
||||
|
||||
if (confirmed.gte(requested)) return { status: 'confirmed' }
|
||||
if (pending.gte(requested)) return { status: 'authorized' }
|
||||
if (pending.gt(0)) return { status: 'insufficientFunds' }
|
||||
return { status: 'notSeen' }
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,16 +26,22 @@ function checkCryptoCode (cryptoCode) {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (acount, cryptoCode, confirmations) {
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getbalance', ['', confirmations]))
|
||||
.then(r => BN(r).shift(unitScale).round())
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode) {
|
||||
return accountBalance(account, cryptoCode, 1)
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
|
||||
|
|
@ -81,13 +87,13 @@ function getStatus (account, toAddress, requested, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return {status: 'confirmed'}
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return {status: 'authorized'}
|
||||
if (pending.gt(0)) return {status: 'insufficientFunds'}
|
||||
return {status: 'notSeen'}
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -96,9 +102,9 @@ function newFunding (account, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountBalance(account, cryptoCode, 0),
|
||||
accountBalance(account, cryptoCode, 1),
|
||||
newAddress(account, {cryptoCode})
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -158,13 +158,13 @@ function getStatus (account, toAddress, cryptoAtoms, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(cryptoAtoms)) return {status: 'confirmed'}
|
||||
if (confirmed.gte(cryptoAtoms)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress)
|
||||
.then(pending => {
|
||||
if (pending.gte(cryptoAtoms)) return {status: 'published'}
|
||||
if (pending.gt(0)) return {status: 'insufficientFunds'}
|
||||
return {status: 'notSeen'}
|
||||
if (pending.gte(cryptoAtoms)) return { receivedCryptoAtoms: pending, status: 'published' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,16 +26,22 @@ function checkCryptoCode (cryptoCode) {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (acount, cryptoCode, confirmations) {
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getbalance', ['*', confirmations]))
|
||||
.then(r => BN(r).shift(unitScale).round())
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode) {
|
||||
return accountBalance(account, cryptoCode, 1)
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
|
||||
|
|
@ -81,13 +87,13 @@ function getStatus (account, toAddress, requested, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return {status: 'confirmed'}
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return {status: 'authorized'}
|
||||
if (pending.gt(0)) return {status: 'insufficientFunds'}
|
||||
return {status: 'notSeen'}
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -96,9 +102,9 @@ function newFunding (account, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountBalance(account, cryptoCode, 0),
|
||||
accountBalance(account, cryptoCode, 1),
|
||||
newAddress(account, {cryptoCode})
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -79,9 +79,9 @@ function newFunding (account, cryptoCode) {
|
|||
function getStatus (account, toAddress, cryptoAtoms, cryptoCode) {
|
||||
const elapsed = Date.now() - t0
|
||||
|
||||
if (elapsed < PUBLISH_TIME) return Promise.resolve({status: 'notSeen'})
|
||||
if (elapsed < AUTHORIZE_TIME) return Promise.resolve({status: 'published'})
|
||||
if (elapsed < CONFIRM_TIME) return Promise.resolve({status: 'authorized'})
|
||||
if (elapsed < PUBLISH_TIME) return Promise.resolve({ receivedCryptoAtoms: BN(0), status: 'notSeen' })
|
||||
if (elapsed < AUTHORIZE_TIME) return Promise.resolve({ receivedCryptoAtoms: cryptoAtoms, status: 'published' })
|
||||
if (elapsed < CONFIRM_TIME) return Promise.resolve({ receivedCryptoAtoms: cryptoAtoms, status: 'authorized' })
|
||||
|
||||
console.log('[%s] DEBUG: Mock wallet has confirmed transaction [%s]', cryptoCode, toAddress.slice(0, 5))
|
||||
|
||||
|
|
|
|||
|
|
@ -26,16 +26,22 @@ function checkCryptoCode (cryptoCode) {
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (acount, cryptoCode, confirmations) {
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getbalance', ['', confirmations]))
|
||||
.then(r => BN(r).shift(unitScale).round())
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => BN(balance).shift(unitScale).round())
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode) {
|
||||
return accountBalance(account, cryptoCode, 1)
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
|
||||
|
|
@ -81,13 +87,13 @@ function getStatus (account, toAddress, requested, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return {status: 'confirmed'}
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return {status: 'authorized'}
|
||||
if (pending.gt(0)) return {status: 'insufficientFunds'}
|
||||
return {status: 'notSeen'}
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -96,9 +102,9 @@ function newFunding (account, cryptoCode) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountBalance(account, cryptoCode, 0),
|
||||
accountBalance(account, cryptoCode, 1),
|
||||
newAddress(account, {cryptoCode})
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
118
lib/routes.js
118
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,49 +91,35 @@ 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')) {
|
||||
// BACKWARDS_COMPATIBILITY 7.4.9
|
||||
// machines before 7.4.9 expect t&c on poll
|
||||
if (!machineVersion || semver.lt(machineVersion, '7.4.9')) {
|
||||
response.terms = config.termsScreenActive && config.termsScreenText ? createTerms(config) : null
|
||||
}
|
||||
|
||||
if (response.idVerificationEnabled) {
|
||||
response.idVerificationLimit = config.idVerificationLimit
|
||||
}
|
||||
|
||||
return res.json(_.assign(response, results))
|
||||
})
|
||||
.catch(next)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ function massage (tx, pi) {
|
|||
cashInFee: BN(r.cashInFee),
|
||||
cashInFeeCrypto: BN(r.cashInFeeCrypto),
|
||||
commissionPercentage: BN(r.commissionPercentage),
|
||||
rawTickerPrice: BN(r.rawTickerPrice),
|
||||
rawTickerPrice: r.rawTickerPrice ? BN(r.rawTickerPrice) : null,
|
||||
minimumTx: BN(r.minimumTx)
|
||||
}
|
||||
: {
|
||||
cryptoAtoms: BN(r.cryptoAtoms),
|
||||
fiat: BN(r.fiat),
|
||||
rawTickerPrice: BN(r.rawTickerPrice),
|
||||
rawTickerPrice: r.rawTickerPrice ? BN(r.rawTickerPrice) : null,
|
||||
commissionPercentage: BN(r.commissionPercentage)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -104,7 +104,7 @@ function mergeStatus (a, b) {
|
|||
if (!a) return b
|
||||
if (!b) return a
|
||||
|
||||
return { status: mergeStatusMode(a.status, b.status) }
|
||||
return { receivedCryptoAtoms: a.receivedCryptoAtoms, status: mergeStatusMode(a.status, b.status) }
|
||||
}
|
||||
|
||||
function mergeStatusMode (a, b) {
|
||||
|
|
@ -122,8 +122,11 @@ function mergeStatusMode (a, b) {
|
|||
}
|
||||
|
||||
function getWalletStatus (settings, tx) {
|
||||
const fudgeFactorEnabled = configManager.unscoped(settings.config).fudgeFactorActive
|
||||
const fudgeFactor = fudgeFactorEnabled ? 100 : 0
|
||||
|
||||
const walletStatusPromise = fetchWallet(settings, tx.cryptoCode)
|
||||
.then(r => r.wallet.getStatus(r.account, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode))
|
||||
.then(r => r.wallet.getStatus(r.account, tx.toAddress, tx.cryptoAtoms.minus(fudgeFactor), tx.cryptoCode))
|
||||
|
||||
return Promise.all([
|
||||
walletStatusPromise,
|
||||
|
|
@ -135,10 +138,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!'))
|
||||
|
|
@ -170,7 +172,7 @@ function getStatus (settings, tx, machineId) {
|
|||
|
||||
const status = isAuthorized ? 'authorized' : unauthorizedStatus
|
||||
|
||||
return { status }
|
||||
return { receivedCryptoAtoms: statusRec.receivedCryptoAtoms, status }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +191,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]
|
||||
|
||||
|
|
|
|||
13
migrations/1581455088977-add-amount-received.js
Normal file
13
migrations/1581455088977-add-amount-received.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN received_crypto_atoms numeric(30) null DEFAULT null'
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -51,7 +52,8 @@ const ETable = ({
|
|||
const [editingId, setEditingId] = useState(null)
|
||||
const [adding, setAdding] = useState(false)
|
||||
|
||||
const innerSave = async it => {
|
||||
const innerSave = async value => {
|
||||
const it = validationSchema.cast(value)
|
||||
const index = R.findIndex(R.propEq('id', it.id))(data)
|
||||
const list = index !== -1 ? R.update(index, it, data) : R.prepend(it, data)
|
||||
|
||||
|
|
@ -107,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 = ({ onClick, error, errorMessage, children, className, size }) => {
|
||||
const classes = useStyles({ size })
|
||||
const cardClasses = { root: classes.cardContentRoot }
|
||||
const classNames = {
|
||||
[classes.tr]: true,
|
||||
|
|
@ -98,7 +88,7 @@ const Tr = ({ error, errorMessage, children, className }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Card className={classnames(classNames, className)}>
|
||||
<Card className={classnames(classNames, className)} onClick={onClick}>
|
||||
<CardContent classes={cardClasses}>
|
||||
<div className={classes.mainContent}>{children}</div>
|
||||
{error && <div className={classes.errorContent}>{errorMessage}</div>}
|
||||
|
|
@ -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': {
|
||||
|
|
@ -89,7 +91,7 @@ export default {
|
|||
'&:before': {
|
||||
height: 0
|
||||
},
|
||||
margin: [[4, 0]],
|
||||
margin: [[4, 0, 0, 0]],
|
||||
width: '100%',
|
||||
boxShadow: [[0, 0, 4, 0, 'rgba(0, 0, 0, 0.08)']]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -38,36 +38,49 @@ const Row = ({
|
|||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const hasPointer = onClick || expandable
|
||||
const trClasses = {
|
||||
[classes.pointer]: hasPointer,
|
||||
[classes.row]: true,
|
||||
[classes.expanded]: expanded
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={onClick && { cursor: 'pointer' }}
|
||||
onClick={() => onClick && onClick(data)}>
|
||||
<Tr
|
||||
className={classnames(classes.row)}
|
||||
error={data.error}
|
||||
errorMessage={data.errorMessage}>
|
||||
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
|
||||
<Td key={idx} {...props}>
|
||||
{view(data)}
|
||||
</Td>
|
||||
))}
|
||||
{expandable && (
|
||||
<Td width={expWidth} textAlign="center">
|
||||
<button
|
||||
onClick={() => expandRow(id)}
|
||||
className={classes.expandButton}>
|
||||
{expanded && <ExpandOpenIcon />}
|
||||
{!expanded && <ExpandClosedIcon />}
|
||||
</button>
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
{expandable && expanded && (
|
||||
<Tr className={classes.detailsRow}>
|
||||
<Td width={width}>
|
||||
<Details it={data} />
|
||||
</Td>
|
||||
<div>
|
||||
<div className={classnames({ [classes.before]: expanded })}>
|
||||
<Tr
|
||||
className={classnames(trClasses)}
|
||||
onClick={() => {
|
||||
expandable && expandRow(id)
|
||||
onClick && onClick(data)
|
||||
}}
|
||||
error={data.error}
|
||||
errorMessage={data.errorMessage}>
|
||||
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
|
||||
<Td key={idx} {...props}>
|
||||
{view(data)}
|
||||
</Td>
|
||||
))}
|
||||
{expandable && (
|
||||
<Td width={expWidth} textAlign="center">
|
||||
<button
|
||||
onClick={() => expandRow(id)}
|
||||
className={classes.expandButton}>
|
||||
{expanded && <ExpandOpenIcon />}
|
||||
{!expanded && <ExpandClosedIcon />}
|
||||
</button>
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
</div>
|
||||
{expandable && expanded && (
|
||||
<div className={classes.after}>
|
||||
<Tr className={classnames({ [classes.expanded]: expanded })}>
|
||||
<Td width={width}>
|
||||
<Details it={data} />
|
||||
</Td>
|
||||
</Tr>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { zircon } from 'src/styling/variables'
|
||||
|
||||
export default {
|
||||
expandButton: {
|
||||
border: 'none',
|
||||
|
|
@ -8,6 +10,19 @@ export default {
|
|||
row: {
|
||||
borderRadius: 0
|
||||
},
|
||||
expanded: {
|
||||
border: [[2, 'solid', zircon]],
|
||||
boxShadow: '0 0 8px 0 rgba(0,0,0,0.08)'
|
||||
},
|
||||
before: {
|
||||
paddingTop: 12
|
||||
},
|
||||
after: {
|
||||
paddingBottom: 12
|
||||
},
|
||||
pointer: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
body: {
|
||||
flex: [[1, 1, 'auto']]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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!) {
|
||||
|
|
@ -42,7 +42,6 @@ const EditableNumber = ({
|
|||
name={name}
|
||||
component={TextInput}
|
||||
textAlign="right"
|
||||
type="text"
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const SingleFieldEditableNumber = ({
|
|||
enableReinitialize
|
||||
initialValues={{ [name]: (data && data[name]) ?? '' }}
|
||||
validationSchema={schema}
|
||||
onSubmit={it => save(section, it)}
|
||||
onSubmit={it => save(section, schema.cast(it))}
|
||||
onReset={() => {
|
||||
setEditing(name, false)
|
||||
}}>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const FiatBalance = ({ section }) => {
|
|||
fiatBalanceCassette2: data?.fiatBalanceCassette2 ?? ''
|
||||
}}
|
||||
validationSchema={schema}
|
||||
onSubmit={it => save(section, it)}
|
||||
onSubmit={it => save(section, schema.cast(it))}
|
||||
onReset={() => {
|
||||
setEditing(NAME, false)
|
||||
}}>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const FiatBalanceOverrides = ({ section }) => {
|
|||
|
||||
const overridenMachines = R.map(override => override.machine, setupValues)
|
||||
const suggestionFilter = R.filter(
|
||||
it => !R.contains(it.code, overridenMachines)
|
||||
it => !R.contains(it.deviceId, overridenMachines)
|
||||
)
|
||||
const suggestions = suggestionFilter(machines)
|
||||
|
||||
|
|
@ -50,17 +50,21 @@ const FiatBalanceOverrides = ({ section }) => {
|
|||
.required()
|
||||
})
|
||||
|
||||
const viewMachine = it =>
|
||||
R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: MACHINE_KEY,
|
||||
width: 238,
|
||||
size: 'sm',
|
||||
view: R.path(['name']),
|
||||
view: viewMachine,
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: it => R.concat(suggestions, findSuggestion(it)),
|
||||
limit: null,
|
||||
getOptionSelected: R.eqProps('display')
|
||||
valueProp: 'deviceId',
|
||||
getLabel: R.path(['name'])
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -90,7 +94,7 @@ const FiatBalanceOverrides = ({ section }) => {
|
|||
enableDelete
|
||||
enableEdit
|
||||
enableCreate
|
||||
save={it => save(section, it)}
|
||||
save={it => save(section, validationSchema.cast(it))}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
forceDisable={isDisabled(NAME) || !machines}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { gql } from 'apollo-boost'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, memo } from 'react'
|
||||
|
||||
import Popper from 'src/components/Popper'
|
||||
|
|
@ -9,6 +10,7 @@ import { Button } from 'src/components/buttons'
|
|||
import { Switch } from 'src/components/inputs'
|
||||
import { H4, P, Label2 } from 'src/components/typography'
|
||||
import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import { mainStyles } from './CoinATMRadar.styles'
|
||||
|
||||
|
|
@ -48,17 +50,25 @@ const CoinATMRadar = memo(() => {
|
|||
// TODO: treat errors on useMutation and useQuery
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
onCompleted: configResponse =>
|
||||
setCoinAtmRadarConfig(configResponse.saveConfig.coinAtmRadar)
|
||||
setCoinAtmRadarConfig(
|
||||
fromNamespace(namespaces.COIN_ATM_RADAR, configResponse.saveConfig)
|
||||
)
|
||||
})
|
||||
useQuery(GET_CONFIG, {
|
||||
onCompleted: configResponse => {
|
||||
setCoinAtmRadarConfig(
|
||||
configResponse?.config?.coinAtmRadar ?? initialValues
|
||||
const response = fromNamespace(
|
||||
namespaces.COIN_ATM_RADAR,
|
||||
configResponse.config
|
||||
)
|
||||
const values = R.merge(initialValues, response)
|
||||
setCoinAtmRadarConfig(values)
|
||||
}
|
||||
})
|
||||
|
||||
const save = it => saveConfig({ variables: { config: { coinAtmRadar: it } } })
|
||||
const save = it =>
|
||||
saveConfig({
|
||||
variables: { config: toNamespace(namespaces.COIN_ATM_RADAR, it) }
|
||||
})
|
||||
|
||||
const handleOpenHelpPopper = event => {
|
||||
setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,37 @@
|
|||
import React, { useState } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import * as Yup from 'yup'
|
||||
import { gql } from 'apollo-boost'
|
||||
import { Form, Formik, Field as FormikField } from 'formik'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import { gql } from 'apollo-boost'
|
||||
import classnames from 'classnames'
|
||||
import { Form, Formik, Field as FormikField } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Info2, Info3, Label1 } from 'src/components/typography'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import RadioGroupFormik from 'src/components/inputs/formik/RadioGroup'
|
||||
import {
|
||||
PhoneNumberInputFormik,
|
||||
maskValue,
|
||||
mask
|
||||
} from 'src/components/inputs/formik/PhoneNumberInput'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import RadioGroupFormik from 'src/components/inputs/formik/RadioGroup'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import { Info2, Info3, Label1, Label3 } from 'src/components/typography'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import {
|
||||
styles as globalStyles,
|
||||
contactInfoStyles
|
||||
} from './OperatorInfo.styles'
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
active: Yup.boolean().required(),
|
||||
name: Yup.string().required(),
|
||||
phone: Yup.string().required(),
|
||||
email: Yup.string()
|
||||
.email('Please enter a valid email address')
|
||||
.required(),
|
||||
website: Yup.string().required(),
|
||||
companyNumber: Yup.string().required()
|
||||
})
|
||||
|
||||
const fieldStyles = {
|
||||
field: {
|
||||
position: 'relative',
|
||||
|
|
@ -38,12 +45,13 @@ const fieldStyles = {
|
|||
'& > p:first-child': {
|
||||
height: 16,
|
||||
lineHeight: '16px',
|
||||
paddingLeft: 3,
|
||||
transform: 'scale(0.75)',
|
||||
transformOrigin: 'left',
|
||||
paddingLeft: 0,
|
||||
margin: [[0, 0, 5, 0]]
|
||||
},
|
||||
'& > p:last-child': {
|
||||
margin: 0,
|
||||
paddingLeft: 4
|
||||
margin: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +70,7 @@ const Field = ({ editing, field, displayValue, ...props }) => {
|
|||
<div className={classnames(classNames)}>
|
||||
{!editing && (
|
||||
<>
|
||||
<Label1>{field.label}</Label1>
|
||||
<Label3>{field.label}</Label3>
|
||||
<Info3>{displayValue(field.value)}</Info3>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -93,9 +101,6 @@ const SAVE_CONFIG = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const INFO_CARD_ENABLED = 'On'
|
||||
const INFO_CARD_DISABLED = 'Off'
|
||||
|
||||
const styles = R.merge(globalStyles, contactInfoStyles)
|
||||
|
||||
const contactUseStyles = makeStyles(styles)
|
||||
|
|
@ -106,8 +111,7 @@ const ContactInfo = () => {
|
|||
const [error, setError] = useState(null)
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
onCompleted: data => {
|
||||
const { operatorInfo } = data.saveConfig
|
||||
setInfo(operatorInfo)
|
||||
setInfo(fromNamespace(namespaces.OPERATOR_INFO, data.saveConfig))
|
||||
setEditing(false)
|
||||
},
|
||||
onError: e => setError(e)
|
||||
|
|
@ -115,13 +119,14 @@ const ContactInfo = () => {
|
|||
|
||||
useQuery(GET_CONFIG, {
|
||||
onCompleted: data => {
|
||||
const { operatorInfo } = data.config
|
||||
setInfo(operatorInfo ?? {})
|
||||
setInfo(fromNamespace(namespaces.OPERATOR_INFO, data.config))
|
||||
}
|
||||
})
|
||||
|
||||
const save = it => {
|
||||
return saveConfig({ variables: { config: { operatorInfo: it } } })
|
||||
return saveConfig({
|
||||
variables: { config: toNamespace(namespaces.OPERATOR_INFO, it) }
|
||||
})
|
||||
}
|
||||
|
||||
const classes = contactUseStyles()
|
||||
|
|
@ -130,45 +135,39 @@ const ContactInfo = () => {
|
|||
|
||||
const fields = [
|
||||
{
|
||||
name: 'infoCardEnabled',
|
||||
name: 'active',
|
||||
label: 'Info Card Enabled',
|
||||
value: info.infoCardEnabled ?? INFO_CARD_DISABLED,
|
||||
type: 'select',
|
||||
value: String(info.active),
|
||||
component: RadioGroupFormik
|
||||
},
|
||||
{
|
||||
name: 'fullName',
|
||||
name: 'name',
|
||||
label: 'Full name',
|
||||
value: info.fullName ?? '',
|
||||
type: 'text',
|
||||
value: info.name ?? '',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
name: 'phoneNumber',
|
||||
name: 'phone',
|
||||
label: 'Phone number',
|
||||
value: maskValue(info.phoneNumber) ?? '',
|
||||
type: 'text',
|
||||
component: PhoneNumberInputFormik
|
||||
value: info.phone ?? '',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
value: info.email ?? '',
|
||||
type: 'text',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
label: 'Website',
|
||||
value: info.website ?? '',
|
||||
type: 'text',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
name: 'companyNumber',
|
||||
label: 'Company number',
|
||||
value: info.companyNumber ?? '',
|
||||
type: 'text',
|
||||
component: TextInputFormik
|
||||
}
|
||||
]
|
||||
|
|
@ -180,33 +179,13 @@ 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')
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
fullName: Yup.string()
|
||||
.max(100, 'Too long')
|
||||
.required(),
|
||||
phoneNumber: Yup.string()
|
||||
.matches(mask, { excludeEmptyString: true })
|
||||
.max(100, 'Too long')
|
||||
.required(),
|
||||
email: Yup.string()
|
||||
.email('Please enter a valid email address')
|
||||
.max(100, 'Too long')
|
||||
.required(),
|
||||
website: Yup.string()
|
||||
.url('Please enter a valid url')
|
||||
.max(100, 'Too long')
|
||||
.required(),
|
||||
companyNumber: Yup.string()
|
||||
.max(30, 'Too long')
|
||||
.required()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -225,21 +204,21 @@ const ContactInfo = () => {
|
|||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={form.initialValues}
|
||||
validationSchema={form.validationSchema}
|
||||
onSubmit={values => save(values)}
|
||||
onReset={(values, bag) => {
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={values => save(validationSchema.cast(values))}
|
||||
onReset={() => {
|
||||
setEditing(false)
|
||||
setError(null)
|
||||
}}>
|
||||
<Form>
|
||||
<div className={classnames(classes.row, classes.radioButtonsRow)}>
|
||||
<Field
|
||||
field={findField('infoCardEnabled')}
|
||||
field={findField('active')}
|
||||
editing={editing}
|
||||
displayValue={displayTextValue}
|
||||
displayValue={it => (it === 'true' ? 'On' : 'Off')}
|
||||
options={[
|
||||
{ label: 'On', value: INFO_CARD_ENABLED },
|
||||
{ label: 'Off', value: INFO_CARD_DISABLED }
|
||||
{ display: 'On', code: 'true' },
|
||||
{ display: 'Off', code: 'false' }
|
||||
]}
|
||||
className={classes.radioButtons}
|
||||
resetError={() => setError(null)}
|
||||
|
|
@ -247,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,27 +1,26 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import * as R from 'ramda'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Title from 'src/components/Title'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
|
||||
import logsStyles from '../Logs.styles'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
|
||||
import CoinAtmRadar from './CoinATMRadar'
|
||||
import ContactInfo from './ContactInfo'
|
||||
import ReceiptPrinting from './ReceiptPrinting'
|
||||
import TermsConditions from './TermsConditions'
|
||||
|
||||
const localStyles = {
|
||||
contentWrapper: {
|
||||
width: '100%',
|
||||
const styles = {
|
||||
grid: {
|
||||
flex: 1,
|
||||
height: '100%'
|
||||
},
|
||||
content: {
|
||||
marginLeft: 48,
|
||||
paddingTop: 15
|
||||
}
|
||||
}
|
||||
|
||||
const styles = R.merge(logsStyles, localStyles)
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CONTACT_INFORMATION = 'Contact information'
|
||||
|
|
@ -39,25 +38,21 @@ const OperatorInfo = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<Title>Operator information</Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.wrapper}>
|
||||
<TitleSection title="Operator information"></TitleSection>
|
||||
<Grid container className={classes.grid}>
|
||||
<Sidebar
|
||||
data={pages}
|
||||
isSelected={isSelected}
|
||||
displayName={it => it}
|
||||
onClick={it => setSelected(it)}
|
||||
/>
|
||||
<div className={classes.contentWrapper}>
|
||||
<div className={classes.content}>
|
||||
{isSelected(CONTACT_INFORMATION) && <ContactInfo />}
|
||||
{isSelected(RECEIPT) && <ReceiptPrinting />}
|
||||
{isSelected(TERMS_CONDITIONS) && <TermsConditions />}
|
||||
{isSelected(COIN_ATM_RADAR) && <CoinAtmRadar />}
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import React, { useState } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import * as Yup from 'yup'
|
||||
import { Form, Formik, Field } from 'formik'
|
||||
import { gql } from 'apollo-boost'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import { gql } from 'apollo-boost'
|
||||
import classnames from 'classnames'
|
||||
import { Form, Formik, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Info2, Label2 } from 'src/components/typography'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import { Info2, Label2 } from 'src/components/typography'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import {
|
||||
styles as globalStyles,
|
||||
|
|
@ -40,9 +41,12 @@ const TermsConditions = () => {
|
|||
const [error, setError] = useState(null)
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
onCompleted: data => {
|
||||
const { termsAndConditions } = data.saveConfig
|
||||
const termsAndConditions = fromNamespace(
|
||||
namespaces.TERMS_CONDITIONS,
|
||||
data.saveConfig
|
||||
)
|
||||
setFormData(termsAndConditions)
|
||||
setShowOnScreen(termsAndConditions.show)
|
||||
setShowOnScreen(termsAndConditions.active)
|
||||
setError(null)
|
||||
},
|
||||
onError: e => setError(e)
|
||||
|
|
@ -52,36 +56,39 @@ const TermsConditions = () => {
|
|||
|
||||
useQuery(GET_CONFIG, {
|
||||
onCompleted: data => {
|
||||
const { termsAndConditions } = data.config
|
||||
const termsAndConditions = fromNamespace(
|
||||
namespaces.TERMS_CONDITIONS,
|
||||
data.config
|
||||
)
|
||||
setFormData(termsAndConditions ?? {})
|
||||
setShowOnScreen(termsAndConditions?.show ?? false)
|
||||
setShowOnScreen(termsAndConditions?.active ?? false)
|
||||
}
|
||||
})
|
||||
|
||||
const save = it => {
|
||||
setError(null)
|
||||
return saveConfig({
|
||||
variables: { config: { termsAndConditions: it } }
|
||||
variables: { config: toNamespace(namespaces.TERMS_CONDITIONS, it) }
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
{
|
||||
|
|
@ -102,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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ const GET_INFO = gql`
|
|||
`
|
||||
|
||||
const SAVE_ACCOUNT = gql`
|
||||
mutation Save($account: JSONObject) {
|
||||
saveAccount(account: $account)
|
||||
mutation Save($accounts: JSONObject) {
|
||||
saveAccounts(accounts: $accounts)
|
||||
}
|
||||
`
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -45,13 +45,11 @@ const Services = ({ key: SCREEN_KEY }) => {
|
|||
|
||||
const classes = useStyles()
|
||||
|
||||
const accounts = data?.accounts ?? []
|
||||
|
||||
const getValue = code => R.find(R.propEq('code', code))(accounts)
|
||||
const accounts = data?.accounts ?? {}
|
||||
|
||||
const getItems = (code, elements) => {
|
||||
const faceElements = R.filter(R.prop('face'))(elements)
|
||||
const values = getValue(code) || {}
|
||||
const values = accounts[code] || {}
|
||||
return R.map(({ display, code, long }) => ({
|
||||
label: display,
|
||||
value: long ? formatLong(values[code]) : values[code]
|
||||
|
|
@ -81,12 +79,12 @@ const Services = ({ key: SCREEN_KEY }) => {
|
|||
<FormRenderer
|
||||
save={it =>
|
||||
saveAccount({
|
||||
variables: { account: { code: editingSchema.code, ...it } }
|
||||
variables: { accounts: { [editingSchema.code]: it } }
|
||||
})
|
||||
}
|
||||
elements={editingSchema.elements}
|
||||
validationSchema={editingSchema.validationSchema}
|
||||
value={getValue(editingSchema.code)}
|
||||
value={accounts[editingSchema.code]}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,7 @@ const DetailsRow = ({ it: tx, ...props }) => {
|
|||
5
|
||||
)
|
||||
const commissionPercentage = Number.parseFloat(tx.commissionPercentage, 2)
|
||||
const commission =
|
||||
tx.txClass === 'cashOut'
|
||||
? fiat * commissionPercentage
|
||||
: fiat * commissionPercentage + Number.parseFloat(tx.fee)
|
||||
const commission = fiat * commissionPercentage
|
||||
const customer = tx.customerIdCardData && {
|
||||
name: `${onlyFirstToUpper(
|
||||
tx.customerIdCardData.firstName
|
||||
|
|
@ -163,6 +160,12 @@ const DetailsRow = ({ it: tx, ...props }) => {
|
|||
100} %)`}
|
||||
</div>
|
||||
</div>
|
||||
{tx.txClass === 'cashIn' && (
|
||||
<div className={classes.innerRow}>
|
||||
<Label>Fixed fee</Label>
|
||||
<div>{Number.parseFloat(tx.cashInFee)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(classes.col, classes.col3)}>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const GET_TRANSACTIONS = gql`
|
|||
machineName
|
||||
deviceId
|
||||
fiat
|
||||
fee
|
||||
cashInFee
|
||||
fiatCode
|
||||
cryptoAtoms
|
||||
cryptoCode
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -11,7 +11,7 @@ import Wizard from './Wizard'
|
|||
import { WalletSchema, getElements } from './helper'
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject, $accounts: [JSONObject]) {
|
||||
mutation Save($config: JSONObject, $accounts: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
saveAccounts(accounts: $accounts)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const filterConfig = (crypto, type) =>
|
|||
|
||||
const getItems = (accountsConfig, accounts, type, crypto) => {
|
||||
const fConfig = filterConfig(crypto, type)(accountsConfig)
|
||||
const find = code => R.find(R.propEq('code', code))(accounts)
|
||||
const find = code => accounts && accounts[code]
|
||||
|
||||
const [filled, unfilled] = R.partition(({ code }) => {
|
||||
const account = find(code)
|
||||
|
|
@ -35,7 +35,7 @@ const Wizard = ({ coin, onClose, accountsConfig, accounts, save, error }) => {
|
|||
const [{ step, config, accountsToSave }, setState] = useState({
|
||||
step: 0,
|
||||
config: { active: true },
|
||||
accountsToSave: []
|
||||
accountsToSave: {}
|
||||
})
|
||||
|
||||
const title = `Enable ${coin.display}`
|
||||
|
|
@ -48,9 +48,11 @@ const Wizard = ({ coin, onClose, accountsConfig, accounts, save, error }) => {
|
|||
|
||||
const getValue = code => R.find(R.propEq('code', code))(accounts)
|
||||
|
||||
const onContinue = async (it, it2) => {
|
||||
const newConfig = R.merge(config, it)
|
||||
const newAccounts = it2 ? R.concat(accountsToSave, [it2]) : accountsToSave
|
||||
const onContinue = async (stepConfig, stepAccount) => {
|
||||
const newConfig = R.merge(config, stepConfig)
|
||||
const newAccounts = stepAccount
|
||||
? R.merge(accountsToSave, stepAccount)
|
||||
: accountsToSave
|
||||
|
||||
if (isLastStep) {
|
||||
return save(toNamespace(coin.code, newConfig), newAccounts)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ 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'
|
||||
|
||||
import styles from './WizardStep.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -41,7 +42,7 @@ const reducer = (state, action) => {
|
|||
iError: false
|
||||
}
|
||||
case 'error':
|
||||
return R.merge(state, { iError: true })
|
||||
return R.merge(state, { innerError: true })
|
||||
case 'reset':
|
||||
return initialState
|
||||
default:
|
||||
|
|
@ -61,7 +62,7 @@ const WizardStep = ({
|
|||
getValue
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const [{ iError, selected, form, isNew }, dispatch] = useReducer(
|
||||
const [{ innerError, selected, form, isNew }, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
)
|
||||
|
|
@ -70,7 +71,7 @@ const WizardStep = ({
|
|||
dispatch({ type: 'reset' })
|
||||
}, [step])
|
||||
|
||||
const iContinue = (config, account) => {
|
||||
const innerContinue = (config, account) => {
|
||||
if (!config || !config[type]) {
|
||||
return dispatch({ type: 'error' })
|
||||
}
|
||||
|
|
@ -81,7 +82,7 @@ const WizardStep = ({
|
|||
const displayName = name ?? type
|
||||
const subtitleClass = {
|
||||
[classes.subtitle]: true,
|
||||
[classes.error]: iError
|
||||
[classes.error]: innerError
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -129,9 +130,7 @@ const WizardStep = ({
|
|||
</div>
|
||||
{form && (
|
||||
<FormRenderer
|
||||
save={it =>
|
||||
iContinue({ [type]: form.code }, R.merge(it, { code: form.code }))
|
||||
}
|
||||
save={it => innerContinue({ [type]: form.code }, { [form.code]: it })}
|
||||
elements={schema[form.code].elements}
|
||||
validationSchema={schema[form.code].validationSchema}
|
||||
value={getValue(form.code)}
|
||||
|
|
@ -143,7 +142,7 @@ const WizardStep = ({
|
|||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button
|
||||
className={classes.button}
|
||||
onClick={() => iContinue({ [type]: selected })}>
|
||||
onClick={() => innerContinue({ [type]: selected })}>
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue