Merge branch 'dev' into fix/lam-235/lamassu-coins-install
This commit is contained in:
commit
0e9f3e5863
66 changed files with 1544 additions and 638 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const { asyncLocalStorage, defaultStore } = require('../lib/async-storage')
|
||||
const userManagement = require('../lib/new-admin/graphql/modules/userManagement')
|
||||
const authErrors = require('../lib/new-admin/graphql/errors/authentication')
|
||||
const options = require('../lib/options')
|
||||
|
||||
const name = process.argv[2]
|
||||
|
|
@ -14,29 +15,25 @@ if (!domain) {
|
|||
}
|
||||
|
||||
if (!name || !role) {
|
||||
console.log('Usage: lamassu-register <username> <role>')
|
||||
console.log('Usage: lamassu-register <email> <role>')
|
||||
console.log('<role> must be \'user\' or \'superuser\'')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const emailRegex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
if (!emailRegex.test(name)) {
|
||||
console.log('Usage: <name> should be in an email format')
|
||||
console.log('Usage: <email> must be in an email format')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
if (role !== 'user' && role !== 'superuser') {
|
||||
console.log('Usage: <role> has two possible values: user | superuser')
|
||||
console.log('Usage: <role> must be \'user\' or \'superuser\'')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
asyncLocalStorage.run(defaultStore(), () => {
|
||||
userManagement.createRegisterToken(name, role).then(token => {
|
||||
if (!token) {
|
||||
console.log(`A user named ${name} already exists!`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
if (domain === 'localhost') {
|
||||
console.log(`https://${domain}:3001/register?t=${token.token}`)
|
||||
} else {
|
||||
|
|
@ -45,6 +42,12 @@ asyncLocalStorage.run(defaultStore(), () => {
|
|||
|
||||
process.exit(0)
|
||||
}).catch(err => {
|
||||
|
||||
if (err instanceof authErrors.UserAlreadyExistsError){
|
||||
console.log(`A user with email ${name} already exists!`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
console.log('Error: %s', err)
|
||||
process.exit(3)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -73,8 +73,9 @@ function unmergeCassettes(cassettes, output) {
|
|||
}
|
||||
|
||||
function makeChangeDuo(cassettes, amount) {
|
||||
const small = cassettes[0]
|
||||
const large = cassettes[1]
|
||||
// Initialize empty cassettes in case of undefined, due to same denomination across all cassettes results in a single merged cassette
|
||||
const small = cassettes[0] ?? { denomination: 0, count: 0 }
|
||||
const large = cassettes[1] ?? { denomination: 0, count: 0 }
|
||||
|
||||
const largeDenom = large.denomination
|
||||
const smallDenom = small.denomination
|
||||
|
|
|
|||
|
|
@ -23,18 +23,18 @@ module.exports = {
|
|||
|
||||
const BINARIES = {
|
||||
BTC: {
|
||||
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.0/bitcoin-0.20.0-x86_64-linux-gnu.tar.gz',
|
||||
defaultDir: 'bitcoin-0.20.0/bin',
|
||||
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
|
||||
defaultDir: 'bitcoin-0.20.1/bin',
|
||||
url: 'https://bitcoincore.org/bin/bitcoin-core-22.0/bitcoin-22.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-22.0/bin'
|
||||
},
|
||||
ETH: {
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.13-7a0c19f8.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.10.13-7a0c19f8'
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.15-8be800ff.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.10.15-8be800ff'
|
||||
},
|
||||
ZEC: {
|
||||
url: 'https://z.cash/downloads/zcash-4.5.1-1-linux64-debian-stretch.tar.gz',
|
||||
dir: 'zcash-4.5.1-1/bin'
|
||||
url: 'https://z.cash/downloads/zcash-4.6.0-1-linux64-debian-stretch.tar.gz',
|
||||
dir: 'zcash-4.6.0-1/bin'
|
||||
},
|
||||
DASH: {
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz',
|
||||
|
|
@ -50,8 +50,8 @@ const BINARIES = {
|
|||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||
},
|
||||
XMR: {
|
||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.2.0.tar.bz2',
|
||||
dir: 'monero-x86_64-linux-gnu-v0.17.2.0',
|
||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.3.0.tar.bz2',
|
||||
dir: 'monero-x86_64-linux-gnu-v0.17.3.0',
|
||||
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,8 +117,9 @@ function run () {
|
|||
_.filter(c => c.type !== 'erc-20'),
|
||||
_.map(c => {
|
||||
const checked = isInstalledSoftware(c) && isInstalledVolume(c)
|
||||
const name = c.code === 'ethereum' ? 'Ethereum and/or USDT' : c.display
|
||||
return {
|
||||
name: c.display,
|
||||
name,
|
||||
value: c.code,
|
||||
checked,
|
||||
disabled: checked && 'Installed'
|
||||
|
|
|
|||
|
|
@ -631,8 +631,8 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
|||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
|
||||
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||
sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat,
|
||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes
|
||||
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
|
||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
|
||||
FROM (
|
||||
SELECT c.id, c.authorized_override,
|
||||
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
|
||||
|
|
@ -640,13 +640,13 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
|||
c.front_camera_path, c.front_camera_override,
|
||||
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
|
||||
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
||||
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes,
|
||||
c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
|
||||
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
||||
coalesce(sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (partition by c.id), 0) AS total_spent, ccf.custom_fields
|
||||
FROM customers c LEFT OUTER JOIN (
|
||||
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
FROM cash_in_txs WHERE send_confirmed = true UNION
|
||||
FROM cash_in_txs WHERE send_confirmed = true OR batched = true UNION
|
||||
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
FROM cash_out_txs WHERE confirmed_at IS NOT NULL) AS t ON c.id = t.customer_id
|
||||
LEFT OUTER JOIN (
|
||||
|
|
@ -686,8 +686,8 @@ function getCustomerById (id) {
|
|||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
|
||||
phone, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||
id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||
sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat,
|
||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes
|
||||
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
|
||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes, is_test_customer
|
||||
FROM (
|
||||
SELECT c.id, c.authorized_override,
|
||||
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
||||
|
|
@ -695,13 +695,13 @@ function getCustomerById (id) {
|
|||
c.front_camera_path, c.front_camera_override, c.front_camera_at,
|
||||
c.phone, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
|
||||
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
|
||||
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes,
|
||||
c.sanctions_at, c.sanctions_override, c.subscriber_info, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
|
||||
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
|
||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,
|
||||
sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (PARTITION BY c.id) AS total_spent, ccf.custom_fields
|
||||
FROM customers c LEFT OUTER JOIN (
|
||||
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
FROM cash_in_txs WHERE send_confirmed = true UNION
|
||||
FROM cash_in_txs WHERE send_confirmed = true OR batched = true UNION
|
||||
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
FROM cash_out_txs WHERE confirmed_at IS NOT NULL) t ON c.id = t.customer_id
|
||||
LEFT OUTER JOIN (
|
||||
|
|
@ -993,6 +993,7 @@ function addCustomField (customerId, label, value) {
|
|||
}
|
||||
})
|
||||
)
|
||||
.then(res => !_.isNil(res))
|
||||
}
|
||||
|
||||
function saveCustomField (customerId, fieldId, newValue) {
|
||||
|
|
@ -1022,6 +1023,16 @@ function getCustomInfoRequestsData (customer) {
|
|||
return db.any(sql, [customer.id]).then(res => _.set('custom_info_request_data', res, customer))
|
||||
}
|
||||
|
||||
function enableTestCustomer (customerId) {
|
||||
const sql = `UPDATE customers SET is_test_customer=true WHERE id=$1`
|
||||
return db.none(sql, [customerId])
|
||||
}
|
||||
|
||||
function disableTestCustomer (customerId) {
|
||||
const sql = `UPDATE customers SET is_test_customer=false WHERE id=$1`
|
||||
return db.none(sql, [customerId])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
add,
|
||||
get,
|
||||
|
|
@ -1040,5 +1051,7 @@ module.exports = {
|
|||
edit,
|
||||
deleteEditedData,
|
||||
updateEditedPhoto,
|
||||
updateTxCustomerPhoto
|
||||
updateTxCustomerPhoto,
|
||||
enableTestCustomer,
|
||||
disableTestCustomer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ const configManager = require('./new-config-manager')
|
|||
const settingsLoader = require('./new-settings-loader')
|
||||
const notifierUtils = require('./notifier/utils')
|
||||
const notifierQueries = require('./notifier/queries')
|
||||
const { ApolloError } = require('apollo-server-errors');
|
||||
|
||||
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||
const stuckStatus = { label: 'Stuck', type: 'error' }
|
||||
|
||||
function getMachines () {
|
||||
return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
|
||||
.then(rr => rr.map(r => ({
|
||||
function toMachineObject (r) {
|
||||
return {
|
||||
deviceId: r.device_id,
|
||||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
|
|
@ -32,10 +32,15 @@ function getMachines () {
|
|||
pairedAt: new Date(r.created),
|
||||
lastPing: new Date(r.last_online),
|
||||
name: r.name,
|
||||
paired: r.paired
|
||||
// TODO: we shall start using this JSON field at some point
|
||||
// location: r.location,
|
||||
paired: r.paired
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
function getMachines () {
|
||||
return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
|
||||
.then(rr => rr.map(toMachineObject))
|
||||
}
|
||||
|
||||
function getConfig (defaultConfig) {
|
||||
|
|
@ -100,21 +105,10 @@ function getMachineName (machineId) {
|
|||
|
||||
function getMachine (machineId, config) {
|
||||
const sql = 'SELECT * FROM devices WHERE device_id=$1'
|
||||
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => ({
|
||||
deviceId: r.device_id,
|
||||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
cassette2: r.cassette2,
|
||||
cassette3: r.cassette3,
|
||||
cassette4: r.cassette4,
|
||||
numberOfCassettes: r.number_of_cassettes,
|
||||
version: r.version,
|
||||
model: r.model,
|
||||
pairedAt: new Date(r.created),
|
||||
lastPing: new Date(r.last_online),
|
||||
name: r.name,
|
||||
paired: r.paired
|
||||
}))
|
||||
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => {
|
||||
if (r === null) throw new ApolloError('Resource doesn\'t exist', 'NOT_FOUND')
|
||||
else return toMachineObject(r)
|
||||
})
|
||||
|
||||
return Promise.all([queryMachine, dbm.machineEvents(), config])
|
||||
.then(([machine, events, config]) => {
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ const reset2FA = (token, userID, code, context) => {
|
|||
}
|
||||
|
||||
const getToken = context => {
|
||||
if (_.isNil(context.req.cookies.lid) || _.isNil(context.req.session.user.id))
|
||||
if (_.isNil(context.req.cookies['lamassu_sid']) || _.isNil(context.req.session.user.id))
|
||||
throw new authErrors.AuthenticationError('Authentication failed')
|
||||
|
||||
return context.req.session.user.id
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const authentication = require('../modules/userManagement')
|
||||
const queries = require('../../services/customInfoRequests')
|
||||
const DataLoader = require('dataloader')
|
||||
|
||||
|
|
@ -21,7 +22,10 @@ const resolvers = {
|
|||
insertCustomInfoRequest: (...[, { customRequest }]) => queries.addCustomInfoRequest(customRequest),
|
||||
removeCustomInfoRequest: (...[, { id }]) => queries.removeCustomInfoRequest(id),
|
||||
editCustomInfoRequest: (...[, { id, customRequest }]) => queries.editCustomInfoRequest(id, customRequest),
|
||||
setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, isAuthorized }]) => queries.setAuthorizedCustomRequest(customerId, infoRequestId, isAuthorized),
|
||||
setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, override }, context]) => {
|
||||
const token = authentication.getToken(context)
|
||||
return queries.setAuthorizedCustomRequest(customerId, infoRequestId, override, token)
|
||||
},
|
||||
setCustomerCustomInfoRequest: (...[, { customerId, infoRequestId, data }]) => queries.setCustomerData(customerId, infoRequestId, data)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const resolvers = {
|
|||
return customers.updateCustomer(customerId, customerInput, token)
|
||||
},
|
||||
addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value),
|
||||
saveCustomField: (...[, { customerId, fieldId, newValue }]) => customers.saveCustomField(customerId, fieldId, newValue),
|
||||
saveCustomField: (...[, { customerId, fieldId, value }]) => customers.saveCustomField(customerId, fieldId, value),
|
||||
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId),
|
||||
editCustomer: async (root, { customerId, customerEdit }, context) => {
|
||||
const token = authentication.getToken(context)
|
||||
|
|
@ -49,7 +49,12 @@ const resolvers = {
|
|||
},
|
||||
deleteCustomerNote: (...[, { noteId }]) => {
|
||||
return customerNotes.deleteCustomerNote(noteId)
|
||||
}
|
||||
},
|
||||
createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }),
|
||||
enableTestCustomer: (...[, { customerId }]) =>
|
||||
customers.enableTestCustomer(customerId),
|
||||
disableTestCustomer: (...[, { customerId }]) =>
|
||||
customers.disableTestCustomer(customerId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ const resolvers = {
|
|||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||
},
|
||||
Query: {
|
||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone, simplified }]) =>
|
||||
transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, simplified)
|
||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers }]) =>
|
||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone, excludeTestingCustomers, simplified }]) =>
|
||||
transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers, simplified)
|
||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']), { fields: txLogFields })),
|
||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||
transactions.getTx(id, txClass).then(data =>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ const typeDef = gql`
|
|||
type CustomRequestData {
|
||||
customerId: ID
|
||||
infoRequestId: ID
|
||||
approved: Boolean
|
||||
override: String
|
||||
overrideAt: Date
|
||||
overrideBy: ID
|
||||
customerData: JSON
|
||||
customInfoRequest: CustomInfoRequest
|
||||
}
|
||||
|
|
@ -47,7 +49,7 @@ const typeDef = gql`
|
|||
insertCustomInfoRequest(customRequest: CustomRequestInput!): CustomInfoRequest @auth
|
||||
removeCustomInfoRequest(id: ID!): CustomInfoRequest @auth
|
||||
editCustomInfoRequest(id: ID!, customRequest: CustomRequestInput!): CustomInfoRequest @auth
|
||||
setAuthorizedCustomRequest(customerId: ID!, infoRequestId: ID!, isAuthorized: Boolean!): Boolean @auth
|
||||
setAuthorizedCustomRequest(customerId: ID!, infoRequestId: ID!, override: String!): Boolean @auth
|
||||
setCustomerCustomInfoRequest(customerId: ID!, infoRequestId: ID!, data: JSON!): Boolean @auth
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
const { gql } = require('apollo-server-express')
|
||||
|
||||
const typeDef = gql`
|
||||
type CustomerCustomField {
|
||||
id: ID
|
||||
label: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type Customer {
|
||||
id: ID!
|
||||
authorizedOverride: String
|
||||
|
|
@ -42,6 +36,7 @@ const typeDef = gql`
|
|||
customFields: [CustomerCustomField]
|
||||
customInfoRequests: [CustomRequestData]
|
||||
notes: [CustomerNote]
|
||||
isTestCustomer: Boolean
|
||||
}
|
||||
|
||||
input CustomerInput {
|
||||
|
|
@ -86,6 +81,12 @@ const typeDef = gql`
|
|||
content: String
|
||||
}
|
||||
|
||||
type CustomerCustomField {
|
||||
id: ID
|
||||
label: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
customers(phone: String, name: String, address: String, id: String): [Customer] @auth
|
||||
customer(customerId: ID!): Customer @auth
|
||||
|
|
@ -94,15 +95,18 @@ const typeDef = gql`
|
|||
|
||||
type Mutation {
|
||||
setCustomer(customerId: ID!, customerInput: CustomerInput): Customer @auth
|
||||
addCustomField(customerId: ID!, label: String!, value: String!): CustomerCustomField @auth
|
||||
saveCustomField(customerId: ID!, fieldId: ID!, value: String!): CustomerCustomField @auth
|
||||
removeCustomField(customerId: ID!, fieldId: ID!): CustomerCustomField @auth
|
||||
addCustomField(customerId: ID!, label: String!, value: String!): Boolean @auth
|
||||
saveCustomField(customerId: ID!, fieldId: ID!, value: String!): Boolean @auth
|
||||
removeCustomField(customerId: ID!, fieldId: ID!): Boolean @auth
|
||||
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
|
||||
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth
|
||||
createCustomerNote(customerId: ID!, title: String!, content: String!): Boolean @auth
|
||||
editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth
|
||||
deleteCustomerNote(noteId: ID!): Boolean @auth
|
||||
createCustomer(phoneNumber: String): Customer @auth
|
||||
enableTestCustomer(customerId: ID!): Boolean @auth
|
||||
disableTestCustomer(customerId: ID!): Boolean @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ const typeDef = gql`
|
|||
}
|
||||
|
||||
type Query {
|
||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String): [Transaction] @auth
|
||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, timezone: String, simplified: Boolean): String @auth
|
||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, excludeTestingCustomers: Boolean): [Transaction] @auth
|
||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
|
||||
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
transactionFilters: [Filter] @auth
|
||||
|
|
|
|||
|
|
@ -35,8 +35,10 @@ const getAllCustomInfoRequestsForCustomer = (customerId) => {
|
|||
return db.any(sql, [customerId]).then(res => res.map(item => ({
|
||||
customerId: item.customer_id,
|
||||
infoRequestId: item.info_request_id,
|
||||
approved: item.approved,
|
||||
customerData: item.customer_data
|
||||
customerData: item.customer_data,
|
||||
override: item.override,
|
||||
overrideAt: item.override_at,
|
||||
overrideBy: item.override_by
|
||||
})))
|
||||
}
|
||||
|
||||
|
|
@ -46,8 +48,10 @@ const getCustomInfoRequestForCustomer = (customerId, infoRequestId) => {
|
|||
return {
|
||||
customerId: item.customer_id,
|
||||
infoRequestId: item.info_request_id,
|
||||
approved: item.approved,
|
||||
customerData: item.customer_data
|
||||
customerData: item.customer_data,
|
||||
override: item.override,
|
||||
overrideAt: item.override_at,
|
||||
overrideBy: item.override_by
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -61,8 +65,10 @@ const batchGetAllCustomInfoRequestsForCustomer = (customerIds) => {
|
|||
return items.map(item => ({
|
||||
customerId: item.customer_id,
|
||||
infoRequestId: item.info_request_id,
|
||||
approved: item.approved,
|
||||
customerData: item.customer_data
|
||||
customerData: item.customer_data,
|
||||
override: item.override,
|
||||
overrideAt: item.override_at,
|
||||
overrideBy: item.override_by
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
|
@ -93,9 +99,9 @@ const batchGetCustomInfoRequest = (infoRequestIds) => {
|
|||
})
|
||||
}
|
||||
|
||||
const setAuthorizedCustomRequest = (customerId, infoRequestId, isAuthorized) => {
|
||||
const sql = `UPDATE customers_custom_info_requests SET approved = $1 WHERE customer_id = $2 AND info_request_id = $3`
|
||||
return db.none(sql, [isAuthorized, customerId, infoRequestId]).then(() => true)
|
||||
const setAuthorizedCustomRequest = (customerId, infoRequestId, override, token) => {
|
||||
const sql = `UPDATE customers_custom_info_requests SET override = $1, override_by = $2, override_at = now() WHERE customer_id = $3 AND info_request_id = $4`
|
||||
return db.none(sql, [override, token, customerId, infoRequestId]).then(() => true)
|
||||
}
|
||||
|
||||
const setCustomerData = (customerId, infoRequestId, data) => {
|
||||
|
|
@ -103,7 +109,7 @@ const setCustomerData = (customerId, infoRequestId, data) => {
|
|||
INSERT INTO customers_custom_info_requests (customer_id, info_request_id, customer_data)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (customer_id, info_request_id)
|
||||
DO UPDATE SET customer_data = $3, approved = null`
|
||||
DO UPDATE SET customer_data = $3`
|
||||
return db.none(sql, [customerId, infoRequestId, data])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ function batch (
|
|||
cryptoCode = null,
|
||||
toAddress = null,
|
||||
status = null,
|
||||
excludeTestingCustomers = false,
|
||||
simplified = false
|
||||
) {
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||
|
|
@ -67,6 +68,7 @@ function batch (
|
|||
AND ($11 is null or txs.crypto_code = $11)
|
||||
AND ($12 is null or txs.to_address = $12)
|
||||
AND ($13 is null or txs.txStatus = $13)
|
||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||
AND (fiat > 0)
|
||||
ORDER BY created DESC limit $4 offset $5`
|
||||
|
||||
|
|
@ -98,6 +100,7 @@ function batch (
|
|||
AND ($11 is null or txs.crypto_code = $11)
|
||||
AND ($12 is null or txs.to_address = $12)
|
||||
AND ($13 is null or txs.txStatus = $13)
|
||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||
AND (fiat > 0)
|
||||
ORDER BY created DESC limit $4 offset $5`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const _ = require('lodash/fp')
|
||||
const { getCustomInfoRequests } = require('./new-admin/services/customInfoRequests')
|
||||
|
||||
const namespaces = {
|
||||
WALLETS: 'wallets',
|
||||
|
|
@ -107,6 +108,8 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
|
|||
const getTriggers = _.get('triggers')
|
||||
|
||||
const getTriggersAutomation = config => {
|
||||
return getCustomInfoRequests(true)
|
||||
.then(infoRequests => {
|
||||
const defaultAutomation = _.get('triggersConfig_automation')(config)
|
||||
const requirements = {
|
||||
sanctions: defaultAutomation,
|
||||
|
|
@ -116,6 +119,10 @@ const getTriggersAutomation = config => {
|
|||
usSsn: defaultAutomation
|
||||
}
|
||||
|
||||
_.forEach(it => {
|
||||
requirements[it.id] = defaultAutomation
|
||||
}, infoRequests)
|
||||
|
||||
const overrides = _.get('triggersConfig_overrides')(config)
|
||||
|
||||
const requirementsOverrides = _.reduce((acc, override) => {
|
||||
|
|
@ -123,6 +130,7 @@ const getTriggersAutomation = config => {
|
|||
}, {}, overrides)
|
||||
|
||||
return _.assign(requirements, requirementsOverrides)
|
||||
})
|
||||
}
|
||||
|
||||
const splitGetFirst = _.compose(_.head, _.split('_'))
|
||||
|
|
|
|||
|
|
@ -20,8 +20,13 @@ const machineLoader = require('../machine-loader')
|
|||
const { loadLatestConfig } = require('../new-settings-loader')
|
||||
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
|
||||
|
||||
function updateCustomerCustomInfoRequest (customerId, dataToSave, req, res) {
|
||||
return customInfoRequestQueries.setCustomerData(customerId, dataToSave.info_request_id, dataToSave)
|
||||
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
|
||||
if (_.isNil(patch.data)) {
|
||||
return customers.getById(customerId)
|
||||
.then(customer => respond(req, res, { customer }))
|
||||
}
|
||||
|
||||
return customInfoRequestQueries.setCustomerData(customerId, patch.infoRequestId, patch)
|
||||
.then(() => customers.getById(customerId))
|
||||
.then(customer => respond(req, res, { customer }))
|
||||
}
|
||||
|
|
@ -35,7 +40,7 @@ function updateCustomer (req, res, next) {
|
|||
const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
|
||||
|
||||
if (patch.customRequestPatch) {
|
||||
return updateCustomerCustomInfoRequest(id, patch.dataToSave, req, res).catch(next)
|
||||
return updateCustomerCustomInfoRequest(id, patch.customRequestPatch, req, res).catch(next)
|
||||
}
|
||||
|
||||
customers.getById(id)
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ const createTerms = terms => (terms.active && terms.text) ? ({
|
|||
const buildTriggers = (allTriggers) => {
|
||||
const normalTriggers = []
|
||||
const customTriggers = _.filter(o => {
|
||||
if (o.customInfoRequestId === '') normalTriggers.push(o)
|
||||
return o.customInfoRequestId !== ''
|
||||
if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o)
|
||||
return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId)
|
||||
}, allTriggers)
|
||||
|
||||
return _.flow([_.map(_.get('customInfoRequestId')), customRequestQueries.batchGetCustomInfoRequest])(customTriggers)
|
||||
|
|
@ -73,7 +73,7 @@ function poll (req, res, next) {
|
|||
const pi = plugins(settings, deviceId)
|
||||
const hasLightning = checkHasLightning(settings)
|
||||
|
||||
const triggersAutomation = configManager.getTriggersAutomation(settings.config)
|
||||
const triggersAutomationPromise = configManager.getTriggersAutomation(settings.config)
|
||||
const triggersPromise = buildTriggers(configManager.getTriggers(settings.config))
|
||||
|
||||
const operatorInfo = configManager.getOperatorInfo(settings.config)
|
||||
|
|
@ -84,8 +84,8 @@ function poll (req, res, next) {
|
|||
|
||||
state.pids[operatorId] = { [deviceId]: { pid, ts: Date.now() } }
|
||||
|
||||
return Promise.all([pi.pollQueries(serialNumber, deviceTime, req.query, machineVersion, machineModel), triggersPromise])
|
||||
.then(([results, triggers]) => {
|
||||
return Promise.all([pi.pollQueries(serialNumber, deviceTime, req.query, machineVersion, machineModel), triggersPromise, triggersAutomationPromise])
|
||||
.then(([results, triggers, triggersAutomation]) => {
|
||||
const cassettes = results.cassettes
|
||||
|
||||
const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid
|
||||
|
|
|
|||
13
migrations/1641394367865-testing-customer-toggle.js
Normal file
13
migrations/1641394367865-testing-customer-toggle.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`ALTER TABLE customers ADD COLUMN is_test_customer BOOLEAN NOT NULL DEFAULT false`,
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
16
migrations/1642518884925-manual-custom-info-requests.js
Normal file
16
migrations/1642518884925-manual-custom-info-requests.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`ALTER TABLE customers_custom_info_requests DROP COLUMN approved`,
|
||||
`ALTER TABLE customers_custom_info_requests ADD COLUMN override verification_type NOT NULL DEFAULT 'automatic'`,
|
||||
`ALTER TABLE customers_custom_info_requests ADD COLUMN override_by UUID REFERENCES users(id)`,
|
||||
`ALTER TABLE customers_custom_info_requests ADD COLUMN override_at TIMESTAMPTZ`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
5
new-lamassu-admin/package-lock.json
generated
5
new-lamassu-admin/package-lock.json
generated
|
|
@ -13870,6 +13870,11 @@
|
|||
"delegate": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"google-libphonenumber": {
|
||||
"version": "3.2.22",
|
||||
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.22.tgz",
|
||||
"integrity": "sha512-lzEllxWc05n/HEv75SsDrA7zdEVvQzTZimItZm/TZ5XBs7cmx2NJmSlA5I0kZbdKNu8GFETBhSpo+SOhx0JslA=="
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"downshift": "3.3.4",
|
||||
"file-saver": "2.0.2",
|
||||
"formik": "2.2.0",
|
||||
"google-libphonenumber": "^3.2.22",
|
||||
"graphql": "^14.5.8",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"jss-plugin-extend": "^10.0.0",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ export const Carousel = memo(({ photosData, slidePhoto }) => {
|
|||
opacity: 1
|
||||
}
|
||||
}}
|
||||
// navButtonsWrapperProps={{
|
||||
// style: {
|
||||
// background: 'linear-gradient(to right, black 10%, transparent 80%)',
|
||||
// opacity: '0.4'
|
||||
// }
|
||||
// }}
|
||||
autoPlay={false}
|
||||
indicators={false}
|
||||
navButtonsAlwaysVisible={true}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,8 @@ const LogsDownloaderPopover = ({
|
|||
fetchLogs({
|
||||
variables: {
|
||||
...args,
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED,
|
||||
excludeTestingCustomers: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -196,7 +197,8 @@ const LogsDownloaderPopover = ({
|
|||
...args,
|
||||
from: range.from,
|
||||
until: range.until,
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED,
|
||||
excludeTestingCustomers: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ const styles = {
|
|||
position: 'relative',
|
||||
marginBottom: spacer / 2,
|
||||
paddingTop: spacer * 1.5,
|
||||
'& > *:first-child': {
|
||||
marginRight: 24
|
||||
},
|
||||
'& > *': {
|
||||
marginRight: 10
|
||||
},
|
||||
|
|
@ -74,7 +77,8 @@ const styles = {
|
|||
notificationContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
width: 300
|
||||
},
|
||||
unread: {
|
||||
backgroundColor: spring3
|
||||
|
|
@ -89,8 +93,7 @@ const styles = {
|
|||
flexGrow: 1
|
||||
},
|
||||
unreadIcon: {
|
||||
marginLeft: spacer,
|
||||
marginTop: 5,
|
||||
marginTop: 2,
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
backgroundColor: secondaryColor,
|
||||
|
|
|
|||
|
|
@ -13,12 +13,27 @@ import styles from './NotificationCenter.styles'
|
|||
const useStyles = makeStyles(styles)
|
||||
|
||||
const types = {
|
||||
transaction: { display: 'Transactions', icon: <Transaction /> },
|
||||
highValueTransaction: { display: 'Transactions', icon: <Transaction /> },
|
||||
fiatBalance: { display: 'Maintenance', icon: <Wrench /> },
|
||||
cryptoBalance: { display: 'Maintenance', icon: <Wrench /> },
|
||||
compliance: { display: 'Compliance', icon: <WarningIcon /> },
|
||||
error: { display: 'Error', icon: <WarningIcon /> }
|
||||
transaction: {
|
||||
display: 'Transactions',
|
||||
icon: <Transaction height={16} width={16} />
|
||||
},
|
||||
highValueTransaction: {
|
||||
display: 'Transactions',
|
||||
icon: <Transaction height={16} width={16} />
|
||||
},
|
||||
fiatBalance: {
|
||||
display: 'Maintenance',
|
||||
icon: <Wrench height={16} width={16} />
|
||||
},
|
||||
cryptoBalance: {
|
||||
display: 'Maintenance',
|
||||
icon: <Wrench height={16} width={16} />
|
||||
},
|
||||
compliance: {
|
||||
display: 'Compliance',
|
||||
icon: <WarningIcon height={16} width={16} />
|
||||
},
|
||||
error: { display: 'Error', icon: <WarningIcon height={16} width={16} /> }
|
||||
}
|
||||
|
||||
const NotificationRow = ({
|
||||
|
|
@ -35,7 +50,9 @@ const NotificationRow = ({
|
|||
const classes = useStyles()
|
||||
|
||||
const typeDisplay = R.path([type, 'display'])(types) ?? null
|
||||
const icon = R.path([type, 'icon'])(types) ?? <Wrench />
|
||||
const icon = R.path([type, 'icon'])(types) ?? (
|
||||
<Wrench height={16} width={16} />
|
||||
)
|
||||
const age = prettyMs(new Date().getTime() - new Date(created).getTime(), {
|
||||
compact: true,
|
||||
verbose: true
|
||||
|
|
@ -57,7 +74,9 @@ const NotificationRow = ({
|
|||
classes.notificationRow,
|
||||
!read && valid ? classes.unread : ''
|
||||
)}>
|
||||
<div className={classes.notificationRowIcon}>{icon}</div>
|
||||
<div className={classes.notificationRowIcon}>
|
||||
<div>{icon}</div>
|
||||
</div>
|
||||
<div className={classes.notificationContent}>
|
||||
<Label2 className={classes.notificationTitle}>
|
||||
{notificationTitle}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ const Header = memo(({ tree, user }) => {
|
|||
return (
|
||||
<NavLink
|
||||
key={idx}
|
||||
to={!R.isNil(it.children) ? it.children[0].route : it.route}
|
||||
to={it.route || it.children[0].route}
|
||||
isActive={match => {
|
||||
if (!match) return false
|
||||
setActive(it)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ const TitleSection = ({
|
|||
buttons = [],
|
||||
children,
|
||||
appendix,
|
||||
appendixClassName
|
||||
appendixRight
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<div className={classnames(classes.titleWrapper, className)}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<Title>{title}</Title>
|
||||
{appendix && <div className={appendixClassName}>{appendix}</div>}
|
||||
{!!appendix && appendix}
|
||||
{error && (
|
||||
<ErrorMessage className={classes.error}>Failed to save</ErrorMessage>
|
||||
)}
|
||||
|
|
@ -46,13 +46,14 @@ const TitleSection = ({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<Box display="flex" flexDirection="row">
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
{(labels ?? []).map(({ icon, label }, idx) => (
|
||||
<Box key={idx} display="flex" alignItems="center">
|
||||
<div className={classes.icon}>{icon}</div>
|
||||
<Label1 className={classes.label}>{label}</Label1>
|
||||
</Box>
|
||||
))}
|
||||
{appendixRight}
|
||||
</Box>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { useQuery } from '@apollo/react-hooks'
|
|||
import { Box } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import { endOfToday } from 'date-fns'
|
||||
import { subDays } from 'date-fns/fp'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
|
@ -42,8 +44,16 @@ const TIME_OPTIONS = {
|
|||
}
|
||||
|
||||
const GET_TRANSACTIONS = gql`
|
||||
query transactions($limit: Int, $from: Date, $until: Date) {
|
||||
transactions(limit: $limit, from: $from, until: $until) {
|
||||
query transactions(
|
||||
$from: Date
|
||||
$until: Date
|
||||
$excludeTestingCustomers: Boolean
|
||||
) {
|
||||
transactions(
|
||||
from: $from
|
||||
until: $until
|
||||
excludeTestingCustomers: $excludeTestingCustomers
|
||||
) {
|
||||
id
|
||||
txClass
|
||||
txHash
|
||||
|
|
@ -116,7 +126,13 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
|
|||
const Analytics = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { data: txResponse, loading: txLoading } = useQuery(GET_TRANSACTIONS)
|
||||
const { data: txResponse, loading: txLoading } = useQuery(GET_TRANSACTIONS, {
|
||||
variables: {
|
||||
from: subDays(65, endOfToday()),
|
||||
until: endOfToday(),
|
||||
excludeTestingCustomers: true
|
||||
}
|
||||
})
|
||||
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||
|
||||
const [representing, setRepresenting] = useState(REPRESENTING_OPTIONS[0])
|
||||
|
|
|
|||
|
|
@ -46,23 +46,24 @@ const GET_USER_DATA = gql`
|
|||
`
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
client: Yup.string()
|
||||
.required('Client field is required!')
|
||||
.email('Username field should be in an email format!'),
|
||||
email: Yup.string()
|
||||
.label('Email')
|
||||
.required()
|
||||
.email(),
|
||||
password: Yup.string().required('Password field is required'),
|
||||
rememberMe: Yup.boolean()
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
client: '',
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
}
|
||||
|
||||
const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (mutationError) return 'Invalid login/password combination'
|
||||
if (formikErrors.client && formikTouched.client) return formikErrors.client
|
||||
if (mutationError) return 'Invalid email/password combination'
|
||||
if (formikErrors.email && formikTouched.email) return formikErrors.email
|
||||
if (formikErrors.password && formikTouched.password)
|
||||
return formikErrors.password
|
||||
return null
|
||||
|
|
@ -142,13 +143,13 @@ const LoginState = ({ state, dispatch, strategy }) => {
|
|||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={values =>
|
||||
submitLogin(values.client, values.password, values.rememberMe)
|
||||
submitLogin(values.email, values.password, values.rememberMe)
|
||||
}>
|
||||
{({ errors, touched }) => (
|
||||
<Form id="login-form">
|
||||
<Field
|
||||
name="client"
|
||||
label="Client"
|
||||
name="email"
|
||||
label="Email"
|
||||
size="lg"
|
||||
component={TextInput}
|
||||
fullWidth
|
||||
|
|
|
|||
|
|
@ -210,6 +210,10 @@ const Register = () => {
|
|||
{!loading && state.result === 'failure' && (
|
||||
<>
|
||||
<Label3>Link has expired</Label3>
|
||||
<Label3>
|
||||
To obtain a new link, run the command{' '}
|
||||
<strong>lamassu-register</strong> in your server’s terminal.
|
||||
</Label3>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -140,14 +140,13 @@ const Setup2FAState = ({ state, dispatch }) => {
|
|||
<>
|
||||
<div className={classes.infoWrapper}>
|
||||
<Label3 className={classes.info2}>
|
||||
We detected that this account does not have its two-factor
|
||||
authentication enabled. In order to protect the resources in the
|
||||
system, a two-factor authentication is enforced.
|
||||
This account does not yet have two-factor authentication enabled. To
|
||||
secure the admin, two-factor authentication is required.
|
||||
</Label3>
|
||||
<Label3 className={classes.info2}>
|
||||
To finish this process, please scan the following QR code or insert
|
||||
the secret further below on an authentication app of your choice,
|
||||
such as Google Authenticator or Authy.
|
||||
To complete the registration process, scan the following QR code or
|
||||
insert the secret below on a 2FA app, such as Google Authenticator
|
||||
or AndOTP.
|
||||
</Label3>
|
||||
</div>
|
||||
<div className={classes.qrCodeWrapper}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { parse, format, isValid } from 'date-fns/fp'
|
||||
import { parse, format } from 'date-fns/fp'
|
||||
import _ from 'lodash/fp'
|
||||
import * as R from 'ramda'
|
||||
import { useState, React } from 'react'
|
||||
|
|
@ -26,6 +26,11 @@ import { URI } from 'src/utils/apollo'
|
|||
|
||||
import styles from './CustomerData.styles.js'
|
||||
import { EditableCard } from './components'
|
||||
import {
|
||||
customerDataElements,
|
||||
customerDataSchemas,
|
||||
formatDates
|
||||
} from './helper.js'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
|
@ -63,7 +68,8 @@ const CustomerData = ({
|
|||
editCustomer,
|
||||
deleteEditedData,
|
||||
updateCustomRequest,
|
||||
authorizeCustomRequest
|
||||
authorizeCustomRequest,
|
||||
updateCustomEntry
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const [listView, setListView] = useState(false)
|
||||
|
|
@ -84,8 +90,8 @@ const CustomerData = ({
|
|||
R.compose(R.toLower, R.path(['customInfoRequest', 'customRequest', 'name']))
|
||||
)
|
||||
|
||||
const customEntries = null // get customer custom entries
|
||||
const customRequirements = [] // get customer custom requirements
|
||||
const customFields = []
|
||||
const customRequirements = []
|
||||
const customInfoRequests = sortByName(
|
||||
R.path(['customInfoRequests'])(customer) ?? []
|
||||
)
|
||||
|
|
@ -94,87 +100,8 @@ const CustomerData = ({
|
|||
|
||||
const getVisibleCards = _.filter(elem => elem.isAvailable)
|
||||
|
||||
const schemas = {
|
||||
idScan: Yup.object().shape({
|
||||
firstName: Yup.string().required(),
|
||||
lastName: Yup.string().required(),
|
||||
documentNumber: Yup.string().required(),
|
||||
dateOfBirth: Yup.string()
|
||||
.test({
|
||||
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
|
||||
})
|
||||
.required(),
|
||||
gender: Yup.string().required(),
|
||||
country: Yup.string().required(),
|
||||
expirationDate: Yup.string()
|
||||
.test({
|
||||
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
|
||||
})
|
||||
.required()
|
||||
}),
|
||||
usSsn: Yup.object().shape({
|
||||
usSsn: Yup.string().required()
|
||||
}),
|
||||
idCardPhoto: Yup.object().shape({
|
||||
idCardPhoto: Yup.mixed().required()
|
||||
}),
|
||||
frontCamera: Yup.object().shape({
|
||||
frontCamera: Yup.mixed().required()
|
||||
})
|
||||
}
|
||||
|
||||
const idScanElements = [
|
||||
{
|
||||
name: 'firstName',
|
||||
label: 'First name',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'documentNumber',
|
||||
label: 'ID number',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'dateOfBirth',
|
||||
label: 'Birthdate',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'gender',
|
||||
label: 'Gender',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
label: 'Last name',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'expirationDate',
|
||||
label: 'Expiration Date',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'country',
|
||||
label: 'Country',
|
||||
component: TextInput
|
||||
}
|
||||
]
|
||||
|
||||
const usSsnElements = [
|
||||
{
|
||||
name: 'usSsn',
|
||||
label: 'US SSN',
|
||||
component: TextInput,
|
||||
size: 190
|
||||
}
|
||||
]
|
||||
|
||||
const idCardPhotoElements = [{ name: 'idCardPhoto' }]
|
||||
const frontCameraElements = [{ name: 'frontCamera' }]
|
||||
|
||||
const initialValues = {
|
||||
idScan: {
|
||||
idCardData: {
|
||||
firstName: R.path(['firstName'])(idData) ?? '',
|
||||
lastName: R.path(['lastName'])(idData) ?? '',
|
||||
documentNumber: R.path(['documentNumber'])(idData) ?? '',
|
||||
|
|
@ -202,19 +129,9 @@ const CustomerData = ({
|
|||
}
|
||||
}
|
||||
|
||||
const formatDates = values => {
|
||||
_.map(
|
||||
elem =>
|
||||
(values[elem] = format('yyyyMMdd')(
|
||||
parse(new Date(), 'yyyy-MM-dd', values[elem])
|
||||
))
|
||||
)(['dateOfBirth', 'expirationDate'])
|
||||
return values
|
||||
}
|
||||
|
||||
const cards = [
|
||||
{
|
||||
fields: idScanElements,
|
||||
fields: customerDataElements.idCardData,
|
||||
title: 'ID Scan',
|
||||
titleIcon: <CardIcon className={classes.cardIcon} />,
|
||||
state: R.path(['idCardDataOverride'])(customer),
|
||||
|
|
@ -226,8 +143,8 @@ const CustomerData = ({
|
|||
editCustomer({
|
||||
idCardData: _.merge(idData, formatDates(values))
|
||||
}),
|
||||
validationSchema: schemas.idScan,
|
||||
initialValues: initialValues.idScan,
|
||||
validationSchema: customerDataSchemas.idCardData,
|
||||
initialValues: initialValues.idCardData,
|
||||
isAvailable: !_.isNil(idData)
|
||||
},
|
||||
{
|
||||
|
|
@ -257,7 +174,7 @@ const CustomerData = ({
|
|||
isAvailable: !_.isNil(sanctions)
|
||||
},
|
||||
{
|
||||
fields: frontCameraElements,
|
||||
fields: customerDataElements.frontCamera,
|
||||
title: 'Front facing camera',
|
||||
titleIcon: <EditIcon className={classes.editIcon} />,
|
||||
state: R.path(['frontCameraOverride'])(customer),
|
||||
|
|
@ -279,12 +196,12 @@ const CustomerData = ({
|
|||
/>
|
||||
) : null,
|
||||
hasImage: true,
|
||||
validationSchema: schemas.frontCamera,
|
||||
validationSchema: customerDataSchemas.frontCamera,
|
||||
initialValues: initialValues.frontCamera,
|
||||
isAvailable: !_.isNil(customer.frontCameraPath)
|
||||
},
|
||||
{
|
||||
fields: idCardPhotoElements,
|
||||
fields: customerDataElements.idCardPhoto,
|
||||
title: 'ID card image',
|
||||
titleIcon: <EditIcon className={classes.editIcon} />,
|
||||
state: R.path(['idCardPhotoOverride'])(customer),
|
||||
|
|
@ -304,27 +221,26 @@ const CustomerData = ({
|
|||
/>
|
||||
) : null,
|
||||
hasImage: true,
|
||||
validationSchema: schemas.idCardPhoto,
|
||||
validationSchema: customerDataSchemas.idCardPhoto,
|
||||
initialValues: initialValues.idCardPhoto,
|
||||
isAvailable: !_.isNil(customer.idCardPhotoPath)
|
||||
},
|
||||
{
|
||||
fields: usSsnElements,
|
||||
fields: customerDataElements.usSsn,
|
||||
title: 'US SSN',
|
||||
titleIcon: <CardIcon className={classes.cardIcon} />,
|
||||
state: R.path(['usSsnOverride'])(customer),
|
||||
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
|
||||
save: values => editCustomer({ usSsn: values.usSsn }),
|
||||
save: values => editCustomer(values),
|
||||
deleteEditedData: () => deleteEditedData({ usSsn: null }),
|
||||
validationSchema: schemas.usSsn,
|
||||
validationSchema: customerDataSchemas.usSsn,
|
||||
initialValues: initialValues.usSsn,
|
||||
isAvailable: !_.isNil(customer.usSsn)
|
||||
}
|
||||
]
|
||||
|
||||
R.forEach(it => {
|
||||
console.log('it', it)
|
||||
customRequirements.push({
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -336,12 +252,13 @@ const CustomerData = ({
|
|||
],
|
||||
title: it.customInfoRequest.customRequest.name,
|
||||
titleIcon: <CardIcon className={classes.cardIcon} />,
|
||||
state: R.path(['override'])(it),
|
||||
authorize: () =>
|
||||
authorizeCustomRequest({
|
||||
variables: {
|
||||
customerId: it.customerId,
|
||||
infoRequestId: it.customInfoRequest.id,
|
||||
isAuthorized: true
|
||||
override: OVERRIDE_AUTHORIZED
|
||||
}
|
||||
}),
|
||||
reject: () =>
|
||||
|
|
@ -349,7 +266,7 @@ const CustomerData = ({
|
|||
variables: {
|
||||
customerId: it.customerId,
|
||||
infoRequestId: it.customInfoRequest.id,
|
||||
isAuthorized: false
|
||||
override: OVERRIDE_REJECTED
|
||||
}
|
||||
}),
|
||||
save: values => {
|
||||
|
|
@ -374,6 +291,34 @@ const CustomerData = ({
|
|||
})
|
||||
}, customInfoRequests)
|
||||
|
||||
R.forEach(it => {
|
||||
customFields.push({
|
||||
fields: [
|
||||
{
|
||||
name: it.label,
|
||||
label: it.label,
|
||||
value: it.value ?? '',
|
||||
component: TextInput
|
||||
}
|
||||
],
|
||||
title: it.label,
|
||||
titleIcon: <EditIcon className={classes.editIcon} />,
|
||||
save: values => {
|
||||
updateCustomEntry({
|
||||
fieldId: it.id,
|
||||
value: values[it.label]
|
||||
})
|
||||
},
|
||||
deleteEditedData: () => {},
|
||||
validationSchema: Yup.object().shape({
|
||||
[it.label]: Yup.string()
|
||||
}),
|
||||
initialValues: {
|
||||
[it.label]: it.value ?? ''
|
||||
}
|
||||
})
|
||||
}, R.path(['customFields'])(customer) ?? [])
|
||||
|
||||
const editableCard = (
|
||||
{
|
||||
title,
|
||||
|
|
@ -415,6 +360,9 @@ const CustomerData = ({
|
|||
<div>
|
||||
<div className={classes.header}>
|
||||
<H3 className={classes.title}>{'Customer data'}</H3>
|
||||
{// TODO: Remove false condition for next release
|
||||
false && (
|
||||
<>
|
||||
<FeatureButton
|
||||
active={!listView}
|
||||
className={classes.viewIcons}
|
||||
|
|
@ -428,6 +376,8 @@ const CustomerData = ({
|
|||
Icon={CustomerListViewIcon}
|
||||
InverseIcon={CustomerListViewReversedIcon}
|
||||
onClick={() => setListView(true)}></FeatureButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!listView && customer && (
|
||||
|
|
@ -444,9 +394,21 @@ const CustomerData = ({
|
|||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{customEntries && (
|
||||
{!_.isEmpty(customFields) && (
|
||||
<div className={classes.wrapper}>
|
||||
<span className={classes.separator}>Custom data entry</span>
|
||||
<Grid container>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{customFields.map((elem, idx) => {
|
||||
return isEven(idx) ? editableCard(elem, idx) : null
|
||||
})}
|
||||
</Grid>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{customFields.map((elem, idx) => {
|
||||
return !isEven(idx) ? editableCard(elem, idx) : null
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
{!R.isEmpty(customRequirements) && (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import React, { memo, useState } from 'react'
|
|||
import { useHistory, useParams } from 'react-router-dom'
|
||||
|
||||
import { ActionButton } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import { Label1, Label2 } from 'src/components/typography'
|
||||
import {
|
||||
OVERRIDE_AUTHORIZED,
|
||||
|
|
@ -18,8 +19,9 @@ import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/bl
|
|||
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/zodiac.svg'
|
||||
import { ReactComponent as DataReversedIcon } from 'src/styling/icons/button/data/white.svg'
|
||||
import { ReactComponent as DataIcon } from 'src/styling/icons/button/data/zodiac.svg'
|
||||
import { ReactComponent as DiscountReversedIcon } from 'src/styling/icons/button/discount/white.svg'
|
||||
import { ReactComponent as Discount } from 'src/styling/icons/button/discount/zodiac.svg'
|
||||
// TODO: Enable for next release
|
||||
// import { ReactComponent as DiscountReversedIcon } from 'src/styling/icons/button/discount/white.svg'
|
||||
// import { ReactComponent as Discount } from 'src/styling/icons/button/discount/zodiac.svg'
|
||||
import { fromNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import CustomerData from './CustomerData'
|
||||
|
|
@ -66,6 +68,7 @@ const GET_CUSTOMER = gql`
|
|||
lastTxClass
|
||||
daysSuspended
|
||||
isSuspended
|
||||
isTestCustomer
|
||||
customFields {
|
||||
id
|
||||
label
|
||||
|
|
@ -95,7 +98,9 @@ const GET_CUSTOMER = gql`
|
|||
}
|
||||
customInfoRequests {
|
||||
customerId
|
||||
approved
|
||||
override
|
||||
overrideBy
|
||||
overrideAt
|
||||
customerData
|
||||
customInfoRequest {
|
||||
id
|
||||
|
|
@ -180,12 +185,12 @@ const SET_AUTHORIZED_REQUEST = gql`
|
|||
mutation setAuthorizedCustomRequest(
|
||||
$customerId: ID!
|
||||
$infoRequestId: ID!
|
||||
$isAuthorized: Boolean!
|
||||
$override: String!
|
||||
) {
|
||||
setAuthorizedCustomRequest(
|
||||
customerId: $customerId
|
||||
infoRequestId: $infoRequestId
|
||||
isAuthorized: $isAuthorized
|
||||
override: $override
|
||||
)
|
||||
}
|
||||
`
|
||||
|
|
@ -230,12 +235,45 @@ const EDIT_NOTE = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const ENABLE_TEST_CUSTOMER = gql`
|
||||
mutation enableTestCustomer($customerId: ID!) {
|
||||
enableTestCustomer(customerId: $customerId)
|
||||
}
|
||||
`
|
||||
|
||||
const DISABLE_TEST_CUSTOMER = gql`
|
||||
mutation disableTestCustomer($customerId: ID!) {
|
||||
disableTestCustomer(customerId: $customerId)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const SET_CUSTOM_ENTRY = gql`
|
||||
mutation addCustomField($customerId: ID!, $label: String!, $value: String!) {
|
||||
addCustomField(customerId: $customerId, label: $label, value: $value)
|
||||
}
|
||||
`
|
||||
|
||||
const EDIT_CUSTOM_ENTRY = gql`
|
||||
mutation saveCustomField($customerId: ID!, $fieldId: ID!, $value: String!) {
|
||||
saveCustomField(customerId: $customerId, fieldId: $fieldId, value: $value)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_ACTIVE_CUSTOM_REQUESTS = gql`
|
||||
query customInfoRequests($onlyEnabled: Boolean) {
|
||||
customInfoRequests(onlyEnabled: $onlyEnabled) {
|
||||
id
|
||||
customRequest
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CustomerProfile = memo(() => {
|
||||
const history = useHistory()
|
||||
|
||||
|
|
@ -255,6 +293,20 @@ const CustomerProfile = memo(() => {
|
|||
|
||||
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||
|
||||
const { data: activeCustomRequests } = useQuery(GET_ACTIVE_CUSTOM_REQUESTS, {
|
||||
variables: {
|
||||
onlyEnabled: true
|
||||
}
|
||||
})
|
||||
|
||||
const [setCustomEntry] = useMutation(SET_CUSTOM_ENTRY, {
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const [editCustomEntry] = useMutation(EDIT_CUSTOM_ENTRY, {
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, {
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
|
@ -294,6 +346,37 @@ const CustomerProfile = memo(() => {
|
|||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const saveCustomEntry = it => {
|
||||
setCustomEntry({
|
||||
variables: {
|
||||
customerId,
|
||||
label: it.title,
|
||||
value: it.data
|
||||
}
|
||||
})
|
||||
setWizard(null)
|
||||
}
|
||||
|
||||
const updateCustomEntry = it => {
|
||||
editCustomEntry({
|
||||
variables: {
|
||||
customerId,
|
||||
fieldId: it.fieldId,
|
||||
value: it.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const [enableTestCustomer] = useMutation(ENABLE_TEST_CUSTOMER, {
|
||||
variables: { customerId },
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const [disableTestCustomer] = useMutation(DISABLE_TEST_CUSTOMER, {
|
||||
variables: { customerId },
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const updateCustomer = it =>
|
||||
setCustomer({
|
||||
variables: {
|
||||
|
|
@ -302,7 +385,7 @@ const CustomerProfile = memo(() => {
|
|||
}
|
||||
})
|
||||
|
||||
const replacePhoto = it =>
|
||||
const replacePhoto = it => {
|
||||
replaceCustomerPhoto({
|
||||
variables: {
|
||||
customerId,
|
||||
|
|
@ -310,14 +393,18 @@ const CustomerProfile = memo(() => {
|
|||
photoType: it.photoType
|
||||
}
|
||||
})
|
||||
setWizard(null)
|
||||
}
|
||||
|
||||
const editCustomer = it =>
|
||||
const editCustomer = it => {
|
||||
editCustomerData({
|
||||
variables: {
|
||||
customerId,
|
||||
customerEdit: it
|
||||
}
|
||||
})
|
||||
setWizard(null)
|
||||
}
|
||||
|
||||
const deleteEditedData = it =>
|
||||
deleteCustomerEditedData({
|
||||
|
|
@ -385,6 +472,12 @@ const CustomerProfile = memo(() => {
|
|||
|
||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||
|
||||
const customInfoRequirementOptions =
|
||||
activeCustomRequests?.customInfoRequests?.map(it => ({
|
||||
value: it.id,
|
||||
display: it.customRequest.name
|
||||
})) ?? []
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
|
|
@ -411,13 +504,12 @@ const CustomerProfile = memo(() => {
|
|||
<div className={classes.panels}>
|
||||
<div className={classes.leftSidePanel}>
|
||||
{!loading && !customerData.isAnonymous && (
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
<CustomerSidebar
|
||||
isSelected={code => code === clickedItem}
|
||||
onClick={onClickSidebarItem}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label1 className={classes.actionLabel}>Actions</Label1>
|
||||
<div className={classes.actionBar}>
|
||||
<ActionButton
|
||||
|
|
@ -428,14 +520,14 @@ const CustomerProfile = memo(() => {
|
|||
onClick={() => setWizard(true)}>
|
||||
{`Manual data entry`}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
{/* <ActionButton
|
||||
className={classes.actionButton}
|
||||
color="primary"
|
||||
Icon={Discount}
|
||||
InverseIcon={DiscountReversedIcon}
|
||||
onClick={() => {}}>
|
||||
{`Add individual discount`}
|
||||
</ActionButton>
|
||||
</ActionButton> */}
|
||||
{isSuspended && (
|
||||
<ActionButton
|
||||
className={classes.actionButton}
|
||||
|
|
@ -487,6 +579,26 @@ const CustomerProfile = memo(() => {
|
|||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label1 className={classes.actionLabel}>
|
||||
{`Special user status`}
|
||||
</Label1>
|
||||
<div className={classes.actionBar}>
|
||||
<div className={classes.userStatusAction}>
|
||||
<Switch
|
||||
checked={!!R.path(['isTestCustomer'])(customerData)}
|
||||
value={!!R.path(['isTestCustomer'])(customerData)}
|
||||
onChange={() =>
|
||||
R.path(['isTestCustomer'])(customerData)
|
||||
? disableTestCustomer()
|
||||
: enableTestCustomer()
|
||||
}
|
||||
/>
|
||||
{`Test user`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.rightSidePanel}>
|
||||
|
|
@ -522,7 +634,8 @@ const CustomerProfile = memo(() => {
|
|||
editCustomer={editCustomer}
|
||||
deleteEditedData={deleteEditedData}
|
||||
updateCustomRequest={setCustomerCustomInfoRequest}
|
||||
authorizeCustomRequest={authorizeCustomRequest}></CustomerData>
|
||||
authorizeCustomRequest={authorizeCustomRequest}
|
||||
updateCustomEntry={updateCustomEntry}></CustomerData>
|
||||
</div>
|
||||
)}
|
||||
{isNotes && (
|
||||
|
|
@ -544,8 +657,11 @@ const CustomerProfile = memo(() => {
|
|||
{wizard && (
|
||||
<Wizard
|
||||
error={error?.message}
|
||||
save={() => {}}
|
||||
save={saveCustomEntry}
|
||||
addPhoto={replacePhoto}
|
||||
addCustomerData={editCustomer}
|
||||
onClose={() => setWizard(null)}
|
||||
customInfoRequirementOptions={customInfoRequirementOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { comet } from 'src/styling/variables'
|
||||
import { comet, subheaderColor } from 'src/styling/variables'
|
||||
|
||||
export default {
|
||||
labelLink: {
|
||||
|
|
@ -34,6 +34,23 @@ export default {
|
|||
width: 1100
|
||||
},
|
||||
leftSidePanel: {
|
||||
width: 300
|
||||
width: 300,
|
||||
'& > *': {
|
||||
marginBottom: 25
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginBottom: 0
|
||||
},
|
||||
'& > *:first-child': {
|
||||
marginBottom: 50
|
||||
}
|
||||
},
|
||||
userStatusAction: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: subheaderColor,
|
||||
borderRadius: 8,
|
||||
padding: [[0, 5]]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { Box, makeStyles } from '@material-ui/core'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
|
@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom'
|
|||
|
||||
import SearchBox from 'src/components/SearchBox'
|
||||
import SearchFilter from 'src/components/SearchFilter'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import baseStyles from 'src/pages/Logs.styles'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
|
|
@ -14,6 +15,7 @@ import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-ou
|
|||
import { fromNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import CustomersList from './CustomersList'
|
||||
import CreateCustomerModal from './components/CreateCustomerModal'
|
||||
|
||||
const GET_CUSTOMER_FILTERS = gql`
|
||||
query filters {
|
||||
|
|
@ -43,14 +45,35 @@ const GET_CUSTOMERS = gql`
|
|||
lastTxFiatCode
|
||||
lastTxClass
|
||||
authorizedOverride
|
||||
frontCameraPath
|
||||
frontCameraOverride
|
||||
idCardPhotoPath
|
||||
idCardPhotoOverride
|
||||
idCardData
|
||||
idCardDataOverride
|
||||
usSsn
|
||||
usSsnOverride
|
||||
sanctions
|
||||
sanctionsOverride
|
||||
daysSuspended
|
||||
isSuspended
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CREATE_CUSTOMER = gql`
|
||||
mutation createCustomer($phoneNumber: String) {
|
||||
createCustomer(phoneNumber: $phoneNumber) {
|
||||
phone
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const useBaseStyles = makeStyles(baseStyles)
|
||||
|
||||
const getFiltersObj = filters =>
|
||||
R.reduce((s, f) => ({ ...s, [f.type]: f.value }), {}, filters)
|
||||
|
||||
const Customers = () => {
|
||||
const baseStyles = useBaseStyles()
|
||||
const history = useHistory()
|
||||
|
|
@ -61,6 +84,7 @@ const Customers = () => {
|
|||
const [filteredCustomers, setFilteredCustomers] = useState([])
|
||||
const [variables, setVariables] = useState({})
|
||||
const [filters, setFilters] = useState([])
|
||||
const [showCreationModal, setShowCreationModal] = useState(false)
|
||||
|
||||
const {
|
||||
data: customersResponse,
|
||||
|
|
@ -75,19 +99,25 @@ const Customers = () => {
|
|||
GET_CUSTOMER_FILTERS
|
||||
)
|
||||
|
||||
const [createNewCustomer] = useMutation(CREATE_CUSTOMER, {
|
||||
onCompleted: () => setShowCreationModal(false),
|
||||
refetchQueries: () => [
|
||||
{
|
||||
query: GET_CUSTOMERS,
|
||||
variables
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const configData = R.path(['config'])(customersResponse) ?? []
|
||||
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
|
||||
const customersData = R.sortWith([R.descend(R.prop('lastActive'))])(
|
||||
filteredCustomers ?? []
|
||||
)
|
||||
const triggers = configData && fromNamespace(namespaces.TRIGGERS, configData)
|
||||
const customersData = R.sortWith([
|
||||
R.descend(it => new Date(R.prop('lastActive', it) ?? '0'))
|
||||
])(filteredCustomers ?? [])
|
||||
|
||||
const onFilterChange = filters => {
|
||||
const filtersObject = R.compose(
|
||||
R.mergeAll,
|
||||
R.map(f => ({
|
||||
[f.type]: f.value
|
||||
}))
|
||||
)(filters)
|
||||
const filtersObject = getFiltersObj(filters)
|
||||
|
||||
setFilters(filters)
|
||||
|
||||
|
|
@ -101,10 +131,38 @@ const Customers = () => {
|
|||
refetch && refetch()
|
||||
}
|
||||
|
||||
const onFilterDelete = filter =>
|
||||
setFilters(
|
||||
R.filter(f => !R.whereEq(R.pick(['type', 'value'], f), filter))(filters)
|
||||
)
|
||||
const onFilterDelete = filter => {
|
||||
const newFilters = R.filter(
|
||||
f => !R.whereEq(R.pick(['type', 'value'], f), filter)
|
||||
)(filters)
|
||||
|
||||
setFilters(newFilters)
|
||||
|
||||
const filtersObject = getFiltersObj(newFilters)
|
||||
|
||||
setVariables({
|
||||
phone: filtersObject.phone,
|
||||
name: filtersObject.name,
|
||||
address: filtersObject.address,
|
||||
id: filtersObject.id
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
}
|
||||
|
||||
const deleteAllFilters = () => {
|
||||
setFilters([])
|
||||
const filtersObject = getFiltersObj([])
|
||||
|
||||
setVariables({
|
||||
phone: filtersObject.phone,
|
||||
name: filtersObject.name,
|
||||
address: filtersObject.address,
|
||||
id: filtersObject.id
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
}
|
||||
|
||||
const filterOptions = R.path(['customerFilters'])(filtersResponse)
|
||||
|
||||
|
|
@ -113,7 +171,7 @@ const Customers = () => {
|
|||
<TitleSection
|
||||
title="Customers"
|
||||
appendix={
|
||||
<div>
|
||||
<div className={baseStyles.buttonsWrapper}>
|
||||
<SearchBox
|
||||
loading={loadingFilters}
|
||||
filters={filters}
|
||||
|
|
@ -123,7 +181,13 @@ const Customers = () => {
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
appendixClassName={baseStyles.buttonsWrapper}
|
||||
appendixRight={
|
||||
<Box display="flex">
|
||||
<Link color="primary" onClick={() => setShowCreationModal(true)}>
|
||||
Add new user
|
||||
</Link>
|
||||
</Box>
|
||||
}
|
||||
labels={[
|
||||
{ label: 'Cash-in', icon: <TxInIcon /> },
|
||||
{ label: 'Cash-out', icon: <TxOutIcon /> }
|
||||
|
|
@ -131,9 +195,10 @@ const Customers = () => {
|
|||
/>
|
||||
{filters.length > 0 && (
|
||||
<SearchFilter
|
||||
entries={customersData.length}
|
||||
filters={filters}
|
||||
onFilterDelete={onFilterDelete}
|
||||
setFilters={setFilters}
|
||||
deleteAllFilters={deleteAllFilters}
|
||||
/>
|
||||
)}
|
||||
<CustomersList
|
||||
|
|
@ -141,6 +206,13 @@ const Customers = () => {
|
|||
locale={locale}
|
||||
onClick={handleCustomerClicked}
|
||||
loading={customerLoading}
|
||||
triggers={triggers}
|
||||
/>
|
||||
<CreateCustomerModal
|
||||
showModal={showCreationModal}
|
||||
handleClose={() => setShowCreationModal(false)}
|
||||
locale={locale}
|
||||
onSubmit={createNewCustomer}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,42 +13,42 @@ import { getAuthorizedStatus, getFormattedPhone, getName } from './helper'
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CustomersList = ({ data, locale, onClick, loading }) => {
|
||||
const CustomersList = ({ data, locale, onClick, loading, triggers }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Phone',
|
||||
width: 175,
|
||||
width: 199,
|
||||
view: it => getFormattedPhone(it.phone, locale.country)
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
width: 247,
|
||||
width: 241,
|
||||
view: getName
|
||||
},
|
||||
{
|
||||
header: 'Total TXs',
|
||||
width: 130,
|
||||
width: 126,
|
||||
textAlign: 'right',
|
||||
view: it => `${Number.parseInt(it.totalTxs)}`
|
||||
},
|
||||
{
|
||||
header: 'Total spent',
|
||||
width: 155,
|
||||
width: 152,
|
||||
textAlign: 'right',
|
||||
view: it =>
|
||||
`${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode ?? ''}`
|
||||
},
|
||||
{
|
||||
header: 'Last active',
|
||||
width: 137,
|
||||
width: 133,
|
||||
view: it =>
|
||||
(it.lastActive && format('yyyy-MM-dd', new Date(it.lastActive))) ?? ''
|
||||
},
|
||||
{
|
||||
header: 'Last transaction',
|
||||
width: 165,
|
||||
width: 161,
|
||||
textAlign: 'right',
|
||||
view: it => {
|
||||
const hasLastTx = !R.isNil(it.lastTxFiatCode)
|
||||
|
|
@ -66,7 +66,7 @@ const CustomersList = ({ data, locale, onClick, loading }) => {
|
|||
{
|
||||
header: 'Status',
|
||||
width: 191,
|
||||
view: it => <MainStatus statuses={[getAuthorizedStatus(it)]} />
|
||||
view: it => <MainStatus statuses={[getAuthorizedStatus(it, triggers)]} />
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@ import Stepper from 'src/components/Stepper'
|
|||
import { Button } from 'src/components/buttons'
|
||||
import { comet } from 'src/styling/variables'
|
||||
|
||||
import { entryType, customElements } from './helper'
|
||||
import {
|
||||
entryType,
|
||||
customElements,
|
||||
requirementElements,
|
||||
formatDates,
|
||||
REQUIREMENT,
|
||||
ID_CARD_DATA
|
||||
} from './helper'
|
||||
|
||||
const LAST_STEP = 2
|
||||
|
||||
|
|
@ -41,23 +48,40 @@ const styles = {
|
|||
margin: [[0, 4, 0, 2]],
|
||||
borderBottom: `1px solid ${comet}`,
|
||||
display: 'inline-block'
|
||||
},
|
||||
dropdownField: {
|
||||
marginTop: 16,
|
||||
minWidth: 155
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const getStep = (step, selectedValues) => {
|
||||
const elements =
|
||||
selectedValues?.entryType === REQUIREMENT &&
|
||||
!R.isNil(selectedValues?.requirement)
|
||||
? requirementElements[selectedValues?.requirement]
|
||||
: customElements[selectedValues?.dataType]
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
return entryType
|
||||
case 2:
|
||||
return customElements[selectedValues?.dataType]
|
||||
return elements
|
||||
default:
|
||||
return Fragment
|
||||
}
|
||||
}
|
||||
|
||||
const Wizard = ({ onClose, save, error }) => {
|
||||
const Wizard = ({
|
||||
onClose,
|
||||
save,
|
||||
error,
|
||||
customInfoRequirementOptions,
|
||||
addCustomerData,
|
||||
addPhoto
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [selectedValues, setSelectedValues] = useState(null)
|
||||
|
|
@ -66,6 +90,10 @@ const Wizard = ({ onClose, save, error }) => {
|
|||
step: 1
|
||||
})
|
||||
|
||||
const isIdCardData = values => values?.requirement === ID_CARD_DATA
|
||||
const formatCustomerData = (it, newConfig) =>
|
||||
isIdCardData(newConfig) ? { [newConfig.requirement]: formatDates(it) } : it
|
||||
|
||||
const isLastStep = step === LAST_STEP
|
||||
const stepOptions = getStep(step, selectedValues)
|
||||
|
||||
|
|
@ -74,7 +102,23 @@ const Wizard = ({ onClose, save, error }) => {
|
|||
setSelectedValues(newConfig)
|
||||
|
||||
if (isLastStep) {
|
||||
switch (stepOptions.saveType) {
|
||||
case 'customerData':
|
||||
return addCustomerData(formatCustomerData(it, newConfig))
|
||||
case 'customerDataUpload':
|
||||
return addPhoto({
|
||||
newPhoto: R.head(R.values(it)),
|
||||
photoType: R.head(R.keys(it))
|
||||
})
|
||||
case 'customEntry':
|
||||
return save(newConfig)
|
||||
case 'customInfoRequirement':
|
||||
return
|
||||
// case 'customerEntryUpload':
|
||||
// break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setState({
|
||||
|
|
@ -106,6 +150,7 @@ const Wizard = ({ onClose, save, error }) => {
|
|||
<Form className={classes.form}>
|
||||
<stepOptions.Component
|
||||
selectedValues={selectedValues}
|
||||
customInfoRequirementOptions={customInfoRequirementOptions}
|
||||
{...stepOptions.props}
|
||||
/>
|
||||
<div className={classes.submit}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import { PhoneNumberFormat, PhoneNumberUtil } from 'google-libphonenumber'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { H1 } from 'src/components/typography'
|
||||
import { spacer, primaryColor, fontPrimary } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
modalTitle: {
|
||||
marginTop: -5,
|
||||
color: primaryColor,
|
||||
fontFamily: fontPrimary
|
||||
},
|
||||
footer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [['auto', 0, spacer * 3, 0]]
|
||||
},
|
||||
form: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
},
|
||||
submit: {
|
||||
margin: [['auto', 0, 0, 'auto']]
|
||||
}
|
||||
}
|
||||
|
||||
const pnUtilInstance = PhoneNumberUtil.getInstance()
|
||||
|
||||
const getValidationSchema = countryCodes =>
|
||||
Yup.object().shape({
|
||||
phoneNumber: Yup.string()
|
||||
.required('A phone number is required')
|
||||
.test('is-valid-number', 'That is not a valid phone number', value => {
|
||||
try {
|
||||
const validMap = R.map(it => {
|
||||
const number = pnUtilInstance.parseAndKeepRawInput(value, it)
|
||||
return pnUtilInstance.isValidNumber(number)
|
||||
}, countryCodes)
|
||||
|
||||
return R.any(it => it === true, validMap)
|
||||
} catch (e) {}
|
||||
})
|
||||
.trim()
|
||||
})
|
||||
|
||||
const formatPhoneNumber = (countryCodes, numberStr) => {
|
||||
const matchedCountry = R.find(it => {
|
||||
const number = pnUtilInstance.parseAndKeepRawInput(numberStr, it)
|
||||
return pnUtilInstance.isValidNumber(number)
|
||||
}, countryCodes)
|
||||
|
||||
const matchedNumber = pnUtilInstance.parseAndKeepRawInput(
|
||||
numberStr,
|
||||
matchedCountry
|
||||
)
|
||||
|
||||
return pnUtilInstance.format(matchedNumber, PhoneNumberFormat.E164)
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
phoneNumber: ''
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const getErrorMsg = (formikErrors, formikTouched) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (formikErrors.phoneNumber && formikTouched.phoneNumber)
|
||||
return formikErrors.phoneNumber
|
||||
return null
|
||||
}
|
||||
|
||||
const CreateCustomerModal = ({ showModal, handleClose, onSubmit, locale }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const possibleCountries = R.append(
|
||||
locale?.country,
|
||||
R.map(it => it.country, locale?.overrides ?? [])
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={300}
|
||||
handleClose={handleClose}
|
||||
open={showModal}>
|
||||
<Formik
|
||||
validationSchema={getValidationSchema(possibleCountries)}
|
||||
initialValues={initialValues}
|
||||
validateOnChange={false}
|
||||
onSubmit={values => {
|
||||
onSubmit({
|
||||
variables: {
|
||||
phoneNumber: formatPhoneNumber(
|
||||
possibleCountries,
|
||||
values.phoneNumber
|
||||
)
|
||||
}
|
||||
})
|
||||
}}>
|
||||
{({ errors, touched }) => (
|
||||
<Form id="customer-registration-form" className={classes.form}>
|
||||
<H1 className={classes.modalTitle}>Create new customer</H1>
|
||||
<Field
|
||||
component={TextInput}
|
||||
name="phoneNumber"
|
||||
width={338}
|
||||
autoFocus
|
||||
label="Phone number"
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{getErrorMsg(errors, touched) && (
|
||||
<ErrorMessage>{getErrorMsg(errors, touched)}</ErrorMessage>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="customer-registration-form"
|
||||
className={classes.submit}>
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateCustomerModal
|
||||
|
|
@ -53,12 +53,12 @@ const SET_AUTHORIZED_REQUEST = gql`
|
|||
mutation setAuthorizedCustomRequest(
|
||||
$customerId: ID!
|
||||
$infoRequestId: ID!
|
||||
$isAuthorized: Boolean!
|
||||
$override: String!
|
||||
) {
|
||||
setAuthorizedCustomRequest(
|
||||
customerId: $customerId
|
||||
infoRequestId: $infoRequestId
|
||||
isAuthorized: $isAuthorized
|
||||
override: $override
|
||||
)
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ export default {
|
|||
backgroundColor: sidebarColor,
|
||||
width: 219,
|
||||
flexDirection: 'column',
|
||||
borderRadius: 5,
|
||||
marginBottom: 50
|
||||
borderRadius: 5
|
||||
},
|
||||
link: {
|
||||
alignItems: 'center',
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ const EditableCard = ({
|
|||
<H3 className={classes.cardTitle}>{title}</H3>
|
||||
<Tooltip width={304}></Tooltip>
|
||||
</div>
|
||||
{state && (
|
||||
{state && authorize && (
|
||||
<div className={classnames(label1ClassNames)}>
|
||||
<MainStatus statuses={[authorized]} />
|
||||
</div>
|
||||
|
|
@ -207,6 +207,8 @@ const EditableCard = ({
|
|||
<div className={classes.edit}>
|
||||
{!editing && (
|
||||
<div className={classes.editButton}>
|
||||
{// TODO: Remove false condition for next release
|
||||
false && (
|
||||
<div className={classes.deleteButton}>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
|
|
@ -217,7 +219,7 @@ const EditableCard = ({
|
|||
{`Delete`}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
)}
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={EditIcon}
|
||||
|
|
@ -279,7 +281,7 @@ const EditableCard = ({
|
|||
Cancel
|
||||
</ActionButton>
|
||||
</div>
|
||||
{authorized.label !== 'Accepted' && (
|
||||
{authorize && authorized.label !== 'Accepted' && (
|
||||
<div className={classes.button}>
|
||||
<ActionButton
|
||||
color="spring"
|
||||
|
|
@ -291,7 +293,7 @@ const EditableCard = ({
|
|||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
{authorized.label !== 'Rejected' && (
|
||||
{authorize && authorized.label !== 'Rejected' && (
|
||||
<ActionButton
|
||||
color="tomato"
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { offColor, subheaderColor } from 'src/styling/variables'
|
|||
const useStyles = makeStyles({
|
||||
box: {
|
||||
boxSizing: 'border-box',
|
||||
marginTop: 40,
|
||||
width: 450,
|
||||
height: 120,
|
||||
borderStyle: 'dashed',
|
||||
|
|
@ -32,6 +31,7 @@ const useStyles = makeStyles({
|
|||
display: 'flex'
|
||||
},
|
||||
board: {
|
||||
marginTop: 40,
|
||||
width: 450,
|
||||
height: 120
|
||||
},
|
||||
|
|
@ -48,12 +48,15 @@ const Upload = ({ type }) => {
|
|||
const { setFieldValue } = useFormikContext()
|
||||
|
||||
const IMAGE = 'image'
|
||||
const isImage = type === IMAGE
|
||||
const ID_CARD_PHOTO = 'idCardPhoto'
|
||||
const FRONT_CAMERA = 'frontCamera'
|
||||
|
||||
const isImage =
|
||||
type === IMAGE || type === FRONT_CAMERA || type === ID_CARD_PHOTO
|
||||
|
||||
const onDrop = useCallback(
|
||||
acceptedData => {
|
||||
// TODO: attach the uploaded data to the form as well
|
||||
setFieldValue(type, R.head(acceptedData).name)
|
||||
setFieldValue(type, R.head(acceptedData))
|
||||
|
||||
setData({
|
||||
preview: isImage
|
||||
|
|
@ -84,12 +87,12 @@ const Upload = ({ type }) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!R.isEmpty(data) && type === IMAGE && (
|
||||
{!R.isEmpty(data) && isImage && (
|
||||
<div key={data.name}>
|
||||
<img src={data.preview} className={classes.box} alt=""></img>
|
||||
</div>
|
||||
)}
|
||||
{!R.isEmpty(data) && type !== IMAGE && (
|
||||
{!R.isEmpty(data) && !isImage && (
|
||||
<div className={classes.box}>
|
||||
<H3 className={classes.uploadContent}>{data.preview}</H3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import { makeStyles, Box } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import { parse, isValid, format } from 'date-fns/fp'
|
||||
import { Field, useFormikContext } from 'formik'
|
||||
import { parsePhoneNumberFromString } from 'libphonenumber-js'
|
||||
import * as R from 'ramda'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { RadioGroup, TextInput } from 'src/components/inputs/formik'
|
||||
import {
|
||||
RadioGroup,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
import { H4 } from 'src/components/typography'
|
||||
import { errorColor } from 'src/styling/variables'
|
||||
import { MANUAL } from 'src/utils/constants'
|
||||
|
||||
import { Upload } from './components'
|
||||
|
||||
|
|
@ -34,19 +40,63 @@ const useStyles = makeStyles({
|
|||
specialGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: [[182, 162, 141]]
|
||||
},
|
||||
picker: {
|
||||
width: 150
|
||||
},
|
||||
field: {
|
||||
'& > *:last-child': {
|
||||
marginBottom: 24
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const CUSTOMER_BLOCKED = 'blocked'
|
||||
const CUSTOM = 'custom'
|
||||
const REQUIREMENT = 'requirement'
|
||||
const ID_CARD_DATA = 'idCardData'
|
||||
|
||||
const getAuthorizedStatus = it =>
|
||||
it.authorizedOverride === CUSTOMER_BLOCKED
|
||||
? { label: 'Blocked', type: 'error' }
|
||||
: it.isSuspended
|
||||
? it.daysSuspended > 0
|
||||
const getAuthorizedStatus = (it, triggers) => {
|
||||
const fields = [
|
||||
'frontCameraPath',
|
||||
'idCardData',
|
||||
'idCardPhotoPath',
|
||||
'usSsn',
|
||||
'sanctions'
|
||||
]
|
||||
|
||||
const isManualField = fieldName => {
|
||||
const manualOverrides = R.filter(
|
||||
ite => R.equals(R.toLower(ite.automation), MANUAL),
|
||||
triggers?.overrides ?? []
|
||||
)
|
||||
|
||||
return (
|
||||
!!R.find(ite => R.equals(ite.requirement, fieldName), manualOverrides) ||
|
||||
R.equals(triggers.automation, MANUAL)
|
||||
)
|
||||
}
|
||||
|
||||
const pendingFieldStatus = R.map(
|
||||
ite =>
|
||||
!R.isNil(it[`${ite}`])
|
||||
? isManualField(ite)
|
||||
? R.equals(it[`${ite}Override`], 'automatic')
|
||||
: false
|
||||
: false,
|
||||
fields
|
||||
)
|
||||
|
||||
if (it.authorizedOverride === CUSTOMER_BLOCKED)
|
||||
return { label: 'Blocked', type: 'error' }
|
||||
if (it.isSuspended)
|
||||
return it.daysSuspended > 0
|
||||
? { label: `${it.daysSuspended} day suspension`, type: 'warning' }
|
||||
: { label: `< 1 day suspension`, type: 'warning' }
|
||||
: { label: 'Authorized', type: 'success' }
|
||||
if (R.any(ite => ite === true, pendingFieldStatus))
|
||||
return { label: 'Pending', type: 'warning' }
|
||||
return { label: 'Authorized', type: 'success' }
|
||||
}
|
||||
|
||||
const getFormattedPhone = (phone, country) => {
|
||||
const phoneNumber =
|
||||
|
|
@ -63,34 +113,46 @@ const getName = it => {
|
|||
) ?? ''}`.trim()
|
||||
}
|
||||
|
||||
// Manual Entry Wizard
|
||||
|
||||
const entryOptions = [
|
||||
{ display: 'Custom entry', code: 'custom' },
|
||||
{ display: 'Populate existing requirement', code: 'requirement' }
|
||||
]
|
||||
|
||||
const dataOptions = [
|
||||
{ display: 'Text', code: 'text' },
|
||||
{ display: 'File', code: 'file' },
|
||||
{ display: 'Image', code: 'image' }
|
||||
{ display: 'Text', code: 'text' }
|
||||
// TODO: Requires backend modifications to support File and Image
|
||||
// { display: 'File', code: 'file' },
|
||||
// { display: 'Image', code: 'image' }
|
||||
]
|
||||
|
||||
const requirementOptions = [
|
||||
{ display: 'Birthdate', code: 'birthdate' },
|
||||
{ display: 'ID card image', code: 'idCardPhoto' },
|
||||
{ display: 'ID data', code: 'idCardData' },
|
||||
{ display: 'Customer camera', code: 'facephoto' },
|
||||
{ display: 'US SSN', code: 'usSsn' }
|
||||
{ display: 'US SSN', code: 'usSsn' },
|
||||
{ display: 'Customer camera', code: 'frontCamera' }
|
||||
]
|
||||
|
||||
const customTextOptions = [
|
||||
{ display: 'Data entry title', code: 'title' },
|
||||
{ display: 'Data entry', code: 'data' }
|
||||
{ label: 'Data entry title', name: 'title' },
|
||||
{ label: 'Data entry', name: 'data' }
|
||||
]
|
||||
|
||||
const customUploadOptions = [{ display: 'Data entry title', code: 'title' }]
|
||||
const customUploadOptions = [{ label: 'Data entry title', name: 'title' }]
|
||||
|
||||
const entryTypeSchema = Yup.object().shape({
|
||||
entryType: Yup.string().required()
|
||||
const entryTypeSchema = Yup.lazy(values => {
|
||||
if (values.entryType === 'custom') {
|
||||
return Yup.object().shape({
|
||||
entryType: Yup.string().required(),
|
||||
dataType: Yup.string().required()
|
||||
})
|
||||
} else if (values.entryType === 'requirement') {
|
||||
return Yup.object().shape({
|
||||
entryType: Yup.string().required(),
|
||||
requirement: Yup.string().required()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const customFileSchema = Yup.object().shape({
|
||||
|
|
@ -108,13 +170,18 @@ const customTextSchema = Yup.object().shape({
|
|||
data: Yup.string().required()
|
||||
})
|
||||
|
||||
const EntryType = () => {
|
||||
const updateRequirementOptions = it => [
|
||||
{
|
||||
display: 'Custom information requirement',
|
||||
code: 'custom'
|
||||
},
|
||||
...it
|
||||
]
|
||||
|
||||
const EntryType = ({ customInfoRequirementOptions }) => {
|
||||
const classes = useStyles()
|
||||
const { values } = useFormikContext()
|
||||
|
||||
const CUSTOM = 'custom'
|
||||
const REQUIREMENT = 'requirement'
|
||||
|
||||
const displayCustomOptions = values.entryType === CUSTOM
|
||||
const displayRequirementOptions = values.entryType === REQUIREMENT
|
||||
|
||||
|
|
@ -154,7 +221,13 @@ const EntryType = () => {
|
|||
<Field
|
||||
component={RadioGroup}
|
||||
name="requirement"
|
||||
options={requirementOptions}
|
||||
options={
|
||||
requirementOptions
|
||||
// TODO: Enable once custom info requirement manual entry is finished
|
||||
// !R.isEmpty(customInfoRequirementOptions)
|
||||
// ? updateRequirementOptions(requirementOptions)
|
||||
// : requirementOptions
|
||||
}
|
||||
labelClassName={classes.label}
|
||||
radioClassName={classes.radio}
|
||||
className={classnames(classes.radioGroup, classes.specialGrid)}
|
||||
|
|
@ -165,18 +238,73 @@ const EntryType = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const CustomData = ({ selectedValues }) => {
|
||||
const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const typeOfEntrySelected = selectedValues?.entryType
|
||||
const dataTypeSelected = selectedValues?.dataType
|
||||
const upload = dataTypeSelected === 'file' || dataTypeSelected === 'image'
|
||||
const requirementSelected = selectedValues?.requirement
|
||||
|
||||
const displayRequirements = typeOfEntrySelected === 'requirement'
|
||||
|
||||
const isCustomInfoRequirement = requirementSelected === CUSTOM
|
||||
|
||||
const updatedRequirementOptions = !R.isEmpty(customInfoRequirementOptions)
|
||||
? updateRequirementOptions(requirementOptions)
|
||||
: requirementOptions
|
||||
|
||||
const requirementName = displayRequirements
|
||||
? R.find(R.propEq('code', requirementSelected))(updatedRequirementOptions)
|
||||
.display
|
||||
: ''
|
||||
|
||||
const title = displayRequirements
|
||||
? `Requirement ${requirementName}`
|
||||
: `Custom ${dataTypeSelected} entry`
|
||||
|
||||
const elements = displayRequirements
|
||||
? requirementElements[requirementSelected]
|
||||
: customElements[dataTypeSelected]
|
||||
|
||||
const upload = displayRequirements
|
||||
? requirementSelected === 'idCardPhoto' ||
|
||||
requirementSelected === 'frontCamera'
|
||||
: dataTypeSelected === 'file' || dataTypeSelected === 'image'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" alignItems="center">
|
||||
<H4>{`Custom ${dataTypeSelected} entry`}</H4>
|
||||
<H4>{title}</H4>
|
||||
</Box>
|
||||
{customElements[dataTypeSelected].options.map(({ display, code }) => (
|
||||
<Field name={code} label={display} component={TextInput} width={390} />
|
||||
{isCustomInfoRequirement && (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
label={`Available requests`}
|
||||
className={classes.picker}
|
||||
getOptionSelected={R.eqProps('code')}
|
||||
labelProp={'display'}
|
||||
options={customInfoRequirementOptions}
|
||||
onChange={(evt, it) => {}}
|
||||
/>
|
||||
)}
|
||||
<div className={classes.field}>
|
||||
{!upload &&
|
||||
!isCustomInfoRequirement &&
|
||||
elements.options.map(({ label, name }) => (
|
||||
<Field
|
||||
name={name}
|
||||
label={label}
|
||||
component={TextInput}
|
||||
width={390}
|
||||
/>
|
||||
))}
|
||||
{upload && <Upload type={dataTypeSelected}></Upload>}
|
||||
</div>
|
||||
{upload && (
|
||||
<Upload
|
||||
type={
|
||||
displayRequirements ? requirementSelected : dataTypeSelected
|
||||
}></Upload>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -185,20 +313,23 @@ const customElements = {
|
|||
text: {
|
||||
schema: customTextSchema,
|
||||
options: customTextOptions,
|
||||
Component: CustomData,
|
||||
initialValues: { data: '', title: '' }
|
||||
Component: ManualDataEntry,
|
||||
initialValues: { data: '', title: '' },
|
||||
saveType: 'customEntry'
|
||||
},
|
||||
file: {
|
||||
schema: customFileSchema,
|
||||
options: customUploadOptions,
|
||||
Component: CustomData,
|
||||
initialValues: { file: '', title: '' }
|
||||
Component: ManualDataEntry,
|
||||
initialValues: { file: null, title: '' },
|
||||
saveType: 'customEntryUpload'
|
||||
},
|
||||
image: {
|
||||
schema: customImageSchema,
|
||||
options: customUploadOptions,
|
||||
Component: CustomData,
|
||||
initialValues: { image: '', title: '' }
|
||||
Component: ManualDataEntry,
|
||||
initialValues: { image: null, title: '' },
|
||||
saveType: 'customEntryUpload'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -209,6 +340,142 @@ const entryType = {
|
|||
initialValues: { entryType: '' }
|
||||
}
|
||||
|
||||
// Customer data
|
||||
|
||||
const customerDataElements = {
|
||||
idCardData: [
|
||||
{
|
||||
name: 'firstName',
|
||||
label: 'First name',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'documentNumber',
|
||||
label: 'ID number',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'dateOfBirth',
|
||||
label: 'Birthdate',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'gender',
|
||||
label: 'Gender',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
label: 'Last name',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'expirationDate',
|
||||
label: 'Expiration Date',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
name: 'country',
|
||||
label: 'Country',
|
||||
component: TextInput
|
||||
}
|
||||
],
|
||||
usSsn: [
|
||||
{
|
||||
name: 'usSsn',
|
||||
label: 'US SSN',
|
||||
component: TextInput,
|
||||
size: 190
|
||||
}
|
||||
],
|
||||
idCardPhoto: [{ name: 'idCardPhoto' }],
|
||||
frontCamera: [{ name: 'frontCamera' }]
|
||||
}
|
||||
|
||||
const customerDataSchemas = {
|
||||
idCardData: Yup.object().shape({
|
||||
firstName: Yup.string().required(),
|
||||
lastName: Yup.string().required(),
|
||||
documentNumber: Yup.string().required(),
|
||||
dateOfBirth: Yup.string()
|
||||
.test({
|
||||
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
|
||||
})
|
||||
.required(),
|
||||
gender: Yup.string().required(),
|
||||
country: Yup.string().required(),
|
||||
expirationDate: Yup.string()
|
||||
.test({
|
||||
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
|
||||
})
|
||||
.required()
|
||||
}),
|
||||
usSsn: Yup.object().shape({
|
||||
usSsn: Yup.string().required()
|
||||
}),
|
||||
idCardPhoto: Yup.object().shape({
|
||||
idCardPhoto: Yup.mixed().required()
|
||||
}),
|
||||
frontCamera: Yup.object().shape({
|
||||
frontCamera: Yup.mixed().required()
|
||||
})
|
||||
}
|
||||
|
||||
const requirementElements = {
|
||||
idCardData: {
|
||||
schema: customerDataSchemas.idCardData,
|
||||
options: customerDataElements.idCardData,
|
||||
Component: ManualDataEntry,
|
||||
initialValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
documentNumber: '',
|
||||
dateOfBirth: '',
|
||||
gender: '',
|
||||
country: '',
|
||||
expirationDate: ''
|
||||
},
|
||||
saveType: 'customerData'
|
||||
},
|
||||
usSsn: {
|
||||
schema: customerDataSchemas.usSsn,
|
||||
options: customerDataElements.usSsn,
|
||||
Component: ManualDataEntry,
|
||||
initialValues: { usSsn: '' },
|
||||
saveType: 'customerData'
|
||||
},
|
||||
idCardPhoto: {
|
||||
schema: customerDataSchemas.idCardPhoto,
|
||||
options: customerDataElements.idCardPhoto,
|
||||
Component: ManualDataEntry,
|
||||
initialValues: { idCardPhoto: null },
|
||||
saveType: 'customerDataUpload'
|
||||
},
|
||||
frontCamera: {
|
||||
schema: customerDataSchemas.frontCamera,
|
||||
options: customerDataElements.frontCamera,
|
||||
Component: ManualDataEntry,
|
||||
initialValues: { frontCamera: null },
|
||||
saveType: 'customerDataUpload'
|
||||
},
|
||||
custom: {
|
||||
// schema: customerDataSchemas.customInfoRequirement,
|
||||
Component: ManualDataEntry,
|
||||
initialValues: { customInfoRequirement: null },
|
||||
saveType: 'customInfoRequirement'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDates = values => {
|
||||
R.map(
|
||||
elem =>
|
||||
(values[elem] = format('yyyyMMdd')(
|
||||
parse(new Date(), 'yyyy-MM-dd', values[elem])
|
||||
))
|
||||
)(['dateOfBirth', 'expirationDate'])
|
||||
return values
|
||||
}
|
||||
|
||||
const mapKeys = pair => {
|
||||
const [key, value] = pair
|
||||
if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') {
|
||||
|
|
@ -245,5 +512,12 @@ export {
|
|||
getName,
|
||||
entryType,
|
||||
customElements,
|
||||
formatPhotosData
|
||||
requirementElements,
|
||||
formatPhotosData,
|
||||
customerDataElements,
|
||||
customerDataSchemas,
|
||||
formatDates,
|
||||
REQUIREMENT,
|
||||
CUSTOM,
|
||||
ID_CARD_DATA
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
[]
|
||||
)
|
||||
|
||||
const filterDay = useMemo(
|
||||
const filterDay = useCallback(
|
||||
x => (timeFrame === 'day' ? x.getUTCHours() === 0 : x.getUTCDate() === 1),
|
||||
[timeFrame]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ const ranges = {
|
|||
}
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
transactions {
|
||||
query getData($excludeTestingCustomers: Boolean) {
|
||||
transactions(excludeTestingCustomers: $excludeTestingCustomers) {
|
||||
fiatCode
|
||||
fiat
|
||||
cashInFee
|
||||
|
|
@ -78,7 +78,9 @@ const reducer = (acc, it) =>
|
|||
const SystemPerformance = () => {
|
||||
const classes = useStyles()
|
||||
const [selectedRange, setSelectedRange] = useState('Day')
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
const { data, loading } = useQuery(GET_DATA, {
|
||||
variables: { excludeTestingCustomers: true }
|
||||
})
|
||||
const fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
|
||||
const timezone = fromNamespace('locale')(data?.config).timezone
|
||||
|
||||
|
|
|
|||
|
|
@ -128,9 +128,8 @@ const MachinesTable = ({ machines = [], numToRender }) => {
|
|||
onClick={() => redirect(machine)}
|
||||
className={classnames(classes.row)}
|
||||
key={machine.deviceId + idx}>
|
||||
<StyledCell
|
||||
align="left"
|
||||
className={classes.machineNameWrapper}>
|
||||
<StyledCell align="left">
|
||||
<div className={classes.machineNameWrapper}>
|
||||
<TL2>{machine.name}</TL2>
|
||||
<MachineLinkIcon
|
||||
className={classnames(
|
||||
|
|
@ -139,6 +138,7 @@ const MachinesTable = ({ machines = [], numToRender }) => {
|
|||
)}
|
||||
onClick={() => redirect(machine)}
|
||||
/>
|
||||
</div>
|
||||
</StyledCell>
|
||||
<StyledCell>
|
||||
<Status status={machine.statuses[0]} />
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ const Transactions = ({ id }) => {
|
|||
width: 140
|
||||
},
|
||||
{
|
||||
header: 'Date (UTC)',
|
||||
header: 'Date',
|
||||
view: it => formatDate(it.created, timezone, 'yyyy-MM-dd'),
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import NavigateNextIcon from '@material-ui/icons/NavigateNext'
|
|||
import classnames from 'classnames'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import React, { useState } from 'react'
|
||||
import { Link, useLocation, useHistory } from 'react-router-dom'
|
||||
|
||||
import { TL1, TL2, Label3 } from 'src/components/typography'
|
||||
|
||||
|
|
@ -58,18 +58,42 @@ const GET_INFO = gql`
|
|||
|
||||
const getMachineID = path => path.slice(path.lastIndexOf('/') + 1)
|
||||
|
||||
const Machines = () => {
|
||||
const MachineRoute = () => {
|
||||
const location = useLocation()
|
||||
const { data, loading, refetch } = useQuery(GET_INFO, {
|
||||
const history = useHistory()
|
||||
|
||||
const id = getMachineID(location.pathname)
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const { data, refetch } = useQuery(GET_INFO, {
|
||||
onCompleted: data => {
|
||||
if (data.machine === null)
|
||||
return history.push('/maintenance/machine-status')
|
||||
|
||||
setLoading(false)
|
||||
},
|
||||
variables: {
|
||||
deviceId: getMachineID(location.pathname),
|
||||
deviceId: id
|
||||
},
|
||||
billFilters: {
|
||||
deviceId: getMachineID(location.pathname),
|
||||
deviceId: id,
|
||||
batch: 'none'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const reload = () => {
|
||||
return history.push(location.pathname)
|
||||
}
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<Machines data={data} refetch={refetch} reload={reload}></Machines>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const Machines = ({ data, refetch, reload }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const timezone = R.path(['config', 'locale_timezone'], data) ?? {}
|
||||
|
|
@ -82,7 +106,6 @@ const Machines = () => {
|
|||
const machineID = R.path(['deviceId'])(machine) ?? null
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<Grid container className={classes.grid}>
|
||||
<Grid item xs={3}>
|
||||
<Grid item xs={12}>
|
||||
|
|
@ -97,7 +120,7 @@ const Machines = () => {
|
|||
{machineName}
|
||||
</TL2>
|
||||
</Breadcrumbs>
|
||||
<Overview data={machine} onActionSuccess={refetch} />
|
||||
<Overview data={machine} onActionSuccess={reload} />
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
@ -129,7 +152,6 @@ const Machines = () => {
|
|||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default Machines
|
||||
export default MachineRoute
|
||||
|
|
|
|||
|
|
@ -28,6 +28,30 @@ import Wizard from './Wizard/Wizard'
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const widthsByNumberOfCassettes = {
|
||||
2: {
|
||||
machine: 250,
|
||||
cashbox: 260,
|
||||
cassette: 300,
|
||||
cassetteGraph: 80,
|
||||
editWidth: 90
|
||||
},
|
||||
3: {
|
||||
machine: 220,
|
||||
cashbox: 215,
|
||||
cassette: 225,
|
||||
cassetteGraph: 60,
|
||||
editWidth: 90
|
||||
},
|
||||
4: {
|
||||
machine: 190,
|
||||
cashbox: 180,
|
||||
cassette: 185,
|
||||
cassetteGraph: 50,
|
||||
editWidth: 90
|
||||
}
|
||||
}
|
||||
|
||||
const ValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
cashbox: Yup.number()
|
||||
|
|
@ -201,14 +225,14 @@ const CashCassettes = () => {
|
|||
{
|
||||
name: 'name',
|
||||
header: 'Machine',
|
||||
width: 184,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine,
|
||||
view: name => <>{name}</>,
|
||||
input: ({ field: { value: name } }) => <>{name}</>
|
||||
},
|
||||
{
|
||||
name: 'cashbox',
|
||||
header: 'Cash box',
|
||||
width: maxNumberOfCassettes > 2 ? 140 : 280,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cashbox,
|
||||
view: (value, { id }) => (
|
||||
<CashIn
|
||||
currency={{ code: fiatCurrency }}
|
||||
|
|
@ -229,7 +253,7 @@ const CashCassettes = () => {
|
|||
elements.push({
|
||||
name: `cassette${it}`,
|
||||
header: `Cassette ${it}`,
|
||||
width: (maxNumberOfCassettes > 2 ? 560 : 650) / maxNumberOfCassettes,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassette,
|
||||
stripe: true,
|
||||
doubleHeader: 'Cash-out',
|
||||
view: (value, { id }) => (
|
||||
|
|
@ -238,7 +262,9 @@ const CashCassettes = () => {
|
|||
denomination={getCashoutSettings(id)?.[`cassette${it}`]}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
width={50}
|
||||
width={
|
||||
widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassetteGraph
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[`fillingPercentageCassette${it}`]
|
||||
}
|
||||
|
|
@ -248,7 +274,7 @@ const CashCassettes = () => {
|
|||
input: CashCassetteInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0,
|
||||
width: 50,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassetteGraph,
|
||||
inputClassName: classes.cashbox
|
||||
}
|
||||
})
|
||||
|
|
@ -260,7 +286,8 @@ const CashCassettes = () => {
|
|||
elements.push({
|
||||
name: 'edit',
|
||||
header: 'Edit',
|
||||
width: 87,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.editWidth,
|
||||
textAlign: 'center',
|
||||
view: (value, { id }) => {
|
||||
return (
|
||||
<IconButton
|
||||
|
|
@ -279,12 +306,14 @@ const CashCassettes = () => {
|
|||
<>
|
||||
<TitleSection
|
||||
title="Cash Boxes & Cassettes"
|
||||
button={{
|
||||
buttons={[
|
||||
{
|
||||
text: 'Cash box history',
|
||||
icon: HistoryIcon,
|
||||
inverseIcon: ReverseHistoryIcon,
|
||||
toggle: setShowHistory
|
||||
}}
|
||||
}
|
||||
]}
|
||||
iconClassName={classes.listViewButton}
|
||||
className={classes.tableWidth}>
|
||||
{!showHistory && (
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const GET_BATCHES = gql`
|
|||
fiat
|
||||
deviceId
|
||||
created
|
||||
cashbox
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ const WizardStep = ({
|
|||
placeholder={originalCassetteCount.toString()}
|
||||
name={cassetteField}
|
||||
className={classes.cashboxBills}
|
||||
autoFocus
|
||||
/>
|
||||
<P>
|
||||
{cassetteDenomination} {fiatCurrency} bills loaded
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ const SessionManagement = () => {
|
|||
}
|
||||
},
|
||||
{
|
||||
header: 'Expiration date (UTC)',
|
||||
header: 'Expiration date',
|
||||
width: 290,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useLazyQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Box } from '@material-ui/core'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { add, differenceInYears, format, sub } from 'date-fns/fp'
|
||||
import { add, differenceInYears, format, sub, parse } from 'date-fns/fp'
|
||||
import FileSaver from 'file-saver'
|
||||
import gql from 'graphql-tag'
|
||||
import JSZip from 'jszip'
|
||||
|
|
@ -116,17 +116,27 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
const exchangeRate = BigNumber(fiat / crypto).toFormat(2)
|
||||
const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}`
|
||||
|
||||
const parseDateString = parse(new Date(), 'yyyyMMdd')
|
||||
|
||||
const customer = tx.customerIdCardData && {
|
||||
name: `${onlyFirstToUpper(
|
||||
tx.customerIdCardData.firstName
|
||||
)} ${onlyFirstToUpper(tx.customerIdCardData.lastName)}`,
|
||||
age: differenceInYears(tx.customerIdCardData.dateOfBirth, new Date()),
|
||||
age:
|
||||
(tx.customerIdCardData.dateOfBirth &&
|
||||
differenceInYears(
|
||||
parseDateString(tx.customerIdCardData.dateOfBirth),
|
||||
new Date()
|
||||
)) ??
|
||||
'',
|
||||
country: tx.customerIdCardData.country,
|
||||
idCardNumber: tx.customerIdCardData.documentNumber,
|
||||
idCardExpirationDate: format(
|
||||
'dd-MM-yyyy',
|
||||
tx.customerIdCardData.expirationDate
|
||||
)
|
||||
idCardExpirationDate:
|
||||
(tx.customerIdCardData.expirationDate &&
|
||||
format('yyyy-MM-dd')(
|
||||
parseDateString(tx.customerIdCardData.expirationDate)
|
||||
)) ??
|
||||
''
|
||||
}
|
||||
|
||||
const from = sub({ minutes: MINUTES_OFFSET }, tx.created)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const GET_TRANSACTIONS_CSV = gql`
|
|||
$from: Date
|
||||
$until: Date
|
||||
$timezone: String
|
||||
$excludeTestingCustomers: Boolean
|
||||
) {
|
||||
transactionsCsv(
|
||||
simplified: $simplified
|
||||
|
|
@ -47,6 +48,7 @@ const GET_TRANSACTIONS_CSV = gql`
|
|||
from: $from
|
||||
until: $until
|
||||
timezone: $timezone
|
||||
excludeTestingCustomers: $excludeTestingCustomers
|
||||
)
|
||||
}
|
||||
`
|
||||
|
|
@ -222,7 +224,7 @@ const Transactions = () => {
|
|||
width: 140
|
||||
},
|
||||
{
|
||||
header: 'Date (UTC)',
|
||||
header: 'Date',
|
||||
view: it =>
|
||||
timezone && formatDate(it.created, timezone, 'yyyy-MM-dd HH:mm:ss'),
|
||||
textAlign: 'right',
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ const TriggerView = ({
|
|||
error={error?.message}
|
||||
save={add}
|
||||
onClose={toggleWizard}
|
||||
customInfoRequests={customInfoRequests}
|
||||
/>
|
||||
)}
|
||||
{R.isEmpty(triggers) && (
|
||||
|
|
|
|||
|
|
@ -48,8 +48,10 @@ const GET_CUSTOM_REQUESTS = gql`
|
|||
const Triggers = () => {
|
||||
const classes = useStyles()
|
||||
const [wizardType, setWizard] = useState(false)
|
||||
const { data, loading } = useQuery(GET_CONFIG)
|
||||
const { data: customInfoReqData } = useQuery(GET_CUSTOM_REQUESTS)
|
||||
const { data, loading: configLoading } = useQuery(GET_CONFIG)
|
||||
const { data: customInfoReqData, loading: customInfoLoading } = useQuery(
|
||||
GET_CUSTOM_REQUESTS
|
||||
)
|
||||
const [error, setError] = useState(null)
|
||||
const [subMenu, setSubMenu] = useState(false)
|
||||
|
||||
|
|
@ -94,6 +96,8 @@ const Triggers = () => {
|
|||
return setWizard(wizardName)
|
||||
}
|
||||
|
||||
const loading = configLoading || customInfoLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection
|
||||
|
|
@ -178,7 +182,7 @@ const Triggers = () => {
|
|||
showWizard={wizardType === 'newTrigger'}
|
||||
config={data?.config ?? {}}
|
||||
toggleWizard={toggleWizard('newTrigger')}
|
||||
customInfoRequests={customInfoRequests}
|
||||
customInfoRequests={enabledCustomInfoRequests}
|
||||
/>
|
||||
)}
|
||||
{!loading && subMenu === 'advancedSettings' && (
|
||||
|
|
|
|||
|
|
@ -48,14 +48,14 @@ const styles = {
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const getStep = (step, currency) => {
|
||||
const getStep = (step, currency, customInfoRequests) => {
|
||||
switch (step) {
|
||||
// case 1:
|
||||
// return txDirection
|
||||
case 1:
|
||||
return type(currency)
|
||||
case 2:
|
||||
return requirements
|
||||
return requirements(customInfoRequests)
|
||||
default:
|
||||
return Fragment
|
||||
}
|
||||
|
|
@ -202,7 +202,7 @@ const GetValues = ({ setValues }) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const Wizard = ({ onClose, save, error, currency }) => {
|
||||
const Wizard = ({ onClose, save, error, currency, customInfoRequests }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [liveValues, setLiveValues] = useState({})
|
||||
|
|
@ -211,7 +211,7 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
|||
})
|
||||
|
||||
const isLastStep = step === LAST_STEP
|
||||
const stepOptions = getStep(step, currency)
|
||||
const stepOptions = getStep(step, currency, customInfoRequests)
|
||||
|
||||
const onContinue = async it => {
|
||||
const newConfig = R.merge(config, stepOptions.schema.cast(it))
|
||||
|
|
@ -230,12 +230,18 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
|||
const triggerType = values?.triggerType
|
||||
const containsType = R.contains(triggerType)
|
||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||
const isCustom = values?.requirement?.requirement === 'custom'
|
||||
|
||||
const hasRequirementError =
|
||||
!!errors.requirement &&
|
||||
!!touched.requirement?.suspensionDays &&
|
||||
(!values.requirement?.suspensionDays ||
|
||||
values.requirement?.suspensionDays < 0)
|
||||
const hasRequirementError = requirements().hasRequirementError(
|
||||
errors,
|
||||
touched,
|
||||
values
|
||||
)
|
||||
const hasCustomRequirementError = requirements().hasCustomRequirementError(
|
||||
errors,
|
||||
touched,
|
||||
values
|
||||
)
|
||||
|
||||
const hasAmountError =
|
||||
!!errors.threshold &&
|
||||
|
|
@ -258,7 +264,11 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
|||
)
|
||||
return errors.threshold
|
||||
|
||||
if (isSuspend && hasRequirementError) return errors.requirement
|
||||
if (
|
||||
(isSuspend && hasRequirementError) ||
|
||||
(isCustom && hasCustomRequirementError)
|
||||
)
|
||||
return errors.requirement
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -28,13 +28,34 @@ const GET_INFO = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const GET_CUSTOM_REQUESTS = gql`
|
||||
query customInfoRequests {
|
||||
customInfoRequests {
|
||||
id
|
||||
customRequest
|
||||
enabled
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const AdvancedTriggersSettings = memo(() => {
|
||||
const SCREEN_KEY = namespaces.TRIGGERS
|
||||
const [error, setError] = useState(null)
|
||||
const [isEditingDefault, setEditingDefault] = useState(false)
|
||||
const [isEditingOverrides, setEditingOverrides] = useState(false)
|
||||
|
||||
const { data } = useQuery(GET_INFO)
|
||||
const { data, loading: configLoading } = useQuery(GET_INFO)
|
||||
const { data: customInfoReqData, loading: customInfoLoading } = useQuery(
|
||||
GET_CUSTOM_REQUESTS
|
||||
)
|
||||
|
||||
const customInfoRequests =
|
||||
R.path(['customInfoRequests'])(customInfoReqData) ?? []
|
||||
const enabledCustomInfoRequests = R.filter(R.propEq('enabled', true))(
|
||||
customInfoRequests
|
||||
)
|
||||
|
||||
const loading = configLoading || customInfoLoading
|
||||
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
refetchQueries: () => ['getData'],
|
||||
|
|
@ -67,6 +88,7 @@ const AdvancedTriggersSettings = memo(() => {
|
|||
const onEditingOverrides = (it, editing) => setEditingOverrides(editing)
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<>
|
||||
<Section>
|
||||
<EditableTable
|
||||
|
|
@ -95,15 +117,19 @@ const AdvancedTriggersSettings = memo(() => {
|
|||
enableCreate
|
||||
initialValues={overridesDefaults}
|
||||
save={saveOverrides}
|
||||
validationSchema={getOverridesSchema(requirementsOverrides)}
|
||||
validationSchema={getOverridesSchema(
|
||||
requirementsOverrides,
|
||||
enabledCustomInfoRequests
|
||||
)}
|
||||
data={requirementsOverrides}
|
||||
elements={getOverrides()}
|
||||
elements={getOverrides(enabledCustomInfoRequests)}
|
||||
setEditing={onEditingOverrides}
|
||||
forceDisable={isEditingDefault}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
export default AdvancedTriggersSettings
|
||||
|
|
|
|||
|
|
@ -4,18 +4,29 @@ import * as Yup from 'yup'
|
|||
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
|
||||
import { getView } from 'src/pages/Triggers/helper'
|
||||
|
||||
const advancedRequirementOptions = [
|
||||
const buildAdvancedRequirementOptions = customInfoRequests => {
|
||||
const base = [
|
||||
{ display: 'Sanctions', code: 'sanctions' },
|
||||
{ display: 'ID card image', code: 'idCardPhoto' },
|
||||
{ display: 'ID data', code: 'idCardData' },
|
||||
{ display: 'Customer camera', code: 'facephoto' },
|
||||
{ display: 'US SSN', code: 'usSsn' }
|
||||
]
|
||||
]
|
||||
|
||||
const displayRequirement = code => {
|
||||
const custom = R.map(it => ({
|
||||
display: it.customRequest.name,
|
||||
code: it.id
|
||||
}))(customInfoRequests)
|
||||
|
||||
return R.concat(base, custom)
|
||||
}
|
||||
|
||||
const displayRequirement = (code, customInfoRequests) => {
|
||||
return R.prop(
|
||||
'display',
|
||||
R.find(R.propEq('code', code))(advancedRequirementOptions)
|
||||
R.find(R.propEq('code', code))(
|
||||
buildAdvancedRequirementOptions(customInfoRequests)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +40,7 @@ const defaultSchema = Yup.object().shape({
|
|||
.required()
|
||||
})
|
||||
|
||||
const getOverridesSchema = values => {
|
||||
const getOverridesSchema = (values, customInfoRequests) => {
|
||||
return Yup.object().shape({
|
||||
id: Yup.string()
|
||||
.label('Requirement')
|
||||
|
|
@ -40,7 +51,8 @@ const getOverridesSchema = values => {
|
|||
if (R.find(R.propEq('requirement', requirement))(values)) {
|
||||
return this.createError({
|
||||
message: `Requirement ${displayRequirement(
|
||||
requirement
|
||||
requirement,
|
||||
customInfoRequests
|
||||
)} already overriden`
|
||||
})
|
||||
}
|
||||
|
|
@ -84,17 +96,20 @@ const getDefaultSettings = () => {
|
|||
]
|
||||
}
|
||||
|
||||
const getOverrides = () => {
|
||||
const getOverrides = customInfoRequests => {
|
||||
return [
|
||||
{
|
||||
name: 'requirement',
|
||||
header: 'Requirement',
|
||||
width: 196,
|
||||
size: 'sm',
|
||||
view: getView(advancedRequirementOptions, 'display'),
|
||||
view: getView(
|
||||
buildAdvancedRequirementOptions(customInfoRequests),
|
||||
'display'
|
||||
),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: advancedRequirementOptions,
|
||||
options: buildAdvancedRequirementOptions(customInfoRequests),
|
||||
labelProp: 'display',
|
||||
valueProp: 'code'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles, Box } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import { Field, useFormikContext } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
|
@ -479,21 +477,43 @@ const requirementSchema = Yup.object()
|
|||
otherwise: Yup.number()
|
||||
.nullable()
|
||||
.transform(() => null)
|
||||
}),
|
||||
customInfoRequestId: Yup.string().when('requirement', {
|
||||
is: value => value === 'custom',
|
||||
then: Yup.string(),
|
||||
otherwise: Yup.string()
|
||||
.nullable()
|
||||
.transform(() => '')
|
||||
})
|
||||
}).required()
|
||||
})
|
||||
.test(({ requirement }, context) => {
|
||||
const requirementValidator = requirement =>
|
||||
requirement.requirement === 'suspend'
|
||||
const requirementValidator = (requirement, type) => {
|
||||
switch (type) {
|
||||
case 'suspend':
|
||||
return requirement.requirement === type
|
||||
? requirement.suspensionDays > 0
|
||||
: true
|
||||
case 'custom':
|
||||
return requirement.requirement === type
|
||||
? !R.isNil(requirement.customInfoRequestId)
|
||||
: true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (requirement && requirementValidator(requirement)) return
|
||||
|
||||
if (requirement && !requirementValidator(requirement, 'suspend'))
|
||||
return context.createError({
|
||||
path: 'requirement',
|
||||
message: 'Suspension days must be greater than 0'
|
||||
})
|
||||
|
||||
if (requirement && !requirementValidator(requirement, 'custom'))
|
||||
return context.createError({
|
||||
path: 'requirement',
|
||||
message: 'You must select an item'
|
||||
})
|
||||
})
|
||||
|
||||
const requirementOptions = [
|
||||
|
|
@ -508,16 +528,19 @@ const requirementOptions = [
|
|||
{ display: 'Block', code: 'block' }
|
||||
]
|
||||
|
||||
const GET_ACTIVE_CUSTOM_REQUESTS = gql`
|
||||
query customInfoRequests($onlyEnabled: Boolean) {
|
||||
customInfoRequests(onlyEnabled: $onlyEnabled) {
|
||||
id
|
||||
customRequest
|
||||
}
|
||||
}
|
||||
`
|
||||
const hasRequirementError = (errors, touched, values) =>
|
||||
!!errors.requirement &&
|
||||
!!touched.requirement?.suspensionDays &&
|
||||
(!values.requirement?.suspensionDays ||
|
||||
values.requirement?.suspensionDays < 0)
|
||||
|
||||
const Requirement = () => {
|
||||
const hasCustomRequirementError = (errors, touched, values) =>
|
||||
!!errors.requirement &&
|
||||
!!touched.requirement?.customInfoRequestId &&
|
||||
(!values.requirement?.customInfoRequestId ||
|
||||
!R.isNil(values.requirement?.customInfoRequestId))
|
||||
|
||||
const Requirement = ({ customInfoRequests }) => {
|
||||
const classes = useStyles()
|
||||
const {
|
||||
touched,
|
||||
|
|
@ -526,11 +549,6 @@ const Requirement = () => {
|
|||
handleChange,
|
||||
setTouched
|
||||
} = useFormikContext()
|
||||
const { data } = useQuery(GET_ACTIVE_CUSTOM_REQUESTS, {
|
||||
variables: {
|
||||
onlyEnabled: true
|
||||
}
|
||||
})
|
||||
|
||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||
const isCustom = values?.requirement?.requirement === 'custom'
|
||||
|
|
@ -540,24 +558,19 @@ const Requirement = () => {
|
|||
display: it.customRequest.name
|
||||
}))
|
||||
|
||||
const hasRequirementError =
|
||||
!!errors.requirement &&
|
||||
!!touched.requirement?.suspensionDays &&
|
||||
(!values.requirement?.suspensionDays ||
|
||||
values.requirement?.suspensionDays < 0)
|
||||
|
||||
const customInfoRequests = R.path(['customInfoRequests'])(data) ?? []
|
||||
const enableCustomRequirement = customInfoRequests.length > 0
|
||||
const enableCustomRequirement = customInfoRequests?.length > 0
|
||||
const customInfoOption = {
|
||||
display: 'Custom information requirement',
|
||||
code: 'custom'
|
||||
}
|
||||
const options = enableCustomRequirement
|
||||
? [...requirementOptions, customInfoOption]
|
||||
: [...requirementOptions, { ...customInfoOption, disabled: true }]
|
||||
: [...requirementOptions]
|
||||
const titleClass = {
|
||||
[classes.error]:
|
||||
(!!errors.requirement && !isSuspend) || (isSuspend && hasRequirementError)
|
||||
(!!errors.requirement && !isSuspend && !isCustom) ||
|
||||
(isSuspend && hasRequirementError(errors, touched, values)) ||
|
||||
(isCustom && hasCustomRequirementError(errors, touched, values))
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -586,7 +599,7 @@ const Requirement = () => {
|
|||
label="Days"
|
||||
size="lg"
|
||||
name="requirement.suspensionDays"
|
||||
error={hasRequirementError}
|
||||
error={hasRequirementError(errors, touched, values)}
|
||||
/>
|
||||
)}
|
||||
{isCustom && (
|
||||
|
|
@ -604,10 +617,13 @@ const Requirement = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const requirements = {
|
||||
const requirements = customInfoRequests => ({
|
||||
schema: requirementSchema,
|
||||
options: requirementOptions,
|
||||
Component: Requirement,
|
||||
props: { customInfoRequests },
|
||||
hasRequirementError: hasRequirementError,
|
||||
hasCustomRequirementError: hasCustomRequirementError,
|
||||
initialValues: {
|
||||
requirement: {
|
||||
requirement: '',
|
||||
|
|
@ -615,7 +631,7 @@ const requirements = {
|
|||
customInfoRequestId: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getView = (data, code, compare) => it => {
|
||||
if (!data) return ''
|
||||
|
|
|
|||
|
|
@ -140,12 +140,14 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
|||
<div className={classes.header}>
|
||||
<TitleSection
|
||||
title="Wallet Settings"
|
||||
button={{
|
||||
buttons={[
|
||||
{
|
||||
text: 'Advanced settings',
|
||||
icon: SettingsIcon,
|
||||
inverseIcon: ReverseSettingsIcon,
|
||||
toggle: setAdvancedSettings
|
||||
}}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Box alignItems="center" justifyContent="end">
|
||||
<Label1 className={classes.feeDiscountLabel}>Fee discount</Label1>
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ const getLamassuRoutes = () => [
|
|||
},
|
||||
{
|
||||
key: 'services',
|
||||
label: '3rd party services',
|
||||
label: '3rd Party Services',
|
||||
route: '/settings/3rd-party-services',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Services
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ const getPazuzRoutes = () => [
|
|||
},
|
||||
{
|
||||
key: 'services',
|
||||
label: '3rd party services',
|
||||
label: '3rd Party Services',
|
||||
route: '/settings/3rd-party-services',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Services
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ export default {
|
|||
pointerEvents: 'none'
|
||||
},
|
||||
html: {
|
||||
height: fill
|
||||
height: fill,
|
||||
'@media screen and (max-height: 900px)': {
|
||||
scrollbarGutter: 'stable'
|
||||
}
|
||||
},
|
||||
body: {
|
||||
width: mainWidth,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<svg width="32px" height="32px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="↳-notification-center" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="notification-center_v01a#2-(open)" transform="translate(-1023.000000, -459.000000)" stroke="#1B2559">
|
||||
<g id="Group-5" transform="translate(1000.000000, 0.000000)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Loading…
Add table
Add a link
Reference in a new issue