lamassu-server/packages/server/lib/routes/customerRoutes.js
2025-05-28 10:19:01 +01:00

432 lines
13 KiB
JavaScript

const express = require('express')
const router = express.Router()
const _ = require('lodash/fp')
const { zonedTimeToUtc, utcToZonedTime } = require('date-fns-tz/fp')
const { add, intervalToDuration } = require('date-fns/fp')
const uuid = require('uuid')
const sms = require('../sms')
const BN = require('../bn')
const compliance = require('../compliance')
const complianceTriggers = require('../compliance-triggers')
const configManager = require('../new-config-manager')
const customers = require('../customers')
const txs = require('../new-admin/services/transactions')
const httpError = require('../route-helpers').httpError
const notifier = require('../notifier')
const respond = require('../respond')
const { getTx } = require('../new-admin/services/transactions.js')
const machineLoader = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
const T = require('../time')
const plugins = require('../plugins')
const Tx = require('../tx')
const loyalty = require('../loyalty')
const logger = require('../logger')
const externalCompliance = require('../compliance-external')
function updateCustomerCustomInfoRequest(customerId, patch) {
const promise = _.isNil(patch.data)
? Promise.resolve(null)
: customInfoRequestQueries.setCustomerDataViaMachine(
customerId,
patch.infoRequestId,
patch,
)
return promise.then(() => customers.getById(customerId))
}
const createPendingManualComplianceNotifs = (settings, customer, deviceId) => {
const customInfoRequests = _.reduce(
(reqs, req) => _.set(req.info_request_id, req, reqs),
{},
_.get(['customInfoRequestData'], customer),
)
const isPending = field =>
uuid.validate(field)
? _.get([field, 'override'], customInfoRequests) === 'automatic'
: customer[`${field}At`] &&
(!customer[`${field}OverrideAt`] ||
customer[`${field}OverrideAt`].getTime() <
customer[`${field}At`].getTime())
const unnestCustomTriggers = triggersAutomation => {
const customTriggers = _.fromPairs(
_.map(({ id, type }) => [id, type], triggersAutomation.custom),
)
return _.flow(
_.unset('custom'),
_.mapKeys(k => (k === 'facephoto' ? 'frontCamera' : k)),
_.assign(customTriggers),
)(triggersAutomation)
}
const isManual = v => v === 'Manual'
const hasManualAutomation = triggersAutomation =>
_.any(isManual, _.values(triggersAutomation))
configManager
.getTriggersAutomation(
customInfoRequestQueries.getCustomInfoRequests(true),
settings.config,
)
.then(triggersAutomation => {
triggersAutomation = unnestCustomTriggers(triggersAutomation)
if (!hasManualAutomation(triggersAutomation)) return
const pendingFields = _.filter(
field => isManual(triggersAutomation[field]) && isPending(field),
_.keys(triggersAutomation),
)
if (!_.isEmpty(pendingFields))
notifier.complianceNotify(
settings,
customer,
deviceId,
'PENDING_COMPLIANCE',
)
})
}
function updateCustomer(req, res, next) {
const id = req.params.id
const patch = req.body
const deviceId = req.deviceId
const settings = req.settings
if (patch.customRequestPatch) {
return updateCustomerCustomInfoRequest(id, patch.customRequestPatch)
.then(customer => {
createPendingManualComplianceNotifs(settings, customer, deviceId)
respond(req, res, { customer })
})
.catch(next)
}
customers
.getById(id)
.then(customer =>
!customer ? Promise.reject(httpError('Not Found', 404)) : {},
)
.then(_.merge(patch))
.then(newPatch => customers.updatePhotoCard(id, newPatch))
.then(newPatch => customers.updateFrontCamera(id, newPatch))
.then(newPatch => customers.update(id, newPatch, null))
.then(customer => {
createPendingManualComplianceNotifs(settings, customer, deviceId)
respond(req, res, { customer })
})
.catch(next)
}
function updateIdCardData(req, res, next) {
const id = req.params.id
const patch = req.body
customers
.getById(id)
.then(customer => {
if (!customer) {
throw httpError('Not Found', 404)
}
return customers.updateIdCardData(patch, id).then(() => customer)
})
.then(customer => respond(req, res, { customer }))
.catch(next)
}
function triggerSanctions(req, res, next) {
const id = req.params.id
customers
.getById(id)
.then(customer => {
if (!customer) {
throw httpError('Not Found', 404)
}
return compliance
.validationPatch(req.deviceId, customer)
.then(patch => customers.update(id, patch))
})
.then(customer => respond(req, res, { customer }))
.catch(next)
}
function triggerBlock(req, res, next) {
const id = req.params.id
const settings = req.settings
customers
.update(id, { authorizedOverride: 'blocked' })
.then(customer => {
notifier.complianceNotify(settings, customer, req.deviceId, 'BLOCKED')
return respond(req, res, { customer })
})
.catch(next)
}
function triggerSuspend(req, res, next) {
const id = req.params.id
const triggerId = req.body.triggerId
const settings = req.settings
const triggers = configManager.getTriggers(req.settings.config)
const getSuspendDays = _.compose(
_.get('suspensionDays'),
_.find(_.matches({ id: triggerId })),
)
const days = _.includes(triggerId, ['no-ff-camera', 'id-card-photo-disabled'])
? 1
: getSuspendDays(triggers)
const suspensionDuration = intervalToDuration({ start: 0, end: T.day * days })
customers
.update(id, { suspendedUntil: add(suspensionDuration, new Date()) })
.then(customer => {
notifier.complianceNotify(
settings,
customer,
req.deviceId,
'SUSPENDED',
days,
)
return respond(req, res, { customer })
})
.catch(next)
}
function updateTxCustomerPhoto(req, res, next) {
const customerId = req.params.id
const txId = req.params.txId
const tcPhotoData = req.body.tcPhotoData
const direction = req.body.direction
Promise.all([customers.getById(customerId), txs.getTx(txId, direction)])
.then(([customer, tx]) => {
if (!customer || !tx) return
return customers
.updateTxCustomerPhoto(tcPhotoData)
.then(newPatch =>
txs.updateTxCustomerPhoto(customerId, txId, direction, newPatch),
)
})
.then(() => respond(req, res, {}))
.catch(next)
}
function buildSms(data, receiptOptions) {
return Promise.all([
getTx(data.session, data.txClass),
loadLatestConfig(),
]).then(([tx, config]) => {
return Promise.all([
customers.getCustomerById(tx.customer_id),
machineLoader.getMachine(tx.device_id, config),
]).then(([customer, deviceConfig]) => {
const formattedTx = _.mapKeys(_.camelCase)(tx)
const localeConfig = configManager.getLocale(formattedTx.deviceId, config)
const timezone = localeConfig.timezone
const cashInCommission = new BN(1).plus(
new BN(formattedTx.commissionPercentage),
)
const rate = new BN(formattedTx.rawTickerPrice)
.multipliedBy(cashInCommission)
.decimalPlaces(2)
const date = utcToZonedTime(
timezone,
zonedTimeToUtc(process.env.TZ, new Date()),
)
const dateString = `${date.toISOString().replace('T', ' ').slice(0, 19)}`
const data = {
operatorInfo: configManager.getOperatorInfo(config),
location: deviceConfig.machineLocation,
customerName: customer.name,
customerPhone: customer.phone,
session: formattedTx.id,
time: dateString,
direction: formattedTx.txClass === 'cashIn' ? 'Cash-in' : 'Cash-out',
fiat: `${formattedTx.fiat.toString()} ${formattedTx.fiatCode}`,
crypto: `${sms.toCryptoUnits(BN(formattedTx.cryptoAtoms), formattedTx.cryptoCode)} ${formattedTx.cryptoCode}`,
rate: `1 ${formattedTx.cryptoCode} = ${rate} ${formattedTx.fiatCode}`,
address: formattedTx.toAddress,
txId: formattedTx.txHash,
}
return sms.formatSmsReceipt(data, receiptOptions)
})
})
}
function sendSmsReceipt(req, res, next) {
const receiptOptions = _.omit(
['active', 'sms'],
configManager.getReceipt(req.settings.config),
)
buildSms(req.body.data, receiptOptions).then(smsRequest => {
sms
.sendMessage(req.settings, smsRequest)
.then(() => respond(req, res, {}))
.catch(next)
})
}
function getExternalComplianceLink(req, res, next) {
const customerId = req.query.customer
const triggerId = req.query.trigger
const isRetry = req.query.isRetry
if (_.isNil(customerId) || _.isNil(triggerId))
return next(httpError('Not Found', 404))
const settings = req.settings
const triggers = configManager.getTriggers(settings.config)
const trigger = _.find(it => it.id === triggerId)(triggers)
const externalService = trigger.externalService
if (isRetry) {
return externalCompliance
.createLink(settings, externalService, customerId)
.then(url => respond(req, res, { url }))
}
return externalCompliance
.createApplicant(settings, externalService, customerId)
.then(applicant =>
customers.addExternalCompliance(
customerId,
externalService,
applicant.id,
),
)
.then(() =>
externalCompliance.createLink(settings, externalService, customerId),
)
.then(url => respond(req, res, { url }))
}
function addOrUpdateCustomer(
customerData,
deviceId,
config,
isEmailAuth,
cryptoCode,
) {
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 => {
customers.updateLastAuthAttempt(customer.id, deviceId).catch(() => {
logger.info(
'failure updating last auth attempt for customer ',
customer.id,
)
})
return customer
})
.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 }))
})
.then(customer => {
const enableLastUsedAddress = !!configManager.getWalletSettings(
cryptoCode,
config,
).enableLastUsedAddress
if (!cryptoCode || !enableLastUsedAddress) return customer
return customers
.getLastUsedAddress(customer.id, cryptoCode)
.then(lastUsedAddress => {
return { ...customer, lastUsedAddress }
})
})
}
function getOrAddCustomerPhone(req, res, next) {
const deviceId = req.deviceId
const customerData = req.body
const pi = plugins(req.settings, deviceId)
const phone = req.body.phone
const cryptoCode = req.query.cryptoCode
return pi
.getPhoneCode(phone)
.then(code => {
return addOrUpdateCustomer(
customerData,
deviceId,
req.settings.config,
false,
cryptoCode,
).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 deviceId = req.deviceId
const customerData = req.body
const cryptoCode = req.query.cryptoCode
const pi = plugins(req.settings, req.deviceId)
const email = req.body.email
return pi
.getEmailCode(email)
.then(code => {
return addOrUpdateCustomer(
customerData,
deviceId,
req.settings.config,
true,
cryptoCode,
).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/sanctions', triggerSanctions)
router.patch('/:id/block', triggerBlock)
router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
router.post('/:id/smsreceipt', sendSmsReceipt)
router.get('/external', getExternalComplianceLink)
router.post('/phone_code', getOrAddCustomerPhone)
router.post('/email_code', getOrAddCustomerEmail)
module.exports = router