feat: new compliance options

This commit is contained in:
Taranto 2020-09-14 23:25:35 +01:00 committed by Josh Harvey
parent ccf7eacfad
commit f2080c32e9
23 changed files with 161 additions and 121 deletions

10
dev/get-compat-trigger.js Normal file
View file

@ -0,0 +1,10 @@
const complianceTriggers = require('../lib/compliance-triggers')
const settingsLoader = require('../lib/new-settings-loader')
const configManager = require('../lib/new-config-manager')
settingsLoader.loadLatest().then(settings => {
const triggers = configManager.getTriggers(settings.config)
const response = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
console.log(response)
})

View file

@ -8,13 +8,9 @@ const customer = {
idCardData: {firstName, lastName, dateOfBirth} idCardData: {firstName, lastName, dateOfBirth}
} }
const config = {
sanctionsVerificationActive: true
}
const deviceId = 'test-device' const deviceId = 'test-device'
ofac.load() ofac.load()
.then(() => compliance.validationPatch(deviceId, config, customer)) .then(() => compliance.validationPatch(deviceId, true, customer))
.then(console.log) .then(console.log)
.catch(err => console.log(err)) .catch(err => console.log(err))

View file

@ -127,7 +127,7 @@ function postProcess (r, pi, isBlacklisted, addressReuse) {
settingsLoader.loadLatest().then(it => { settingsLoader.loadLatest().then(it => {
// TODO new-admin: addressReuse doesnt exist // TODO new-admin: addressReuse doesnt exist
// const config = configManager.unscoped(it.config) // const config = configManager.unscoped(it.config)
if (true) { if (false) {
blacklist.addToUsedAddresses(r.tx.toAddress, r.tx.cryptoCode) blacklist.addToUsedAddresses(r.tx.toAddress, r.tx.cryptoCode)
.catch(err => logger.error('Failure adding to addressReuse', err)) .catch(err => logger.error('Failure adding to addressReuse', err))
} }

View file

@ -1,7 +1,7 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
function getBackwardsCompatibleTriggers (triggers) { function getBackwardsCompatibleTriggers (triggers) {
const filtered = _.filter(_.matches({ triggerType: 'txAmount', cashDirection: 'both' }))(triggers) const filtered = _.filter(_.matches({ triggerType: 'txVolume', direction: 'both', thresholdDays: 1 }))(triggers)
const grouped = _.groupBy(_.prop('requirement'))(filtered) const grouped = _.groupBy(_.prop('requirement'))(filtered)
return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped) return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped)
} }

View file

@ -59,8 +59,10 @@ function matchOfac (deviceId, customer) {
}) })
} }
function validateOfac (deviceId, config, customer) { // BACKWARDS_COMPATIBILITY 7.5
if (!config.sanctionsVerificationActive) return Promise.resolve(true) // machines before 7.5 need to test sanctionsActive here
function validateOfac (deviceId, sanctionsActive, customer) {
if (!sanctionsActive) return Promise.resolve(true)
if (customer.sanctionsOverride === 'blocked') return Promise.resolve(false) if (customer.sanctionsOverride === 'blocked') return Promise.resolve(false)
if (customer.sanctionsOverride === 'verified') return Promise.resolve(true) if (customer.sanctionsOverride === 'verified') return Promise.resolve(true)
@ -68,8 +70,8 @@ function validateOfac (deviceId, config, customer) {
.then(didMatch => !didMatch) .then(didMatch => !didMatch)
} }
function validationPatch (deviceId, config, customer) { function validationPatch (deviceId, sanctionsActive, customer) {
return validateOfac(deviceId, config, customer) return validateOfac(deviceId, sanctionsActive, customer)
.then(ofacValidation => { .then(ofacValidation => {
if (_.isNil(customer.sanctions) || customer.sanctions !== ofacValidation) { if (_.isNil(customer.sanctions) || customer.sanctions !== ofacValidation) {
return {sanctions: ofacValidation} return {sanctions: ofacValidation}

View file

@ -5,8 +5,6 @@ const ph = require('./plugin-helper')
function sendMessage (settings, rec) { function sendMessage (settings, rec) {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
// TODO new-admin
// const pluginCode = configManager.unscoped(settings.config).email
const pluginCode = 'mailgun' const pluginCode = 'mailgun'
const plugin = ph.load(ph.EMAIL, pluginCode) const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode] const account = settings.accounts[pluginCode]

View file

@ -3,8 +3,6 @@ const uuid = require('uuid')
const db = require('../db') const db = require('../db')
const NUM_RESULTS = 500
function getServerLogs (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) { function getServerLogs (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) {
const sql = `select id, log_level, timestamp, message from server_logs const sql = `select id, log_level, timestamp, message from server_logs
where timestamp >= $1 and timestamp <= $2 where timestamp >= $1 and timestamp <= $2

View file

@ -342,7 +342,7 @@ function plugins (settings, deviceId) {
const notifications = configManager.getGlobalNotifications(settings.config) const notifications = configManager.getGlobalNotifications(settings.config)
const notificationsEnabled = notifications.sms.transactions || notifications.email.transactions const notificationsEnabled = notifications.sms.transactions || notifications.email.transactions
const highValueTx = tx.fiat.gt(notifications.highValueTransaction) const highValueTx = tx.fiat.gt(notifications.highValueTransaction || Infinity)
if (!notificationsEnabled || !highValueTx) return Promise.resolve() if (!notificationsEnabled || !highValueTx) return Promise.resolve()

View file

@ -62,7 +62,6 @@ function poll (req, res, next) {
const hasLightning = checkHasLightning(settings) const hasLightning = checkHasLightning(settings)
const triggers = configManager.getTriggers(settings.config) const triggers = configManager.getTriggers(settings.config)
const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
const operatorInfo = configManager.getOperatorInfo(settings.config) const operatorInfo = configManager.getOperatorInfo(settings.config)
const cashOutConfig = configManager.getCashOut(deviceId, settings.config) const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
@ -91,18 +90,6 @@ function poll (req, res, next) {
error: null, error: null,
locale, locale,
version, version,
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 === "on", receiptPrintingActive: receipt.active === "on",
cassettes, cassettes,
twoWayMode: cashOutConfig.active, twoWayMode: cashOutConfig.active,
@ -111,7 +98,24 @@ function poll (req, res, next) {
restartServices, restartServices,
hasLightning, hasLightning,
receipt, receipt,
operatorInfo operatorInfo,
triggers
}
// BACKWARDS_COMPATIBILITY 7.5
// machines before 7.5 expect old compliance
if (!machineVersion || semver.lt(machineVersion, '7.5.0-beta.0')) {
const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
response.smsVerificationActive = !!compatTriggers.sms
response.smsVerificationThreshold = compatTriggers.sms
response.idCardDataVerificationActive = !!compatTriggers.idData
response.idCardDataVerificationThreshold = compatTriggers.idData
response.idCardPhotoVerificationActive = !!compatTriggers.idPhoto
response.idCardPhotoVerificationThreshold = compatTriggers.idPhoto
response.sanctionsVerificationActive = !!compatTriggers.sancations
response.sanctionsVerificationThreshold = compatTriggers.sancations
response.frontCameraVerificationActive = !!compatTriggers.facephoto
response.frontCameraVerificationThreshold = compatTriggers.facephoto
} }
// BACKWARDS_COMPATIBILITY 7.4.9 // BACKWARDS_COMPATIBILITY 7.4.9
@ -208,6 +212,7 @@ function verifyTx (req, res, next) {
function addOrUpdateCustomer (req) { function addOrUpdateCustomer (req) {
const customerData = req.body const customerData = req.body
const machineVersion = req.query.version
const triggers = configManager.getTriggers(req.settings.config) const triggers = configManager.getTriggers(req.settings.config)
const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
@ -218,11 +223,24 @@ function addOrUpdateCustomer (req) {
return customers.add(req.body) return customers.add(req.body)
}) })
.then(customer => { .then(customer => {
// BACKWARDS_COMPATIBILITY 7.5
// machines before 7.5 expect customer with sanctions result
const isOlderMachineVersion = !machineVersion || semver.lt(machineVersion, '7.5.0-beta.0')
const shouldRunOfacCompat = !compatTriggers.sanctions && isOlderMachineVersion
if (!shouldRunOfacCompat) return customer
return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, customer) return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, customer)
.then(patch => { .then(patch => {
if (_.isEmpty(patch)) return customer if (_.isEmpty(patch)) return customer
return customers.update(customer.id, patch) return customers.update(customer.id, patch)
}) })
}).then(customer => {
// TODO new-admin: only get customer history till max needed for triggers
return Tx.customerHistory(customer.id)
.then(result => {
customer.txHistory = result
return customer
})
}) })
} }
@ -244,6 +262,7 @@ function getCustomerWithPhoneCode (req, res, next) {
function updateCustomer (req, res, next) { function updateCustomer (req, res, next) {
const id = req.params.id const id = req.params.id
const machineVersion = req.query.version
const txId = req.query.txId const txId = req.query.txId
const patch = req.body const patch = req.body
const triggers = configManager.getTriggers(req.settings.config) const triggers = configManager.getTriggers(req.settings.config)
@ -254,16 +273,57 @@ function updateCustomer (req, res, next) {
if (!customer) { throw httpError('Not Found', 404) } if (!customer) { throw httpError('Not Found', 404) }
const mergedCustomer = _.merge(customer, patch) const mergedCustomer = _.merge(customer, patch)
// BACKWARDS_COMPATIBILITY 7.5
// machines before 7.5 expect customer with sanctions result
const isOlderMachineVersion = !machineVersion || semver.lt(machineVersion, '7.5.0-beta.0')
Promise.resolve({})
.then(emptyObj => {
if (!isOlderMachineVersion) return Promise.resolve(emptyObj)
return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, mergedCustomer) return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, mergedCustomer)
})
.then(_.merge(patch)) .then(_.merge(patch))
.then(newPatch => customers.updatePhotoCard(id, newPatch)) .then(newPatch => customers.updatePhotoCard(id, newPatch))
.then(newPatch => customers.updateFrontCamera(id, newPatch)) .then(newPatch => customers.updateFrontCamera(id, newPatch))
.then(newPatch => customers.update(id, newPatch, null, txId)) .then(newPatch => customers.update(id, newPatch, null, txId))
}) })
.then(customer => respond(req, res, { customer })) .then(customer => respond(req, res, { customer }))
.catch(next) .catch(next)
} }
function triggerSanctions (req, res, next) {
const id = req.params.id
customers.getById(id)
.then(customer => {
if (!customer) { throw httpError('Not Found', 404) }
return compliance.validationPatch(req.deviceId, true, customer)
.then(patch => customers.update(id, patch))
})
.then(customer => respond(req, res, { customer }))
.catch(next)
}
function triggerBlock (req, res, next) {
const id = req.params.id
customers.update(id, { authorizedOverride: 'blocked' })
.then(customer => respond(req, res, { customer }))
.catch(next)
}
function triggerSuspend (req, res, next) {
const id = req.params.id
customers.update(id, { authorizedOverride: 'blocked' })
.then(customer => respond(req, res, { customer }))
.catch(next)
}
function getLastSeen (req, res, next) { function getLastSeen (req, res, next) {
const deviceId = req.deviceId const deviceId = req.deviceId
const timestamp = Date.now() const timestamp = Date.now()
@ -409,6 +469,9 @@ app.post('/verify_transaction', verifyTx)
app.post('/phone_code', getCustomerWithPhoneCode) app.post('/phone_code', getCustomerWithPhoneCode)
app.patch('/customer/:id', updateCustomer) app.patch('/customer/:id', updateCustomer)
app.patch('/customer/:id/sanctions', triggerSanctions)
app.patch('/customer/:id/block', triggerBlock)
app.patch('/customer/:id/suspend', triggerSuspend)
app.post('/tx', postTx) app.post('/tx', postTx)
app.get('/tx/:id', getTx) app.get('/tx/:id', getTx)

View file

@ -1,12 +1,10 @@
// const configManager = require('./config-manager')
const ph = require('./plugin-helper') const ph = require('./plugin-helper')
const argv = require('minimist')(process.argv.slice(2))
function sendMessage (settings, rec) { function sendMessage (settings, rec) {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
// TODO new-admin: how to load mock here? Only on dev? const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
// const pluginCode = configManager.unscoped(settings.config).sms
const pluginCode = 'twilio'
const plugin = ph.load(ph.SMS, pluginCode) const plugin = ph.load(ph.SMS, pluginCode)
const account = settings.accounts[pluginCode] const account = settings.accounts[pluginCode]

View file

@ -1,4 +1,5 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const db = require('./db')
const BN = require('./bn') const BN = require('./bn')
const CashInTx = require('./cash-in/cash-in-tx') const CashInTx = require('./cash-in/cash-in-tx')
const CashOutTx = require('./cash-out/cash-out-tx') const CashOutTx = require('./cash-out/cash-out-tx')
@ -63,4 +64,15 @@ function cancel (txId) {
}) })
} }
module.exports = {post, cancel} function customerHistory (customerId) {
const sql = ` select txIn.id, txIn.created, txIn.fiat, 'cashIn' as direction from cash_in_txs txIn
where txIn.customer_id = $1
union
select txOut.id, txOut.created, txOut.fiat, 'cashOut' as direction from cash_out_txs txOut
where txOut.customer_id = $1
order by created;`
return db.any(sql, [customerId])
}
module.exports = {post, cancel, customerHistory}

View file

@ -139,7 +139,7 @@ function getWalletStatus (settings, tx) {
function authorizeZeroConf (settings, tx, machineId) { function authorizeZeroConf (settings, tx, machineId) {
const plugin = configManager.getWalletSettings(tx.cryptoCode, settings.config).zeroConf const plugin = configManager.getWalletSettings(tx.cryptoCode, settings.config).zeroConf
const cashOutConfig = configManager.cashOutConfig(machineId, settings.config) const cashOutConfig = configManager.getCashOut(machineId, settings.config)
const zeroConfLimit = cashOutConfig.zeroConfLimit const zeroConfLimit = cashOutConfig.zeroConfLimit
if (!_.isObject(tx.fiat)) { if (!_.isObject(tx.fiat)) {

View file

@ -71,9 +71,12 @@ export default {
borderRadius: 8, borderRadius: 8,
backgroundColor: zircon, backgroundColor: zircon,
margin: [[0, 28, 0, 0]], margin: [[0, 28, 0, 0]],
padding: [[30]],
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between' justifyContent: 'center',
display: 'flex'
},
img: {
width: 80
}, },
customerName: { customerName: {
marginBottom: 32 marginBottom: 32

View file

@ -15,7 +15,11 @@ const FrontCameraPhoto = memo(({ frontCameraPath }) => {
return ( return (
<Paper className={classes.photo} elevation={0}> <Paper className={classes.photo} elevation={0}>
{frontCameraPath ? ( {frontCameraPath ? (
<img src={`${URI}/front-camera-photo/${frontCameraPath}`} alt="" /> <img
className={classes.img}
src={`${URI}/front-camera-photo/${frontCameraPath}`}
alt=""
/>
) : ( ) : (
<CrossedCameraIcon /> <CrossedCameraIcon />
)} )}

View file

@ -57,7 +57,7 @@ const Cashboxes = () => {
const [resetCashOut] = useMutation(RESET_CASHOUT_BILLS, { const [resetCashOut] = useMutation(RESET_CASHOUT_BILLS, {
onError: ({ graphQLErrors, message }) => { onError: ({ graphQLErrors, message }) => {
const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message
// TODO: this should not be final // TODO new-admin : this should not be final
alert(JSON.stringify(errorMessage)) alert(JSON.stringify(errorMessage))
} }
}) })

View file

@ -76,7 +76,7 @@ const MachineDetailsRow = ({ it: machine }) => {
<Label>Lamassu Support article</Label> <Label>Lamassu Support article</Label>
<div> <div>
{machine.statuses.map((...[, index]) => ( {machine.statuses.map((...[, index]) => (
// TODO support articles // TODO new-admin: support articles
<span key={index}></span> <span key={index}></span>
))} ))}
</div> </div>

View file

@ -60,15 +60,17 @@ const formatDate = date => {
return moment(date).format('YYYY-MM-DD HH:mm') return moment(date).format('YYYY-MM-DD HH:mm')
} }
const NUM_LOG_RESULTS = 1000
const GET_DATA = gql` const GET_DATA = gql`
{ query ServerData($limit: Int) {
serverVersion serverVersion
uptime { uptime {
name name
state state
uptime uptime
} }
serverLogs { serverLogs(limit: $limit) {
logLevel logLevel
id id
timestamp timestamp
@ -93,7 +95,10 @@ const Logs = () => {
const [logLevel, setLogLevel] = useState(SHOW_ALL) const [logLevel, setLogLevel] = useState(SHOW_ALL)
const { data } = useQuery(GET_DATA, { const { data } = useQuery(GET_DATA, {
onCompleted: () => setSaveMessage('') onCompleted: () => setSaveMessage(''),
variables: {
limit: NUM_LOG_RESULTS
}
}) })
const serverVersion = data?.serverVersion const serverVersion = data?.serverVersion

View file

@ -10,7 +10,7 @@ import { Button } from 'src/components/buttons'
import { H5, Info3 } from 'src/components/typography' import { H5, Info3 } from 'src/components/typography'
import { comet } from 'src/styling/variables' import { comet } from 'src/styling/variables'
import { direction, type, requirements } from './helper' import { txDirection, type, requirements } from './helper'
const LAST_STEP = 3 const LAST_STEP = 3
@ -44,7 +44,7 @@ const useStyles = makeStyles(styles)
const getStep = step => { const getStep = step => {
switch (step) { switch (step) {
case 1: case 1:
return direction return txDirection
case 2: case 2:
return type return type
case 3: case 3:
@ -72,7 +72,7 @@ const orUnderline = value => {
} }
const getDirectionText = config => { const getDirectionText = config => {
switch (config.cashDirection) { switch (config.direction) {
case 'both': case 'both':
return 'both cash-in and cash-out' return 'both cash-in and cash-out'
case 'cashIn': case 'cashIn':
@ -111,9 +111,9 @@ const getRequirementText = config => {
switch (config.requirement?.requirement) { switch (config.requirement?.requirement) {
case 'sms': case 'sms':
return 'asked to enter code provided through SMS verification' return 'asked to enter code provided through SMS verification'
case 'idPhoto': case 'idCardPhoto':
return 'asked to scan a ID with photo' return 'asked to scan a ID with photo'
case 'idData': case 'idCardData':
return 'asked to scan a ID' return 'asked to scan a ID'
case 'facephoto': case 'facephoto':
return 'asked to have a photo taken' return 'asked to have a photo taken'

View file

@ -68,7 +68,7 @@ const useStyles = makeStyles({
} }
}) })
const cashDirection = Yup.string().required('Required') const direction = Yup.string().required('Required')
const triggerType = Yup.string().required('Required') const triggerType = Yup.string().required('Required')
const threshold = Yup.object().shape({ const threshold = Yup.object().shape({
threshold: Yup.number(), threshold: Yup.number(),
@ -83,11 +83,11 @@ const Schema = Yup.object().shape({
triggerType, triggerType,
requirement, requirement,
threshold, threshold,
cashDirection direction
}) })
// Direction // Direction
const directionSchema = Yup.object().shape({ cashDirection }) const directionSchema = Yup.object().shape({ direction })
const directionOptions = [ const directionOptions = [
{ {
@ -143,7 +143,7 @@ const Direction = () => {
const { errors } = useFormikContext() const { errors } = useFormikContext()
const titleClass = { const titleClass = {
[classes.error]: errors.cashDirection [classes.error]: errors.direction
} }
return ( return (
@ -155,7 +155,7 @@ const Direction = () => {
</Box> </Box>
<Field <Field
component={RadioGroup} component={RadioGroup}
name="cashDirection" name="direction"
options={directionOptions} options={directionOptions}
labelClassName={classes.radioLabel} labelClassName={classes.radioLabel}
radioClassName={classes.radio} radioClassName={classes.radio}
@ -165,11 +165,11 @@ const Direction = () => {
) )
} }
const direction = { const txDirection = {
schema: directionSchema, schema: directionSchema,
options: directionOptions, options: directionOptions,
Component: Direction, Component: Direction,
initialValues: { cashDirection: '' } initialValues: { direction: '' }
} }
// TYPE // TYPE
@ -257,11 +257,11 @@ const requirementSchema = Yup.object().shape({
const requirementOptions = [ const requirementOptions = [
{ display: 'SMS verification', code: 'sms' }, { display: 'SMS verification', code: 'sms' },
{ display: 'ID card image', code: 'idPhoto' }, { display: 'ID card image', code: 'idCardPhoto' },
{ display: 'ID data', code: 'idData' }, { display: 'ID data', code: 'idCardData' },
{ display: 'Customer camera', code: 'facephoto' }, { display: 'Customer camera', code: 'facephoto' },
{ display: 'Sanctions', code: 'sanctions' }, { display: 'Sanctions', code: 'sanctions' },
{ display: 'Super user', code: 'superuser' }, // { display: 'Super user', code: 'superuser' },
{ display: 'Suspend', code: 'suspend' }, { display: 'Suspend', code: 'suspend' },
{ display: 'Block', code: 'block' } { display: 'Block', code: 'block' }
] ]
@ -506,7 +506,7 @@ const getElements = (currency, classes) => [
view: (it, config) => <ThresholdView config={config} currency={currency} /> view: (it, config) => <ThresholdView config={config} currency={currency} />
}, },
{ {
name: 'cashDirection', name: 'direction',
size: 'sm', size: 'sm',
width: 282, width: 282,
view: it => <DirectionDisplay code={it} />, view: it => <DirectionDisplay code={it} />,
@ -554,7 +554,7 @@ const toServer = triggers =>
export { export {
Schema, Schema,
getElements, getElements,
direction, txDirection,
type, type,
requirements, requirements,
sortBy, sortBy,

View file

@ -1,49 +0,0 @@
Overall:
- caching the page
- transitions
- error handling
- validation is bad rn, negatives being allowed
- locale based mil separators 1.000 1,000
- Table should be loaded on slow internet (we want to load the table with no data)
- tooltip like components should close on esc
- saving should be a one time thing. disable buttons so user doesnt spam it
- disable edit on non-everrides => overrides
- splash screens and home
- maybe a indication that there's more to search on dropdown
- required signifier on form fields - (required) or *
- stop line breaking on multi select
- input width should be enough to hold values without cutting text
Locale:
- Only allow one override per machine
Notifications:
- one of the crypto balance alerts has to be optional because of migration
Server:
- Takes too long to load. Investigate
Operator Info:
- That should be paginated with routes!
CoinATMRadar:
- relay facephoto info
- we should show the highest amount that per requirement
Customers:
- add status
Machine name:
- update the db with whatever name is on the old config (check 1509439657189-add_machine_name_to_devices)
Compliance:
- Currently admin only handles { type: 'amount', direction: 'both' }
- Sanctions should have more care in customers.js, currently just looking if is active as if old config
Ideas
- Transactions could have a link to the customer
- Transactions table on customer should have a link to "transactions"
Feedback needed
- font sizes could be better (I've bumped all font sizes by 1px, looks pretty good as fonts do a good vertical bump in size. Maybe some of the fonts don't like even values)

16
package-lock.json generated
View file

@ -2589,7 +2589,7 @@
}, },
"blake2b": { "blake2b": {
"version": "git+https://github.com/BitGo/blake2b.git#6268e6dd678661e0acc4359e9171b97eb1ebf8ac", "version": "git+https://github.com/BitGo/blake2b.git#6268e6dd678661e0acc4359e9171b97eb1ebf8ac",
"from": "blake2b@git+https://github.com/BitGo/blake2b.git#6268e6dd678661e0acc4359e9171b97eb1ebf8ac", "from": "git+https://github.com/BitGo/blake2b.git#6268e6dd678661e0acc4359e9171b97eb1ebf8ac",
"requires": { "requires": {
"blake2b-wasm": "git+https://github.com/BitGo/blake2b-wasm.git#193cdb71656c1a6c7f89b05d0327bb9b758d071b", "blake2b-wasm": "git+https://github.com/BitGo/blake2b-wasm.git#193cdb71656c1a6c7f89b05d0327bb9b758d071b",
"nanoassert": "^1.0.0" "nanoassert": "^1.0.0"
@ -2597,7 +2597,7 @@
}, },
"blake2b-wasm": { "blake2b-wasm": {
"version": "git+https://github.com/BitGo/blake2b-wasm.git#193cdb71656c1a6c7f89b05d0327bb9b758d071b", "version": "git+https://github.com/BitGo/blake2b-wasm.git#193cdb71656c1a6c7f89b05d0327bb9b758d071b",
"from": "blake2b-wasm@git+https://github.com/BitGo/blake2b-wasm.git#193cdb71656c1a6c7f89b05d0327bb9b758d071b", "from": "git+https://github.com/BitGo/blake2b-wasm.git#193cdb71656c1a6c7f89b05d0327bb9b758d071b",
"requires": { "requires": {
"nanoassert": "^1.0.0" "nanoassert": "^1.0.0"
} }
@ -5623,7 +5623,7 @@
"got": { "got": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz",
"integrity": "sha1-BUUP2ECU5rvqVvRRpDqcKJFmOFo=", "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==",
"requires": { "requires": {
"decompress-response": "^3.2.0", "decompress-response": "^3.2.0",
"duplexer3": "^0.1.4", "duplexer3": "^0.1.4",
@ -6947,7 +6947,7 @@
"isurl": { "isurl": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz",
"integrity": "sha1-sn9PSfPNqj6kSgpbfzRi5u3DnWc=", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==",
"requires": { "requires": {
"has-to-string-tag-x": "^1.2.0", "has-to-string-tag-x": "^1.2.0",
"is-object": "^1.0.1" "is-object": "^1.0.1"
@ -7792,7 +7792,7 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -8015,7 +8015,7 @@
"npmlog": { "npmlog": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"requires": { "requires": {
"are-we-there-yet": "~1.1.2", "are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0", "console-control-strings": "~1.1.0",
@ -8404,7 +8404,7 @@
"p-cancelable": { "p-cancelable": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
"integrity": "sha1-ueEjgAvOu3rBOkeb4ZW1B7mNMPo=" "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw=="
}, },
"p-defer": { "p-defer": {
"version": "1.0.0", "version": "1.0.0",
@ -11899,7 +11899,7 @@
"dependencies": { "dependencies": {
"bignumber.js": { "bignumber.js": {
"version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
"from": "bignumber.js@git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git"
} }
} }
}, },

View file

@ -3,7 +3,7 @@
"triggers": [ "triggers": [
{ {
"id": "ce8f240f-7235-4948-b833-4af56ccfa90f", "id": "ce8f240f-7235-4948-b833-4af56ccfa90f",
"cashDirection": "cash-out", "direction": "cash-out",
"triggerType": "volume", "triggerType": "volume",
"threshold": "12", "threshold": "12",
"requirement": "block" "requirement": "block"

View file

@ -9,7 +9,7 @@ const config = settings.config
test('first examples', () => { test('first examples', () => {
const triggers = configManager.getTriggers(config) const triggers = configManager.getTriggers(config)
const filtered = _.filter(_.matches({ triggerType: 'volume', cashDirection: 'both' }))(triggers) const filtered = _.filter(_.matches({ triggerType: 'volume', direction: 'both' }))(triggers)
const grouped = _.groupBy(_.prop('requirement'))(filtered) const grouped = _.groupBy(_.prop('requirement'))(filtered)
const final = _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped) const final = _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped)