feat: customer auth via email
This commit is contained in:
parent
92a3f16c80
commit
ab304093f3
22 changed files with 252 additions and 27 deletions
|
|
@ -26,4 +26,9 @@ const hasPhone = hasRequirement('sms')
|
||||||
const hasFacephoto = hasRequirement('facephoto')
|
const hasFacephoto = hasRequirement('facephoto')
|
||||||
const hasIdScan = hasRequirement('idCardData')
|
const hasIdScan = hasRequirement('idCardData')
|
||||||
|
|
||||||
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan }
|
const AUTH_METHODS = {
|
||||||
|
SMS: 'SMS',
|
||||||
|
EMAIL: 'EMAIL'
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS }
|
||||||
|
|
@ -6,13 +6,10 @@ const makeDir = require('make-dir')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
const { sub, differenceInHours } = require('date-fns/fp')
|
|
||||||
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const BN = require('./bn')
|
|
||||||
const anonymous = require('../lib/constants').anonymousCustomer
|
const anonymous = require('../lib/constants').anonymousCustomer
|
||||||
const complianceOverrides = require('./compliance_overrides')
|
const complianceOverrides = require('./compliance_overrides')
|
||||||
const users = require('./users')
|
|
||||||
const writeFile = util.promisify(fs.writeFile)
|
const writeFile = util.promisify(fs.writeFile)
|
||||||
const notifierQueries = require('./notifier/queries')
|
const notifierQueries = require('./notifier/queries')
|
||||||
const notifierUtils = require('./notifier/utils')
|
const notifierUtils = require('./notifier/utils')
|
||||||
|
|
@ -43,6 +40,12 @@ function add (customer) {
|
||||||
.then(camelize)
|
.then(camelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addWithEmail (customer) {
|
||||||
|
const sql = 'insert into customers (id, email, email_at) values ($1, $2, now()) returning *'
|
||||||
|
return db.one(sql, [uuid.v4(), customer.email])
|
||||||
|
.then(camelize)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get single customer by phone
|
* Get single customer by phone
|
||||||
* Phone numbers are unique per customer
|
* Phone numbers are unique per customer
|
||||||
|
|
@ -60,6 +63,12 @@ function get (phone) {
|
||||||
.then(camelize)
|
.then(camelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWithEmail (email) {
|
||||||
|
const sql = 'select * from customers where email=$1'
|
||||||
|
return db.oneOrNone(sql, [email])
|
||||||
|
.then(camelize)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update customer record
|
* Update customer record
|
||||||
*
|
*
|
||||||
|
|
@ -308,7 +317,7 @@ const updateSubscriberData = (customerId, data, userToken) => {
|
||||||
*
|
*
|
||||||
* Used for the machine.
|
* Used for the machine.
|
||||||
*/
|
*/
|
||||||
function getById (id, userToken) {
|
function getById (id) {
|
||||||
const sql = 'select * from customers where id=$1'
|
const sql = 'select * from customers where id=$1'
|
||||||
return db.oneOrNone(sql, [id])
|
return db.oneOrNone(sql, [id])
|
||||||
.then(assignCustomerData)
|
.then(assignCustomerData)
|
||||||
|
|
@ -349,6 +358,7 @@ function camelizeDeep (customer) {
|
||||||
function getComplianceTypes () {
|
function getComplianceTypes () {
|
||||||
return [
|
return [
|
||||||
'sms',
|
'sms',
|
||||||
|
'email',
|
||||||
'id_card_data',
|
'id_card_data',
|
||||||
'id_card_photo',
|
'id_card_photo',
|
||||||
'front_camera',
|
'front_camera',
|
||||||
|
|
@ -482,7 +492,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
||||||
|
|
||||||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
|
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,
|
phone, email, 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, GREATEST(created, last_transaction, last_data_provided) AS last_active, fiat AS last_tx_fiat,
|
sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_data_provided) AS last_active, fiat AS last_tx_fiat,
|
||||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
|
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
|
||||||
|
|
@ -491,9 +501,9 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
|
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
|
||||||
c.suspended_until > NOW() AS is_suspended,
|
c.suspended_until > NOW() AS is_suspended,
|
||||||
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.email, 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,
|
||||||
GREATEST(c.phone_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
|
GREATEST(c.phone_at, c.email_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
|
||||||
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,
|
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,
|
||||||
|
|
@ -540,7 +550,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
function getCustomerById (id) {
|
function getCustomerById (id) {
|
||||||
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
||||||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
|
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
|
||||||
phone, phone_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
|
phone, phone_at, email, email_at, phone_override, 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, subscriber_info_at, custom_fields, notes, is_test_customer
|
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, subscriber_info_at, custom_fields, notes, is_test_customer
|
||||||
|
|
@ -549,7 +559,7 @@ function getCustomerById (id) {
|
||||||
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
||||||
c.suspended_until > now() AS is_suspended,
|
c.suspended_until > now() AS is_suspended,
|
||||||
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.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.phone, c.phone_at, c.email, c.email_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.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, 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,
|
||||||
|
|
@ -912,7 +922,9 @@ function disableTestCustomer (customerId) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
add,
|
add,
|
||||||
|
addWithEmail,
|
||||||
get,
|
get,
|
||||||
|
getWithEmail,
|
||||||
batch,
|
batch,
|
||||||
getCustomersList,
|
getCustomersList,
|
||||||
getCustomerById,
|
getCustomerById,
|
||||||
|
|
@ -930,7 +942,5 @@ module.exports = {
|
||||||
updateEditedPhoto,
|
updateEditedPhoto,
|
||||||
updateTxCustomerPhoto,
|
updateTxCustomerPhoto,
|
||||||
enableTestCustomer,
|
enableTestCustomer,
|
||||||
disableTestCustomer,
|
disableTestCustomer
|
||||||
selectLatestData,
|
|
||||||
getEditedData
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
configManager.getOperatorInfo(settings.config),
|
configManager.getOperatorInfo(settings.config),
|
||||||
configManager.getReceipt(settings.config),
|
configManager.getReceipt(settings.config),
|
||||||
!!configManager.getCashOut(deviceId, settings.config).active,
|
!!configManager.getCashOut(deviceId, settings.config).active,
|
||||||
|
configManager.getCustomerAuthenticationMethod(settings.config)
|
||||||
])
|
])
|
||||||
.then(([
|
.then(([
|
||||||
enablePaperWalletOnly,
|
enablePaperWalletOnly,
|
||||||
|
|
@ -125,6 +126,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
operatorInfo,
|
operatorInfo,
|
||||||
receiptInfo,
|
receiptInfo,
|
||||||
twoWayMode,
|
twoWayMode,
|
||||||
|
customerAuthentication,
|
||||||
]) =>
|
]) =>
|
||||||
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
||||||
null :
|
null :
|
||||||
|
|
@ -141,6 +143,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
},
|
},
|
||||||
machineInfo: { deviceId, deviceName },
|
machineInfo: { deviceId, deviceName },
|
||||||
twoWayMode,
|
twoWayMode,
|
||||||
|
customerAuthentication,
|
||||||
speedtestFiles,
|
speedtestFiles,
|
||||||
urlsToPing,
|
urlsToPing,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,11 @@ type Terms {
|
||||||
details: TermsDetails
|
details: TermsDetails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CustomerAuthentication {
|
||||||
|
EMAIL
|
||||||
|
SMS
|
||||||
|
}
|
||||||
|
|
||||||
type StaticConfig {
|
type StaticConfig {
|
||||||
configVersion: Int!
|
configVersion: Int!
|
||||||
|
|
||||||
|
|
@ -132,6 +137,7 @@ type StaticConfig {
|
||||||
serverVersion: String!
|
serverVersion: String!
|
||||||
timezone: Int!
|
timezone: Int!
|
||||||
twoWayMode: Boolean!
|
twoWayMode: Boolean!
|
||||||
|
customerAuthentication: CustomerAuthentication!
|
||||||
|
|
||||||
localeInfo: LocaleInfo!
|
localeInfo: LocaleInfo!
|
||||||
operatorInfo: OperatorInfo
|
operatorInfo: OperatorInfo
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const typeDef = gql`
|
||||||
frontCameraAt: Date
|
frontCameraAt: Date
|
||||||
frontCameraOverride: String
|
frontCameraOverride: String
|
||||||
phone: String
|
phone: String
|
||||||
|
email: String
|
||||||
isAnonymous: Boolean
|
isAnonymous: Boolean
|
||||||
smsOverride: String
|
smsOverride: String
|
||||||
idCardData: JSONObject
|
idCardData: JSONObject
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
const {AUTH_METHODS} = require('./compliance-triggers')
|
||||||
|
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const { validate } = require('uuid')
|
const { validate } = require('uuid')
|
||||||
|
|
||||||
|
|
@ -120,6 +122,10 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
|
||||||
|
|
||||||
const getTriggers = _.get('triggers')
|
const getTriggers = _.get('triggers')
|
||||||
|
|
||||||
|
function getCustomerAuthenticationMethod(config) {
|
||||||
|
return _.get('triggersConfig_customerAuthentication')(config)
|
||||||
|
}
|
||||||
|
|
||||||
/* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */
|
/* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */
|
||||||
const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => {
|
const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => {
|
||||||
return customInfoRequests
|
return customInfoRequests
|
||||||
|
|
@ -193,4 +199,5 @@ module.exports = {
|
||||||
getCryptosFromWalletNamespace,
|
getCryptosFromWalletNamespace,
|
||||||
getCryptoUnits,
|
getCryptoUnits,
|
||||||
setTermsConditions,
|
setTermsConditions,
|
||||||
|
getCustomerAuthenticationMethod,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -779,6 +779,27 @@ function plugins (settings, deviceId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEmailCode (toEmail) {
|
||||||
|
const notifications = configManager.getNotifications(settings.config)
|
||||||
|
|
||||||
|
const code = notifications.thirdParty_email === 'mock-email'
|
||||||
|
? '123'
|
||||||
|
: randomCode()
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
email: {
|
||||||
|
toEmail,
|
||||||
|
subject: 'Your cryptomat code',
|
||||||
|
text: `Your cryptomat code: ${code}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(code)
|
||||||
|
return Promise.resolve(code)
|
||||||
|
// return sms.sendMessage(settings, rec)
|
||||||
|
// .then(() => code)
|
||||||
|
}
|
||||||
|
|
||||||
function sweepHdRow (row) {
|
function sweepHdRow (row) {
|
||||||
const txId = row.id
|
const txId = row.id
|
||||||
const cryptoCode = row.crypto_code
|
const cryptoCode = row.crypto_code
|
||||||
|
|
@ -862,6 +883,7 @@ function plugins (settings, deviceId) {
|
||||||
isZeroConf,
|
isZeroConf,
|
||||||
getStatus,
|
getStatus,
|
||||||
getPhoneCode,
|
getPhoneCode,
|
||||||
|
getEmailCode,
|
||||||
executeTrades,
|
executeTrades,
|
||||||
pong,
|
pong,
|
||||||
clearOldLogs,
|
clearOldLogs,
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ const NAME = 'Mailgun'
|
||||||
|
|
||||||
function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
|
function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
|
||||||
const mailgun = Mailgun({apiKey, domain})
|
const mailgun = Mailgun({apiKey, domain})
|
||||||
|
const to = req.email.toEmail ?? toEmail
|
||||||
|
|
||||||
const emailData = {
|
const emailData = {
|
||||||
from: `Lamassu Server ${fromEmail}`,
|
from: `Lamassu Server ${fromEmail}`,
|
||||||
to: toEmail,
|
to,
|
||||||
subject: rec.email.subject,
|
subject: rec.email.subject,
|
||||||
text: rec.email.body
|
text: rec.email.body
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,12 @@ function toCashOutTx (row) {
|
||||||
return _.set('direction', 'cashOut', newObj)
|
return _.set('direction', 'cashOut', newObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchPhoneTx (phone) {
|
function fetchEmailOrPhoneTx (data, type) {
|
||||||
const sql = `select * from cash_out_txs
|
const sql = `select * from cash_out_txs
|
||||||
where phone=$1 and dispense=$2
|
where ${type === 'email' ? 'email' : 'phone'}=$1 and dispense=$2
|
||||||
and (extract(epoch from (now() - created))) * 1000 < $3`
|
and (extract(epoch from (now() - created))) * 1000 < $3`
|
||||||
|
|
||||||
const values = [phone, false, TRANSACTION_EXPIRATION]
|
const values = [data, false, TRANSACTION_EXPIRATION]
|
||||||
|
|
||||||
return db.any(sql, values)
|
return db.any(sql, values)
|
||||||
.then(_.map(toCashOutTx))
|
.then(_.map(toCashOutTx))
|
||||||
|
|
@ -72,6 +72,13 @@ function fetchPhoneTx (phone) {
|
||||||
throw httpError('No transactions', 404)
|
throw httpError('No transactions', 404)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
function fetchEmailTx (email) {
|
||||||
|
return fetchEmailOrPhoneTx(email, 'email')
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPhoneTx (phone) {
|
||||||
|
return fetchEmailOrPhoneTx(phone, 'phone')
|
||||||
|
}
|
||||||
|
|
||||||
function fetchStatusTx (txId, status) {
|
function fetchStatusTx (txId, status) {
|
||||||
const sql = 'select * from cash_out_txs where id=$1'
|
const sql = 'select * from cash_out_txs where id=$1'
|
||||||
|
|
@ -88,6 +95,7 @@ function fetchStatusTx (txId, status) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stateChange,
|
stateChange,
|
||||||
fetchPhoneTx,
|
fetchPhoneTx,
|
||||||
|
fetchEmailTx,
|
||||||
fetchStatusTx,
|
fetchStatusTx,
|
||||||
httpError
|
httpError
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,10 @@ app.use('/verify_user', verifyUserRoutes)
|
||||||
app.use('/verify_transaction', verifyTxRoutes)
|
app.use('/verify_transaction', verifyTxRoutes)
|
||||||
app.use('/verify_promo_code', verifyPromoCodeRoutes)
|
app.use('/verify_promo_code', verifyPromoCodeRoutes)
|
||||||
|
|
||||||
|
// BACKWARDS_COMPATIBILITY 9.0
|
||||||
|
// machines before 9.0 still use the phone_code route
|
||||||
app.use('/phone_code', phoneCodeRoutes)
|
app.use('/phone_code', phoneCodeRoutes)
|
||||||
|
|
||||||
app.use('/customer', customerRoutes)
|
app.use('/customer', customerRoutes)
|
||||||
|
|
||||||
app.use('/tx', txRoutes)
|
app.use('/tx', txRoutes)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ const machineLoader = require('../machine-loader')
|
||||||
const { loadLatestConfig } = require('../new-settings-loader')
|
const { loadLatestConfig } = require('../new-settings-loader')
|
||||||
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
|
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
|
||||||
const T = require('../time')
|
const T = require('../time')
|
||||||
|
const plugins = require('../plugins')
|
||||||
|
const Tx = require('../tx')
|
||||||
|
const loyalty = require('../loyalty')
|
||||||
|
|
||||||
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
|
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
|
||||||
if (_.isNil(patch.data)) {
|
if (_.isNil(patch.data)) {
|
||||||
|
|
@ -185,6 +188,70 @@ function sendSmsReceipt (req, res, next) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addOrUpdateCustomer (customerData, config, isEmailAuth) {
|
||||||
|
const triggers = configManager.getTriggers(config)
|
||||||
|
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
|
||||||
|
|
||||||
|
const customerKey = isEmailAuth ? customerData.email : customerData.phone
|
||||||
|
const getFunc = isEmailAuth ? customers.getWithEmail : customers.get
|
||||||
|
const addFunction = isEmailAuth ? customers.addWithEmail : customers.add
|
||||||
|
|
||||||
|
return getFunc(customerKey)
|
||||||
|
.then(customer => {
|
||||||
|
if (customer) return customer
|
||||||
|
|
||||||
|
return addFunction(customerData)
|
||||||
|
})
|
||||||
|
.then(customer => customers.getById(customer.id))
|
||||||
|
.then(customer => {
|
||||||
|
return Tx.customerHistory(customer.id, maxDaysThreshold)
|
||||||
|
.then(result => {
|
||||||
|
customer.txHistory = result
|
||||||
|
return customer
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(customer => {
|
||||||
|
return loyalty.getCustomerActiveIndividualDiscount(customer.id)
|
||||||
|
.then(discount => ({ ...customer, discount }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrAddCustomerPhone (req, res, next) {
|
||||||
|
const customerData = req.body
|
||||||
|
|
||||||
|
const pi = plugins(req.settings, req.deviceId)
|
||||||
|
const phone = req.body.phone
|
||||||
|
|
||||||
|
return pi.getPhoneCode(phone)
|
||||||
|
.then(code => {
|
||||||
|
return addOrUpdateCustomer(customerData, req.settings.config, false)
|
||||||
|
.then(customer => respond(req, res, { code, customer }))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
.catch(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrAddCustomerEmail (req, res, next) {
|
||||||
|
const customerData = req.body
|
||||||
|
|
||||||
|
const pi = plugins(req.settings, req.deviceId)
|
||||||
|
const email = req.body.email
|
||||||
|
|
||||||
|
return pi.getEmailCode(email)
|
||||||
|
.then(code => {
|
||||||
|
return addOrUpdateCustomer(customerData, req.settings.config, true)
|
||||||
|
.then(customer => respond(req, res, { code, customer }))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
.catch(next)
|
||||||
|
}
|
||||||
|
|
||||||
router.patch('/:id', updateCustomer)
|
router.patch('/:id', updateCustomer)
|
||||||
router.patch('/:id/sanctions', triggerSanctions)
|
router.patch('/:id/sanctions', triggerSanctions)
|
||||||
router.patch('/:id/block', triggerBlock)
|
router.patch('/:id/block', triggerBlock)
|
||||||
|
|
@ -192,5 +259,7 @@ router.patch('/:id/suspend', triggerSuspend)
|
||||||
router.patch('/:id/photos/idcarddata', updateIdCardData)
|
router.patch('/:id/photos/idcarddata', updateIdCardData)
|
||||||
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
|
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
|
||||||
router.post('/:id/smsreceipt', sendSmsReceipt)
|
router.post('/:id/smsreceipt', sendSmsReceipt)
|
||||||
|
router.post('/phone_code', getOrAddCustomerPhone)
|
||||||
|
router.post('/email_code', getOrAddCustomerEmail)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,19 @@ function getPhoneTx (req, res, next) {
|
||||||
return next(httpError('Not Found', 404))
|
return next(httpError('Not Found', 404))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEmailTx (req, res, next) {
|
||||||
|
if (req.query.email) {
|
||||||
|
return helpers.fetchEmailTx(req.query.email)
|
||||||
|
.then(r => res.json(r))
|
||||||
|
.catch(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(httpError('Not Found', 404))
|
||||||
|
}
|
||||||
|
|
||||||
router.post('/', postTx)
|
router.post('/', postTx)
|
||||||
router.get('/:id', getTx)
|
router.get('/:id', getTx)
|
||||||
router.get('/', getPhoneTx)
|
router.get('/', getPhoneTx)
|
||||||
|
router.get('/', getEmailTx)
|
||||||
|
|
||||||
module.exports = { postTx, getTx, getPhoneTx, router }
|
module.exports = { postTx, getTx, getPhoneTx, getEmailTx, router }
|
||||||
|
|
|
||||||
14
migrations/1700123461281-customer-email.js
Normal file
14
migrations/1700123461281-customer-email.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
let sql = [
|
||||||
|
'ALTER TABLE customers ADD COLUMN email text unique',
|
||||||
|
'ALTER TABLE customers ADD COLUMN email_at timestamptz',
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
18
migrations/1700123461282-customer-auth-advanced-trigger.js
Normal file
18
migrations/1700123461282-customer-auth-advanced-trigger.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
const { migrationSaveConfig } = require('../lib/new-settings-loader')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
const triggersDefault = {
|
||||||
|
triggersConfig_customerAuthentication: 'SMS',
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrationSaveConfig(triggersDefault)
|
||||||
|
.then(() => next())
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err.message)
|
||||||
|
return next(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
14
migrations/1700123461283-phone-on-tx.js
Normal file
14
migrations/1700123461283-phone-on-tx.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
let sql = [
|
||||||
|
'ALTER TABLE cash_in_txs ADD COLUMN email text',
|
||||||
|
'ALTER TABLE cash_out_txs ADD COLUMN email text',
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,7 @@ const GET_CUSTOMER = gql`
|
||||||
frontCameraAt
|
frontCameraAt
|
||||||
frontCameraOverride
|
frontCameraOverride
|
||||||
phone
|
phone
|
||||||
|
email
|
||||||
isAnonymous
|
isAnonymous
|
||||||
smsOverride
|
smsOverride
|
||||||
idCardData
|
idCardData
|
||||||
|
|
@ -132,6 +133,7 @@ const SET_CUSTOMER = gql`
|
||||||
frontCameraPath
|
frontCameraPath
|
||||||
frontCameraOverride
|
frontCameraOverride
|
||||||
phone
|
phone
|
||||||
|
email
|
||||||
smsOverride
|
smsOverride
|
||||||
idCardData
|
idCardData
|
||||||
idCardDataOverride
|
idCardDataOverride
|
||||||
|
|
@ -516,6 +518,8 @@ const CustomerProfile = memo(() => {
|
||||||
})) ?? []
|
})) ?? []
|
||||||
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
const email = R.path(['email'])(customerData)
|
||||||
|
const phone = R.path(['phone'])(customerData)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -532,10 +536,9 @@ const CustomerProfile = memo(() => {
|
||||||
<Label2 noMargin className={classes.labelLink}>
|
<Label2 noMargin className={classes.labelLink}>
|
||||||
{name.length
|
{name.length
|
||||||
? name
|
? name
|
||||||
: getFormattedPhone(
|
: email?.length
|
||||||
R.path(['phone'])(customerData),
|
? email
|
||||||
locale.country
|
: getFormattedPhone(phone, locale.country)}
|
||||||
)}
|
|
||||||
</Label2>
|
</Label2>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div className={classes.panels}>
|
<div className={classes.panels}>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ const GET_CUSTOMERS = gql`
|
||||||
id
|
id
|
||||||
idCardData
|
idCardData
|
||||||
phone
|
phone
|
||||||
|
email
|
||||||
totalTxs
|
totalTxs
|
||||||
totalSpent
|
totalSpent
|
||||||
lastActive
|
lastActive
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ const CustomersList = ({
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
header: 'Phone',
|
header: 'Phone/email',
|
||||||
width: 199,
|
width: 199,
|
||||||
view: it => getFormattedPhone(it.phone, locale.country)
|
view: it => `${getFormattedPhone(it.phone, locale.country)} ${it.email}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
|
||||||
|
|
||||||
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
|
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
|
||||||
const usSsn = R.path(['usSsn'])(customer)
|
const usSsn = R.path(['usSsn'])(customer)
|
||||||
|
const name = getName(customer)
|
||||||
|
const email = R.path(['email'])(customer)
|
||||||
|
const phone = R.path(['phone'])(customer)
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
|
|
@ -40,7 +43,12 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
|
||||||
value: usSsn
|
value: usSsn
|
||||||
})
|
})
|
||||||
|
|
||||||
const name = getName(customer)
|
if (email)
|
||||||
|
elements.push({
|
||||||
|
header: 'Email',
|
||||||
|
size: 190,
|
||||||
|
value: email
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
|
|
@ -51,7 +59,9 @@ const CustomerDetails = memo(({ customer, photosData, locale, timezone }) => {
|
||||||
<H2 noMargin>
|
<H2 noMargin>
|
||||||
{name.length
|
{name.length
|
||||||
? name
|
? name
|
||||||
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
|
: email?.length
|
||||||
|
? email
|
||||||
|
: getFormattedPhone(phone, locale.country)}
|
||||||
</H2>
|
</H2>
|
||||||
</div>
|
</div>
|
||||||
<Box display="flex" mt="auto">
|
<Box display="flex" mt="auto">
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,8 @@ const getTypeText = (config, currency, classes) => {
|
||||||
|
|
||||||
const getRequirementText = (config, classes) => {
|
const getRequirementText = (config, classes) => {
|
||||||
switch (config.requirement?.requirement) {
|
switch (config.requirement?.requirement) {
|
||||||
|
case 'email':
|
||||||
|
return <>asked to enter code provided through email verification</>
|
||||||
case 'sms':
|
case 'sms':
|
||||||
return <>asked to enter code provided through SMS verification</>
|
return <>asked to enter code provided through SMS verification</>
|
||||||
case 'idCardPhoto':
|
case 'idCardPhoto':
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,21 @@ const getDefaultSettings = () => {
|
||||||
labelProp: 'display',
|
labelProp: 'display',
|
||||||
valueProp: 'code'
|
valueProp: 'code'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customerAuthentication',
|
||||||
|
header: 'Customer Auth',
|
||||||
|
width: 196,
|
||||||
|
size: 'sm',
|
||||||
|
input: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: [
|
||||||
|
{ code: 'SMS', display: 'SMS' },
|
||||||
|
{ code: 'EMAIL', display: 'EMAIL' }
|
||||||
|
],
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +159,8 @@ const getOverrides = customInfoRequests => {
|
||||||
const defaults = [
|
const defaults = [
|
||||||
{
|
{
|
||||||
expirationTime: 'Forever',
|
expirationTime: 'Forever',
|
||||||
automation: 'Automatic'
|
automation: 'Automatic',
|
||||||
|
customerAuth: 'SMS'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -522,6 +522,7 @@ const requirementSchema = Yup.object()
|
||||||
|
|
||||||
const requirementOptions = [
|
const requirementOptions = [
|
||||||
{ display: 'SMS verification', code: 'sms' },
|
{ display: 'SMS verification', code: 'sms' },
|
||||||
|
{ display: 'Email verification', code: 'email' },
|
||||||
{ display: 'ID card image', code: 'idCardPhoto' },
|
{ display: 'ID card image', code: 'idCardPhoto' },
|
||||||
{ display: 'ID data', code: 'idCardData' },
|
{ display: 'ID data', code: 'idCardData' },
|
||||||
{ display: 'Customer camera', code: 'facephoto' },
|
{ display: 'Customer camera', code: 'facephoto' },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue