feat: add testing customer toggle

This commit is contained in:
Sérgio Salgado 2022-01-05 17:40:15 +00:00
parent abcce7ff06
commit 556d8433fa
14 changed files with 191 additions and 92 deletions

View file

@ -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, 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, 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, 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 ( FROM (
SELECT c.id, c.authorized_override, SELECT c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended, 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.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.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.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, 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 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 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, 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, 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, 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 ( FROM (
SELECT c.id, c.authorized_override, SELECT c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended, 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.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.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.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, 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 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 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)) 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 = { module.exports = {
add, add,
get, get,
@ -1041,5 +1051,7 @@ module.exports = {
edit, edit,
deleteEditedData, deleteEditedData,
updateEditedPhoto, updateEditedPhoto,
updateTxCustomerPhoto updateTxCustomerPhoto,
enableTestCustomer,
disableTestCustomer
} }

View file

@ -50,7 +50,11 @@ const resolvers = {
deleteCustomerNote: (...[, { noteId }]) => { deleteCustomerNote: (...[, { noteId }]) => {
return customerNotes.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)
} }
} }

View file

@ -30,10 +30,10 @@ const resolvers = {
isAnonymous: parent => (parent.customerId === anonymous.uuid) isAnonymous: parent => (parent.customerId === anonymous.uuid)
}, },
Query: { Query: {
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) => 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), 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, simplified }]) => 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, 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 })), .then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']), { fields: txLogFields })),
transactionCsv: (...[, { id, txClass, timezone }]) => transactionCsv: (...[, { id, txClass, timezone }]) =>
transactions.getTx(id, txClass).then(data => transactions.getTx(id, txClass).then(data =>

View file

@ -36,6 +36,7 @@ const typeDef = gql`
customFields: [CustomerCustomField] customFields: [CustomerCustomField]
customInfoRequests: [CustomRequestData] customInfoRequests: [CustomRequestData]
notes: [CustomerNote] notes: [CustomerNote]
isTestCustomer: Boolean
} }
input CustomerInput { input CustomerInput {
@ -104,6 +105,8 @@ const typeDef = gql`
editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth
deleteCustomerNote(noteId: ID!): Boolean @auth deleteCustomerNote(noteId: ID!): Boolean @auth
createCustomer(phoneNumber: String): Customer @auth createCustomer(phoneNumber: String): Customer @auth
enableTestCustomer(customerId: ID!): Boolean @auth
disableTestCustomer(customerId: ID!): Boolean @auth
} }
` `

View file

