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 ) } `