diff --git a/lib/compliance-external.js b/lib/compliance-external.js
index 87436f8f..e8e817c9 100644
--- a/lib/compliance-external.js
+++ b/lib/compliance-external.js
@@ -1,55 +1,88 @@
const _ = require('lodash/fp')
+const logger = require('./logger')
const configManager = require('./new-config-manager')
const ph = require('./plugin-helper')
-const getPlugin = settings => {
- const pluginCodes = ['sumsub']
- const enabledAccounts = _.filter(_plugin => _plugin.enabled, _.map(code => ph.getAccountInstance(settings.accounts[code], code), pluginCodes))
- if (_.isEmpty(enabledAccounts)) {
- throw new Error('No external compliance services are active. Please check your 3rd party service configuration')
- }
-
- if (_.size(enabledAccounts) > 1) {
- throw new Error('Multiple external compliance services are active. Please check your 3rd party service configuration')
- }
- const account = _.head(enabledAccounts)
- const plugin = ph.load(ph.COMPLIANCE, account.code, account.enabled)
+const getPlugin = (settings, pluginCode) => {
+ const account = settings.accounts[pluginCode]
+ const plugin = ph.load(ph.COMPLIANCE, pluginCode)
return ({ plugin, account })
}
-const createApplicant = (settings, customer, applicantLevel) => {
- const { plugin } = getPlugin(settings)
- const { id } = customer
- return plugin.createApplicant({ levelName: applicantLevel, externalUserId: id })
-}
-
-const getApplicant = (settings, customer) => {
+const getStatus = (settings, service, customerId) => {
try {
- const { plugin } = getPlugin(settings)
- const { id } = customer
- return plugin.getApplicant({ externalUserId: id }, false)
- .then(res => ({
- provider: plugin.CODE,
- ...res.data
+ const { plugin, account } = getPlugin(settings, service)
+
+ return plugin.getApplicantStatus(account, customerId)
+ .then((status) => ({
+ service,
+ status
}))
- .catch(() => ({}))
- } catch (e) {
- return {}
+ .catch((error) => {
+ logger.error(`Error getting applicant for service ${service}:`, error)
+ return {
+ service: service,
+ status: null,
+ }
+ })
+ } catch (error) {
+ logger.error(`Error loading plugin for service ${service}:`, error)
+ return Promise.resolve({
+ service: service,
+ status: null,
+ })
}
+
}
-const createApplicantExternalLink = (settings, customerId, triggerId) => {
+const getStatusMap = (settings, customerExternalCompliance) => {
const triggers = configManager.getTriggers(settings.config)
- const trigger = _.find(it => it.id === triggerId)(triggers)
- const { plugin } = getPlugin(settings)
- return plugin.createApplicantExternalLink({ levelName: trigger.externalServiceApplicantLevel, userId: customerId })
- .then(r => r.data.url)
+ const services = _.flow(
+ _.map('externalService'),
+ _.compact,
+ _.uniq
+ )(triggers)
+
+ const applicantPromises = _.map(service => {
+ return getStatus(settings, service, customerExternalCompliance)
+ })(services)
+
+ return Promise.all(applicantPromises)
+ .then((applicantResults) => {
+ return _.reduce((map, result) => {
+ map[result.service] = result.status
+ return map
+ }, {})(applicantResults)
+ })
+}
+
+const createApplicant = (settings, externalService, customerId) => {
+ const account = settings.accounts[externalService]
+ const { plugin } = getPlugin(settings, externalService)
+
+ return plugin.createApplicant(account, customerId, account.applicantLevel)
+}
+
+const createLink = (settings, externalService, customerId) => {
+ const account = settings.accounts[externalService]
+ const { plugin } = getPlugin(settings, externalService)
+
+ 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,
- getApplicant,
- createApplicantExternalLink
+ getApplicantByExternalId,
+ createLink
}
diff --git a/lib/customers.js b/lib/customers.js
index f605c269..164bf807 100644
--- a/lib/customers.js
+++ b/lib/customers.js
@@ -932,13 +932,30 @@ function updateLastAuthAttempt (customerId) {
function getExternalCustomerData (customer) {
return settingsLoader.loadLatest()
- .then(settings => externalCompliance.getApplicant(settings, customer))
- .then(externalCompliance => {
- customer.externalCompliance = externalCompliance
- return customer
+ .then(settings => externalCompliance.getStatusMap(settings, customer.id))
+ .then(statusMap => {
+ return updateExternalCompliance(customer.id, statusMap)
+ .then(() => customer.externalCompliance = statusMap)
+ .then(() => customer)
})
}
+function updateExternalCompliance(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 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])
+}
+
+
module.exports = {
add,
addWithEmail,
@@ -962,5 +979,6 @@ module.exports = {
updateTxCustomerPhoto,
enableTestCustomer,
disableTestCustomer,
- updateLastAuthAttempt
+ updateLastAuthAttempt,
+ addExternalCompliance
}
diff --git a/lib/graphql/types.js b/lib/graphql/types.js
index 8d086151..7977e522 100644
--- a/lib/graphql/types.js
+++ b/lib/graphql/types.js
@@ -82,7 +82,6 @@ type CustomInput {
constraintType: String!
label1: String
label2: String
- label3: String
choiceList: [String]
}
diff --git a/lib/new-admin/config/accounts.js b/lib/new-admin/config/accounts.js
index 0041ba93..8bbb15e8 100644
--- a/lib/new-admin/config/accounts.js
+++ b/lib/new-admin/config/accounts.js
@@ -15,6 +15,7 @@ const ID_VERIFIER = 'idVerifier'
const EMAIL = 'email'
const ZERO_CONF = 'zeroConf'
const WALLET_SCORING = 'wallet_scoring'
+const COMPLIANCE = 'compliance'
const ALL_ACCOUNTS = [
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
@@ -60,7 +61,9 @@ const ALL_ACCOUNTS = [
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDT_TRON, TRX] },
- { code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true }
+ { code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
+ { code: 'sumsub', display: 'Sumsub', class: COMPLIANCE },
+ { code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true },
]
const devMode = require('minimist')(process.argv.slice(2)).dev
diff --git a/lib/new-admin/graphql/resolvers/externalCompliance.resolver.js b/lib/new-admin/graphql/resolvers/externalCompliance.resolver.js
deleted file mode 100644
index c25afdc7..00000000
--- a/lib/new-admin/graphql/resolvers/externalCompliance.resolver.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const externalCompliance = require('../../../compliance-external')
-const { loadLatest } = require('../../../new-settings-loader')
-
-const resolvers = {
- Query: {
- getApplicantExternalLink: (...[, { customerId, triggerId }]) => loadLatest()
- .then(settings => externalCompliance.createApplicantExternalLink(settings, customerId, triggerId))
- }
-}
-
-module.exports = resolvers
diff --git a/lib/new-admin/graphql/resolvers/index.js b/lib/new-admin/graphql/resolvers/index.js
index f10e10cc..81e79614 100644
--- a/lib/new-admin/graphql/resolvers/index.js
+++ b/lib/new-admin/graphql/resolvers/index.js
@@ -7,7 +7,6 @@ const config = require('./config.resolver')
const currency = require('./currency.resolver')
const customer = require('./customer.resolver')
const customInfoRequests = require('./customInfoRequests.resolver')
-const externalCompliance = require('./externalCompliance.resolver')
const funding = require('./funding.resolver')
const log = require('./log.resolver')
const loyalty = require('./loyalty.resolver')
@@ -31,7 +30,6 @@ const resolvers = [
currency,
customer,
customInfoRequests,
- externalCompliance,
funding,
log,
loyalty,
diff --git a/lib/new-admin/graphql/types/externalCompliance.type.js b/lib/new-admin/graphql/types/externalCompliance.type.js
deleted file mode 100644
index f05f3153..00000000
--- a/lib/new-admin/graphql/types/externalCompliance.type.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const { gql } = require('apollo-server-express')
-
-const typeDef = gql`
- type Query {
- getApplicantExternalLink(customerId: ID, triggerId: ID): String
- }
-`
-
-module.exports = typeDef
diff --git a/lib/new-admin/graphql/types/index.js b/lib/new-admin/graphql/types/index.js
index 11bfee20..a1886a28 100644
--- a/lib/new-admin/graphql/types/index.js
+++ b/lib/new-admin/graphql/types/index.js
@@ -7,7 +7,6 @@ const config = require('./config.type')
const currency = require('./currency.type')
const customer = require('./customer.type')
const customInfoRequests = require('./customInfoRequests.type')
-const externalCompliance = require('./externalCompliance.type')
const funding = require('./funding.type')
const log = require('./log.type')
const loyalty = require('./loyalty.type')
@@ -31,7 +30,6 @@ const types = [
currency,
customer,
customInfoRequests,
- externalCompliance,
funding,
log,
loyalty,
diff --git a/lib/plugins/compliance/consts.js b/lib/plugins/compliance/consts.js
new file mode 100644
index 00000000..3700c1d3
--- /dev/null
+++ b/lib/plugins/compliance/consts.js
@@ -0,0 +1,6 @@
+module.exports = {
+ WAIT: 'WAIT',
+ RETRY: 'RETRY',
+ APPROVED: 'APPROVED',
+ REJECTED: 'REJECTED'
+}
\ No newline at end of file
diff --git a/lib/plugins/compliance/mock-compliance/mock-compliance.js b/lib/plugins/compliance/mock-compliance/mock-compliance.js
new file mode 100644
index 00000000..366b1861
--- /dev/null
+++ b/lib/plugins/compliance/mock-compliance/mock-compliance.js
@@ -0,0 +1,14 @@
+const CODE = 'mock-compliance'
+
+const createLink = (settings, userId, level) => {
+ return `this is a mock external link, ${userId}, ${level}`
+}
+
+const getApplicantStatus = (settings, userId) => {
+}
+
+module.exports = {
+ CODE,
+ createLink,
+ getApplicantStatus
+}
diff --git a/lib/plugins/compliance/sumsub/request.js b/lib/plugins/compliance/sumsub/request.js
index 588acc23..f102f996 100644
--- a/lib/plugins/compliance/sumsub/request.js
+++ b/lib/plugins/compliance/sumsub/request.js
@@ -2,40 +2,33 @@ const axios = require('axios')
const crypto = require('crypto')
const _ = require('lodash/fp')
const FormData = require('form-data')
-const settingsLoader = require('../../../new-settings-loader')
-
-const ph = require('../../../plugin-helper')
const axiosConfig = {
baseURL: 'https://api.sumsub.com'
}
-const axiosInstance = axios.create(axiosConfig)
+const getSigBuilder = (apiToken, secretKey) => config => {
+ const timestamp = Math.floor(Date.now() / 1000)
+ const signature = crypto.createHmac('sha256', secretKey)
-const buildSignature = config => {
- return settingsLoader.loadLatest()
- .then(({ accounts }) => ph.getAccountInstance(accounts.sumsub, 'sumsub'))
- .then(({ secretKey, apiToken }) => {
- const timestamp = Math.floor(Date.now() / 1000)
- const signature = crypto.createHmac('sha256', secretKey)
-
- signature.update(`${timestamp}${_.toUpper(config.method)}${config.url}`)
- if (config.data instanceof FormData) {
- signature.update(config.data.getBuffer())
- } else if (config.data) {
- signature.update(config.data)
- }
+ signature.update(`${timestamp}${_.toUpper(config.method)}${config.url}`)
+ if (config.data instanceof FormData) {
+ signature.update(config.data.getBuffer())
+ } else if (config.data) {
+ signature.update(JSON.stringify(config.data))
+ }
- config.headers['X-App-Token'] = apiToken
- config.headers['X-App-Access-Sig'] = signature.digest('hex')
- config.headers['X-App-Access-Ts'] = timestamp
+ config.headers['X-App-Token'] = apiToken
+ config.headers['X-App-Access-Sig'] = signature.digest('hex')
+ config.headers['X-App-Access-Ts'] = timestamp
- return config
- })
+ return config
}
-axiosInstance.interceptors.request.use(buildSignature, Promise.reject)
-
-const request = config => axiosInstance(config)
+const request = ((account, config) => {
+ const instance = axios.create(axiosConfig)
+ instance.interceptors.request.use(getSigBuilder(account.apiToken, account.secretKey), Promise.reject)
+ return instance(config)
+})
module.exports = request
diff --git a/lib/plugins/compliance/sumsub/sumsub.api.js b/lib/plugins/compliance/sumsub/sumsub.api.js
new file mode 100644
index 00000000..f611678f
--- /dev/null
+++ b/lib/plugins/compliance/sumsub/sumsub.api.js
@@ -0,0 +1,99 @@
+const request = require('./request')
+
+const createApplicant = (account, userId, level) => {
+ if (!userId || !level) {
+ return Promise.reject(`Missing required fields: userId: ${userId}, level: ${level}`)
+ }
+
+ const config = {
+ method: 'POST',
+ url: `/resources/applicants?levelName=${level}`,
+ data: {
+ externalUserId: userId,
+ sourceKey: 'lamassu'
+ },
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+const createLink = (account, userId, level) => {
+ if (!userId || !level) {
+ return Promise.reject(`Missing required fields: userId: ${userId}, level: ${level}`)
+ }
+
+ const config = {
+ method: 'POST',
+ url: `/resources/sdkIntegrations/levels/${level}/websdkLink?ttlInSecs=${600}&externalUserId=${userId}`,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+const getApplicantByExternalId = (account, id) => {
+ console.log('id', id)
+ if (!id) {
+ return Promise.reject('Missing required fields: id')
+ }
+
+ const config = {
+ method: 'GET',
+ url: `/resources/applicants/-;externalUserId=${id}/one`,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+const getApplicantStatus = (account, id) => {
+ if (!id) {
+ return Promise.reject(`Missing required fields: id`)
+ }
+
+ const config = {
+ method: 'GET',
+ url: `/resources/applicants/${id}/status`,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+const getApplicantById = (account, id) => {
+ if (!id) {
+ return Promise.reject(`Missing required fields: id`)
+ }
+
+ const config = {
+ method: 'GET',
+ url: `/resources/applicants/${id}/one`,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+module.exports = {
+ createLink,
+ createApplicant,
+ getApplicantByExternalId,
+ getApplicantById,
+ getApplicantStatus
+}
diff --git a/lib/plugins/compliance/sumsub/sumsub.js b/lib/plugins/compliance/sumsub/sumsub.js
index 0fdd9838..8f036f97 100644
--- a/lib/plugins/compliance/sumsub/sumsub.js
+++ b/lib/plugins/compliance/sumsub/sumsub.js
@@ -1,462 +1,55 @@
const _ = require('lodash/fp')
-const request = require('./request')
+const sumsubApi = require('./sumsub.api')
+const { WAIT, RETRY, APPROVED, REJECTED } = require('../consts')
const CODE = 'sumsub'
-const hasRequiredFields = fields => obj => _.every(_.partial(_.has, [_, obj]), fields)
-const getMissingRequiredFields = (fields, obj) =>
- _.reduce(
- (acc, value) => {
- if (!_.has(value, obj)) {
- acc.push(value)
- }
- return acc
- },
- [],
- fields
- )
-
-const createApplicantExternalLink = opts => {
- const REQUIRED_FIELDS = ['userId', 'levelName']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/sdkIntegrations/levels/${opts.levelName}/websdkLink?ttlInSecs=${600}&externalUserId=${opts.userId}`,
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json'
- }
- })
+const getApplicantByExternalId = (account, userId) => {
+ return sumsubApi.getApplicantByExternalId(account, userId)
+ .then(r => r.data)
}
-const createApplicant = opts => {
- const REQUIRED_FIELDS = ['levelName', 'externalUserId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants?levelName=${opts.levelName}`,
- headers: {
- 'Content-Type': 'application/json'
- },
- data: {
- externalUserId: opts.externalUserId
- }
- })
+const createApplicant = (account, userId, level) => {
+ return sumsubApi.createApplicant(account, userId, level)
+ .then(r => r.data)
+ .catch(err => {
+ if (err.response.status === 409) return getApplicantByExternalId(account, userId)
+ throw err
+ })
}
-const changeRequiredLevel = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'levelName']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/moveToLevel?name=${opts.levelName}`,
- headers: {
- 'Content-Type': 'application/json'
- }
- })
+const createLink = (account, userId, level) => {
+ return sumsubApi.createLink(account, userId, level)
+ .then(r => r.data.url)
}
-const getApplicant = (opts, knowsApplicantId = true) => {
- const REQUIRED_FIELDS = knowsApplicantId
- ? ['applicantId']
- : ['externalUserId']
+const getApplicantStatus = (account, userId) => {
+ return sumsubApi.getApplicantByExternalId(account, userId)
+ .then(r => {
+ const levelName = _.get('data.review.levelName', r)
+ const reviewStatus = _.get('data.review.reviewStatus', r)
+ const reviewAnswer = _.get('data.review.reviewResult.reviewAnswer', r)
+ const reviewRejectType = _.get('data.review.reviewResult.reviewRejectType', r)
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
+ console.log('levelName', levelName)
+ console.log('reviewStatus', reviewStatus)
+ console.log('reviewAnswer', reviewAnswer)
+ console.log('reviewRejectType', reviewRejectType)
- return request({
- method: 'GET',
- url: knowsApplicantId ? `/resources/applicants/${opts.applicantId}/one` : `/resources/applicants/-;externalUserId=${opts.externalUserId}/one`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
+ let answer = WAIT
+ if (reviewAnswer === 'GREEN') answer = APPROVED
+ if (reviewAnswer === 'RED' && reviewRejectType === 'RETRY') answer = RETRY
+ if (reviewAnswer === 'RED' && reviewRejectType === 'FINAL') answer = REJECTED
-const getApplicantStatus = opts => {
- const REQUIRED_FIELDS = ['applicantId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'GET',
- url: `/resources/applicants/${opts.applicantId}/status`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const getApplicantIdDocsStatus = opts => {
- const REQUIRED_FIELDS = ['applicantId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'GET',
- url: `/resources/applicants/${opts.applicantId}/requiredIdDocsStatus`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const addIdDocument = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'metadata', 'metadata.idDocType', 'metadata.country']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- const form = new FormData()
- form.append('metadata', opts.metadata)
- form.append('content', opts.content)
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/info/idDoc`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'multipart/form-data',
- 'X-Return-Doc-Warnings': 'true'
- },
- data: form
- })
-}
-
-const changeApplicantFixedInfo = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'newData']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'PATCH',
- url: `/resources/applicants/${opts.applicantId}/fixedInfo`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- },
- data: opts.newData
- })
-}
-
-const getApplicantRejectReasons = opts => {
- const REQUIRED_FIELDS = ['applicantId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'GET',
- url: `/resources/moderationStates/-;applicantId=${opts.applicantId}`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const requestApplicantCheck = opts => {
- const REQUIRED_FIELDS = ['applicantId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/status/pending${!_.isNil(opts.reason) ? `?reason=${opts.reason}` : ``}`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const requestApplicantCheckDiffVerificationType = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'reasonCode']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/status/pending${!_.isNil(opts.reasonCode) ? `?reasonCode=${opts.reasonCode}` : ``}`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const getDocumentImages = opts => {
- const REQUIRED_FIELDS = ['inspectionId', 'imageId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'GET',
- url: `/resources/inspections/${opts.inspectionId}/resources/${opts.imageId}`,
- headers: {
- 'Accept': 'image/jpeg, image/png, application/pdf, video/mp4, video/webm, video/quicktime',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const blockApplicant = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'note']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/blacklist?note=${opts.note}`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const generateShareToken = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'clientId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/accessTokens/-/shareToken?applicantId=${opts.applicantId}&forClientId=${opts.clientId}${!_.isNil(opts.ttlInSecs) ? `&ttlInSecs=${opts.ttlInSecs}` : ``}`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const importRawApplicant = opts => {
- const REQUIRED_FIELDS = ['applicantObj']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/-/ingestCompleted`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- },
- data: opts.applicantObj
- })
-}
-
-const importApplicantFromPartnerService = opts => {
- const REQUIRED_FIELDS = ['shareToken']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/-/import?shareToken=${opts.shareToken}`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const resetVerificationStep = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'idDocSetType']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/resetStep/${opts.idDocSetType}`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const resetApplicant = opts => {
- const REQUIRED_FIELDS = ['applicantId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/reset`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- }
- })
-}
-
-const patchApplicantTopLevelInfo = opts => {
- const REQUIRED_FIELDS = ['applicantId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/`,
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- },
- data: {
- id: opts.applicantId,
- externalUserId: opts.externalUserId,
- email: opts.email,
- phone: opts.phone,
- sourceKey: opts.sourceKey,
- type: opts.type,
- lang: opts.lang,
- questionnaires: opts.questionnaires,
- metadata: opts.metadata,
- deleted: opts.deleted
- }
- })
-}
-
-const setApplicantRiskLevel = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'comment', 'riskLevel']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/riskLevel/entries`,
- headers: {
- 'Content-Type': 'application/json'
- },
- data: {
- comment: opts.comment,
- riskLevel: opts.riskLevel
- }
- })
-}
-
-const addApplicantTags = opts => {
- const REQUIRED_FIELDS = ['applicantId', 'tags']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'POST',
- url: `/resources/applicants/${opts.applicantId}/tags`,
- headers: {
- 'Content-Type': 'application/json'
- },
- data: opts.tags
- })
-}
-
-const markImageAsInactive = opts => {
- const REQUIRED_FIELDS = ['inspectionId', 'imageId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'DELETE',
- url: `/resources/inspections/${opts.inspectionId}/resources/${opts.imageId}?revert=false`
- })
-}
-
-const markImageAsActive = opts => {
- const REQUIRED_FIELDS = ['inspectionId', 'imageId']
-
- if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
- return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
- }
-
- return request({
- method: 'DELETE',
- url: `/resources/inspections/${opts.inspectionId}/resources/${opts.imageId}?revert=true`
- })
-}
-
-const getApiHealth = () => {
- return request({
- method: 'GET',
- url: `/resources/status/api`
- })
+ return { level: levelName, answer }
+ })
}
module.exports = {
CODE,
- createApplicantExternalLink,
createApplicant,
- getApplicant,
- addIdDocument,
- changeApplicantFixedInfo,
getApplicantStatus,
- getApplicantIdDocsStatus,
- getApplicantRejectReasons,
- requestApplicantCheck,
- requestApplicantCheckDiffVerificationType,
- getDocumentImages,
- blockApplicant,
- generateShareToken,
- importRawApplicant,
- importApplicantFromPartnerService,
- resetVerificationStep,
- resetApplicant,
- patchApplicantTopLevelInfo,
- setApplicantRiskLevel,
- addApplicantTags,
- markImageAsInactive,
- markImageAsActive,
- getApiHealth,
- changeRequiredLevel
-}
+ getApplicantByExternalId,
+ createLink
+}
\ No newline at end of file
diff --git a/lib/plugins/compliance/sumsub/utils.js b/lib/plugins/compliance/sumsub/utils.js
deleted file mode 100644
index b81bfe48..00000000
--- a/lib/plugins/compliance/sumsub/utils.js
+++ /dev/null
@@ -1,455 +0,0 @@
-const ADD_ID_DOCUMENT_WARNINGS = {
- badSelfie: 'Make sure that your face and the photo in the document are clearly visible',
- dataReadability: 'Please make sure that the information in the document is easy to read',
- inconsistentDocument: 'Please ensure that all uploaded photos are of the same document',
- maybeExpiredDoc: 'Your document appears to be expired',
- documentTooMuchOutside: 'Please ensure that the document completely fits the photo'
-}
-
-const ADD_ID_DOCUMENT_ERRORS = {
- forbiddenDocument: 'Unsupported or unacceptable type/country of document',
- differentDocTypeOrCountry: 'Document type or country mismatches ones that was sent with metadata',
- missingImportantInfo: 'Not all required document data can be recognized',
- dataNotReadable: 'There is no available data to recognize from image',
- expiredDoc: 'Document validity date is expired',
- documentWayTooMuchOutside: 'Not all parts of the documents are visible',
- grayscale: 'Black and white image',
- noIdDocFacePhoto: 'Face is not clearly visible on the document',
- selfieFaceBadQuality: 'Face is not clearly visible on the selfie',
- screenRecapture: 'Image might be a photo of screen',
- screenshot: 'Image is a screenshot',
- sameSides: 'Image of the same side of document was uploaded as front and back sides',
- shouldBeMrzDocument: 'Sent document type should have an MRZ, but there is no readable MRZ on the image',
- shouldBeDoubleSided: 'Two sides of the sent document should be presented',
- shouldBeDoublePaged: 'The full double-page of the document are required',
- documentDeclinedBefore: 'The same image was uploaded and declined earlier'
-}
-
-const SUPPORTED_DOCUMENT_TYPES = {
- ID_CARD: {
- code: 'ID_CARD',
- description: 'An ID card'
- },
- PASSPORT: {
- code: 'PASSPORT',
- description: 'A passport'
- },
- DRIVERS: {
- code: 'DRIVERS',
- description: 'A driving license'
- },
- RESIDENCE_PERMIT: {
- code: 'RESIDENCE_PERMIT',
- description: 'Residence permit or registration document in the foreign city/country'
- },
- UTILITY_BILL: {
- code: 'UTILITY_BILL',
- description: 'Proof of address document'
- },
- SELFIE: {
- code: 'SELFIE',
- description: 'A selfie with a document'
- },
- VIDEO_SELFIE: {
- code: 'VIDEO_SELFIE',
- description: 'A selfie video'
- },
- PROFILE_IMAGE: {
- code: 'PROFILE_IMAGE',
- description: 'A profile image, i.e. avatar'
- },
- ID_DOC_PHOTO: {
- code: 'ID_DOC_PHOTO',
- description: 'Photo from an ID doc (like a photo from a passport)'
- },
- AGREEMENT: {
- code: 'AGREEMENT',
- description: 'Agreement of some sort, e.g. for processing personal info'
- },
- CONTRACT: {
- code: 'CONTRACT',
- description: 'Some sort of contract'
- },
- DRIVERS_TRANSLATION: {
- code: 'DRIVERS_TRANSLATION',
- description: 'Translation of the driving license required in the target country'
- },
- INVESTOR_DOC: {
- code: 'INVESTOR_DOC',
- description: 'A document from an investor, e.g. documents which disclose assets of the investor'
- },
- VEHICLE_REGISTRATION_CERTIFICATE: {
- code: 'VEHICLE_REGISTRATION_CERTIFICATE',
- description: 'Certificate of vehicle registration'
- },
- INCOME_SOURCE: {
- code: 'INCOME_SOURCE',
- description: 'A proof of income'
- },
- PAYMENT_METHOD: {
- code: 'PAYMENT_METHOD',
- description: 'Entity confirming payment (like bank card, crypto wallet, etc)'
- },
- BANK_CARD: {
- code: 'BANK_CARD',
- description: 'A bank card, like Visa or Maestro'
- },
- COVID_VACCINATION_FORM: {
- code: 'COVID_VACCINATION_FORM',
- description: 'COVID vaccination document'
- },
- OTHER: {
- code: 'OTHER',
- description: 'Should be used only when nothing else applies'
- },
-}
-
-const VERIFICATION_RESULTS = {
- GREEN: {
- code: 'GREEN',
- description: 'Everything is fine'
- },
- RED: {
- code: 'RED',
- description: 'Some violations found'
- }
-}
-
-const REVIEW_REJECT_TYPES = {
- FINAL: {
- code: 'FINAL',
- description: 'Final reject, e.g. when a person is a fraudster, or a client does not want to accept such kinds of clients in their system'
- },
- RETRY: {
- code: 'RETRY',
- description: 'Decline that can be fixed, e.g. by uploading an image of better quality'
- }
-}
-
-const REVIEW_REJECT_LABELS = {
- FORGERY: {
- code: 'FORGERY',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'Forgery attempt has been made'
- },
- DOCUMENT_TEMPLATE: {
- code: 'DOCUMENT_TEMPLATE',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'Documents supplied are templates, downloaded from internet'
- },
- LOW_QUALITY: {
- code: 'LOW_QUALITY',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Documents have low-quality that does not allow definitive conclusions to be made'
- },
- SPAM: {
- code: 'SPAM',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'An applicant has been created by mistake or is just a spam user (irrelevant images were supplied)'
- },
- NOT_DOCUMENT: {
- code: 'NOT_DOCUMENT',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Documents supplied are not relevant for the verification procedure'
- },
- SELFIE_MISMATCH: {
- code: 'SELFIE_MISMATCH',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'A user photo (profile image) does not match a photo on the provided documents'
- },
- ID_INVALID: {
- code: 'ID_INVALID',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'A document that identifies a person (like a passport or an ID card) is not valid'
- },
- FOREIGNER: {
- code: 'FOREIGNER',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'When a client does not accept applicants from a different country or e.g. without a residence permit'
- },
- DUPLICATE: {
- code: 'DUPLICATE',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'This applicant was already created for this client, and duplicates are not allowed by the regulations'
- },
- BAD_AVATAR: {
- code: 'BAD_AVATAR',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'When avatar does not meet the client\'s requirements'
- },
- WRONG_USER_REGION: {
- code: 'WRONG_USER_REGION',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'When applicants from certain regions/countries are not allowed to be registered'
- },
- INCOMPLETE_DOCUMENT: {
- code: 'INCOMPLETE_DOCUMENT',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Some information is missing from the document, or it\'s partially visible'
- },
- BLACKLIST: {
- code: 'BLACKLIST',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'User is blocklisted'
- },
- UNSATISFACTORY_PHOTOS: {
- code: 'UNSATISFACTORY_PHOTOS',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'There were problems with the photos, like poor quality or masked information'
- },
- DOCUMENT_PAGE_MISSING: {
- code: 'DOCUMENT_PAGE_MISSING',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Some pages of a document are missing (if applicable)'
- },
- DOCUMENT_DAMAGED: {
- code: 'DOCUMENT_DAMAGED',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Document is damaged'
- },
- REGULATIONS_VIOLATIONS: {
- code: 'REGULATIONS_VIOLATIONS',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'Regulations violations'
- },
- INCONSISTENT_PROFILE: {
- code: 'INCONSISTENT_PROFILE',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'Data or documents of different persons were uploaded to one applicant'
- },
- PROBLEMATIC_APPLICANT_DATA: {
- code: 'PROBLEMATIC_APPLICANT_DATA',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Applicant data does not match the data in the documents'
- },
- ADDITIONAL_DOCUMENT_REQUIRED: {
- code: 'ADDITIONAL_DOCUMENT_REQUIRED',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Additional documents required to pass the check'
- },
- AGE_REQUIREMENT_MISMATCH: {
- code: 'AGE_REQUIREMENT_MISMATCH',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'Age requirement is not met (e.g. cannot rent a car to a person below 25yo)'
- },
- EXPERIENCE_REQUIREMENT_MISMATCH: {
- code: 'EXPERIENCE_REQUIREMENT_MISMATCH',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'Not enough experience (e.g. driving experience is not enough)'
- },
- CRIMINAL: {
- code: 'CRIMINAL',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'The user is involved in illegal actions'
- },
- WRONG_ADDRESS: {
- code: 'WRONG_ADDRESS',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The address from the documents doesn\'t match the address that the user entered'
- },
- GRAPHIC_EDITOR: {
- code: 'GRAPHIC_EDITOR',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The document has been edited by a graphical editor'
- },
- DOCUMENT_DEPRIVED: {
- code: 'DOCUMENT_DEPRIVED',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user has been deprived of the document'
- },
- COMPROMISED_PERSONS: {
- code: 'COMPROMISED_PERSONS',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'The user does not correspond to Compromised Person Politics'
- },
- PEP: {
- code: 'PEP',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'The user belongs to the PEP category'
- },
- ADVERSE_MEDIA: {
- code: 'ADVERSE_MEDIA',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'The user was found in the adverse media'
- },
- FRAUDULENT_PATTERNS: {
- code: 'FRAUDULENT_PATTERNS',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'Fraudulent behavior was detected'
- },
- SANCTIONS: {
- code: 'SANCTIONS',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'The user was found on sanction lists'
- },
- NOT_ALL_CHECKS_COMPLETED: {
- code: 'NOT_ALL_CHECKS_COMPLETED',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'All checks were not completed'
- },
- FRONT_SIDE_MISSING: {
- code: 'FRONT_SIDE_MISSING',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Front side of the document is missing'
- },
- BACK_SIDE_MISSING: {
- code: 'BACK_SIDE_MISSING',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Back side of the document is missing'
- },
- SCREENSHOTS: {
- code: 'SCREENSHOTS',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded screenshots'
- },
- BLACK_AND_WHITE: {
- code: 'BLACK_AND_WHITE',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded black and white photos of documents'
- },
- INCOMPATIBLE_LANGUAGE: {
- code: 'INCOMPATIBLE_LANGUAGE',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user should upload translation of his document'
- },
- EXPIRATION_DATE: {
- code: 'EXPIRATION_DATE',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded expired document'
- },
- UNFILLED_ID: {
- code: 'UNFILLED_ID',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded the document without signatures and stamps'
- },
- BAD_SELFIE: {
- code: 'BAD_SELFIE',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded a bad selfie'
- },
- BAD_VIDEO_SELFIE: {
- code: 'BAD_VIDEO_SELFIE',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded a bad video selfie'
- },
- BAD_FACE_MATCHING: {
- code: 'BAD_FACE_MATCHING',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Face check between document and selfie failed'
- },
- BAD_PROOF_OF_IDENTITY: {
- code: 'BAD_PROOF_OF_IDENTITY',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded a bad ID document'
- },
- BAD_PROOF_OF_ADDRESS: {
- code: 'BAD_PROOF_OF_ADDRESS',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded a bad proof of address'
- },
- BAD_PROOF_OF_PAYMENT: {
- code: 'BAD_PROOF_OF_PAYMENT',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user uploaded a bad proof of payment'
- },
- SELFIE_WITH_PAPER: {
- code: 'SELFIE_WITH_PAPER',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'The user should upload a special selfie (e.g. selfie with paper and date on it)'
- },
- FRAUDULENT_LIVENESS: {
- code: 'FRAUDULENT_LIVENESS',
- rejectType: REVIEW_REJECT_TYPES.FINAL,
- description: 'There was an attempt to bypass liveness check'
- },
- OTHER: {
- code: 'OTHER',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Some unclassified reason'
- },
- REQUESTED_DATA_MISMATCH: {
- code: 'REQUESTED_DATA_MISMATCH',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Provided info doesn\'t match with recognized from document data'
- },
- OK: {
- code: 'OK',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Custom reject label'
- },
- COMPANY_NOT_DEFINED_STRUCTURE: {
- code: 'COMPANY_NOT_DEFINED_STRUCTURE',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Could not establish the entity\'s control structure'
- },
- COMPANY_NOT_DEFINED_BENEFICIARIES: {
- code: 'COMPANY_NOT_DEFINED_BENEFICIARIES',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Could not identify and duly verify the entity\'s beneficial owners'
- },
- COMPANY_NOT_VALIDATED_BENEFICIARIES: {
- code: 'COMPANY_NOT_VALIDATED_BENEFICIARIES',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Beneficiaries are not validated'
- },
- COMPANY_NOT_DEFINED_REPRESENTATIVES: {
- code: 'COMPANY_NOT_DEFINED_REPRESENTATIVES',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Representatives are not defined'
- },
- COMPANY_NOT_VALIDATED_REPRESENTATIVES: {
- code: 'COMPANY_NOT_VALIDATED_REPRESENTATIVES',
- rejectType: REVIEW_REJECT_TYPES.RETRY,
- description: 'Representatives are not validated'
- },
-}
-
-const REVIEW_STATUS = {
- init: 'Initial registration has started. A client is still in the process of filling out the applicant profile. Not all required documents are currently uploaded',
- pending: 'An applicant is ready to be processed',
- prechecked: 'The check is in a half way of being finished',
- queued: 'The checks have been started for the applicant',
- completed: 'The check has been completed',
- onHold: 'Applicant waits for a final decision from compliance officer (manual check was initiated) or waits for all beneficiaries to pass KYC in case of company verification',
-}
-
-const RESETTABLE_VERIFICATION_STEPS = {
- PHONE_VERIFICATION: {
- code: 'PHONE_VERIFICATION',
- description: 'Phone verification step'
- },
- EMAIL_VERIFICATION: {
- code: 'EMAIL_VERIFICATION',
- description: 'Email verification step'
- },
- QUESTIONNAIRE: {
- code: 'QUESTIONNAIRE',
- description: 'Questionnaire'
- },
- APPLICANT_DATA: {
- code: 'APPLICANT_DATA',
- description: 'Applicant data'
- },
- IDENTITY: {
- code: 'IDENTITY',
- description: 'Identity step'
- },
- PROOF_OF_RESIDENCE: {
- code: 'PROOF_OF_RESIDENCE',
- description: 'Proof of residence'
- },
- SELFIE: {
- code: 'SELFIE',
- description: 'Selfie step'
- },
-}
-
-module.exports = {
- ADD_ID_DOCUMENT_WARNINGS,
- ADD_ID_DOCUMENT_ERRORS,
- SUPPORTED_DOCUMENT_TYPES,
- VERIFICATION_RESULTS,
- REVIEW_REJECT_LABELS,
- REVIEW_STATUS,
- RESETTABLE_VERIFICATION_STEPS
-}
diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js
index ea9ed016..75fe6641 100644
--- a/lib/routes/customerRoutes.js
+++ b/lib/routes/customerRoutes.js
@@ -18,7 +18,7 @@ const notifier = require('../notifier')
const respond = require('../respond')
const { getTx } = require('../new-admin/services/transactions.js')
const machineLoader = require('../machine-loader')
-const { loadLatest, loadLatestConfig } = require('../new-settings-loader')
+const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
const T = require('../time')
const plugins = require('../plugins')
@@ -243,12 +243,12 @@ function getExternalComplianceLink (req, res, next) {
const settings = req.settings
const triggers = configManager.getTriggers(settings.config)
const trigger = _.find(it => it.id === triggerId)(triggers)
+ const externalService = trigger.externalService
- return externalCompliance.createApplicantExternalLink(settings, customerId, trigger.id)
- .then(url => {
- process.env.NODE_ENV === 'development' && console.log(url)
- return respond(req, res, { url: 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, config, isEmailAuth) {
diff --git a/migrations/1667945906700-integrate-sumsub.js b/migrations/1667945906700-integrate-sumsub.js
deleted file mode 100644
index 0080faa6..00000000
--- a/migrations/1667945906700-integrate-sumsub.js
+++ /dev/null
@@ -1,13 +0,0 @@
-var db = require('./db')
-
-exports.up = function (next) {
- var sql = [
- `ALTER TABLE customers ADD COLUMN applicant_id TEXT`
- ]
-
- db.multi(sql, next)
-}
-
-exports.down = function (next) {
- next()
-}
diff --git a/migrations/1718464437502-integrate-sumsub.js b/migrations/1718464437502-integrate-sumsub.js
new file mode 100644
index 00000000..0e49de6e
--- /dev/null
+++ b/migrations/1718464437502-integrate-sumsub.js
@@ -0,0 +1,21 @@
+const db = require('./db')
+
+exports.up = function (next) {
+ let sql = [
+ `CREATE TYPE EXTERNAL_COMPLIANCE_STATUS AS ENUM('WAIT', 'APPROVED', 'REJECTED', 'RETRY')`,
+ `CREATE TABLE CUSTOMER_EXTERNAL_COMPLIANCE (
+ customer_id UUID NOT NULL REFERENCES customers(id),
+ service TEXT NOT NULL,
+ external_id TEXT NOT NULL,
+ last_known_status EXTERNAL_COMPLIANCE_STATUS,
+ last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE (customer_id, service)
+ )`
+ ]
+
+ db.multi(sql, next)
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/new-lamassu-admin/src/pages/Compliance/Sumsub.js b/new-lamassu-admin/src/pages/Compliance/Sumsub.js
deleted file mode 100644
index 2e38cd66..00000000
--- a/new-lamassu-admin/src/pages/Compliance/Sumsub.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { useQuery } from '@apollo/react-hooks'
-import SumsubWebSdk from '@sumsub/websdk-react'
-import gql from 'graphql-tag'
-import React from 'react'
-import { useLocation } from 'react-router-dom'
-
-const QueryParams = () => new URLSearchParams(useLocation().search)
-
-const CREATE_NEW_TOKEN = gql`
- query getApplicantAccessToken($customerId: ID, $triggerId: ID) {
- getApplicantAccessToken(customerId: $customerId, triggerId: $triggerId)
- }
-`
-
-const Sumsub = () => {
- const token = QueryParams().get('t')
- const customerId = QueryParams().get('customer')
- const triggerId = QueryParams().get('trigger')
-
- const { refetch: getNewToken } = useQuery(CREATE_NEW_TOKEN, {
- skip: true,
- variables: { customerId: customerId, triggerId: triggerId }
- })
-
- const config = {
- lang: 'en'
- }
-
- const options = {
- addViewportTag: true,
- adaptIframeHeight: true
- }
-
- const updateAccessToken = () =>
- getNewToken().then(res => {
- const { getApplicantAccessToken: _token } = res.data
- return _token
- })
-
- return (
-
Comments about this decision:
- {R.map( - it => ( -{it}
- ), - R.split('\n', comment) - )} -Relevant labels: {R.join(',', labels)}
-- Before linking the Sumsub 3rd party service to the Lamassu Admin, make - sure you have configured the required parameters in your personal Sumsub - Dashboard. -
-- These parameters include the Sumsub Global Settings, Applicant Levels, - Twilio and Webhooks. -
-