@ -55,8 +55,8 @@ const typeDef = gql`
} }
type Query { 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 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, simplified: Boolean): String @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 transactionCsv(id: ID, txClass: String, timezone: String): String @auth
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
transactionFilters: [Filter] @auth transactionFilters: [Filter] @auth

View file

@ -39,6 +39,7 @@ function batch (
cryptoCode = null, cryptoCode = null,
toAddress = null, toAddress = null,
status = null, status = null,
excludeTestingCustomers = false,
simplified = false simplified = false
) { ) {
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames) 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 ($11 is null or txs.crypto_code = $11)
AND ($12 is null or txs.to_address = $12) AND ($12 is null or txs.to_address = $12)
AND ($13 is null or txs.txStatus = $13) AND ($13 is null or txs.txStatus = $13)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (fiat > 0) AND (fiat > 0)
ORDER BY created DESC limit $4 offset $5` ORDER BY created DESC limit $4 offset $5`
@ -98,6 +100,7 @@ function batch (
AND ($11 is null or txs.crypto_code = $11) AND ($11 is null or txs.crypto_code = $11)
AND ($12 is null or txs.to_address = $12) AND ($12 is null or txs.to_address = $12)
AND ($13 is null or txs.txStatus = $13) AND ($13 is null or txs.txStatus = $13)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (fiat > 0) AND (fiat > 0)
ORDER BY created DESC limit $4 offset $5` ORDER BY created DESC limit $4 offset $5`

View file

@ -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()
}

View file

@ -181,7 +181,8 @@ const LogsDownloaderPopover = ({
fetchLogs({ fetchLogs({
variables: { variables: {
...args, ...args,
simplified: selectedAdvancedRadio === SIMPLIFIED simplified: selectedAdvancedRadio === SIMPLIFIED,
excludeTestingCustomers: true
} }
}) })
} }
@ -196,7 +197,8 @@ const LogsDownloaderPopover = ({
...args, ...args,
from: range.from, from: range.from,
until: range.until, until: range.until,
simplified: selectedAdvancedRadio === SIMPLIFIED simplified: selectedAdvancedRadio === SIMPLIFIED,
excludeTestingCustomers: true
} }
}) })
} }

View file

@ -42,8 +42,8 @@ const TIME_OPTIONS = {
} }
const GET_TRANSACTIONS = gql` const GET_TRANSACTIONS = gql`
query transactions($limit: Int, $from: Date, $until: Date) { query transactions($excludeTestingCustomers: Boolean) {
transactions(limit: $limit, from: $from, until: $until) { transactions(excludeTestingCustomers: $excludeTestingCustomers) {
id id
txClass txClass
txHash txHash
@ -116,7 +116,9 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
const Analytics = () => { const Analytics = () => {
const classes = useStyles() 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 { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const [representing, setRepresenting] = useState(REPRESENTING_OPTIONS[0]) const [representing, setRepresenting] = useState(REPRESENTING_OPTIONS[0])

View file

@ -7,6 +7,7 @@ import React, { memo, useState } from 'react'
import { useHistory, useParams } from 'react-router-dom' import { useHistory, useParams } from 'react-router-dom'
import { ActionButton } from 'src/components/buttons' import { ActionButton } from 'src/components/buttons'
import { Switch } from 'src/components/inputs'
import { Label1, Label2 } from 'src/components/typography' import { Label1, Label2 } from 'src/components/typography'
import { import {
OVERRIDE_AUTHORIZED, OVERRIDE_AUTHORIZED,
@ -67,6 +68,7 @@ const GET_CUSTOMER = gql`
lastTxClass lastTxClass
daysSuspended daysSuspended
isSuspended isSuspended
isTestCustomer
customFields { customFields {
id id
label 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` const GET_DATA = gql`
query getData { query getData {
config 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 => const updateCustomer = it =>
setCustomer({ setCustomer({
variables: { variables: {
@ -478,13 +502,12 @@ const CustomerProfile = memo(() => {
<div className={classes.panels}> <div className={classes.panels}>
<div className={classes.leftSidePanel}> <div className={classes.leftSidePanel}>
{!loading && !customerData.isAnonymous && ( {!loading && !customerData.isAnonymous && (
<div> <>
<div>
<CustomerSidebar <CustomerSidebar
isSelected={code => code === clickedItem} isSelected={code => code === clickedItem}
onClick={onClickSidebarItem} onClick={onClickSidebarItem}
/> />
</div> <div>
<Label1 className={classes.actionLabel}>Actions</Label1> <Label1 className={classes.actionLabel}>Actions</Label1>
<div className={classes.actionBar}> <div className={classes.actionBar}>
<ActionButton <ActionButton
@ -495,17 +518,14 @@ const CustomerProfile = memo(() => {
onClick={() => setWizard(true)}> onClick={() => setWizard(true)}>
{`Manual data entry`} {`Manual data entry`}
</ActionButton> </ActionButton>
{ {/* <ActionButton
// TODO: Enable for next release
/* <ActionButton
className={classes.actionButton} className={classes.actionButton}
color="primary" color="primary"
Icon={Discount} Icon={Discount}
InverseIcon={DiscountReversedIcon} InverseIcon={DiscountReversedIcon}
onClick={() => {}}> onClick={() => {}}>
{`Add individual discount`} {`Add individual discount`}
</ActionButton> */ </ActionButton> */}
}
{isSuspended && ( {isSuspended && (
<ActionButton <ActionButton
className={classes.actionButton} className={classes.actionButton}
@ -557,6 +577,26 @@ const CustomerProfile = memo(() => {
</ActionButton> </ActionButton>
</div> </div>
</div> </div>
<div>
<Label1 className={classes.actionLabel}>
{`Special user status`}
</Label1>
<div className={classes.actionBar}>
<div className={classes.userStatusAction}>
<Switch
checked={!!R.path(['isTestCustomer'])(customerData)}
value={!!R.path(['isTestCustomer'])(customerData)}
onChange={() =>
R.path(['isTestCustomer'])(customerData)
? disableTestCustomer()
: enableTestCustomer()
}
/>
{`Test user`}
</div>
</div>
</div>
</>
)} )}
</div> </div>
<div className={classes.rightSidePanel}> <div className={classes.rightSidePanel}>

View file

@ -1,4 +1,4 @@
import { comet } from 'src/styling/variables' import { comet, subheaderColor } from 'src/styling/variables'
export default { export default {
labelLink: { labelLink: {
@ -34,6 +34,23 @@ export default {
width: 1100 width: 1100
}, },
leftSidePanel: { 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]]
} }
} }

View file

@ -11,8 +11,7 @@ export default {
backgroundColor: sidebarColor, backgroundColor: sidebarColor,
width: 219, width: 219,
flexDirection: 'column', flexDirection: 'column',
borderRadius: 5, borderRadius: 5
marginBottom: 50
}, },
link: { link: {
alignItems: 'center', alignItems: 'center',

View file

@ -52,8 +52,8 @@ const ranges = {
} }
const GET_DATA = gql` const GET_DATA = gql`
query getData { query getData($excludeTestingCustomers: Boolean) {
transactions { transactions(excludeTestingCustomers: $excludeTestingCustomers) {
fiatCode fiatCode
fiat fiat
cashInFee cashInFee
@ -78,7 +78,9 @@ const reducer = (acc, it) =>
const SystemPerformance = () => { const SystemPerformance = () => {
const classes = useStyles() const classes = useStyles()
const [selectedRange, setSelectedRange] = useState('Day') 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 fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
const timezone = fromNamespace('locale')(data?.config).timezone const timezone = fromNamespace('locale')(data?.config).timezone

View file

@ -40,6 +40,7 @@ const GET_TRANSACTIONS_CSV = gql`
$from: Date $from: Date
$until: Date $until: Date
$timezone: String $timezone: String
$excludeTestingCustomers: Boolean
) { ) {
transactionsCsv( transactionsCsv(
simplified: $simplified simplified: $simplified
@ -47,6 +48,7 @@ const GET_TRANSACTIONS_CSV = gql`
from: $from from: $from
until: $until until: $until
timezone: $timezone timezone: $timezone
excludeTestingCustomers: $excludeTestingCustomers
) )
} }
` `