const uuid = require('uuid') const Pgp = require('pg-promise')() const _ = require('lodash/fp') const crypto = require('crypto') const makeDir = require('make-dir') const path = require('path') const fs = require('fs') const util = require('util') const db = require('./db') const complianceOverrides = require('./compliance_overrides') const writeFile = util.promisify(fs.writeFile) const notifierQueries = require('./notifier/queries') const notifierUtils = require('./notifier/utils') const sms = require('./sms') const settingsLoader = require('./new-settings-loader') const logger = require('./logger') const externalCompliance = require('./compliance-external') const { customers: { getCustomerList }, } = require('typesafe-db') const { APPROVED, RETRY } = require('./plugins/compliance/consts') const TX_PASSTHROUGH_ERROR_CODES = [ 'operatorCancel', 'scoreThresholdReached', 'walletScoringError', ] const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR /** * Add new customer * * @name add * @function * * @param {object} Customer object (with phone number) * * @returns {object} Newly created customer */ function add(customer) { const sql = 'insert into customers (id, phone, phone_at) values ($1, $2, now()) returning *' return db.one(sql, [uuid.v4(), customer.phone]).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 * Phone numbers are unique per customer * * @name get * @function * * @param {string} phone Customer's phone number * * @returns {object} Customer */ function get(phone) { const sql = 'select * from customers where phone=$1' return db.oneOrNone(sql, [phone]).then(camelize) } function getWithEmail(email) { const sql = 'select * from customers where email=$1' return db.oneOrNone(sql, [email]).then(camelize) } /** * Update customer record * * @name update * @function * * @param {string} id Customer's id * @param {object} data Fields to update * @param {string} userToken Acting user's token * * @returns {Promise} Newly updated Customer */ function update(id, data, userToken) { const formattedData = _.omit(['id'], _.mapKeys(_.snakeCase, data)) const enhancedUpdateData = enhanceAtFields( enhanceOverrideFields(formattedData, userToken), ) const updateData = updateOverride(enhancedUpdateData) const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') + ' where id=$1 returning *' return db .one(sql, [id]) .then(assignCustomerData) .then(addComplianceOverrides(id, updateData, userToken)) .then(getCustomInfoRequestsData) .then(camelize) } /** * Update customer record * * @name updateCustomer * @function * * @param {string} id Customer's id * @param {object} data Fields to update * * @returns {Promise} Newly updated Customer */ async function updateCustomer(id, data, userToken) { const formattedData = _.pick( [ 'sanctions', 'authorized_override', 'id_card_photo_override', 'id_card_data_override', 'sms_override', 'us_ssn_override', 'sanctions_override', 'front_camera_override', 'suspended_until', 'phone_override', ], _.mapKeys(_.snakeCase, data), ) const enhancedUpdateData = enhanceAtFields( enhanceOverrideFields(formattedData, userToken), ) const updateData = updateOverride(enhancedUpdateData) if (!_.isEmpty(updateData)) { const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') + ' where id=$1' await db.none(sql, [id]) } invalidateCustomerNotifications(id, formattedData) return getCustomerById(id) } /** * Update all customer record * * @name save * @function * * @param {string} id Customer's id * @param {object} data Fields to update * * @returns {Promise} Newly updated Customer */ function edit(id, data, userToken) { const defaults = [ 'front_camera', 'id_card_data', 'id_card_photo', 'us_ssn', 'subscriber_info', 'name', ] const filteredData = _.pick( defaults, _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data)), ) if (_.isEmpty(filteredData)) return getCustomerById(id) const formattedData = enhanceEditedPhotos( enhanceEditedFields(filteredData, userToken), ) const defaultDbData = { customer_id: id, created: new Date(), ...formattedData, } const cs = new Pgp.helpers.ColumnSet(_.keys(defaultDbData), { table: 'edited_customer_data', }) const onConflict = ' ON CONFLICT (customer_id) DO UPDATE SET ' + cs.assignColumns({ from: 'EXCLUDED', skip: ['customer_id', 'created'] }) const upsert = Pgp.helpers.insert(defaultDbData, cs) + onConflict return db.none(upsert).then(getCustomerById(id)) } /** * Add *edited_by and *edited_at fields with acting user's token * and date of override respectively before saving to db. * * @name enhanceEditedFields * @function * * @param {object} fields Fields to be enhanced * @param {string} userToken Acting user's token * @returns {object} fields enhanced with *_by and *_at fields */ function enhanceEditedFields(fields, userToken) { if (!userToken) return fields _.mapKeys(field => { fields[field + '_by'] = userToken fields[field + '_at'] = 'now()^' }, fields) return fields } /** * Add *_path to edited photos fields * * @name enhanceEditedFields * @function * * @param {object} fields Fields to be enhanced * @returns {object} fields enhanced with *_path */ function enhanceEditedPhotos(fields) { return _.mapKeys(field => { if (_.includes(field, ['front_camera', 'id_card_photo'])) { return field + '_path' } return field }, fields) } /** * Remove the edited data from the db record * * @name deleteEditedData * @function * * @param {string} id Customer's id * @param {object} data Fields to be deleted * * @returns {Promise} Newly updated Customer * */ function deleteEditedData(id, data) { // TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION const defaults = [ 'front_camera', 'id_card_data', 'id_card_photo', 'us_ssn', 'subscriber_info', 'name', ] const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, data)) if (_.isEmpty(filteredData)) return getCustomerById(id) const cs = new Pgp.helpers.ColumnSet(_.keys(filteredData), { table: 'edited_customer_data', }) const update = Pgp.helpers.update(filteredData, cs) db.none(update) return getCustomerById(id) } /** * Replace customer's compliance photos * * @name save * @function * * @param {string} id Customer's id * @param {File} photo New photo data * @param {string} photoType Photo's compliance type * * @returns {object} path New photo path * */ async function updateEditedPhoto(id, photo, photoType) { const newPatch = {} const baseDir = photoType === 'frontCamera' ? FRONT_CAMERA_DIR : ID_PHOTO_CARD_DIR const { createReadStream, filename } = photo const stream = createReadStream() const randomString = uuid.v4().toString() + '/' // i.e. ..62ed29c5-f37e-4fb7-95bb-c52d4a3738f7/filename.jpg const rpath = path.join(randomString, filename) // create the directory tree if needed _.attempt(() => makeDir.sync(path.join(baseDir, randomString))) // i.e. ..//idphotocard/62ed29c5-f37e-4fb7-95bb-c52d4a3738f7/filename.jpg const pathName = path.join(baseDir, rpath) await stream.pipe(fs.createWriteStream(pathName)) newPatch[photoType] = rpath return newPatch } const invalidateCustomerNotifications = (id, data) => { if (data.authorized_override !== 'verified') return Promise.resolve() const detailB = notifierUtils.buildDetail({ code: 'BLOCKED', customerId: id }) return notifierQueries.invalidateNotification(detailB, 'compliance') } /** * Get customer by id * * @name getById * @function * * @param {string} id Customer's unique id * @param {string} userToken Acting user's token * * @returns {object} Customer found * * Used for the machine. */ function getById(id) { const sql = 'select * from customers where id=$1' return db .oneOrNone(sql, [id]) .then(assignCustomerData) .then(getCustomInfoRequestsData) .then(getExternalComplianceMachine) .then(camelize) } /** * Camelize customer fields * Note: return null if customer is undefined * * @name camelize * @function * * @param {object} customer Customer with snake_case fields * @returns {object} Camelized Customer object */ function camelize(customer) { return customer ? _.mapKeys(_.camelCase, customer) : null } function camelizeDeep(customer) { return _.flow(camelize, it => ({ ...it, notes: (it.notes ?? []).map(camelize), externalCompliance: (it.externalCompliance ?? []).map(camelize), }))(customer) } /** * Get all available complianceTypes * that can be overridden (excluding hard_limit) * * @name getComplianceTypes * @function * * @returns {array} Array of compliance types' names */ function getComplianceTypes() { return [ 'sms', 'email', 'id_card_data', 'id_card_photo', 'front_camera', 'sanctions', 'authorized', 'us_ssn', ] } function updateOverride(fields) { const updateableFields = [ 'id_card_data', 'id_card_photo_path', 'front_camera_path', 'authorized', 'us_ssn', ] const removePathSuffix = _.map(_.replace('_path', '')) const getPairs = _.map(f => [`${f}_override`, 'automatic']) const updatedFields = _.intersection(updateableFields, _.keys(fields)) const overrideFields = _.compose( _.fromPairs, getPairs, removePathSuffix, )(updatedFields) return _.merge(fields, overrideFields) } function enhanceAtFields(fields) { const updateableFields = [ 'id_card_data', 'id_card_photo', 'front_camera', 'sanctions', 'authorized', 'us_ssn', ] const updatedFields = _.intersection(updateableFields, _.keys(fields)) const atFields = _.fromPairs(_.map(f => [`${f}_at`, 'now()^'], updatedFields)) return _.merge(fields, atFields) } /** * Add *override_by and *override_at fields with acting user's token * and date of override respectively before saving to db. * * @name enhanceOverrideFields * @function * * @param {object} fields Override fields to be enhanced * @param {string} userToken Acting user's token * @returns {object} fields enhanced with *_by and *_at fields */ function enhanceOverrideFields(fields, userToken) { if (!userToken) return fields // Populate with computedFields (user who overrode and overridden timestamps date) return _.reduce( _.assign, fields, _.map(type => { return fields[type + '_override'] ? { [type + '_override_by']: userToken, [type + '_override_at']: 'now()^', } : {} }, getComplianceTypes()), ) } /** * Save new compliance override records * * Take the override fields that are modified in customer and create * a compliance override record in db for each compliance type. * * @name addComplianceOverrides * @function * * @param {string} id Customer's id * @param {object} customer Customer that is updating * @param {string} userToken Acting user's token * * @returns {promise} Result from compliance_overrides creation */ function addComplianceOverrides(id, customer, userToken) { // Prepare compliance overrides to save const overrides = _.map(field => { const complianceName = field + '_override' return customer[complianceName] ? { customerId: id, complianceType: field, overrideBy: userToken, verification: customer[complianceName], } : null }, getComplianceTypes()) // Save all the updated override fields return Promise.all(_.map(complianceOverrides.add, _.compact(overrides))).then( () => customer, ) } function getSlimCustomerByIdBatch(ids) { const sql = `SELECT id, phone, id_card_data FROM customers WHERE id = ANY($1::uuid[])` return db.any(sql, [ids]).then(customers => _.map(camelize, customers)) } function getCustomersList() { return getCustomerList({ withCustomInfoRequest: true }) } /** * Query a specific customer, ordered by last activity * and with aggregate columns based on their * transactions * * @returns {array} A single customer instance with non edited * * Used for the server. */ 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_path, front_camera_at, front_camera_override, 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, sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_auth_attempt, last_data_provided) 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, last_used_machine FROM ( SELECT c.id, c.authorized_override, greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended, 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.suspended_until > now() AS is_suspended, c.front_camera_path, c.front_camera_override, c.front_camera_at, c.last_auth_attempt, c.last_used_machine, 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.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, 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 OR batched = 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 LEFT OUTER JOIN ( SELECT customer_id, coalesce(json_agg(customer_notes.*), '[]'::json) AS notes FROM customer_notes GROUP BY customer_notes.customer_id ) cn ON c.id = cn.customer_id WHERE c.id = $2 ) AS cl WHERE rn = 1` return db .oneOrNone(sql, [passableErrorCodes, id]) .then(assignCustomerData) .then(getCustomInfoRequestsData) .then(getExternalCompliance) .then(camelizeDeep) .then(formatSubscriberInfo) } function assignCustomerData(customer) { return getEditedData(customer.id).then(customerEditedData => selectLatestData(customer, customerEditedData), ) } function formatSubscriberInfo(customer) { const subscriberInfo = customer.subscriberInfo if (!subscriberInfo) return customer const result = subscriberInfo.result if (_.isEmpty(result)) return _.omit(['subscriberInfo'], customer) const name = _.get('belongs_to.name')(result) const street = _.get('current_addresses[0].street_line_1')(result) const city = _.get('current_addresses[0].city')(result) const stateCode = _.get('current_addresses[0].state_code')(result) const postalCode = _.get('current_addresses[0].postal_code')(result) customer.subscriberInfo = { name, address: `${street ?? ''} ${city ?? ''}${street || city ? ',' : ''} ${stateCode ?? ''} ${postalCode ?? ''}`, } return customer } /** * Query the specific customer manually edited data * * @param {String} id customer id * * @returns {array} A single customer instance with the most recent edited data */ function getEditedData(id) { const sql = `SELECT * FROM edited_customer_data WHERE customer_id = $1` return db.oneOrNone(sql, [id]).then(_.omitBy(_.isNil)) } function selectLatestData(customerData, customerEditedData) { const defaults = [ 'front_camera', 'id_card_data', 'id_card_photo', 'us_ssn', 'subscriber_info', 'name', ] _.map(field => { const atField = field + '_at' const byField = field + '_by' if (_.includes(field, ['front_camera', 'id_card_photo'])) field = field + '_path' if (!_.has(field, customerData) || !_.has(field, customerEditedData)) return if (customerData[atField] < customerEditedData[atField]) { customerData[field] = customerEditedData[field] customerData[atField] = customerEditedData[atField] customerData[byField] = customerEditedData[byField] } }, defaults) return customerData } /** * @param {String} id customer id * @param {Object} patch customer update record * @returns {Promise} new patch to be applied */ function updatePhotoCard(id, patch) { return Promise.resolve(patch).then(patch => { // Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ.. const imageData = _.get('idCardPhotoData', patch) if (_.isEmpty(imageData)) { return patch } // remove idCardPhotoData from the update record const newPatch = _.omit('idCardPhotoData', patch) // decode the base64 string to binary data const decodedImageData = Buffer.from(imageData, 'base64') // workout the image hash // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 const hash = crypto.createHash('sha256').update(imageData).digest('hex') // workout the image folder // i.e. 24/0e/85 const rpath = _.join( path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))), ) // i.e. ..//idphotocard/24/0e/85 const dirname = path.join(ID_PHOTO_CARD_DIR, rpath) // create the directory tree if needed _.attempt(() => makeDir.sync(dirname)) // i.e. ..//idphotocard/24/0e/85/240e85ff2e4bb931f235985dd01....jpg const filename = path.join(dirname, hash + '.jpg') // update db record patch // i.e. { // "idCardPhotoPath": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", // "idCardPhotoAt": "now()" // } newPatch.idCardPhotoPath = path.join(rpath, hash + '.jpg') newPatch.idCardPhotoAt = 'now()' // write image file return writeFile(filename, decodedImageData).then(() => newPatch) }) } /** * @param {String} imageData image encoded * @param {String} directory directory path of id card data for a certain user */ function updatePhotos(imagesData, id, dir) { return Promise.resolve(imagesData).then(imagesData => { const newPatch = {} if (_.isEmpty(imagesData)) { return newPatch } // i.e. ..////idcarddata const dirname = path.join(dir) // create the directory tree if needed _.attempt(() => makeDir.sync(dirname)) const promises = imagesData.map((imageData, index) => { // decode the base64 string to binary data const decodedImageData = Buffer.from(imageData, 'base64') // i.e. ..////idcarddata/1.jpg const filename = path.join(dirname, index + '.jpg') return writeFile(filename, decodedImageData) }) return Promise.all(promises).then(() => { newPatch.idCardData = path.join(dirname) newPatch.idCardDataAt = 'now()' return newPatch }) }) } /** * @param {String} id customer id * @param {Object} patch customer latest id card photos * @returns {Promise} new patch to be applied */ function updateIdCardData(patch, id) { /* TODO: fetch operator id */ const operatorId = 'id-operator' const directory = `${OPERATOR_DATA_DIR}/${operatorId}/${id}/` return Promise.resolve(patch).then(patch => { const imagesData = _.get('photos', patch) return updatePhotos(imagesData, id, directory).catch(err => logger.error('while saving the image: ', err), ) }) } /** * @param {String} imageData customer t&c photo data * @returns {Promise} new patch to be applied */ function updateTxCustomerPhoto(imageData) { return Promise.resolve(imageData).then(imageData => { const newPatch = {} const directory = `${OPERATOR_DATA_DIR}/customersphotos` if (_.isEmpty(imageData)) { return } // decode the base64 string to binary data const decodedImageData = Buffer.from(imageData, 'base64') // workout the image hash // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 const hash = crypto.createHash('sha256').update(imageData).digest('hex') // workout the image folder // i.e. 24/0e/85 const rpath = _.join( path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))), ) // i.e. ..///customersphotos/24/0e/85 const dirname = path.join(directory, rpath) // create the directory tree if needed _.attempt(() => makeDir.sync(dirname)) // i.e. ..///customersphotos/24/0e/85/240e85ff2e4bb931f235985dd01....jpg const filename = path.join(dirname, hash + '.jpg') // update db record patch // i.e. { // "idCustomerTxPhoto": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", // "idCustomerTxPhotoAt": "now()" // } newPatch.txCustomerPhotoPath = path.join(rpath, hash + '.jpg') newPatch.txCustomerPhotoAt = 'now()' // write image file return writeFile(filename, decodedImageData).then(() => newPatch) }) } function updateFrontCamera(id, patch) { return Promise.resolve(patch).then(patch => { // Base64 encoded image /9j/4AAQSkZJRgABAQAAAQ.. const imageData = _.get('frontCameraData', patch) if (_.isEmpty(imageData)) { return patch } // remove idCardPhotoData from the update record const newPatch = _.omit('frontCameraData', patch) // decode the base64 string to binary data const decodedImageData = Buffer.from(imageData, 'base64') // workout the image hash // i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50 const hash = crypto.createHash('sha256').update(imageData).digest('hex') // workout the image folder // i.e. 24/0e/85 const rpath = _.join( path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))), ) // i.e. ..//idphotocard/24/0e/85 const dirname = path.join(FRONT_CAMERA_DIR, rpath) // create the directory tree if needed _.attempt(() => makeDir.sync(dirname)) // i.e. ..//idphotocard/24/0e/85/240e85ff2e4bb931f235985dd01....jpg const filename = path.join(dirname, hash + '.jpg') // update db record patch // i.e. { // "idCardPhotoPath": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg", // "idCardPhotoAt": "now()" // } newPatch.frontCameraPath = path.join(rpath, hash + '.jpg') newPatch.frontCameraAt = 'now()' // write image file return writeFile(filename, decodedImageData).then(() => newPatch) }) } function addCustomField(customerId, label, value) { const sql = `SELECT * FROM custom_field_definitions WHERE label=$1 LIMIT 1` return db .oneOrNone(sql, [label]) .then(res => db.tx(t => { if (_.isNil(res)) { const fieldId = uuid.v4() const q1 = t.none( `INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, [fieldId, label], ) const q2 = t.none( `INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, fieldId, value], ) return t.batch([q1, q2]) } if (!_.isNil(res) && !res.active) { const q1 = t.none( `UPDATE custom_field_definitions SET active = true WHERE id=$1`, [res.id], ) const q2 = t.none( `INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value], ) return t.batch([q1, q2]) } else if (!_.isNil(res) && res.active) { const q1 = t.none( `INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value], ) return t.batch([q1]) } }), ) .then(res => !_.isNil(res)) } function saveCustomField(customerId, fieldId, newValue) { const sql = `UPDATE customer_custom_field_pairs SET value=$1 WHERE customer_id=$2 AND custom_field_id=$3` return db.none(sql, [newValue, customerId, fieldId]) } function removeCustomField(customerId, fieldId) { const sql = `SELECT * FROM customer_custom_field_pairs WHERE custom_field_id=$1` return db.any(sql, [fieldId]).then(res => db.tx(t => { // Is the field to be removed the only one of its kind in the pairs table? if (_.size(res) === 1) { const q1 = t.none( `DELETE FROM customer_custom_field_pairs WHERE customer_id=$1 AND custom_field_id=$2`, [customerId, fieldId], ) const q2 = t.none( `UPDATE custom_field_definitions SET active = false WHERE id=$1`, [fieldId], ) return t.batch([q1, q2]) } else { const q1 = t.none( `DELETE FROM customer_custom_field_pairs WHERE customer_id=$1 AND custom_field_id=$2`, [customerId, fieldId], ) return t.batch([q1]) } }), ) } function getCustomInfoRequestsData(customer) { if (!customer) return const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1` return db .any(sql, [customer.id]) .then(res => _.set('custom_info_request_data', res, customer)) } function enableTestCustomer(customerId) { const sql = `UPDATE customers SET is_test_customer=true WHERE id=$1` return db.none(sql, [customerId]) } function disableTestCustomer(customerId) { const sql = `UPDATE customers SET is_test_customer=false WHERE id=$1` return db.none(sql, [customerId]) } function updateLastAuthAttempt(customerId, deviceId) { const sql = `UPDATE customers SET last_auth_attempt=NOW(), last_used_machine=$2 WHERE id=$1` return db.none(sql, [customerId, deviceId]) } function getExternalComplianceMachine(customer) { return settingsLoader .loadLatest() .then(settings => externalCompliance.getStatusMap(settings, customer.id)) .then(statusMap => { return updateExternalComplianceByMap(customer.id, statusMap) .then(() => (customer.externalCompliance = statusMap)) .then(() => customer) }) } function updateExternalCompliance(customerId, service, status) { const sql = ` UPDATE customer_external_compliance SET last_known_status = $1, last_updated = now() WHERE customer_id=$2 AND service=$3 ` return db.none(sql, [status, customerId, service]) } function updateExternalComplianceByMap(customerId, serviceMap) { const sql = ` UPDATE customer_external_compliance SET last_known_status = $1, last_updated = now() WHERE customer_id=$2 AND service=$3 ` const pairs = _.toPairs(serviceMap) const promises = _.map(([service, status]) => db.none(sql, [status.answer, customerId, service]), )(pairs) return Promise.all(promises) } function getExternalCompliance(customer) { const sql = `SELECT external_id, service, last_known_status, last_updated FROM customer_external_compliance where customer_id=$1` return db .manyOrNone(sql, [customer.id]) .then(compliance => { customer.externalCompliance = compliance }) .then(() => customer) } function getOpenExternalCompliance() { const sql = `SELECT customer_id, service, last_known_status FROM customer_external_compliance where last_known_status in ('PENDING', 'RETRY') or last_known_status is null` return db.manyOrNone(sql) } function notifyRetryExternalCompliance(settings, customerId, service) { const sql = 'SELECT phone FROM customers WHERE id=$1' const promises = [ db.one(sql, [customerId]), externalCompliance.createLink(settings, service, customerId), ] return Promise.all(promises).then(([toNumber, link]) => { const body = `Your external compliance verification has failed. Please try again. Link for retry: ${link}` return sms.sendMessage(settings, { toNumber, body }) }) } function notifyApprovedExternalCompliance(settings, customerId) { const sql = 'SELECT phone FROM customers WHERE id=$1' return db.one(sql, [customerId]).then(toNumber => { const body = 'Your external compliance verification has been approved.' return sms.sendMessage(settings, { toNumber, body }) }) } function checkExternalCompliance(settings) { return getOpenExternalCompliance().then(externals => { const promises = _.map(external => { return externalCompliance .getStatus(settings, external.service, external.customer_id) .then(status => { if (status.status.answer === RETRY) notifyRetryExternalCompliance( settings, external.customer_id, status.service, ) if (status.status.answer === APPROVED) notifyApprovedExternalCompliance(settings, external.customer_id) return updateExternalCompliance( external.customer_id, external.service, status.status.answer, ) }) }, externals) return Promise.all(promises) }) } function addExternalCompliance(customerId, service, id) { const sql = `INSERT INTO customer_external_compliance (customer_id, external_id, service) VALUES ($1, $2, $3)` return db.none(sql, [customerId, id, service]) } function getLastUsedAddress(id, cryptoCode) { const sql = `SELECT to_address FROM cash_in_txs WHERE customer_id=$1 AND crypto_code=$2 AND fiat > 0 ORDER BY created DESC LIMIT 1` return db.oneOrNone(sql, [id, cryptoCode]).then(it => it?.to_address) } module.exports = { add, addWithEmail, get, getWithEmail, getSlimCustomerByIdBatch, getCustomersList, getCustomerById, getById, update, updateCustomer, updatePhotoCard, updateFrontCamera, updateIdCardData, addCustomField, saveCustomField, removeCustomField, edit, deleteEditedData, updateEditedPhoto, updateTxCustomerPhoto, enableTestCustomer, disableTestCustomer, updateLastAuthAttempt, addExternalCompliance, checkExternalCompliance, getLastUsedAddress, }