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') const { doesTxReuseAddress } = require('../cash-in/cash-in-tx') 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 } }) }) .then(customer => { const { rejectAddressReuse } = configManager.getCompliance(config) if (!rejectAddressReuse || !customer.lastUsedAddress) return customer return doesTxReuseAddress({ toAddress: customer.lastUsedAddress, customerId: customer.id, }).then(isReused => { const newAddress = isReused ? null : customer.lastUsedAddress return { ...customer, lastUsedAddress: newAddress } }) }) } 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