diff --git a/lib/customers.js b/lib/customers.js index 0f68f1ed1..fdd5c6105 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -55,7 +55,7 @@ function add (customer) { * @returns {object} Customer */ function get (phone) { - const sql = 'select * from customers where phone=$1' + const sql = 'select * from customers where phone=$1 and enabled' return db.oneOrNone(sql, [phone]) .then(camelize) } @@ -459,7 +459,7 @@ function addComplianceOverrides (id, customer, userToken) { */ function batch () { const sql = `select * from customers - where id != $1 + where id != $1 and enabled order by created desc limit $2` return db.any(sql, [ anonymous.uuid, NUM_RESULTS ]) .then(customers => Promise.all(_.map(customer => { @@ -493,7 +493,8 @@ 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.is_test_customer, 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 @@ -513,11 +514,12 @@ function getCustomersList (phone = null, name = null, address = null, id = null) GROUP BY customer_notes.customer_id ) cn ON c.id = cn.customer_id WHERE c.id != $2 + AND c.enabled ) AS cl WHERE rn = 1 AND ($4 IS NULL OR phone = $4) - AND ($5 IS NULL OR CONCAT(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $5 OR id_card_data::json->>'firstName' = $5 OR id_card_data::json->>'lastName' = $5) - AND ($6 IS NULL OR id_card_data::json->>'address' = $6) - AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7) + AND ($5 IS NULL OR CONCAT(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $5 OR id_card_data::json->>'firstName' = $5 OR id_card_data::json->>'lastName' = $5) + AND ($6 IS NULL OR id_card_data::json->>'address' = $6) + AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7) limit $3` return db.any(sql, [ passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ]) .then(customers => Promise.all(_.map(customer => @@ -550,7 +552,8 @@ function getCustomerById (id) { c.front_camera_path, c.front_camera_override, c.front_camera_at, c.phone, c.phone_at, c.phone_override, 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.subscriber_info_at, c.is_test_customer, 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.subscriber_info_at, c.is_test_customer, c.created, c.enabled, + 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 @@ -570,6 +573,7 @@ function getCustomerById (id) { GROUP BY customer_notes.customer_id ) cn ON c.id = cn.customer_id WHERE c.id = $2 + AND c.enabled ) AS cl WHERE rn = 1` return db.oneOrNone(sql, [passableErrorCodes, id]) .then(assignCustomerData) @@ -909,6 +913,11 @@ function disableTestCustomer (customerId) { return db.none(sql, [customerId]) } +function deleteCustomer (customerId) { + const sql = `UPDATE customers SET enabled=false WHERE id=$1 AND is_test_customer` + return db.none(sql, [customerId]) +} + module.exports = { add, get, @@ -931,5 +940,6 @@ module.exports = { enableTestCustomer, disableTestCustomer, selectLatestData, - getEditedData + getEditedData, + deleteCustomer } diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js index 19b54f790..9d8dcdbcc 100644 --- a/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -54,7 +54,9 @@ const resolvers = { enableTestCustomer: (...[, { customerId }]) => customers.enableTestCustomer(customerId), disableTestCustomer: (...[, { customerId }]) => - customers.disableTestCustomer(customerId) + customers.disableTestCustomer(customerId), + deleteCustomer: (...[, { customerId }]) => + customers.deleteCustomer(customerId) } } diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js index 5af0c4582..4a8e6aeaf 100644 --- a/lib/new-admin/graphql/types/customer.type.js +++ b/lib/new-admin/graphql/types/customer.type.js @@ -111,6 +111,7 @@ const typeDef = gql` createCustomer(phoneNumber: String): Customer @auth enableTestCustomer(customerId: ID!): Boolean @auth disableTestCustomer(customerId: ID!): Boolean @auth + deleteCustomer(customerId: ID!): Boolean @auth(requires: [SUPERUSER]) } ` diff --git a/migrations/1649353351891-delete-test-customers.js b/migrations/1649353351891-delete-test-customers.js new file mode 100644 index 000000000..e01f516be --- /dev/null +++ b/migrations/1649353351891-delete-test-customers.js @@ -0,0 +1,15 @@ +var db = require('./db') + +exports.up = function (next) { + var sql = [ + `ALTER TABLE customers ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT true`, + `ALTER TABLE customers DROP CONSTRAINT customers_phone_key`, + `CREATE UNIQUE INDEX customers_phone_key ON customers (phone) WHERE enabled` + ] + + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index cbc792968..b1795d379 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -10,9 +10,11 @@ import { import NavigateNextIcon from '@material-ui/icons/NavigateNext' import gql from 'graphql-tag' import * as R from 'ramda' -import React, { memo, useState } from 'react' +import React, { memo, useState, useContext } from 'react' import { useHistory, useParams } from 'react-router-dom' +import AppContext from 'src/AppContext' +import { ConfirmDialog } from 'src/components/ConfirmDialog' import ErrorMessage from 'src/components/ErrorMessage' import { Button, IconButton, ActionButton } from 'src/components/buttons' import { Switch } from 'src/components/inputs' @@ -22,6 +24,8 @@ import { OVERRIDE_REJECTED } from 'src/pages/Customers/components/propertyCard' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' +import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' +import { ReactComponent as DeleteIconReversedIcon } from 'src/styling/icons/action/delete/white.svg' import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg' import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg' import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/block/white.svg' @@ -260,6 +264,12 @@ const DISABLE_TEST_CUSTOMER = gql` } ` +const DELETE_CUSTOMER = gql` + mutation deleteCustomer($customerId: ID!) { + deleteCustomer(customerId: $customerId) + } +` + const GET_DATA = gql` query getData { config @@ -289,10 +299,12 @@ const GET_ACTIVE_CUSTOM_REQUESTS = gql` const CustomerProfile = memo(() => { const history = useHistory() + const { userData } = useContext(AppContext) const [retrieve, setRetrieve] = useState(false) const [showCompliance, setShowCompliance] = useState(false) const [wizard, setWizard] = useState(false) + const [toDelete, setToDelete] = useState(false) const [error, setError] = useState(null) const [clickedItem, setClickedItem] = useState('overview') const { id: customerId } = useParams() @@ -395,6 +407,11 @@ const CustomerProfile = memo(() => { onCompleted: () => getCustomer() }) + const [deleteCustomer] = useMutation(DELETE_CUSTOMER, { + variables: { customerId }, + onCompleted: () => history.push('/compliance/customers') + }) + const updateCustomer = it => setCustomer({ variables: { @@ -595,6 +612,17 @@ const CustomerProfile = memo(() => { }> {`${blocked ? 'Authorize' : 'Block'} customer`} + {Boolean(R.path(['isTestCustomer'])(customerData)) && + userData?.role === 'superuser' && ( + setToDelete(true)}> + {`Delete customer`} + + )}
@@ -701,6 +729,17 @@ const CustomerProfile = memo(() => { customInfoRequirementOptions={customInfoRequirementOptions} /> )} + {toDelete && ( + deleteCustomer()} + onDismissed={() => setToDelete(false)} + /> + )}
)