diff --git a/lib/compliance-external.js b/lib/compliance-external.js index 26e6b60f..466b77e9 100644 --- a/lib/compliance-external.js +++ b/lib/compliance-external.js @@ -21,7 +21,7 @@ const getStatus = (settings, service, customerId) => { status })) .catch((error) => { - logger.error(`Error getting applicant for service ${service}:`, error) + if (error.response.status !== 404) logger.error(`Error getting applicant for service ${service}:`, error.message) return { service: service, status: null, @@ -45,7 +45,6 @@ const getStatusMap = (settings, customerExternalCompliance) => { _.uniq )(triggers) - const meta = {} const applicantPromises = _.map(service => { return getStatus(settings, service, customerExternalCompliance) })(services) @@ -53,7 +52,7 @@ const getStatusMap = (settings, customerExternalCompliance) => { return Promise.all(applicantPromises) .then((applicantResults) => { return _.reduce((map, result) => { - map[result.service] = result.status + if (result.status) map[result.service] = result.status return map }, {})(applicantResults) }) @@ -73,16 +72,9 @@ const createLink = (settings, externalService, customerId) => { return plugin.createLink(account, customerId, account.applicantLevel) } -const getApplicantByExternalId = (settings, externalService, customerId) => { - const account = settings.accounts[externalService] - const { plugin } = getPlugin(settings, externalService) - - return plugin.getApplicantByExternalId(account, customerId) -} - module.exports = { getStatusMap, + getStatus, createApplicant, - getApplicantByExternalId, createLink } diff --git a/lib/customers.js b/lib/customers.js index 34ac8020..cb7c588f 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -19,6 +19,8 @@ const settingsLoader = require('./new-settings-loader') const logger = require('./logger') const externalCompliance = require('./compliance-external') +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 @@ -938,13 +940,21 @@ function getExternalComplianceMachine (customer) { return settingsLoader.loadLatest() .then(settings => externalCompliance.getStatusMap(settings, customer.id)) .then(statusMap => { - return updateExternalCompliance(customer.id, statusMap) + return updateExternalComplianceByMap(customer.id, statusMap) .then(() => customer.externalCompliance = statusMap) .then(() => customer) }) } -function updateExternalCompliance(customerId, serviceMap) { +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 @@ -959,12 +969,56 @@ function getExternalCompliance(customer) { FROM customer_external_compliance where customer_id=$1` return db.manyOrNone(sql, [customer.id]) .then(compliance => { - console.log(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 => { + console.log(externals) + const promises = _.map(external => { + return externalCompliance.getStatus(settings, external.service, external.customer_id) + .then(status => { + console.log('status', status, external.customer_id, external.service) + 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]) @@ -995,5 +1049,6 @@ module.exports = { enableTestCustomer, disableTestCustomer, updateLastAuthAttempt, - addExternalCompliance + addExternalCompliance, + checkExternalCompliance } diff --git a/lib/graphql/types.js b/lib/graphql/types.js index 7977e522..26622c37 100644 --- a/lib/graphql/types.js +++ b/lib/graphql/types.js @@ -109,6 +109,7 @@ type Trigger { thresholdDays: Int customInfoRequestId: String customInfoRequest: CustomInfoRequest + externalService: String } type TermsDetails { diff --git a/lib/plugins/compliance/consts.js b/lib/plugins/compliance/consts.js index 3700c1d3..c6291189 100644 --- a/lib/plugins/compliance/consts.js +++ b/lib/plugins/compliance/consts.js @@ -1,5 +1,5 @@ module.exports = { - WAIT: 'WAIT', + PENDING: 'PENDING', RETRY: 'RETRY', APPROVED: 'APPROVED', REJECTED: 'REJECTED' diff --git a/lib/plugins/compliance/mock-compliance/mock-compliance.js b/lib/plugins/compliance/mock-compliance/mock-compliance.js index 366b1861..954b8072 100644 --- a/lib/plugins/compliance/mock-compliance/mock-compliance.js +++ b/lib/plugins/compliance/mock-compliance/mock-compliance.js @@ -1,14 +1,31 @@ +const uuid = require('uuid') + +const {APPROVED} = require('../consts') + const CODE = 'mock-compliance' const createLink = (settings, userId, level) => { return `this is a mock external link, ${userId}, ${level}` } -const getApplicantStatus = (settings, userId) => { +const getApplicantStatus = (account, userId) => { + return Promise.resolve({ + service: CODE, + status: { + level: account.applicantLevel, answer: APPROVED + } + }) +} + +const createApplicant = () => { + return Promise.resolve({ + id: uuid.v4() + }) } module.exports = { CODE, - createLink, - getApplicantStatus + createApplicant, + getApplicantStatus, + createLink } diff --git a/lib/plugins/compliance/sumsub/sumsub.js b/lib/plugins/compliance/sumsub/sumsub.js index a0a42e3b..80e2d2ea 100644 --- a/lib/plugins/compliance/sumsub/sumsub.js +++ b/lib/plugins/compliance/sumsub/sumsub.js @@ -1,7 +1,7 @@ const _ = require('lodash/fp') const sumsubApi = require('./sumsub.api') -const { WAIT, RETRY, APPROVED, REJECTED } = require('../consts') +const { PENDING, RETRY, APPROVED, REJECTED } = require('../consts') const CODE = 'sumsub' @@ -31,17 +31,16 @@ const getApplicantStatus = (account, userId) => { const reviewStatus = _.get('data.review.reviewStatus', r) const reviewAnswer = _.get('data.review.reviewResult.reviewAnswer', r) const reviewRejectType = _.get('data.review.reviewResult.reviewRejectType', r) - const sumsubUserId = _.get('data.review.reviewResult.reviewRejectType', r) // if last review was from a different level, return the current level and RETRY - if (levelName !== account.applicantLevel) return { thirdPartyId: sumsubUserId, level: account.applicantLevel, answer: RETRY } + if (levelName !== account.applicantLevel) return { level: account.applicantLevel, answer: RETRY } - let answer = WAIT + let answer = PENDING if (reviewAnswer === 'GREEN' && reviewStatus === 'completed') answer = APPROVED if (reviewAnswer === 'RED' && reviewRejectType === 'RETRY') answer = RETRY if (reviewAnswer === 'RED' && reviewRejectType === 'FINAL') answer = REJECTED - return { thirdPartyId: sumsubUserId, level: levelName, answer } + return { level: levelName, answer } }) } @@ -49,6 +48,5 @@ module.exports = { CODE, createApplicant, getApplicantStatus, - getApplicantByExternalId, createLink } \ No newline at end of file diff --git a/lib/poller.js b/lib/poller.js index ca002612..3c33c93b 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -6,6 +6,7 @@ const T = require('./time') const logger = require('./logger') const cashOutTx = require('./cash-out/cash-out-tx') const cashInTx = require('./cash-in/cash-in-tx') +const customers = require('./customers') const sanctionsUpdater = require('./ofac/update') const sanctions = require('./ofac/index') const coinAtmRadar = require('./coinatmradar/coinatmradar') @@ -31,6 +32,7 @@ const RADAR_UPDATE_INTERVAL = 5 * T.minutes const PRUNE_MACHINES_HEARTBEAT = 1 * T.day const TRANSACTION_BATCH_LIFECYCLE = 20 * T.minutes const TICKER_RATES_INTERVAL = 59 * T.seconds +const EXTERNAL_COMPLIANCE_INTERVAL = 1 * T.minutes const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds const PENDING_INTERVAL = 10 * T.seconds @@ -127,6 +129,10 @@ function updateCoinAtmRadar () { .then(rates => coinAtmRadar.update(rates, settings())) } +// function checkExternalCompliance (settings) { +// return customers.checkExternalCompliance(settings) +// } + function initializeEachSchema (schemas = ['public']) { // for each schema set "thread variables" and do polling return _.forEach(schema => { @@ -190,6 +196,7 @@ function doPolling (schema) { pi().sweepHd() notifier.checkNotification(pi()) updateCoinAtmRadar() + // checkExternalCompliance(settings()) addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST) addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST) @@ -206,6 +213,7 @@ function doPolling (schema) { addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, schema, QUEUE.SLOW) addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, schema, QUEUE.SLOW) addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, schema, QUEUE.SLOW, settings) + // addToQueue(checkExternalCompliance, EXTERNAL_COMPLIANCE_INTERVAL, schema, QUEUE.SLOW, settings) } function setup (schemasToAdd = [], schemasToRemove = []) { diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js index 75fe6641..0ebb4128 100644 --- a/lib/routes/customerRoutes.js +++ b/lib/routes/customerRoutes.js @@ -238,6 +238,7 @@ function sendSmsReceipt (req, res, 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 @@ -245,6 +246,11 @@ function getExternalComplianceLink (req, res, next) { 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)) diff --git a/migrations/1718464437502-integrate-sumsub.js b/migrations/1718464437502-integrate-sumsub.js index 0e49de6e..e47348c3 100644 --- a/migrations/1718464437502-integrate-sumsub.js +++ b/migrations/1718464437502-integrate-sumsub.js @@ -2,7 +2,7 @@ const db = require('./db') exports.up = function (next) { let sql = [ - `CREATE TYPE EXTERNAL_COMPLIANCE_STATUS AS ENUM('WAIT', 'APPROVED', 'REJECTED', 'RETRY')`, + `CREATE TYPE EXTERNAL_COMPLIANCE_STATUS AS ENUM('PENDING', 'APPROVED', 'REJECTED', 'RETRY')`, `CREATE TABLE CUSTOMER_EXTERNAL_COMPLIANCE ( customer_id UUID NOT NULL REFERENCES customers(id), service TEXT NOT NULL,