From eb6e0909361d42ba46160f25e0339fe93259c990 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 16 Dec 2021 11:31:03 +0100 Subject: [PATCH 01/51] fix: redirect to m-status on invalid machine ID --- .../src/pages/Machines/Machines.js | 40 ++++++++++++++++--- new-lamassu-admin/src/routing/routes.js | 4 +- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/new-lamassu-admin/src/pages/Machines/Machines.js b/new-lamassu-admin/src/pages/Machines/Machines.js index 89c13f24..19381f20 100644 --- a/new-lamassu-admin/src/pages/Machines/Machines.js +++ b/new-lamassu-admin/src/pages/Machines/Machines.js @@ -7,7 +7,7 @@ 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 { Link, useLocation, useHistory } from 'react-router-dom' import { TL1, TL2, Label3 } from 'src/components/typography' @@ -50,13 +50,43 @@ const GET_INFO = gql` } ` +const GET_MACHINES = gql` + { + machines { + name + deviceId + } + } +` + const getMachineID = path => path.slice(path.lastIndexOf('/') + 1) -const Machines = () => { +const MachineRoute = () => { const location = useLocation() + const history = useHistory() + + const id = getMachineID(location.pathname) + + const { loading } = useQuery(GET_MACHINES, { + onCompleted: data => { + const machines = data.machines + const machineFound = machines.map(m => m.deviceId).includes(id) + + if (!machineFound) return history.push('/maintenance/machine-status') + } + }) + + const reload = () => { + return history.push(location.pathname) + } + + return !loading && +} + +const Machines = ({ id, reload }) => { const { data, loading, refetch } = useQuery(GET_INFO, { variables: { - deviceId: getMachineID(location.pathname) + deviceId: id } }) const classes = useStyles() @@ -85,7 +115,7 @@ const Machines = () => { {machineName} - + @@ -119,4 +149,4 @@ const Machines = () => { ) } -export default Machines +export default MachineRoute diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 18876686..5aa939bb 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -17,7 +17,7 @@ import Register from 'src/pages/Authentication/Register' import Reset2FA from 'src/pages/Authentication/Reset2FA' import ResetPassword from 'src/pages/Authentication/ResetPassword' import Dashboard from 'src/pages/Dashboard' -import Machines from 'src/pages/Machines' +import MachineRoute from 'src/pages/Machines' import Wizard from 'src/pages/Wizard' import PrivateRoute from './PrivateRoute' @@ -140,7 +140,7 @@ const Routes = () => { } /> - + {/* */} From 586ad4879b0ab120d4832aaef92cb08b7ef9a533 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 16 Dec 2021 11:41:09 +0100 Subject: [PATCH 02/51] chore: revert unnecessary changes --- new-lamassu-admin/src/routing/routes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 5aa939bb..18876686 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -17,7 +17,7 @@ import Register from 'src/pages/Authentication/Register' import Reset2FA from 'src/pages/Authentication/Reset2FA' import ResetPassword from 'src/pages/Authentication/ResetPassword' import Dashboard from 'src/pages/Dashboard' -import MachineRoute from 'src/pages/Machines' +import Machines from 'src/pages/Machines' import Wizard from 'src/pages/Wizard' import PrivateRoute from './PrivateRoute' @@ -140,7 +140,7 @@ const Routes = () => { } /> - + {/* */} From f620927f3bddb9bdeac016280c11a02e981f5b91 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 16 Dec 2021 12:35:27 +0100 Subject: [PATCH 03/51] fix: use state instead apollo `loading` result --- new-lamassu-admin/src/pages/Machines/Machines.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/new-lamassu-admin/src/pages/Machines/Machines.js b/new-lamassu-admin/src/pages/Machines/Machines.js index 19381f20..c4b5635f 100644 --- a/new-lamassu-admin/src/pages/Machines/Machines.js +++ b/new-lamassu-admin/src/pages/Machines/Machines.js @@ -6,7 +6,7 @@ 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 React, { useState } from 'react' import { Link, useLocation, useHistory } from 'react-router-dom' import { TL1, TL2, Label3 } from 'src/components/typography' @@ -67,12 +67,16 @@ const MachineRoute = () => { const id = getMachineID(location.pathname) - const { loading } = useQuery(GET_MACHINES, { + const [loading, setLoading] = useState(true) + + useQuery(GET_MACHINES, { onCompleted: data => { const machines = data.machines const machineFound = machines.map(m => m.deviceId).includes(id) if (!machineFound) return history.push('/maintenance/machine-status') + + setLoading(false) } }) From 79c2aa8988e24bb65947ec35f777f0ad7bc9da18 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Tue, 21 Dec 2021 18:17:22 +0100 Subject: [PATCH 04/51] fix: cash cassette wizard autofocus --- new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js | 1 + 1 file changed, 1 insertion(+) diff --git a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js index d28081c5..db1e242e 100644 --- a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js +++ b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js @@ -277,6 +277,7 @@ const WizardStep = ({ placeholder={originalCassetteCount.toString()} name={cassetteField} className={classes.cashboxBills} + autoFocus />

{cassetteDenomination} {fiatCurrency} bills loaded From 326e395aa095ed84af96db4ff01527a38301dfdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 22 Dec 2021 15:03:30 +0000 Subject: [PATCH 05/51] fix: customers page filter deletion --- .../src/pages/Customers/Customers.js | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index f263de86..09565cb4 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -51,6 +51,9 @@ const GET_CUSTOMERS = gql` 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() @@ -82,12 +85,7 @@ const Customers = () => { ) 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 +99,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) @@ -131,9 +157,10 @@ const Customers = () => { /> {filters.length > 0 && ( )} Date: Wed, 22 Dec 2021 22:02:57 +0100 Subject: [PATCH 06/51] fix: use `useEfect` for focus --- .../src/pages/Maintenance/Wizard/WizardStep.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js index db1e242e..cb5a1adf 100644 --- a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js +++ b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js @@ -139,6 +139,11 @@ const WizardStep = ({ R.clamp(0, 100) ) + const textInput = React.useRef(null) + React.useEffect(() => { + textInput.current?.children[0].children[0]?.focus() + }, [step]) + return (

@@ -277,7 +282,7 @@ const WizardStep = ({ placeholder={originalCassetteCount.toString()} name={cassetteField} className={classes.cashboxBills} - autoFocus + innerRef={textInput} />

{cassetteDenomination} {fiatCurrency} bills loaded From 5b7b342788925b805661ef304e0df25d09eb1f60 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:24:36 +0100 Subject: [PATCH 07/51] fix: machine table row --- .../Dashboard/SystemStatus/MachinesTable.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/new-lamassu-admin/src/pages/Dashboard/SystemStatus/MachinesTable.js b/new-lamassu-admin/src/pages/Dashboard/SystemStatus/MachinesTable.js index e97805f9..ee85d4af 100644 --- a/new-lamassu-admin/src/pages/Dashboard/SystemStatus/MachinesTable.js +++ b/new-lamassu-admin/src/pages/Dashboard/SystemStatus/MachinesTable.js @@ -128,17 +128,17 @@ const MachinesTable = ({ machines = [], numToRender }) => { onClick={() => redirect(machine)} className={classnames(classes.row)} key={machine.deviceId + idx}> - - {machine.name} - redirect(machine)} - /> + +

+ {machine.name} + redirect(machine)} + /> +
From deef6e52a37aba7d5e24339d4a04d2744c39fe53 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Fri, 24 Dec 2021 23:30:25 +0100 Subject: [PATCH 08/51] fix: throw error on missing resource --- lib/machine-loader.js | 58 ++++----- .../src/pages/Machines/Machines.js | 117 ++++++++---------- 2 files changed, 79 insertions(+), 96 deletions(-) diff --git a/lib/machine-loader.js b/lib/machine-loader.js index b2ae3afb..5790673f 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -12,30 +12,35 @@ 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 toMachineObject (r) { + return { + 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 + // TODO: we shall start using this JSON field at some point + // location: r.location, + } +} + function getMachines () { return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created') - .then(rr => rr.map(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, - // TODO: we shall start using this JSON field at some point - // location: r.location, - paired: r.paired - }))) + .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]) => { diff --git a/new-lamassu-admin/src/pages/Machines/Machines.js b/new-lamassu-admin/src/pages/Machines/Machines.js index c4b5635f..66fcf19d 100644 --- a/new-lamassu-admin/src/pages/Machines/Machines.js +++ b/new-lamassu-admin/src/pages/Machines/Machines.js @@ -50,15 +50,6 @@ const GET_INFO = gql` } ` -const GET_MACHINES = gql` - { - machines { - name - deviceId - } - } -` - const getMachineID = path => path.slice(path.lastIndexOf('/') + 1) const MachineRoute = () => { @@ -69,14 +60,15 @@ const MachineRoute = () => { const [loading, setLoading] = useState(true) - useQuery(GET_MACHINES, { + const { data, refetch } = useQuery(GET_INFO, { onCompleted: data => { - const machines = data.machines - const machineFound = machines.map(m => m.deviceId).includes(id) - - if (!machineFound) return history.push('/maintenance/machine-status') + if (data.machine === null) + return history.push('/maintenance/machine-status') setLoading(false) + }, + variables: { + deviceId: id } }) @@ -84,15 +76,14 @@ const MachineRoute = () => { return history.push(location.pathname) } - return !loading && + return ( + !loading && ( + + ) + ) } -const Machines = ({ id, reload }) => { - const { data, loading, refetch } = useQuery(GET_INFO, { - variables: { - deviceId: id - } - }) +const Machines = ({ data, refetch, reload }) => { const classes = useStyles() const timezone = R.path(['config', 'locale_timezone'], data) ?? {} @@ -104,52 +95,50 @@ const Machines = ({ id, reload }) => { const machineID = R.path(['deviceId'])(machine) ?? null return ( - !loading && ( - - - -
- }> - - - Dashboard - - - - {machineName} - - - -
-
-
- -
-
- {'Details'} -
-
-
- {'Cash cassettes'} - -
-
- {'Latest transactions'} - -
-
- {'Commissions'} - -
+ + + +
+ }> + + + Dashboard + + + + {machineName} + + +
- ) + +
+
+ {'Details'} +
+
+
+ {'Cash cassettes'} + +
+
+ {'Latest transactions'} + +
+
+ {'Commissions'} + +
+
+
+
) } From 6521d45ab51caeaf7fb58f05ebdeff3773abf9e3 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Fri, 24 Dec 2021 23:47:31 +0100 Subject: [PATCH 09/51] fix: conflicts --- .../src/pages/Machines/Machines.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/new-lamassu-admin/src/pages/Machines/Machines.js b/new-lamassu-admin/src/pages/Machines/Machines.js index 66fcf19d..7fdbd5cb 100644 --- a/new-lamassu-admin/src/pages/Machines/Machines.js +++ b/new-lamassu-admin/src/pages/Machines/Machines.js @@ -20,7 +20,7 @@ import styles from './Machines.styles' const useStyles = makeStyles(styles) const GET_INFO = gql` - query getMachine($deviceId: ID!) { + query getMachine($deviceId: ID!, $billFilters: JSONObject) { machine(deviceId: $deviceId) { name deviceId @@ -46,6 +46,12 @@ const GET_INFO = gql` note } } + bills(filters: $billFilters) { + id + fiat + deviceId + created + } config } ` @@ -69,6 +75,10 @@ const MachineRoute = () => { }, variables: { deviceId: id + }, + billFilters: { + deviceId: id, + batch: 'none' } }) @@ -83,7 +93,7 @@ const MachineRoute = () => { ) } -const Machines = ({ data, refetch, reload }) => { +const Machines = ({ data, refetch, reload, bills }) => { const classes = useStyles() const timezone = R.path(['config', 'locale_timezone'], data) ?? {} @@ -121,11 +131,12 @@ const Machines = ({ data, refetch, reload }) => {
- {'Cash cassettes'} + {'Cash box & cassettes'}
From 258d727b1aab5ce43755255c5c90193f79359766 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 27 Dec 2021 18:39:12 -0500 Subject: [PATCH 10/51] chore: update zec, xmr, eth versions --- lib/blockchain/common.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index 0f0625e6..ccec88a6 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -29,12 +29,12 @@ const BINARIES = { 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.14-11a3a350.tar.gz', + dir: 'geth-linux-amd64-1.10.14-11a3a350' }, 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-linux64-debian-stretch.tar.gz', + dir: 'zcash-4.6.0/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']] } } From 4d5d883532661ca5c92b7f62725912fe814151ef Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Tue, 28 Dec 2021 17:43:33 +0100 Subject: [PATCH 11/51] fix: capitalize labels --- new-lamassu-admin/src/routing/lamassu.routes.js | 2 +- new-lamassu-admin/src/routing/pazuz.routes.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/new-lamassu-admin/src/routing/lamassu.routes.js b/new-lamassu-admin/src/routing/lamassu.routes.js index 2626e30b..e647c65b 100644 --- a/new-lamassu-admin/src/routing/lamassu.routes.js +++ b/new-lamassu-admin/src/routing/lamassu.routes.js @@ -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 diff --git a/new-lamassu-admin/src/routing/pazuz.routes.js b/new-lamassu-admin/src/routing/pazuz.routes.js index eaed555c..153d0205 100644 --- a/new-lamassu-admin/src/routing/pazuz.routes.js +++ b/new-lamassu-admin/src/routing/pazuz.routes.js @@ -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 From 4a630f0f5348867bdb8843b1768546a189433f15 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Tue, 28 Dec 2021 20:09:43 +0100 Subject: [PATCH 12/51] fix: replace `client` and `username` with `email` fix: error handling in lamassu-register --- bin/lamassu-register | 19 +++++++++++-------- .../src/pages/Authentication/LoginState.js | 19 ++++++++++--------- .../src/pages/Authentication/Register.js | 4 ++++ .../src/pages/Authentication/Setup2FAState.js | 11 +++++------ 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/bin/lamassu-register b/bin/lamassu-register index c24a59b6..7d5284bc 100755 --- a/bin/lamassu-register +++ b/bin/lamassu-register @@ -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 ') + console.log('Usage: lamassu-register ') + console.log(' 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: should be in an email format') + console.log('Usage: must be in an email format') process.exit(2) } if (role !== 'user' && role !== 'superuser') { - console.log('Usage: has two possible values: user | superuser') + console.log('Usage: 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) }) diff --git a/new-lamassu-admin/src/pages/Authentication/LoginState.js b/new-lamassu-admin/src/pages/Authentication/LoginState.js index aaa91fe9..4cc4444b 100644 --- a/new-lamassu-admin/src/pages/Authentication/LoginState.js +++ b/new-lamassu-admin/src/pages/Authentication/LoginState.js @@ -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 }) => (
{ {!loading && state.result === 'failure' && ( <> Link has expired + + To obtain a new link, run the command{' '} + lamassu-register in your server’s terminal. + )}
diff --git a/new-lamassu-admin/src/pages/Authentication/Setup2FAState.js b/new-lamassu-admin/src/pages/Authentication/Setup2FAState.js index 410b49e4..618ed89e 100644 --- a/new-lamassu-admin/src/pages/Authentication/Setup2FAState.js +++ b/new-lamassu-admin/src/pages/Authentication/Setup2FAState.js @@ -140,14 +140,13 @@ const Setup2FAState = ({ state, dispatch }) => { <>
- 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. - 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.
From fab755dc629086f59e9696964d1b8c51f7e03dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Thu, 30 Dec 2021 14:23:14 +0000 Subject: [PATCH 13/51] fix: bitcoind install --- lib/blockchain/install.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/blockchain/install.js b/lib/blockchain/install.js index 8596d8f9..554e3951 100644 --- a/lib/blockchain/install.js +++ b/lib/blockchain/install.js @@ -71,7 +71,10 @@ function processCryptos (codes) { ) | crontab -` common.es(rsyncCmd) - _.forEach(updateCrypto, selectedCryptos) + _.forEach(c => { + updateCrypto(c) + common.es(`sudo supervisorctl start ${c.code}`) + }, selectedCryptos) logger.info('Installation complete.') } @@ -98,7 +101,9 @@ function setupCrypto (crypto) { function updateCrypto (crypto) { if (!common.isUpdateDependent(crypto.cryptoCode)) return const cryptoPlugin = plugin(crypto) - cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode)) + const status = common.es(`sudo supervisorctl status ${crypto.code} | awk '{ print $2 }'`).trim() + const isCurrentlyRunning = status === 'RUNNING' + cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode), isCurrentlyRunning) } function plugin (crypto) { From 0a27c14079e886cecb2aab5e85bd0cbb60069c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Thu, 30 Dec 2021 14:26:43 +0000 Subject: [PATCH 14/51] fix: Monero install --- lib/blockchain/common.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index 0f0625e6..273515e2 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -123,9 +123,7 @@ function fetchAndInstall (coinRec) { const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir es(`wget -q ${url}`) - es(`echo ${downloadFile} | awk -F. '{print $NF}'`) === 'bz2' - ? es(`tar -xf ${downloadFile}`) - : es(`tar -xzf ${downloadFile}`) + es(`tar -xf ${downloadFile}`) if (_.isEmpty(binaries.files)) { es(`sudo cp ${binDir}/* /usr/local/bin`) From ba7b1d3d97bf02ef29fea507f9fd99ec9f9809b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Thu, 30 Dec 2021 15:16:18 +0000 Subject: [PATCH 15/51] refactor: filter Tether/USDT from install candidates --- lib/blockchain/install.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/blockchain/install.js b/lib/blockchain/install.js index 554e3951..efc48107 100644 --- a/lib/blockchain/install.js +++ b/lib/blockchain/install.js @@ -113,15 +113,18 @@ function plugin (crypto) { } function run () { - const choices = _.map(c => { - const checked = isInstalledSoftware(c) && isInstalledVolume(c) - return { - name: c.display, - value: c.code, - checked, - disabled: checked && 'Installed' - } - }, cryptos) + const choices = _.flow([ + _.filter(c => c.type !== 'erc-20'), + _.map(c => { + const checked = isInstalledSoftware(c) && isInstalledVolume(c) + return { + name: c.display, + value: c.code, + checked, + disabled: checked && 'Installed' + } + }), + ])(cryptos) const questions = [] From 09f62e3a16a41c34f6ecb2396f1c57a074027881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20S=C3=A1?= Date: Thu, 30 Dec 2021 18:05:30 +0000 Subject: [PATCH 16/51] chore: rename "Ethereum" to "Ethereum and/or USDT" Only for the `lamassu-coins` script. --- lib/blockchain/install.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/blockchain/install.js b/lib/blockchain/install.js index efc48107..717c9945 100644 --- a/lib/blockchain/install.js +++ b/lib/blockchain/install.js @@ -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' From 33052cddb9cea2d4a75a12dfc4873190fb19db9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Tue, 4 Jan 2022 16:03:38 +0000 Subject: [PATCH 17/51] fix: session token retriever --- lib/new-admin/graphql/modules/userManagement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/new-admin/graphql/modules/userManagement.js b/lib/new-admin/graphql/modules/userManagement.js index 13c42e70..362e150f 100644 --- a/lib/new-admin/graphql/modules/userManagement.js +++ b/lib/new-admin/graphql/modules/userManagement.js @@ -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 From b41fe5e89423fa777c10df150387c103f8abe04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Tue, 4 Jan 2022 20:05:47 +0000 Subject: [PATCH 18/51] fix: incorrect useMemo on a function --- .../pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js index f2eb4e78..ef092185 100644 --- a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js +++ b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js @@ -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] ) From 13c4603de52b27abf4124730a2c0979494b0a351 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Fri, 7 Jan 2022 17:27:39 +0100 Subject: [PATCH 19/51] fix: revert commit --- .../src/pages/Maintenance/Wizard/WizardStep.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js index cb5a1adf..db1e242e 100644 --- a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js +++ b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js @@ -139,11 +139,6 @@ const WizardStep = ({ R.clamp(0, 100) ) - const textInput = React.useRef(null) - React.useEffect(() => { - textInput.current?.children[0].children[0]?.focus() - }, [step]) - return (
@@ -282,7 +277,7 @@ const WizardStep = ({ placeholder={originalCassetteCount.toString()} name={cassetteField} className={classes.cashboxBills} - innerRef={textInput} + autoFocus />

{cassetteDenomination} {fiatCurrency} bills loaded From bc8c1eb1ca76539fa1fcc738123623d2e4b49734 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Fri, 7 Jan 2022 19:52:25 +0100 Subject: [PATCH 20/51] fix: remove 'UTC' label --- .../Machines/MachineComponents/Transactions/Transactions.js | 2 +- .../src/pages/SessionManagement/SessionManagement.js | 2 +- new-lamassu-admin/src/pages/Transactions/Transactions.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/new-lamassu-admin/src/pages/Machines/MachineComponents/Transactions/Transactions.js b/new-lamassu-admin/src/pages/Machines/MachineComponents/Transactions/Transactions.js index a5902611..c5e71c95 100644 --- a/new-lamassu-admin/src/pages/Machines/MachineComponents/Transactions/Transactions.js +++ b/new-lamassu-admin/src/pages/Machines/MachineComponents/Transactions/Transactions.js @@ -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', diff --git a/new-lamassu-admin/src/pages/SessionManagement/SessionManagement.js b/new-lamassu-admin/src/pages/SessionManagement/SessionManagement.js index 13d1fe0c..83457474 100644 --- a/new-lamassu-admin/src/pages/SessionManagement/SessionManagement.js +++ b/new-lamassu-admin/src/pages/SessionManagement/SessionManagement.js @@ -79,7 +79,7 @@ const SessionManagement = () => { } }, { - header: 'Expiration date (UTC)', + header: 'Expiration date', width: 290, textAlign: 'right', size: 'sm', diff --git a/new-lamassu-admin/src/pages/Transactions/Transactions.js b/new-lamassu-admin/src/pages/Transactions/Transactions.js index 016c8b37..009335c3 100644 --- a/new-lamassu-admin/src/pages/Transactions/Transactions.js +++ b/new-lamassu-admin/src/pages/Transactions/Transactions.js @@ -222,7 +222,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', From f9d3b4abc0323a9af1b760e24aef3e7886bef991 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Fri, 7 Jan 2022 19:53:05 +0100 Subject: [PATCH 21/51] fix: add scrollbar gutter --- new-lamassu-admin/src/styling/global/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/new-lamassu-admin/src/styling/global/index.js b/new-lamassu-admin/src/styling/global/index.js index 7c57661e..4e5083ef 100644 --- a/new-lamassu-admin/src/styling/global/index.js +++ b/new-lamassu-admin/src/styling/global/index.js @@ -28,7 +28,10 @@ export default { pointerEvents: 'none' }, html: { - height: fill + height: fill, + '@media screen and (max-height: 900px)': { + scrollbarGutter: 'stable' + } }, body: { width: mainWidth, From 951a12714dc5e84cdcf32623dee26eadd97e6972 Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 12 Jan 2022 11:40:05 -0500 Subject: [PATCH 22/51] chore: update wallet daemons --- lib/blockchain/common.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index ab9c9382..4bcd7a93 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -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.14-11a3a350.tar.gz', - dir: 'geth-linux-amd64-1.10.14-11a3a350' + 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.6.0-linux64-debian-stretch.tar.gz', - dir: 'zcash-4.6.0/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', From 24bf700eba717548680fd0e3fed8f8edbab9cb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 12 Jan 2022 18:01:47 +0000 Subject: [PATCH 23/51] fix: customer list sorting --- new-lamassu-admin/src/pages/Customers/Customers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index 09565cb4..70e182f6 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -80,9 +80,9 @@ const Customers = () => { const configData = R.path(['config'])(customersResponse) ?? [] const locale = configData && fromNamespace(namespaces.LOCALE, configData) - const customersData = R.sortWith([R.descend(R.prop('lastActive'))])( - filteredCustomers ?? [] - ) + const customersData = R.sortWith([ + R.descend(it => new Date(R.prop('lastActive', it) ?? '0')) + ])(filteredCustomers ?? []) const onFilterChange = filters => { const filtersObject = getFiltersObj(filters) From 5f259dc9d3618d473232d41f667e2b132e60d11c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 12 Jan 2022 19:01:43 +0000 Subject: [PATCH 24/51] fix: cassettes width --- .../src/pages/Maintenance/CashCassettes.js | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js index 294cb13e..9b78e65d 100644 --- a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js +++ b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js @@ -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 }) => ( { 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 ( Date: Wed, 12 Jan 2022 23:48:43 +0000 Subject: [PATCH 25/51] fix: stop writing to database on null data --- lib/routes/customerRoutes.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js index de4e356d..8b0e51d6 100644 --- a/lib/routes/customerRoutes.js +++ b/lib/routes/customerRoutes.js @@ -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) From 94eed283cb63eb990a7afe01df1e4d27d09efa02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 15 Sep 2021 14:42:46 +0100 Subject: [PATCH 26/51] feat: add customer creation modal --- .../graphql/resolvers/customer.resolver.js | 3 +- lib/new-admin/graphql/types/customer.type.js | 1 + new-lamassu-admin/package-lock.json | 5 + new-lamassu-admin/package.json | 1 + .../src/components/layout/TitleSection.js | 7 +- .../src/pages/Customers/Customers.js | 35 +++++- .../src/pages/Customers/CustomersList.js | 2 +- .../components/CreateCustomerModal.js | 108 ++++++++++++++++++ 8 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js index d3563669..58461fd8 100644 --- a/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -49,7 +49,8 @@ const resolvers = { }, deleteCustomerNote: (...[, { noteId }]) => { return customerNotes.deleteCustomerNote(noteId) - } + }, + createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }) } } diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js index bdbf3a94..ce8cb3cb 100644 --- a/lib/new-admin/graphql/types/customer.type.js +++ b/lib/new-admin/graphql/types/customer.type.js @@ -103,6 +103,7 @@ const typeDef = gql` 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 } ` diff --git a/new-lamassu-admin/package-lock.json b/new-lamassu-admin/package-lock.json index e64cc12a..b54297ef 100644 --- a/new-lamassu-admin/package-lock.json +++ b/new-lamassu-admin/package-lock.json @@ -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", diff --git a/new-lamassu-admin/package.json b/new-lamassu-admin/package.json index bd97a32c..ab5f4f1c 100644 --- a/new-lamassu-admin/package.json +++ b/new-lamassu-admin/package.json @@ -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", diff --git a/new-lamassu-admin/src/components/layout/TitleSection.js b/new-lamassu-admin/src/components/layout/TitleSection.js index 6858ed5f..f0777103 100644 --- a/new-lamassu-admin/src/components/layout/TitleSection.js +++ b/new-lamassu-admin/src/components/layout/TitleSection.js @@ -19,14 +19,14 @@ const TitleSection = ({ buttons = [], children, appendix, - appendixClassName + appendixRight }) => { const classes = useStyles() return (

{title} - {appendix &&
{appendix}
} + {!!appendix && appendix} {error && ( Failed to save )} @@ -46,13 +46,14 @@ const TitleSection = ({ )}
- + {(labels ?? []).map(({ icon, label }, idx) => (
{icon}
{label}
))} + {appendixRight}
{children}
diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index 70e182f6..05c770c3 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -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 { @@ -49,6 +51,14 @@ const GET_CUSTOMERS = gql` } ` +const CREATE_CUSTOMER = gql` + mutation createCustomer($phoneNumber: String) { + createCustomer(phoneNumber: $phoneNumber) { + phone + } + } +` + const useBaseStyles = makeStyles(baseStyles) const getFiltersObj = filters => @@ -64,6 +74,7 @@ const Customers = () => { const [filteredCustomers, setFilteredCustomers] = useState([]) const [variables, setVariables] = useState({}) const [filters, setFilters] = useState([]) + const [showCreationModal, setShowCreationModal] = useState(false) const { data: customersResponse, @@ -78,6 +89,11 @@ const Customers = () => { GET_CUSTOMER_FILTERS ) + const [createNewCustomer] = useMutation(CREATE_CUSTOMER, { + onCompleted: () => setShowCreationModal(false), + refetchQueries: () => ['configAndCustomers'] + }) + const configData = R.path(['config'])(customersResponse) ?? [] const locale = configData && fromNamespace(namespaces.LOCALE, configData) const customersData = R.sortWith([ @@ -139,7 +155,7 @@ const Customers = () => { +
{ />
} - appendixClassName={baseStyles.buttonsWrapper} + appendixRight={ + + setShowCreationModal(true)}> + Add new user + + + } labels={[ { label: 'Cash-in', icon: }, { label: 'Cash-out', icon: } @@ -169,6 +191,11 @@ const Customers = () => { onClick={handleCustomerClicked} loading={customerLoading} /> + setShowCreationModal(false)} + onSubmit={createNewCustomer} + /> ) } diff --git a/new-lamassu-admin/src/pages/Customers/CustomersList.js b/new-lamassu-admin/src/pages/Customers/CustomersList.js index 43e40c2a..6b9e4167 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomersList.js +++ b/new-lamassu-admin/src/pages/Customers/CustomersList.js @@ -19,7 +19,7 @@ const CustomersList = ({ data, locale, onClick, loading }) => { const elements = [ { header: 'Phone', - width: 175, + width: 199, view: it => getFormattedPhone(it.phone, locale.country) }, { diff --git a/new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js b/new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js new file mode 100644 index 00000000..f36048a6 --- /dev/null +++ b/new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js @@ -0,0 +1,108 @@ +import { makeStyles } from '@material-ui/core/styles' +import { Field, Form, Formik } from 'formik' +import { PhoneNumberUtil } from 'google-libphonenumber' +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 validationSchema = 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 number = pnUtilInstance.parseAndKeepRawInput(value, 'US') + return pnUtilInstance.isValidNumber(number) + } catch (e) {} + }) +}) + +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 }) => { + const classes = useStyles() + + return ( + + { + onSubmit({ + variables: { phoneNumber: values.phoneNumber } + }) + }}> + {({ errors, touched }) => ( + +

Create new customer

+ +
+ {getErrorMsg(errors, touched) && ( + {getErrorMsg(errors, touched)} + )} + +
+ + )} +
+
+ ) +} + +export default CreateCustomerModal From 18d5d7372f4143710565e1e2785e362955559566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 17 Sep 2021 19:18:18 +0100 Subject: [PATCH 27/51] fix: add phone number input validation --- .../src/pages/Customers/Customers.js | 1 + .../components/CreateCustomerModal.js | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index 05c770c3..1875d44d 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -194,6 +194,7 @@ const Customers = () => { setShowCreationModal(false)} + locale={locale} onSubmit={createNewCustomer} /> diff --git a/new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js b/new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js index f36048a6..984d9d64 100644 --- a/new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js +++ b/new-lamassu-admin/src/pages/Customers/components/CreateCustomerModal.js @@ -1,6 +1,7 @@ import { makeStyles } from '@material-ui/core/styles' import { Field, Form, Formik } from 'formik' -import { PhoneNumberUtil } from 'google-libphonenumber' +import { PhoneNumberFormat, PhoneNumberUtil } from 'google-libphonenumber' +import * as R from 'ramda' import React from 'react' import * as Yup from 'yup' @@ -34,16 +35,36 @@ const styles = { const pnUtilInstance = PhoneNumberUtil.getInstance() -const validationSchema = 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 number = pnUtilInstance.parseAndKeepRawInput(value, 'US') - return pnUtilInstance.isValidNumber(number) - } catch (e) {} - }) -}) +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: '' @@ -58,9 +79,14 @@ const getErrorMsg = (formikErrors, formikTouched) => { return null } -const CreateCustomerModal = ({ showModal, handleClose, onSubmit }) => { +const CreateCustomerModal = ({ showModal, handleClose, onSubmit, locale }) => { const classes = useStyles() + const possibleCountries = R.append( + locale?.country, + R.map(it => it.country, locale?.overrides ?? []) + ) + return ( { handleClose={handleClose} open={showModal}> { onSubmit({ - variables: { phoneNumber: values.phoneNumber } + variables: { + phoneNumber: formatPhoneNumber( + possibleCountries, + values.phoneNumber + ) + } }) }}> {({ errors, touched }) => ( From cf968aeaa32f90d208f28b68401eae649053668e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 12 Nov 2021 15:58:44 +0000 Subject: [PATCH 28/51] fix: customer list column widths --- new-lamassu-admin/src/pages/Customers/CustomersList.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/CustomersList.js b/new-lamassu-admin/src/pages/Customers/CustomersList.js index 6b9e4167..2a3cca6a 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomersList.js +++ b/new-lamassu-admin/src/pages/Customers/CustomersList.js @@ -24,31 +24,31 @@ const CustomersList = ({ data, locale, onClick, loading }) => { }, { 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) From bd0701236c24890f5e16e6683cd21d7a32ca174b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Thu, 13 Jan 2022 15:30:31 +0000 Subject: [PATCH 29/51] feat: enable custom entries and custom information requirements --- lib/customers.js | 1 + lib/new-admin/graphql/types/customer.type.js | 18 +-- .../src/pages/Customers/CustomerData.js | 139 ++++++------------ .../src/pages/Customers/CustomerProfile.js | 47 +++++- .../src/pages/Customers/Wizard.js | 57 +++++-- .../Customers/components/EditableCard.js | 6 +- .../src/pages/Customers/helper.js | 114 ++++++++++++-- .../Graphs/RefScatterplot.js | 2 +- .../src/pages/Triggers/helper.js | 2 +- 9 files changed, 257 insertions(+), 129 deletions(-) diff --git a/lib/customers.js b/lib/customers.js index e96155f9..0f765bc9 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -993,6 +993,7 @@ function addCustomField (customerId, label, value) { } }) ) + .then(res => !_.isNil(res)) } function saveCustomField (customerId, fieldId, newValue) { diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js index bdbf3a94..765f65e1 100644 --- a/lib/new-admin/graphql/types/customer.type.js +++ b/lib/new-admin/graphql/types/customer.type.js @@ -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 @@ -86,6 +80,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,9 +94,9 @@ 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 diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index 18a53cac..a5346a0e 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -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,7 @@ import { URI } from 'src/utils/apollo' import styles from './CustomerData.styles.js' import { EditableCard } from './components' +import { customerDataElements, customerDataschemas } from './helper.js' const useStyles = makeStyles(styles) @@ -84,8 +85,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,85 +95,6 @@ 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: { firstName: R.path(['firstName'])(idData) ?? '', @@ -214,7 +136,7 @@ const CustomerData = ({ const cards = [ { - fields: idScanElements, + fields: customerDataElements.idScanElements, title: 'ID Scan', titleIcon: , state: R.path(['idCardDataOverride'])(customer), @@ -226,7 +148,7 @@ const CustomerData = ({ editCustomer({ idCardData: _.merge(idData, formatDates(values)) }), - validationSchema: schemas.idScan, + validationSchema: customerDataschemas.idScan, initialValues: initialValues.idScan, isAvailable: !_.isNil(idData) }, @@ -257,7 +179,7 @@ const CustomerData = ({ isAvailable: !_.isNil(sanctions) }, { - fields: frontCameraElements, + fields: customerDataElements.frontCameraElements, title: 'Front facing camera', titleIcon: , state: R.path(['frontCameraOverride'])(customer), @@ -279,12 +201,12 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: schemas.frontCamera, + validationSchema: customerDataschemas.frontCamera, initialValues: initialValues.frontCamera, isAvailable: !_.isNil(customer.frontCameraPath) }, { - fields: idCardPhotoElements, + fields: customerDataElements.idCardPhotoElements, title: 'ID card image', titleIcon: , state: R.path(['idCardPhotoOverride'])(customer), @@ -304,12 +226,12 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: schemas.idCardPhoto, + validationSchema: customerDataschemas.idCardPhoto, initialValues: initialValues.idCardPhoto, isAvailable: !_.isNil(customer.idCardPhotoPath) }, { - fields: usSsnElements, + fields: customerDataElements.usSsnElements, title: 'US SSN', titleIcon: , state: R.path(['usSsnOverride'])(customer), @@ -317,7 +239,7 @@ const CustomerData = ({ reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }), save: values => editCustomer({ usSsn: values.usSsn }), deleteEditedData: () => deleteEditedData({ usSsn: null }), - validationSchema: schemas.usSsn, + validationSchema: customerDataschemas.usSsn, initialValues: initialValues.usSsn, isAvailable: !_.isNil(customer.usSsn) } @@ -374,6 +296,29 @@ const CustomerData = ({ }) }, customInfoRequests) + R.forEach(it => { + customFields.push({ + fields: [ + { + name: it.label, + label: it.label, + value: it.value ?? '', + component: TextInput + } + ], + title: it.label, + titleIcon: , + save: () => {}, + deleteEditedData: () => {}, + validationSchema: Yup.object().shape({ + [it.label]: Yup.string() + }), + initialValues: { + [it.label]: it.value ?? '' + } + }) + }, R.path(['customFields'])(customer) ?? []) + const editableCard = ( { title, @@ -444,9 +389,21 @@ const CustomerData = ({ )} - {customEntries && ( + {!_.isEmpty(customFields) && (
Custom data entry + + + {customFields.map((elem, idx) => { + return isEven(idx) ? editableCard(elem, idx) : null + })} + + + {customFields.map((elem, idx) => { + return !isEven(idx) ? editableCard(elem, idx) : null + })} + +
)} {!R.isEmpty(customRequirements) && ( diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 91b076ee..d83960d3 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -236,6 +236,21 @@ const GET_DATA = gql` } ` +const SET_CUSTOM_ENTRY = gql` + mutation addCustomField($customerId: ID!, $label: String!, $value: String!) { + addCustomField(customerId: $customerId, label: $label, 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 +270,16 @@ 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 [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, { onCompleted: () => getCustomer() }) @@ -294,6 +319,17 @@ const CustomerProfile = memo(() => { onCompleted: () => getCustomer() }) + const saveCustomEntry = it => { + setCustomEntry({ + variables: { + customerId, + label: it.title, + value: it.data + } + }) + setWizard(null) + } + const updateCustomer = it => setCustomer({ variables: { @@ -385,6 +421,12 @@ const CustomerProfile = memo(() => { const timezone = R.path(['config', 'locale_timezone'], configResponse) + const customRequirementOptions = + activeCustomRequests?.customInfoRequests?.map(it => ({ + value: it.id, + display: it.customRequest.name + })) ?? [] + const classes = useStyles() return ( @@ -544,8 +586,11 @@ const CustomerProfile = memo(() => { {wizard && ( {}} + save={saveCustomEntry} + addPhoto={replacePhoto} + addCustomerData={editCustomer} onClose={() => setWizard(null)} + customRequirementOptions={customRequirementOptions} /> )}
diff --git a/new-lamassu-admin/src/pages/Customers/Wizard.js b/new-lamassu-admin/src/pages/Customers/Wizard.js index 87bebaa8..16db1647 100644 --- a/new-lamassu-admin/src/pages/Customers/Wizard.js +++ b/new-lamassu-admin/src/pages/Customers/Wizard.js @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core' -import { Form, Formik } from 'formik' +import { Form, Formik, Field } from 'formik' import * as R from 'ramda' import React, { useState, Fragment } from 'react' @@ -7,6 +7,7 @@ import ErrorMessage from 'src/components/ErrorMessage' import Modal from 'src/components/Modal' import Stepper from 'src/components/Stepper' import { Button } from 'src/components/buttons' +import { Dropdown } from 'src/components/inputs/formik' import { comet } from 'src/styling/variables' import { entryType, customElements } from './helper' @@ -41,6 +42,10 @@ const styles = { margin: [[0, 4, 0, 2]], borderBottom: `1px solid ${comet}`, display: 'inline-block' + }, + dropdownField: { + marginTop: 16, + minWidth: 155 } } @@ -57,7 +62,14 @@ const getStep = (step, selectedValues) => { } } -const Wizard = ({ onClose, save, error }) => { +const Wizard = ({ + onClose, + save, + error, + customRequirementOptions, + addCustomerData, + addPhoto +}) => { const classes = useStyles() const [selectedValues, setSelectedValues] = useState(null) @@ -66,6 +78,7 @@ const Wizard = ({ onClose, save, error }) => { step: 1 }) + const isCustom = values => values?.requirement === 'custom' const isLastStep = step === LAST_STEP const stepOptions = getStep(step, selectedValues) @@ -103,18 +116,34 @@ const Wizard = ({ onClose, save, error }) => { onSubmit={onContinue} initialValues={stepOptions.initialValues} validationSchema={stepOptions.schema}> -
- -
- {error && Failed to save} - -
- + {({ values }) => ( +
+ + {isCustom(values) && ( +
+ +
+ )} +
+ {error && Failed to save} + +
+ + )} diff --git a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js index 0239db8a..65507e91 100644 --- a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js +++ b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js @@ -150,7 +150,7 @@ const EditableCard = ({

{title}

- {state && ( + {state && authorize && (
@@ -279,7 +279,7 @@ const EditableCard = ({ Cancel
- {authorized.label !== 'Accepted' && ( + {authorize && authorized.label !== 'Accepted' && (
)} - {authorized.label !== 'Rejected' && ( + {authorize && authorized.label !== 'Rejected' && ( { ) ?? ''}`.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: 'facephoto' } ] const customTextOptions = [ @@ -108,7 +111,7 @@ const customTextSchema = Yup.object().shape({ data: Yup.string().required() }) -const EntryType = () => { +const EntryType = ({ hasCustomRequirementOptions }) => { const classes = useStyles() const { values } = useFormikContext() @@ -154,7 +157,17 @@ const EntryType = () => { 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 mapKeys = pair => { const [key, value] = pair if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') { @@ -245,5 +339,7 @@ export { getName, entryType, customElements, - formatPhotosData + formatPhotosData, + customerDataElements, + customerDataschemas } diff --git a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js index f2eb4e78..ef092185 100644 --- a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js +++ b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js @@ -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] ) diff --git a/new-lamassu-admin/src/pages/Triggers/helper.js b/new-lamassu-admin/src/pages/Triggers/helper.js index fcf91e71..20a11664 100644 --- a/new-lamassu-admin/src/pages/Triggers/helper.js +++ b/new-lamassu-admin/src/pages/Triggers/helper.js @@ -554,7 +554,7 @@ const Requirement = () => { } const options = enableCustomRequirement ? [...requirementOptions, customInfoOption] - : [...requirementOptions, { ...customInfoOption, disabled: true }] + : [...requirementOptions] const titleClass = { [classes.error]: (!!errors.requirement && !isSuspend) || (isSuspend && hasRequirementError) From 70b34458846dab339d207232c1dd42e4511eb2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Thu, 13 Jan 2022 16:19:48 +0000 Subject: [PATCH 30/51] fix: subpage buttons missing --- .../src/pages/Maintenance/CashCassettes.js | 14 ++++++++------ new-lamassu-admin/src/pages/Wallet/Wallet.js | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js index 294cb13e..9ae0a6bd 100644 --- a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js +++ b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js @@ -279,12 +279,14 @@ const CashCassettes = () => { <> {!showHistory && ( diff --git a/new-lamassu-admin/src/pages/Wallet/Wallet.js b/new-lamassu-admin/src/pages/Wallet/Wallet.js index b80ea805..1fff8153 100644 --- a/new-lamassu-admin/src/pages/Wallet/Wallet.js +++ b/new-lamassu-admin/src/pages/Wallet/Wallet.js @@ -140,12 +140,14 @@ const Wallet = ({ name: SCREEN_KEY }) => {
Fee discount From 0d88e6eba2c47d157fc877fd5e47e7e77886b7e4 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Thu, 13 Jan 2022 18:25:33 +0100 Subject: [PATCH 31/51] fix: date parsing --- new-lamassu-admin/src/pages/Transactions/DetailsCard.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js index 334ee3a5..e33ca0a7 100644 --- a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js +++ b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js @@ -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' @@ -123,9 +123,8 @@ const DetailsRow = ({ it: tx, timezone }) => { age: differenceInYears(tx.customerIdCardData.dateOfBirth, new Date()), country: tx.customerIdCardData.country, idCardNumber: tx.customerIdCardData.documentNumber, - idCardExpirationDate: format( - 'dd-MM-yyyy', - tx.customerIdCardData.expirationDate + idCardExpirationDate: format('yyyy-MM-dd')( + parse(new Date(), 'yyyyMMdd', tx.customerIdCardData.expirationDate) ) } From 681b76739f6dc6efe859e713b5eb3e84ea60cee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Thu, 13 Jan 2022 17:42:22 +0000 Subject: [PATCH 32/51] fix: initialize empty cassettes in case of undefined values --- lib/bill-math.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/bill-math.js b/lib/bill-math.js index d4801c0f..fbb3ee99 100644 --- a/lib/bill-math.js +++ b/lib/bill-math.js @@ -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 From 7b069c3e7b9ccee2edb60e4cd24ffe52403645d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 14 Jan 2022 07:08:20 +0000 Subject: [PATCH 33/51] fix: revert header changes fix: null pointer check --- new-lamassu-admin/src/components/layout/Header.js | 2 +- .../src/pages/Maintenance/CashCassettes.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/new-lamassu-admin/src/components/layout/Header.js b/new-lamassu-admin/src/components/layout/Header.js index 12e57ae5..0c62549a 100644 --- a/new-lamassu-admin/src/components/layout/Header.js +++ b/new-lamassu-admin/src/components/layout/Header.js @@ -132,7 +132,7 @@ const Header = memo(({ tree, user }) => { return ( { if (!match) return false setActive(it) diff --git a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js index 9b78e65d..7cec8ec7 100644 --- a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js +++ b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js @@ -225,14 +225,14 @@ const CashCassettes = () => { { name: 'name', header: 'Machine', - width: widthsByNumberOfCassettes[maxNumberOfCassettes].machine, + width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine, view: name => <>{name}, input: ({ field: { value: name } }) => <>{name} }, { name: 'cashbox', header: 'Cash box', - width: widthsByNumberOfCassettes[maxNumberOfCassettes].cashbox, + width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cashbox, view: (value, { id }) => ( { elements.push({ name: `cassette${it}`, header: `Cassette ${it}`, - width: widthsByNumberOfCassettes[maxNumberOfCassettes].cassette, + width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassette, stripe: true, doubleHeader: 'Cash-out', view: (value, { id }) => ( @@ -263,7 +263,7 @@ const CashCassettes = () => { currency={{ code: fiatCurrency }} notes={value} width={ - widthsByNumberOfCassettes[maxNumberOfCassettes].cassetteGraph + widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassetteGraph } threshold={ fillingPercentageSettings[`fillingPercentageCassette${it}`] @@ -274,7 +274,7 @@ const CashCassettes = () => { input: CashCassetteInput, inputProps: { decimalPlaces: 0, - width: widthsByNumberOfCassettes[maxNumberOfCassettes].cassetteGraph, + width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassetteGraph, inputClassName: classes.cashbox } }) @@ -286,7 +286,7 @@ const CashCassettes = () => { elements.push({ name: 'edit', header: 'Edit', - width: widthsByNumberOfCassettes[maxNumberOfCassettes].editWidth, + width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.editWidth, textAlign: 'center', view: (value, { id }) => { return ( From 88637c4776af8dda572702c8150b282335de655f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 14 Jan 2022 19:38:17 +0000 Subject: [PATCH 34/51] fix: improve UX when creating a trigger using custom requirements --- .../src/pages/Triggers/TriggerView.js | 4 +++ .../src/pages/Triggers/Wizard.js | 8 +++--- .../src/pages/Triggers/helper.js | 26 ++++--------------- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/new-lamassu-admin/src/pages/Triggers/TriggerView.js b/new-lamassu-admin/src/pages/Triggers/TriggerView.js index 52256f55..93bbdc87 100644 --- a/new-lamassu-admin/src/pages/Triggers/TriggerView.js +++ b/new-lamassu-admin/src/pages/Triggers/TriggerView.js @@ -75,6 +75,10 @@ const TriggerView = ({ error={error?.message} save={add} onClose={toggleWizard} + customInfoRequests={R.filter( + it => it.enabled === true, + customInfoRequests + )} /> )} {R.isEmpty(triggers) && ( diff --git a/new-lamassu-admin/src/pages/Triggers/Wizard.js b/new-lamassu-admin/src/pages/Triggers/Wizard.js index 44eb7875..dfc5fd03 100644 --- a/new-lamassu-admin/src/pages/Triggers/Wizard.js +++ b/new-lamassu-admin/src/pages/Triggers/Wizard.js @@ -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)) diff --git a/new-lamassu-admin/src/pages/Triggers/helper.js b/new-lamassu-admin/src/pages/Triggers/helper.js index fcf91e71..f62b5ddf 100644 --- a/new-lamassu-admin/src/pages/Triggers/helper.js +++ b/new-lamassu-admin/src/pages/Triggers/helper.js @@ -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' @@ -508,16 +506,7 @@ const requirementOptions = [ { display: 'Block', code: 'block' } ] -const GET_ACTIVE_CUSTOM_REQUESTS = gql` - query customInfoRequests($onlyEnabled: Boolean) { - customInfoRequests(onlyEnabled: $onlyEnabled) { - id - customRequest - } - } -` - -const Requirement = () => { +const Requirement = ({ customInfoRequests }) => { const classes = useStyles() const { touched, @@ -526,11 +515,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' @@ -546,8 +530,7 @@ const Requirement = () => { (!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' @@ -604,10 +587,11 @@ const Requirement = () => { ) } -const requirements = { +const requirements = customInfoRequests => ({ schema: requirementSchema, options: requirementOptions, Component: Requirement, + props: { customInfoRequests }, initialValues: { requirement: { requirement: '', @@ -615,7 +599,7 @@ const requirements = { customInfoRequestId: '' } } -} +}) const getView = (data, code, compare) => it => { if (!data) return '' From dc1f8f82f9d7142ed06b2be185e84862798ad544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 14 Jan 2022 19:45:18 +0000 Subject: [PATCH 35/51] fix: remove duplicate code --- new-lamassu-admin/src/pages/Triggers/TriggerView.js | 5 +---- new-lamassu-admin/src/pages/Triggers/Triggers.js | 10 +++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/new-lamassu-admin/src/pages/Triggers/TriggerView.js b/new-lamassu-admin/src/pages/Triggers/TriggerView.js index 93bbdc87..d07bdce9 100644 --- a/new-lamassu-admin/src/pages/Triggers/TriggerView.js +++ b/new-lamassu-admin/src/pages/Triggers/TriggerView.js @@ -75,10 +75,7 @@ const TriggerView = ({ error={error?.message} save={add} onClose={toggleWizard} - customInfoRequests={R.filter( - it => it.enabled === true, - customInfoRequests - )} + customInfoRequests={customInfoRequests} /> )} {R.isEmpty(triggers) && ( diff --git a/new-lamassu-admin/src/pages/Triggers/Triggers.js b/new-lamassu-admin/src/pages/Triggers/Triggers.js index af4046bc..c7e82550 100644 --- a/new-lamassu-admin/src/pages/Triggers/Triggers.js +++ b/new-lamassu-admin/src/pages/Triggers/Triggers.js @@ -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 ( <> { showWizard={wizardType === 'newTrigger'} config={data?.config ?? {}} toggleWizard={toggleWizard('newTrigger')} - customInfoRequests={customInfoRequests} + customInfoRequests={enabledCustomInfoRequests} /> )} {!loading && subMenu === 'advancedSettings' && ( From ebfdbbb3fc8ca098ca5e5eb09761d2b2fe613434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 14 Jan 2022 21:05:55 +0000 Subject: [PATCH 36/51] fix: identify customers with manual pending data --- .../src/pages/Customers/Customers.js | 12 +++++ .../src/pages/Customers/CustomersList.js | 4 +- .../src/pages/Customers/helper.js | 46 ++++++++++++++++--- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index 70e182f6..ea584f9c 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -43,6 +43,16 @@ const GET_CUSTOMERS = gql` lastTxFiatCode lastTxClass authorizedOverride + frontCameraPath + frontCameraOverride + idCardPhotoPath + idCardPhotoOverride + idCardData + idCardDataOverride + usSsn + usSsnOverride + sanctions + sanctionsOverride daysSuspended isSuspended } @@ -80,6 +90,7 @@ const Customers = () => { const configData = R.path(['config'])(customersResponse) ?? [] const locale = configData && fromNamespace(namespaces.LOCALE, configData) + const triggers = configData && fromNamespace(namespaces.TRIGGERS, configData) const customersData = R.sortWith([ R.descend(it => new Date(R.prop('lastActive', it) ?? '0')) ])(filteredCustomers ?? []) @@ -168,6 +179,7 @@ const Customers = () => { locale={locale} onClick={handleCustomerClicked} loading={customerLoading} + triggers={triggers} /> ) diff --git a/new-lamassu-admin/src/pages/Customers/CustomersList.js b/new-lamassu-admin/src/pages/Customers/CustomersList.js index 43e40c2a..2684e36e 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomersList.js +++ b/new-lamassu-admin/src/pages/Customers/CustomersList.js @@ -13,7 +13,7 @@ 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 = [ @@ -66,7 +66,7 @@ const CustomersList = ({ data, locale, onClick, loading }) => { { header: 'Status', width: 191, - view: it => + view: it => } ] diff --git a/new-lamassu-admin/src/pages/Customers/helper.js b/new-lamassu-admin/src/pages/Customers/helper.js index 719ae628..c21f1d7e 100644 --- a/new-lamassu-admin/src/pages/Customers/helper.js +++ b/new-lamassu-admin/src/pages/Customers/helper.js @@ -8,6 +8,7 @@ import * as Yup from 'yup' import { RadioGroup, TextInput } 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' @@ -39,14 +40,47 @@ const useStyles = makeStyles({ const CUSTOMER_BLOCKED = 'blocked' -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 = From 8ad127c6c4819987f283e19cecec9e77410e8328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Mon, 17 Jan 2022 00:03:18 +0000 Subject: [PATCH 37/51] feat: enable customer data manual entry --- .../src/pages/Customers/CustomerData.js | 48 ++-- .../src/pages/Customers/CustomerProfile.js | 35 ++- .../src/pages/Customers/Wizard.js | 60 +++-- .../src/pages/Customers/components/Upload.js | 15 +- .../src/pages/Customers/helper.js | 212 ++++++++++++++---- 5 files changed, 274 insertions(+), 96 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index a5346a0e..3f792db3 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -26,7 +26,11 @@ import { URI } from 'src/utils/apollo' import styles from './CustomerData.styles.js' import { EditableCard } from './components' -import { customerDataElements, customerDataschemas } from './helper.js' +import { + customerDataElements, + customerDataSchemas, + formatDates +} from './helper.js' const useStyles = makeStyles(styles) @@ -64,7 +68,8 @@ const CustomerData = ({ editCustomer, deleteEditedData, updateCustomRequest, - authorizeCustomRequest + authorizeCustomRequest, + updateCustomEntry }) => { const classes = useStyles() const [listView, setListView] = useState(false) @@ -96,7 +101,7 @@ const CustomerData = ({ const getVisibleCards = _.filter(elem => elem.isAvailable) const initialValues = { - idScan: { + idCardData: { firstName: R.path(['firstName'])(idData) ?? '', lastName: R.path(['lastName'])(idData) ?? '', documentNumber: R.path(['documentNumber'])(idData) ?? '', @@ -124,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: customerDataElements.idScanElements, + fields: customerDataElements.idCardData, title: 'ID Scan', titleIcon: , state: R.path(['idCardDataOverride'])(customer), @@ -148,8 +143,8 @@ const CustomerData = ({ editCustomer({ idCardData: _.merge(idData, formatDates(values)) }), - validationSchema: customerDataschemas.idScan, - initialValues: initialValues.idScan, + validationSchema: customerDataSchemas.idCardData, + initialValues: initialValues.idCardData, isAvailable: !_.isNil(idData) }, { @@ -179,7 +174,7 @@ const CustomerData = ({ isAvailable: !_.isNil(sanctions) }, { - fields: customerDataElements.frontCameraElements, + fields: customerDataElements.frontCamera, title: 'Front facing camera', titleIcon: , state: R.path(['frontCameraOverride'])(customer), @@ -201,12 +196,12 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: customerDataschemas.frontCamera, + validationSchema: customerDataSchemas.frontCamera, initialValues: initialValues.frontCamera, isAvailable: !_.isNil(customer.frontCameraPath) }, { - fields: customerDataElements.idCardPhotoElements, + fields: customerDataElements.idCardPhoto, title: 'ID card image', titleIcon: , state: R.path(['idCardPhotoOverride'])(customer), @@ -226,20 +221,20 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: customerDataschemas.idCardPhoto, + validationSchema: customerDataSchemas.idCardPhoto, initialValues: initialValues.idCardPhoto, isAvailable: !_.isNil(customer.idCardPhotoPath) }, { - fields: customerDataElements.usSsnElements, + fields: customerDataElements.usSsn, title: 'US SSN', titleIcon: , 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: customerDataschemas.usSsn, + validationSchema: customerDataSchemas.usSsn, initialValues: initialValues.usSsn, isAvailable: !_.isNil(customer.usSsn) } @@ -308,7 +303,12 @@ const CustomerData = ({ ], title: it.label, titleIcon: , - save: () => {}, + save: values => { + updateCustomEntry({ + fieldId: it.id, + value: values[it.label] + }) + }, deleteEditedData: () => {}, validationSchema: Yup.object().shape({ [it.label]: Yup.string() diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index d83960d3..71b24ba2 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -242,6 +242,12 @@ const SET_CUSTOM_ENTRY = gql` } ` +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) { @@ -280,6 +286,10 @@ const CustomerProfile = memo(() => { onCompleted: () => getCustomer() }) + const [editCustomEntry] = useMutation(EDIT_CUSTOM_ENTRY, { + onCompleted: () => getCustomer() + }) + const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, { onCompleted: () => getCustomer() }) @@ -330,6 +340,16 @@ const CustomerProfile = memo(() => { setWizard(null) } + const updateCustomEntry = it => { + editCustomEntry({ + variables: { + customerId, + fieldId: it.fieldId, + value: it.value + } + }) + } + const updateCustomer = it => setCustomer({ variables: { @@ -338,7 +358,7 @@ const CustomerProfile = memo(() => { } }) - const replacePhoto = it => + const replacePhoto = it => { replaceCustomerPhoto({ variables: { customerId, @@ -346,14 +366,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({ @@ -421,7 +445,7 @@ const CustomerProfile = memo(() => { const timezone = R.path(['config', 'locale_timezone'], configResponse) - const customRequirementOptions = + const customInfoRequirementOptions = activeCustomRequests?.customInfoRequests?.map(it => ({ value: it.id, display: it.customRequest.name @@ -564,7 +588,8 @@ const CustomerProfile = memo(() => { editCustomer={editCustomer} deleteEditedData={deleteEditedData} updateCustomRequest={setCustomerCustomInfoRequest} - authorizeCustomRequest={authorizeCustomRequest}> + authorizeCustomRequest={authorizeCustomRequest} + updateCustomEntry={updateCustomEntry}>
)} {isNotes && ( @@ -590,7 +615,7 @@ const CustomerProfile = memo(() => { addPhoto={replacePhoto} addCustomerData={editCustomer} onClose={() => setWizard(null)} - customRequirementOptions={customRequirementOptions} + customInfoRequirementOptions={customInfoRequirementOptions} /> )}
diff --git a/new-lamassu-admin/src/pages/Customers/Wizard.js b/new-lamassu-admin/src/pages/Customers/Wizard.js index 16db1647..f257e23f 100644 --- a/new-lamassu-admin/src/pages/Customers/Wizard.js +++ b/new-lamassu-admin/src/pages/Customers/Wizard.js @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core' -import { Form, Formik, Field } from 'formik' +import { Form, Formik } from 'formik' import * as R from 'ramda' import React, { useState, Fragment } from 'react' @@ -7,10 +7,16 @@ import ErrorMessage from 'src/components/ErrorMessage' import Modal from 'src/components/Modal' import Stepper from 'src/components/Stepper' import { Button } from 'src/components/buttons' -import { Dropdown } from 'src/components/inputs/formik' 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 @@ -52,11 +58,17 @@ const styles = { 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 } @@ -66,7 +78,7 @@ const Wizard = ({ onClose, save, error, - customRequirementOptions, + customInfoRequirementOptions, addCustomerData, addPhoto }) => { @@ -78,7 +90,10 @@ const Wizard = ({ step: 1 }) - const isCustom = values => values?.requirement === 'custom' + 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) @@ -87,7 +102,23 @@ const Wizard = ({ setSelectedValues(newConfig) if (isLastStep) { - return save(newConfig) + 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({ @@ -120,22 +151,9 @@ const Wizard = ({
- {isCustom(values) && ( -
- -
- )}
{error && Failed to save}
)} - {!R.isEmpty(data) && type === IMAGE && ( + {!R.isEmpty(data) && isImage && (
)} - {!R.isEmpty(data) && type !== IMAGE && ( + {!R.isEmpty(data) && !isImage && (

{data.preview}

diff --git a/new-lamassu-admin/src/pages/Customers/helper.js b/new-lamassu-admin/src/pages/Customers/helper.js index e6834459..009b21a5 100644 --- a/new-lamassu-admin/src/pages/Customers/helper.js +++ b/new-lamassu-admin/src/pages/Customers/helper.js @@ -1,12 +1,16 @@ import { makeStyles, Box } from '@material-ui/core' import classnames from 'classnames' -import { parse, isValid } from 'date-fns/fp' +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' @@ -35,10 +39,16 @@ const useStyles = makeStyles({ specialGrid: { display: 'grid', gridTemplateColumns: [[182, 162, 141]] + }, + picker: { + width: 150 } }) const CUSTOMER_BLOCKED = 'blocked' +const CUSTOM = 'custom' +const REQUIREMENT = 'requirement' +const ID_CARD_DATA = 'idCardData' const getAuthorizedStatus = it => it.authorizedOverride === CUSTOMER_BLOCKED @@ -82,18 +92,28 @@ const requirementOptions = [ { display: 'ID card image', code: 'idCardPhoto' }, { display: 'ID data', code: 'idCardData' }, { display: 'US SSN', code: 'usSsn' }, - { display: 'Customer camera', code: 'facephoto' } + { 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({ @@ -111,13 +131,18 @@ const customTextSchema = Yup.object().shape({ data: Yup.string().required() }) -const EntryType = ({ hasCustomRequirementOptions }) => { +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 @@ -158,14 +183,8 @@ const EntryType = ({ hasCustomRequirementOptions }) => { component={RadioGroup} name="requirement" options={ - hasCustomRequirementOptions - ? [ - { - display: 'Custom information requirement', - code: 'custom' - }, - ...requirementOptions - ] + !R.isEmpty(customInfoRequirementOptions) + ? updateRequirementOptions(requirementOptions) : requirementOptions } labelClassName={classes.label} @@ -178,18 +197,68 @@ const EntryType = ({ hasCustomRequirementOptions }) => { ) } -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 ( <> -

{`Custom ${dataTypeSelected} entry`}

+

{title}

- {customElements[dataTypeSelected].options.map(({ display, code }) => ( - - ))} - {upload && } + {isCustomInfoRequirement && ( + { + // dispatch({ type: 'form', form: it }) + }} + /> + )} + {!upload && + !isCustomInfoRequirement && + elements.options.map(({ label, name }) => ( + + ))} + {upload && ( + + )} ) } @@ -198,20 +267,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' } } @@ -225,7 +297,7 @@ const entryType = { // Customer data const customerDataElements = { - idScanElements: [ + idCardData: [ { name: 'firstName', label: 'First name', @@ -262,7 +334,7 @@ const customerDataElements = { component: TextInput } ], - usSsnElements: [ + usSsn: [ { name: 'usSsn', label: 'US SSN', @@ -270,12 +342,12 @@ const customerDataElements = { size: 190 } ], - idCardPhotoElements: [{ name: 'idCardPhoto' }], - frontCameraElements: [{ name: 'frontCamera' }] + idCardPhoto: [{ name: 'idCardPhoto' }], + frontCamera: [{ name: 'frontCamera' }] } -const customerDataschemas = { - idScan: Yup.object().shape({ +const customerDataSchemas = { + idCardData: Yup.object().shape({ firstName: Yup.string().required(), lastName: Yup.string().required(), documentNumber: Yup.string().required(), @@ -303,6 +375,61 @@ const customerDataschemas = { }) } +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') { @@ -339,7 +466,12 @@ export { getName, entryType, customElements, + requirementElements, formatPhotosData, customerDataElements, - customerDataschemas + customerDataSchemas, + formatDates, + REQUIREMENT, + CUSTOM, + ID_CARD_DATA } From f4f4eb13357f4a283a90eb4b59dc24499c982292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Mon, 17 Jan 2022 16:57:37 +0000 Subject: [PATCH 38/51] fix: disable unfinished features --- .../graphql/resolvers/customer.resolver.js | 2 +- new-lamassu-admin/src/components/Carousel.js | 6 ++++ .../src/pages/Customers/CustomerData.js | 31 +++++++++++-------- .../src/pages/Customers/CustomerProfile.js | 11 ++++--- .../Customers/components/EditableCard.js | 24 +++++++------- .../src/pages/Customers/helper.js | 30 +++++++++++++----- 6 files changed, 67 insertions(+), 37 deletions(-) diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js index d3563669..bbe46195 100644 --- a/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -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) diff --git a/new-lamassu-admin/src/components/Carousel.js b/new-lamassu-admin/src/components/Carousel.js index f751cd1f..35f4ef20 100644 --- a/new-lamassu-admin/src/components/Carousel.js +++ b/new-lamassu-admin/src/components/Carousel.js @@ -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} diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index 3f792db3..1a06b233 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -360,19 +360,24 @@ const CustomerData = ({

{'Customer data'}

- setListView(false)} - /> - setListView(true)}> + {// TODO: Remove false condition for next release + false && ( + <> + setListView(false)} + /> + setListView(true)}> + + )}
{!listView && customer && ( diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 71b24ba2..8ea827a1 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -18,8 +18,8 @@ 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' +// 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' @@ -494,14 +494,17 @@ const CustomerProfile = memo(() => { onClick={() => setWizard(true)}> {`Manual data entry`} - {}}> {`Add individual discount`} - + */ + } {isSuspended && ( {!editing && (
-
- deleteEditedData()}> - {`Delete`} - -
- + {// TODO: Remove false condition for next release + false && ( +
+ deleteEditedData()}> + {`Delete`} + +
+ )} *:last-child': { + marginBottom: 24 + } } }) @@ -183,9 +188,11 @@ const EntryType = ({ customInfoRequirementOptions }) => { component={RadioGroup} name="requirement" options={ - !R.isEmpty(customInfoRequirementOptions) - ? updateRequirementOptions(requirementOptions) - : requirementOptions + requirementOptions + // TODO: Enable once custom info requirement manual entry is finished + // !R.isEmpty(customInfoRequirementOptions) + // ? updateRequirementOptions(requirementOptions) + // : requirementOptions } labelClassName={classes.label} radioClassName={classes.radio} @@ -248,11 +255,18 @@ const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => { }} /> )} - {!upload && - !isCustomInfoRequirement && - elements.options.map(({ label, name }) => ( - - ))} +
+ {!upload && + !isCustomInfoRequirement && + elements.options.map(({ label, name }) => ( + + ))} +
{upload && ( Date: Mon, 17 Jan 2022 17:06:45 +0000 Subject: [PATCH 39/51] fix: mark TODOs --- new-lamassu-admin/src/pages/Customers/CustomerProfile.js | 1 + new-lamassu-admin/src/pages/Customers/helper.js | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 8ea827a1..59d0202c 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -18,6 +18,7 @@ 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' +// 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' diff --git a/new-lamassu-admin/src/pages/Customers/helper.js b/new-lamassu-admin/src/pages/Customers/helper.js index c0d86f55..b991e76d 100644 --- a/new-lamassu-admin/src/pages/Customers/helper.js +++ b/new-lamassu-admin/src/pages/Customers/helper.js @@ -250,9 +250,7 @@ const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => { getOptionSelected={R.eqProps('code')} labelProp={'display'} options={customInfoRequirementOptions} - onChange={(evt, it) => { - // dispatch({ type: 'form', form: it }) - }} + onChange={(evt, it) => {}} /> )}
From dc4a8471d47c4b6f619c8415c0be034f2636d299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 17 Jan 2022 17:48:00 +0000 Subject: [PATCH 40/51] fix: set notification card design up to spec --- .../NotificationCenter.styles.js | 9 +++-- .../NotificationCenter/NotificationRow.js | 35 ++++++++++++++----- .../styling/icons/action/wrench/zodiac.svg | 2 +- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/new-lamassu-admin/src/components/NotificationCenter/NotificationCenter.styles.js b/new-lamassu-admin/src/components/NotificationCenter/NotificationCenter.styles.js index 49f0456f..91d1b720 100644 --- a/new-lamassu-admin/src/components/NotificationCenter/NotificationCenter.styles.js +++ b/new-lamassu-admin/src/components/NotificationCenter/NotificationCenter.styles.js @@ -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, diff --git a/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js b/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js index e318f0c2..f5cdea17 100644 --- a/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js +++ b/new-lamassu-admin/src/components/NotificationCenter/NotificationRow.js @@ -13,12 +13,27 @@ import styles from './NotificationCenter.styles' const useStyles = makeStyles(styles) const types = { - transaction: { display: 'Transactions', icon: }, - highValueTransaction: { display: 'Transactions', icon: }, - fiatBalance: { display: 'Maintenance', icon: }, - cryptoBalance: { display: 'Maintenance', icon: }, - compliance: { display: 'Compliance', icon: }, - error: { display: 'Error', icon: } + transaction: { + display: 'Transactions', + icon: + }, + highValueTransaction: { + display: 'Transactions', + icon: + }, + fiatBalance: { + display: 'Maintenance', + icon: + }, + cryptoBalance: { + display: 'Maintenance', + icon: + }, + compliance: { + display: 'Compliance', + icon: + }, + error: { display: 'Error', icon: } } 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) ?? + const icon = R.path([type, 'icon'])(types) ?? ( + + ) 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 : '' )}> -
{icon}
+
+
{icon}
+
{notificationTitle} diff --git a/new-lamassu-admin/src/styling/icons/action/wrench/zodiac.svg b/new-lamassu-admin/src/styling/icons/action/wrench/zodiac.svg index 58db5b0d..0cf3417a 100644 --- a/new-lamassu-admin/src/styling/icons/action/wrench/zodiac.svg +++ b/new-lamassu-admin/src/styling/icons/action/wrench/zodiac.svg @@ -1,5 +1,5 @@ - + From f550da4d1e84e2622b5d8e8b4f040527cf239906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 14 Jan 2022 18:24:13 +0000 Subject: [PATCH 41/51] fix: add batched but not confirmed customer transactions to last active timestamp --- lib/customers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/customers.js b/lib/customers.js index e96155f9..954c550d 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -646,7 +646,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null) 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 ( @@ -701,7 +701,7 @@ function getCustomerById (id) { 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 ( From 5e9d00fa29ac9b35cff19f767219a31fb9d20702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 17 Jan 2022 18:11:34 +0000 Subject: [PATCH 42/51] fix: make customer creation count as customer activity --- lib/customers.js | 8 ++++---- new-lamassu-admin/src/pages/Customers/Customers.js | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/customers.js b/lib/customers.js index 954c550d..1c93d261 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -631,7 +631,7 @@ 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, + 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 FROM ( SELECT c.id, c.authorized_override, @@ -640,7 +640,7 @@ 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.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 @@ -686,7 +686,7 @@ 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, + 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 FROM ( SELECT c.id, c.authorized_override, @@ -695,7 +695,7 @@ 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.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 diff --git a/new-lamassu-admin/src/pages/Customers/Customers.js b/new-lamassu-admin/src/pages/Customers/Customers.js index 1875d44d..e24e5586 100644 --- a/new-lamassu-admin/src/pages/Customers/Customers.js +++ b/new-lamassu-admin/src/pages/Customers/Customers.js @@ -91,7 +91,12 @@ const Customers = () => { const [createNewCustomer] = useMutation(CREATE_CUSTOMER, { onCompleted: () => setShowCreationModal(false), - refetchQueries: () => ['configAndCustomers'] + refetchQueries: () => [ + { + query: GET_CUSTOMERS, + variables + } + ] }) const configData = R.path(['config'])(customersResponse) ?? [] From 1db68114a2aef458cd7f01123c4b5f86aecb8177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 17 Jan 2022 18:27:26 +0000 Subject: [PATCH 43/51] fix: remove undefined field querying --- new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js | 1 - 1 file changed, 1 deletion(-) diff --git a/new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js b/new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js index 6eef4ae4..c84d4b82 100644 --- a/new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js +++ b/new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js @@ -28,7 +28,6 @@ const GET_BATCHES = gql` fiat deviceId created - cashbox } } } From 586c3f12cdcdad2c5ab96b4ebb4257ef7fd135d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Mon, 17 Jan 2022 21:27:29 +0000 Subject: [PATCH 44/51] fix: remove formik values --- .../src/pages/Customers/Wizard.js | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/Wizard.js b/new-lamassu-admin/src/pages/Customers/Wizard.js index f257e23f..6f81b9de 100644 --- a/new-lamassu-admin/src/pages/Customers/Wizard.js +++ b/new-lamassu-admin/src/pages/Customers/Wizard.js @@ -147,21 +147,19 @@ const Wizard = ({ onSubmit={onContinue} initialValues={stepOptions.initialValues} validationSchema={stepOptions.schema}> - {({ values }) => ( -
- -
- {error && Failed to save} - -
- - )} +
+ +
+ {error && Failed to save} + +
+ From 807c5bfc8592a0c9a238957a36d731640eb3b61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 22 Dec 2021 18:31:32 +0000 Subject: [PATCH 45/51] fix: triggers wizard allowing empty custom requirement field fix: issue with custom requirement filtering when in presence of older triggers refactor: pull up methods fix: remove log --- lib/routes/pollingRoutes.js | 4 +- .../src/pages/Triggers/Wizard.js | 22 +++++-- .../src/pages/Triggers/helper.js | 66 ++++++++++++++----- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index ad7d0118..b57db854 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -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 (o.customInfoRequestId === '' || _.isNil(o.customInfoRequestId)) normalTriggers.push(o) + return !_.isNil(o.customInfoRequestId) }, allTriggers) return _.flow([_.map(_.get('customInfoRequestId')), customRequestQueries.batchGetCustomInfoRequest])(customTriggers) diff --git a/new-lamassu-admin/src/pages/Triggers/Wizard.js b/new-lamassu-admin/src/pages/Triggers/Wizard.js index dfc5fd03..050f272e 100644 --- a/new-lamassu-admin/src/pages/Triggers/Wizard.js +++ b/new-lamassu-admin/src/pages/Triggers/Wizard.js @@ -230,12 +230,18 @@ const Wizard = ({ onClose, save, error, currency, customInfoRequests }) => { 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, customInfoRequests }) => { ) return errors.threshold - if (isSuspend && hasRequirementError) return errors.requirement + if ( + (isSuspend && hasRequirementError) || + (isCustom && hasCustomRequirementError) + ) + return errors.requirement } return ( diff --git a/new-lamassu-admin/src/pages/Triggers/helper.js b/new-lamassu-admin/src/pages/Triggers/helper.js index f62b5ddf..6889fc66 100644 --- a/new-lamassu-admin/src/pages/Triggers/helper.js +++ b/new-lamassu-admin/src/pages/Triggers/helper.js @@ -477,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' - ? requirement.suspensionDays > 0 - : true + 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' + }) - 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 = [ @@ -506,6 +528,18 @@ const requirementOptions = [ { display: 'Block', code: 'block' } ] +const hasRequirementError = (errors, touched, values) => + !!errors.requirement && + !!touched.requirement?.suspensionDays && + (!values.requirement?.suspensionDays || + values.requirement?.suspensionDays < 0) + +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 { @@ -524,12 +558,6 @@ const Requirement = ({ customInfoRequests }) => { display: it.customRequest.name })) - const hasRequirementError = - !!errors.requirement && - !!touched.requirement?.suspensionDays && - (!values.requirement?.suspensionDays || - values.requirement?.suspensionDays < 0) - const enableCustomRequirement = customInfoRequests?.length > 0 const customInfoOption = { display: 'Custom information requirement', @@ -540,7 +568,9 @@ const Requirement = ({ customInfoRequests }) => { : [...requirementOptions, { ...customInfoOption, disabled: true }] const titleClass = { [classes.error]: - (!!errors.requirement && !isSuspend) || (isSuspend && hasRequirementError) + (!!errors.requirement && !isSuspend && !isCustom) || + (isSuspend && hasRequirementError(errors, touched, values)) || + (isCustom && hasCustomRequirementError(errors, touched, values)) } return ( @@ -569,7 +599,7 @@ const Requirement = ({ customInfoRequests }) => { label="Days" size="lg" name="requirement.suspensionDays" - error={hasRequirementError} + error={hasRequirementError(errors, touched, values)} /> )} {isCustom && ( @@ -592,6 +622,8 @@ const requirements = customInfoRequests => ({ options: requirementOptions, Component: Requirement, props: { customInfoRequests }, + hasRequirementError: hasRequirementError, + hasCustomRequirementError: hasCustomRequirementError, initialValues: { requirement: { requirement: '', From c241c458d32b853c2696317e4b00fe176074a7e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 17 Jan 2022 23:59:43 +0000 Subject: [PATCH 46/51] fix: custom info request batching fix: issue from rebase --- lib/routes/pollingRoutes.js | 4 ++-- new-lamassu-admin/src/pages/Triggers/Wizard.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index b57db854..b4089d54 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -41,8 +41,8 @@ const createTerms = terms => (terms.active && terms.text) ? ({ const buildTriggers = (allTriggers) => { const normalTriggers = [] const customTriggers = _.filter(o => { - if (o.customInfoRequestId === '' || _.isNil(o.customInfoRequestId)) normalTriggers.push(o) - return !_.isNil(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) diff --git a/new-lamassu-admin/src/pages/Triggers/Wizard.js b/new-lamassu-admin/src/pages/Triggers/Wizard.js index 050f272e..b78fc5fa 100644 --- a/new-lamassu-admin/src/pages/Triggers/Wizard.js +++ b/new-lamassu-admin/src/pages/Triggers/Wizard.js @@ -232,12 +232,12 @@ const Wizard = ({ onClose, save, error, currency, customInfoRequests }) => { const isSuspend = values?.requirement?.requirement === 'suspend' const isCustom = values?.requirement?.requirement === 'custom' - const hasRequirementError = requirements.hasRequirementError( + const hasRequirementError = requirements().hasRequirementError( errors, touched, values ) - const hasCustomRequirementError = requirements.hasCustomRequirementError( + const hasCustomRequirementError = requirements().hasCustomRequirementError( errors, touched, values From 87a3b718db3a75c7a726f3e47f425fce0d302d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Tue, 18 Jan 2022 18:46:55 +0000 Subject: [PATCH 47/51] fix: allow for custom info requests to be allowed or blocked fix: improve custom info requests backend fix: add custom info requests to trigger overrides --- .../resolvers/customInfoRequests.resolver.js | 6 +- .../graphql/types/customInfoRequests.type.js | 6 +- lib/new-admin/services/customInfoRequests.js | 26 +++-- lib/new-config-manager.js | 34 +++--- lib/routes/pollingRoutes.js | 6 +- ...42518884925-manual-custom-info-requests.js | 16 +++ .../src/pages/Customers/CustomerData.js | 6 +- .../src/pages/Customers/CustomerProfile.js | 8 +- .../components/CustomInfoRequestsData.js | 4 +- .../Triggers/components/AdvancedTriggers.js | 100 +++++++++++------- .../src/pages/Triggers/components/helper.js | 43 +++++--- 11 files changed, 167 insertions(+), 88 deletions(-) create mode 100644 migrations/1642518884925-manual-custom-info-requests.js diff --git a/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js b/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js index 897b415c..fefdcf6b 100644 --- a/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js +++ b/lib/new-admin/graphql/resolvers/customInfoRequests.resolver.js @@ -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) } } diff --git a/lib/new-admin/graphql/types/customInfoRequests.type.js b/lib/new-admin/graphql/types/customInfoRequests.type.js index 5a9ed909..917c9f6f 100644 --- a/lib/new-admin/graphql/types/customInfoRequests.type.js +++ b/lib/new-admin/graphql/types/customInfoRequests.type.js @@ -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 } ` diff --git a/lib/new-admin/services/customInfoRequests.js b/lib/new-admin/services/customInfoRequests.js index c19c112a..7c703443 100644 --- a/lib/new-admin/services/customInfoRequests.js +++ b/lib/new-admin/services/customInfoRequests.js @@ -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]) } diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js index 531186ea..0e721736 100644 --- a/lib/new-config-manager.js +++ b/lib/new-config-manager.js @@ -1,4 +1,5 @@ const _ = require('lodash/fp') +const { getCustomInfoRequests } = require('./new-admin/services/customInfoRequests') const namespaces = { WALLETS: 'wallets', @@ -107,22 +108,29 @@ const getGlobalNotifications = config => getNotifications(null, null, config) const getTriggers = _.get('triggers') const getTriggersAutomation = config => { - const defaultAutomation = _.get('triggersConfig_automation')(config) - const requirements = { - sanctions: defaultAutomation, - idCardPhoto: defaultAutomation, - idCardData: defaultAutomation, - facephoto: defaultAutomation, - usSsn: defaultAutomation - } + return getCustomInfoRequests(true) + .then(infoRequests => { + const defaultAutomation = _.get('triggersConfig_automation')(config) + const requirements = { + sanctions: defaultAutomation, + idCardPhoto: defaultAutomation, + idCardData: defaultAutomation, + facephoto: defaultAutomation, + usSsn: defaultAutomation + } - const overrides = _.get('triggersConfig_overrides')(config) + _.forEach(it => { + requirements[it.id] = defaultAutomation + }, infoRequests) - const requirementsOverrides = _.reduce((acc, override) => { - return _.assign(acc, { [override.requirement]: override.automation }) - }, {}, overrides) + const overrides = _.get('triggersConfig_overrides')(config) - return _.assign(requirements, requirementsOverrides) + const requirementsOverrides = _.reduce((acc, override) => { + return _.assign(acc, { [override.requirement]: override.automation }) + }, {}, overrides) + + return _.assign(requirements, requirementsOverrides) + }) } const splitGetFirst = _.compose(_.head, _.split('_')) diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index b4089d54..c4568233 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -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 diff --git a/migrations/1642518884925-manual-custom-info-requests.js b/migrations/1642518884925-manual-custom-info-requests.js new file mode 100644 index 00000000..9a912c6d --- /dev/null +++ b/migrations/1642518884925-manual-custom-info-requests.js @@ -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() +} diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index 18a53cac..dde30bef 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -324,7 +324,6 @@ const CustomerData = ({ ] R.forEach(it => { - console.log('it', it) customRequirements.push({ fields: [ { @@ -336,12 +335,13 @@ const CustomerData = ({ ], title: it.customInfoRequest.customRequest.name, titleIcon: , + state: R.path(['override'])(it), authorize: () => authorizeCustomRequest({ variables: { customerId: it.customerId, infoRequestId: it.customInfoRequest.id, - isAuthorized: true + override: OVERRIDE_AUTHORIZED } }), reject: () => @@ -349,7 +349,7 @@ const CustomerData = ({ variables: { customerId: it.customerId, infoRequestId: it.customInfoRequest.id, - isAuthorized: false + override: OVERRIDE_REJECTED } }), save: values => { diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 91b076ee..b127b5ec 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -95,7 +95,9 @@ const GET_CUSTOMER = gql` } customInfoRequests { customerId - approved + override + overrideBy + overrideAt customerData customInfoRequest { id @@ -180,12 +182,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 ) } ` diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js b/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js index ced31cef..d8ca647d 100644 --- a/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js +++ b/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js @@ -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 ) } ` diff --git a/new-lamassu-admin/src/pages/Triggers/components/AdvancedTriggers.js b/new-lamassu-admin/src/pages/Triggers/components/AdvancedTriggers.js index cb58bfa2..038a6825 100644 --- a/new-lamassu-admin/src/pages/Triggers/components/AdvancedTriggers.js +++ b/new-lamassu-admin/src/pages/Triggers/components/AdvancedTriggers.js @@ -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,42 +88,47 @@ const AdvancedTriggersSettings = memo(() => { const onEditingOverrides = (it, editing) => setEditingOverrides(editing) return ( - <> -
- -
-
- -
- + !loading && ( + <> +
+ +
+
+ +
+ + ) ) }) diff --git a/new-lamassu-admin/src/pages/Triggers/components/helper.js b/new-lamassu-admin/src/pages/Triggers/components/helper.js index 0737bca8..e7c660df 100644 --- a/new-lamassu-admin/src/pages/Triggers/components/helper.js +++ b/new-lamassu-admin/src/pages/Triggers/components/helper.js @@ -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 = [ - { 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 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' } From 556d8433fa209293989ce85a8a0ee1559b62a7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Wed, 5 Jan 2022 17:40:15 +0000 Subject: [PATCH 48/51] feat: add testing customer toggle --- lib/customers.js | 22 ++- .../graphql/resolvers/customer.resolver.js | 6 +- .../graphql/resolvers/transaction.resolver.js | 8 +- lib/new-admin/graphql/types/customer.type.js | 3 + .../graphql/types/transaction.type.js | 4 +- lib/new-admin/services/transactions.js | 3 + .../1641394367865-testing-customer-toggle.js | 13 ++ .../src/components/LogsDownloaderPopper.js | 6 +- .../src/pages/Analytics/Analytics.js | 8 +- .../src/pages/Customers/CustomerProfile.js | 176 +++++++++++------- .../pages/Customers/CustomerProfile.styles.js | 21 ++- .../components/CustomerSidebar.styles.js | 3 +- .../SystemPerformance/SystemPerformance.js | 8 +- .../src/pages/Transactions/Transactions.js | 2 + 14 files changed, 191 insertions(+), 92 deletions(-) create mode 100644 migrations/1641394367865-testing-customer-toggle.js diff --git a/lib/customers.js b/lib/customers.js index 1b03b9ea..cf6f30fb 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -632,7 +632,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null) 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, 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 + 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,7 +640,7 @@ 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, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, 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 @@ -687,7 +687,7 @@ function getCustomerById (id) { 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, 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 + 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,7 +695,7 @@ 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, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, 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 @@ -1023,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, @@ -1041,5 +1051,7 @@ module.exports = { edit, deleteEditedData, updateEditedPhoto, - updateTxCustomerPhoto + updateTxCustomerPhoto, + enableTestCustomer, + disableTestCustomer } diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js index 9edbf762..19b54f79 100644 --- a/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -50,7 +50,11 @@ const resolvers = { deleteCustomerNote: (...[, { noteId }]) => { return customerNotes.deleteCustomerNote(noteId) }, - createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }) + createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }), + enableTestCustomer: (...[, { customerId }]) => + customers.enableTestCustomer(customerId), + disableTestCustomer: (...[, { customerId }]) => + customers.disableTestCustomer(customerId) } } diff --git a/lib/new-admin/graphql/resolvers/transaction.resolver.js b/lib/new-admin/graphql/resolvers/transaction.resolver.js index 3dc5a399..96bb406e 100644 --- a/lib/new-admin/graphql/resolvers/transaction.resolver.js +++ b/lib/new-admin/graphql/resolvers/transaction.resolver.js @@ -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 => diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js index f302c263..bf099647 100644 --- a/lib/new-admin/graphql/types/customer.type.js +++ b/lib/new-admin/graphql/types/customer.type.js @@ -36,6 +36,7 @@ const typeDef = gql` customFields: [CustomerCustomField] customInfoRequests: [CustomRequestData] notes: [CustomerNote] + isTestCustomer: Boolean } input CustomerInput { @@ -104,6 +105,8 @@ const typeDef = gql` 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 } ` diff --git a/lib/new-admin/graphql/types/transaction.type.js b/lib/new-admin/graphql/types/transaction.type.js index c121d0bd..a0212e91 100644 --- a/lib/new-admin/graphql/types/transaction.type.js +++ b/lib/new-admin/graphql/types/transaction.type.js @@ -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 diff --git a/lib/new-admin/services/transactions.js b/lib/new-admin/services/transactions.js index 494630f9..e420355a 100644 --- a/lib/new-admin/services/transactions.js +++ b/lib/new-admin/services/transactions.js @@ -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` diff --git a/migrations/1641394367865-testing-customer-toggle.js b/migrations/1641394367865-testing-customer-toggle.js new file mode 100644 index 00000000..174aaa58 --- /dev/null +++ b/migrations/1641394367865-testing-customer-toggle.js @@ -0,0 +1,13 @@ +var db = require('./db') + +exports.up = function (next) { + var sql = [ + `ALTER TABLE customers ADD COLUMN is_test_customer BOOLEAN DEFAULT false`, + ] + + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/src/components/LogsDownloaderPopper.js b/new-lamassu-admin/src/components/LogsDownloaderPopper.js index 9c02760a..f41dde88 100644 --- a/new-lamassu-admin/src/components/LogsDownloaderPopper.js +++ b/new-lamassu-admin/src/components/LogsDownloaderPopper.js @@ -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 } }) } diff --git a/new-lamassu-admin/src/pages/Analytics/Analytics.js b/new-lamassu-admin/src/pages/Analytics/Analytics.js index 8b00c2fc..4d1d6114 100644 --- a/new-lamassu-admin/src/pages/Analytics/Analytics.js +++ b/new-lamassu-admin/src/pages/Analytics/Analytics.js @@ -42,8 +42,8 @@ const TIME_OPTIONS = { } const GET_TRANSACTIONS = gql` - query transactions($limit: Int, $from: Date, $until: Date) { - transactions(limit: $limit, from: $from, until: $until) { + query transactions($excludeTestingCustomers: Boolean) { + transactions(excludeTestingCustomers: $excludeTestingCustomers) { id txClass txHash @@ -116,7 +116,9 @@ 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: { excludeTestingCustomers: true } + }) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA) const [representing, setRepresenting] = useState(REPRESENTING_OPTIONS[0]) diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 59d0202c..5e2de3c1 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -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, @@ -67,6 +68,7 @@ const GET_CUSTOMER = gql` lastTxClass daysSuspended isSuspended + isTestCustomer customFields { id label @@ -231,6 +233,18 @@ 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 @@ -351,6 +365,16 @@ const CustomerProfile = memo(() => { }) } + 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: { @@ -478,85 +502,101 @@ const CustomerProfile = memo(() => {
{!loading && !customerData.isAnonymous && ( -
+ <> + code === clickedItem} + onClick={onClickSidebarItem} + />
- code === clickedItem} - onClick={onClickSidebarItem} - /> -
- Actions -
- setWizard(true)}> - {`Manual data entry`} - - { - // TODO: Enable for next release - /* {}}> - {`Add individual discount`} - */ - } - {isSuspended && ( + Actions +
setWizard(true)}> + {`Manual data entry`} + + {/* {}}> + {`Add individual discount`} + */} + {isSuspended && ( + + updateCustomer({ + suspendedUntil: null + }) + }> + {`Unsuspend customer`} + + )} + updateCustomer({ - suspendedUntil: null + authorizedOverride: blocked + ? OVERRIDE_AUTHORIZED + : OVERRIDE_REJECTED }) }> - {`Unsuspend customer`} + {`${blocked ? 'Authorize' : 'Block'} customer`} - )} - - updateCustomer({ - authorizedOverride: blocked - ? OVERRIDE_AUTHORIZED - : OVERRIDE_REJECTED - }) - }> - {`${blocked ? 'Authorize' : 'Block'} customer`} - - - setCustomer({ - variables: { - customerId, - customerInput: { - subscriberInfo: true + + setCustomer({ + variables: { + customerId, + customerInput: { + subscriberInfo: true + } } - } - }) - }> - {`Retrieve information`} - + }) + }> + {`Retrieve information`} + +
-
+
+ + {`Special user status`} + +
+
+ + R.path(['isTestCustomer'])(customerData) + ? disableTestCustomer() + : enableTestCustomer() + } + /> + {`Test user`} +
+
+
+ )}
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js index ee9a7e82..a486abee 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js @@ -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]] } } diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js index 87ef5087..9485c7f4 100644 --- a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js +++ b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js @@ -11,8 +11,7 @@ export default { backgroundColor: sidebarColor, width: 219, flexDirection: 'column', - borderRadius: 5, - marginBottom: 50 + borderRadius: 5 }, link: { alignItems: 'center', diff --git a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/SystemPerformance.js b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/SystemPerformance.js index 29b3dca1..d437b6e5 100644 --- a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/SystemPerformance.js +++ b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/SystemPerformance.js @@ -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 diff --git a/new-lamassu-admin/src/pages/Transactions/Transactions.js b/new-lamassu-admin/src/pages/Transactions/Transactions.js index 009335c3..546f7476 100644 --- a/new-lamassu-admin/src/pages/Transactions/Transactions.js +++ b/new-lamassu-admin/src/pages/Transactions/Transactions.js @@ -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 ) } ` From a21bb1cbd5ec29315a46395e6dafcc23c34c10ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 17 Jan 2022 23:22:30 +0000 Subject: [PATCH 49/51] fix: limit transactions from analytics page to the last 2 months --- .../1641394367865-testing-customer-toggle.js | 2 +- .../src/pages/Analytics/Analytics.js | 20 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/migrations/1641394367865-testing-customer-toggle.js b/migrations/1641394367865-testing-customer-toggle.js index 174aaa58..a8b8d522 100644 --- a/migrations/1641394367865-testing-customer-toggle.js +++ b/migrations/1641394367865-testing-customer-toggle.js @@ -2,7 +2,7 @@ var db = require('./db') exports.up = function (next) { var sql = [ - `ALTER TABLE customers ADD COLUMN is_test_customer BOOLEAN DEFAULT false`, + `ALTER TABLE customers ADD COLUMN is_test_customer BOOLEAN NOT NULL DEFAULT false`, ] db.multi(sql, next) diff --git a/new-lamassu-admin/src/pages/Analytics/Analytics.js b/new-lamassu-admin/src/pages/Analytics/Analytics.js index 4d1d6114..936db2ba 100644 --- a/new-lamassu-admin/src/pages/Analytics/Analytics.js +++ b/new-lamassu-admin/src/pages/Analytics/Analytics.js @@ -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($excludeTestingCustomers: Boolean) { - transactions(excludeTestingCustomers: $excludeTestingCustomers) { + query transactions( + $from: Date + $until: Date + $excludeTestingCustomers: Boolean + ) { + transactions( + from: $from + until: $until + excludeTestingCustomers: $excludeTestingCustomers + ) { id txClass txHash @@ -117,7 +127,11 @@ const Analytics = () => { const classes = useStyles() const { data: txResponse, loading: txLoading } = useQuery(GET_TRANSACTIONS, { - variables: { excludeTestingCustomers: true } + variables: { + from: subDays(65, endOfToday()), + until: endOfToday(), + excludeTestingCustomers: true + } }) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA) From 3d8281fb7349f7e69ba491273c6825104fdb1676 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Wed, 19 Jan 2022 15:05:34 +0100 Subject: [PATCH 50/51] fix: guard against null `expirationDate` fix: NaN age --- .../src/pages/Transactions/DetailsCard.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js index e33ca0a7..73051ac8 100644 --- a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js +++ b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js @@ -116,16 +116,27 @@ const DetailsRow = ({ it: tx, timezone }) => { const exchangeRate = BigNumber(fiat / crypto).toFormat(2) const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}` + const parseDateString = d => parse(new Date(), 'yyyyMMdd', d) + 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('yyyy-MM-dd')( - parse(new Date(), 'yyyyMMdd', tx.customerIdCardData.expirationDate) - ) + idCardExpirationDate: + (tx.customerIdCardData.expirationDate && + format('yyyy-MM-dd')( + parseDateString(tx.customerIdCardData.expirationDate) + )) ?? + '' } const from = sub({ minutes: MINUTES_OFFSET }, tx.created) From e89b1c233a6cec9891eaca0efb50e1bbb3ed6263 Mon Sep 17 00:00:00 2001 From: Nikola Ubavic <53820106+ubavic@users.noreply.github.com> Date: Wed, 19 Jan 2022 15:11:18 +0100 Subject: [PATCH 51/51] chore: eta reduction --- new-lamassu-admin/src/pages/Transactions/DetailsCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js index 73051ac8..c960a3ee 100644 --- a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js +++ b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js @@ -116,7 +116,7 @@ const DetailsRow = ({ it: tx, timezone }) => { const exchangeRate = BigNumber(fiat / crypto).toFormat(2) const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}` - const parseDateString = d => parse(new Date(), 'yyyyMMdd', d) + const parseDateString = parse(new Date(), 'yyyyMMdd') const customer = tx.customerIdCardData && { name: `${onlyFirstToUpper(