diff --git a/lib/customer-notes.js b/lib/customer-notes.js
new file mode 100644
index 00000000..e89c457d
--- /dev/null
+++ b/lib/customer-notes.js
@@ -0,0 +1,30 @@
+const uuid = require('uuid')
+const _ = require('lodash/fp')
+
+const db = require('./db')
+
+const getCustomerNotes = customerId => {
+ const sql = `SELECT * FROM customer_notes WHERE customer_id=$1 LIMIT 1`
+ return db.oneOrNone(sql, [customerId]).then(res => _.mapKeys((_, key) => _.camelize(key), res))
+}
+
+const createCustomerNotes = (customerId, userId, content) => {
+ const sql = `INSERT INTO customer_notes (id, customer_id, last_edited_by, last_edited_at, content) VALUES ($1, $2, $3, now(), $4)`
+ return db.none(sql, [uuid.v4(), customerId, userId, content])
+}
+
+const updateCustomerNotes = (customerId, userId, content) => {
+ const sql = `UPDATE customer_notes SET last_edited_at=now(), last_edited_by=$1, content=$2 WHERE customer_id=$3 RETURNING *`
+ return db.any(sql, [userId, content, customerId])
+ .then(res => {
+ if (_.isEmpty(res)) {
+ createCustomerNotes(customerId, userId, content)
+ }
+ })
+}
+
+module.exports = {
+ getCustomerNotes,
+ createCustomerNotes,
+ updateCustomerNotes
+}
diff --git a/lib/customers.js b/lib/customers.js
index 5384a320..fc089d05 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, created AS last_active, fiat AS last_tx_fiat,
- fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields
+ fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, content AS notes
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, t.tx_class, t.fiat, t.fiat_code, t.created,
+ c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created, cn.content,
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
@@ -655,6 +655,9 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id
) cf GROUP BY cf.customer_id
) ccf ON c.id = ccf.customer_id
+ LEFT OUTER JOIN (
+ SELECT customer_id, content FROM customer_notes
+ ) cn ON c.id = cn.customer_id
WHERE c.id != $2
) AS cl WHERE rn = 1
AND ($4 IS NULL OR phone = $4)
@@ -678,35 +681,38 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
*/
function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
- const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_at, 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_at, id_card_photo_path, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
- sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
- fiat_code as last_tx_fiat_code, tx_class as last_tx_class, subscriber_info, custom_fields
- from (
- select c.id, c.authorized_override,
- greatest(0, date_part('day', c.suspended_until - now())) as days_suspended,
- c.suspended_until > now() as is_suspended,
- c.front_camera_path, c.front_camera_at, c.front_camera_override,
- 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,
- row_number() over (partition by c.id order by t.created desc) as rn,
- sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs,
- sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id) as total_spent, ccf.custom_fields
- from customers c left outer join (
- select 'cashIn' as tx_class, id, fiat, fiat_code, created, customer_id, error_code
- from cash_in_txs where send_confirmed = true union
- 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
+ const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
+ phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
+ id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
+ sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat,
+ fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, content AS notes
+ FROM (
+ SELECT c.id, c.authorized_override,
+ greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
+ c.suspended_until > now() AS is_suspended,
+ 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.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, cn.content,
+ row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
+ sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,
+ sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (PARTITION BY c.id) AS total_spent, ccf.custom_fields
+ FROM customers c LEFT OUTER JOIN (
+ SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
+ FROM cash_in_txs WHERE send_confirmed = true UNION
+ 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 (
SELECT cf.customer_id, json_agg(json_build_object('id', cf.custom_field_id, 'label', cf.label, 'value', cf.value)) AS custom_fields FROM (
SELECT ccfp.custom_field_id, ccfp.customer_id, cfd.label, ccfp.value FROM custom_field_definitions cfd
LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id
) cf GROUP BY cf.customer_id
) ccf ON c.id = ccf.customer_id
- where c.id = $2
- ) as cl where rn = 1`
+ LEFT OUTER JOIN (
+ SELECT customer_id, content FROM customer_notes
+ ) cn ON c.id = cn.customer_id
+ WHERE c.id = $2
+ ) AS cl WHERE rn = 1`
return db.oneOrNone(sql, [passableErrorCodes, id])
.then(customerData => {
return getEditedData(id)
diff --git a/lib/new-admin/graphql/errors/authentication.js b/lib/new-admin/graphql/errors/authentication.js
index 7ddaede5..ac9b0153 100644
--- a/lib/new-admin/graphql/errors/authentication.js
+++ b/lib/new-admin/graphql/errors/authentication.js
@@ -1,4 +1,4 @@
-const { ApolloError } = require('apollo-server-express')
+const { ApolloError, AuthenticationError } = require('apollo-server-express')
class InvalidCredentialsError extends ApolloError {
constructor(message) {
@@ -29,6 +29,7 @@ class InvalidUrlError extends ApolloError {
}
module.exports = {
+ AuthenticationError,
InvalidCredentialsError,
UserAlreadyExistsError,
InvalidTwoFactorError,
diff --git a/lib/new-admin/graphql/modules/userManagement.js b/lib/new-admin/graphql/modules/userManagement.js
index eeee2a26..13c42e70 100644
--- a/lib/new-admin/graphql/modules/userManagement.js
+++ b/lib/new-admin/graphql/modules/userManagement.js
@@ -239,6 +239,13 @@ const reset2FA = (token, userID, code, context) => {
.then(() => true)
}
+const getToken = context => {
+ if (_.isNil(context.req.cookies.lid) || _.isNil(context.req.session.user.id))
+ throw new authErrors.AuthenticationError('Authentication failed')
+
+ return context.req.session.user.id
+}
+
module.exports = {
authenticateUser,
getUserData,
@@ -259,5 +266,6 @@ module.exports = {
createRegisterToken,
register,
resetPassword,
- reset2FA
+ reset2FA,
+ getToken
}
diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js
index 36a24b0f..bc258284 100644
--- a/lib/new-admin/graphql/resolvers/customer.resolver.js
+++ b/lib/new-admin/graphql/resolvers/customer.resolver.js
@@ -1,6 +1,8 @@
+const authentication = require('../modules/authentication')
const anonymous = require('../../../constants').anonymousCustomer
const customers = require('../../../customers')
const filters = require('../../filters')
+const customerNotes = require('../../../customer-notes')
const resolvers = {
@@ -14,8 +16,7 @@ const resolvers = {
},
Mutation: {
setCustomer: (root, { customerId, customerInput }, context, info) => {
- // TODO: To be replaced by function that fetchs the token
- const token = !!context.req.cookies.lamassu_sid && context.req.session.user.id
+ const token = authentication.getToken(context)
if (customerId === anonymous.uuid) return customers.getCustomerById(customerId)
return customers.updateCustomer(customerId, customerInput, token)
},
@@ -23,14 +24,12 @@ const resolvers = {
saveCustomField: (...[, { customerId, fieldId, newValue }]) => customers.saveCustomField(customerId, fieldId, newValue),
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId),
editCustomer: async (root, { customerId, customerEdit }, context) => {
- // TODO: To be replaced by function that fetchs the token
- const token = !!context.req.cookies.lid && context.req.session.user.id
+ const token = authentication.getToken(context)
const editedData = await customerEdit
return customers.edit(customerId, editedData, token)
},
replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => {
- // TODO: To be replaced by function that fetchs the token
- const token = !!context.req.cookies.lid && context.req.session.user.id
+ const token = authentication.getToken(context)
const photo = await newPhoto
if (!photo) return customers.getCustomerById(customerId)
return customers.updateEditedPhoto(customerId, photo, photoType)
@@ -39,6 +38,10 @@ const resolvers = {
deleteEditedData: (root, { customerId, customerEdit }) => {
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
return customers.getCustomerById(customerId)
+ },
+ setCustomerNotes: (...[, { customerId, newContent }, context]) => {
+ const token = authentication.getToken(context)
+ return customerNotes.updateCustomerNotes(customerId, token, newContent)
}
}
}
diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js
index 14336086..adc6d2d6 100644
--- a/lib/new-admin/graphql/types/customer.type.js
+++ b/lib/new-admin/graphql/types/customer.type.js
@@ -41,6 +41,7 @@ const typeDef = gql`
subscriberInfo: JSONObject
customFields: [CustomerCustomField]
customInfoRequests: [CustomRequestData]
+ notes: String
}
input CustomerInput {
@@ -89,6 +90,7 @@ const typeDef = gql`
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth
+ setCustomerNotes(customerId: ID!, newContent: String!): Customer @auth
}
`
diff --git a/migrations/1627868356883-customer-custom-notes.js b/migrations/1627868356883-customer-custom-notes.js
new file mode 100644
index 00000000..87a16e5e
--- /dev/null
+++ b/migrations/1627868356883-customer-custom-notes.js
@@ -0,0 +1,20 @@
+var db = require('./db')
+
+exports.up = function (next) {
+ var sql = [
+ `CREATE TABLE customer_notes (
+ id UUID PRIMARY KEY,
+ customer_id UUID NOT NULL REFERENCES customers(id),
+ created TIMESTAMPTZ NOT NULL DEFAULT now(),
+ last_edited_at TIMESTAMPTZ,
+ last_edited_by UUID REFERENCES users(id),
+ content TEXT NOT NULL DEFAULT ''
+ )`
+ ]
+
+ 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 39f3d25e..e0be120c 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
@@ -69,6 +69,7 @@ const GET_CUSTOMER = gql`
label
value
}
+ notes
transactions {
txClass
id
diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerNotes.js b/new-lamassu-admin/src/pages/Customers/components/CustomerNotes.js
new file mode 100644
index 00000000..130b8e7d
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Customers/components/CustomerNotes.js
@@ -0,0 +1,98 @@
+import { useMutation } from '@apollo/react-hooks'
+import { makeStyles } from '@material-ui/core'
+import { Formik, Form, Field } from 'formik'
+import gql from 'graphql-tag'
+import * as R from 'ramda'
+import React, { useState } from 'react'
+import * as Yup from 'yup'
+
+import { TextInput } from 'src/components/inputs/formik'
+import { Info3 } from 'src/components/typography'
+
+import styles from './CustomerNotes.styles'
+import { PropertyCard } from './propertyCard'
+
+const validationSchema = Yup.object().shape({
+ notes: Yup.string()
+})
+
+const SAVE_CUSTOMER_NOTES = gql`
+ mutation setCustomerNotes($customerId: ID!, $newContent: String!) {
+ setCustomerNotes(customerId: $customerId, newContent: $newContent) {
+ notes
+ }
+ }
+`
+
+const useStyles = makeStyles(styles)
+
+const CustomerNotes = ({ customer }) => {
+ const classes = useStyles()
+
+ const [editing, setEditing] = useState(false)
+ const [setNotes] = useMutation(SAVE_CUSTOMER_NOTES, {
+ refetchQueries: () => ['customer']
+ })
+
+ const initialValues = {
+ notes: R.path(['notes'])(customer) ?? ''
+ }
+
+ const handleConfirm = values => {
+ setNotes({
+ variables: {
+ customerId: customer.id,
+ newContent: values.notes
+ }
+ })
+ setEditing(false)
+ }
+
+ const getFormattedNotes = content => {
+ const fragments = R.split(/\n/)(content ?? '')
+ return R.map((it, idx) => {
+ if (idx === fragments.length) return <>{it}>
+ return (
+ <>
+ {it}
+
+ >
+ )
+ }, fragments)
+ }
+
+ return (
+