refactor: yagni and flow of external compliance
This commit is contained in:
parent
b06927fd1c
commit
04eea85a0d
29 changed files with 389 additions and 1417 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ type CustomInput {
|
|||
constraintType: String!
|
||||
label1: String
|
||||
label2: String
|
||||
label3: String
|
||||
choiceList: [String]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
const { gql } = require('apollo-server-express')
|
||||
|
||||
const typeDef = gql`
|
||||
type Query {
|
||||
getApplicantExternalLink(customerId: ID, triggerId: ID): String
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = typeDef
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
6
lib/plugins/compliance/consts.js
Normal file
6
lib/plugins/compliance/consts.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
WAIT: 'WAIT',
|
||||
RETRY: 'RETRY',
|
||||
APPROVED: 'APPROVED',
|
||||
REJECTED: 'REJECTED'
|
||||
}
|
||||
14
lib/plugins/compliance/mock-compliance/mock-compliance.js
Normal file
14
lib/plugins/compliance/mock-compliance/mock-compliance.js
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
99
lib/plugins/compliance/sumsub/sumsub.api.js
Normal file
99
lib/plugins/compliance/sumsub/sumsub.api.js
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
21
migrations/1718464437502-integrate-sumsub.js
Normal file
21
migrations/1718464437502-integrate-sumsub.js
Normal file
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<SumsubWebSdk
|
||||
accessToken={token}
|
||||
expirationHandler={updateAccessToken}
|
||||
config={config}
|
||||
options={options}
|
||||
onMessage={console.log}
|
||||
onError={console.error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sumsub
|
||||
|
|
@ -11,8 +11,7 @@ import { TextInput } from 'src/components/inputs/formik'
|
|||
import { H3, Info3 } from 'src/components/typography'
|
||||
import {
|
||||
OVERRIDE_AUTHORIZED,
|
||||
OVERRIDE_REJECTED,
|
||||
OVERRIDE_PENDING
|
||||
OVERRIDE_REJECTED
|
||||
} from 'src/pages/Customers/components/propertyCard'
|
||||
import { ReactComponent as CardIcon } from 'src/styling/icons/ID/card/comet.svg'
|
||||
import { ReactComponent as PhoneIcon } from 'src/styling/icons/ID/phone/comet.svg'
|
||||
|
|
@ -26,7 +25,7 @@ import { URI } from 'src/utils/apollo'
|
|||
import { onlyFirstToUpper } from 'src/utils/string'
|
||||
|
||||
import styles from './CustomerData.styles.js'
|
||||
import { EditableCard, NonEditableCard } from './components'
|
||||
import { EditableCard } from './components'
|
||||
import {
|
||||
customerDataElements,
|
||||
customerDataSchemas,
|
||||
|
|
@ -401,59 +400,22 @@ const CustomerData = ({
|
|||
})
|
||||
}, R.keys(smsData) ?? [])
|
||||
|
||||
const externalComplianceProvider =
|
||||
R.path([`externalCompliance`, `provider`])(customer) ?? undefined
|
||||
|
||||
const externalComplianceData = {
|
||||
sumsub: {
|
||||
getApplicantInfo: data => {
|
||||
return R.path(['fixedInfo'])(data) ?? {}
|
||||
},
|
||||
getVerificationState: data => {
|
||||
const reviewStatus = R.path(['review', 'reviewStatus'])(data)
|
||||
const reviewResult = R.path(['review', 'reviewResult', 'reviewAnswer'])(
|
||||
data
|
||||
)
|
||||
|
||||
const state =
|
||||
reviewStatus === 'completed'
|
||||
? reviewResult === 'GREEN'
|
||||
? OVERRIDE_AUTHORIZED
|
||||
: OVERRIDE_REJECTED
|
||||
: OVERRIDE_PENDING
|
||||
|
||||
const comment = R.path(['review', 'reviewResult', 'clientComment'])(
|
||||
data
|
||||
)
|
||||
|
||||
const labels = R.path(['review', 'reviewResult', 'rejectLabels'])(data)
|
||||
|
||||
return { state, comment, labels }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const externalComplianceValues = R.path(['externalCompliance'])(customer)
|
||||
|
||||
if (
|
||||
!R.isNil(externalComplianceValues) &&
|
||||
!R.isEmpty(externalComplianceValues)
|
||||
) {
|
||||
externalCompliance.push({
|
||||
fields: R.map(it => ({ name: it[0], label: it[0], value: it[1] }))(
|
||||
R.toPairs(
|
||||
externalComplianceData[externalComplianceProvider]?.getApplicantInfo(
|
||||
externalComplianceValues
|
||||
)
|
||||
)
|
||||
),
|
||||
titleIcon: <CardIcon className={classes.cardIcon} />,
|
||||
state: externalComplianceData[
|
||||
externalComplianceProvider
|
||||
]?.getVerificationState(externalComplianceValues),
|
||||
title: 'External Info'
|
||||
})
|
||||
}
|
||||
// TODO - add external compliance data
|
||||
// R.forEach(outer => {
|
||||
// initialValues.
|
||||
// externalCompliance.push({
|
||||
// fields: [
|
||||
// {
|
||||
// name: 'lastKnownStatus',
|
||||
// label: 'Last Known Status',
|
||||
// component: TextInput
|
||||
// }
|
||||
// ],
|
||||
// titleIcon: <CardIcon className={classes.cardIcon} />,
|
||||
// state: outer.state,
|
||||
// title: 'External Info'
|
||||
// })
|
||||
// })(R.keys(customer.externalCompliance))
|
||||
|
||||
const editableCard = (
|
||||
{
|
||||
|
|
@ -501,13 +463,14 @@ const CustomerData = ({
|
|||
idx
|
||||
) => {
|
||||
return (
|
||||
<NonEditableCard
|
||||
<EditableCard
|
||||
title={title}
|
||||
key={idx}
|
||||
state={state}
|
||||
titleIcon={titleIcon}
|
||||
editable={false}
|
||||
hasImage={hasImage}
|
||||
fields={fields}></NonEditableCard>
|
||||
fields={fields}></EditableCard>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { useState, React } from 'react'
|
|||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { MainStatus } from 'src/components/Status'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { ActionButton } from 'src/components/buttons'
|
||||
import { Label1, P, H3 } from 'src/components/typography'
|
||||
import {
|
||||
|
|
@ -402,85 +401,4 @@ const EditableCard = ({
|
|||
)
|
||||
}
|
||||
|
||||
const NonEditableCard = ({
|
||||
fields,
|
||||
hasImage,
|
||||
state: _state,
|
||||
title,
|
||||
titleIcon
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { state, comment, labels } = _state
|
||||
|
||||
const label1ClassNames = {
|
||||
[classes.label1]: true,
|
||||
[classes.label1Pending]: state === OVERRIDE_PENDING,
|
||||
[classes.label1Rejected]: state === OVERRIDE_REJECTED,
|
||||
[classes.label1Accepted]: state === OVERRIDE_AUTHORIZED
|
||||
}
|
||||
const authorized =
|
||||
state === OVERRIDE_PENDING
|
||||
? { label: 'Pending', type: 'neutral' }
|
||||
: state === OVERRIDE_REJECTED
|
||||
? { label: 'Rejected', type: 'error' }
|
||||
: { label: 'Accepted', type: 'success' }
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className={classes.card}>
|
||||
<CardContent>
|
||||
<div className={classes.headerWrapper}>
|
||||
<div className={classes.cardHeader}>
|
||||
{titleIcon}
|
||||
<H3 className={classes.cardTitle}>{title}</H3>
|
||||
{
|
||||
// TODO: Enable for next release
|
||||
/* <HoverableTooltip width={304}></HoverableTooltip> */
|
||||
}
|
||||
</div>
|
||||
{state && (
|
||||
<div className={classnames(label1ClassNames)}>
|
||||
<MainStatus statuses={[authorized]} />
|
||||
{comment && labels && (
|
||||
<HoverableTooltip width={304}>
|
||||
<P>Comments about this decision:</P>
|
||||
{R.map(
|
||||
it => (
|
||||
<P noMargin>{it}</P>
|
||||
),
|
||||
R.split('\n', comment)
|
||||
)}
|
||||
<P>Relevant labels: {R.join(',', labels)}</P>
|
||||
</HoverableTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.row}>
|
||||
<Grid container>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{!hasImage &&
|
||||
fields?.map((field, idx) => {
|
||||
return idx >= 0 && idx < 4 ? (
|
||||
<ReadOnlyField field={field} value={field.value} />
|
||||
) : null
|
||||
})}
|
||||
</Grid>
|
||||
<Grid container direction="column" item xs={6}>
|
||||
{!hasImage &&
|
||||
fields?.map((field, idx) => {
|
||||
return idx >= 4 ? (
|
||||
<ReadOnlyField field={field} value={field.value} />
|
||||
) : null
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { EditableCard, NonEditableCard }
|
||||
export default EditableCard
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import Wizard from '../Wizard'
|
|||
|
||||
import CustomerDetails from './CustomerDetails'
|
||||
import CustomerSidebar from './CustomerSidebar'
|
||||
import { EditableCard, NonEditableCard } from './EditableCard'
|
||||
import EditableCard from './EditableCard'
|
||||
import Field from './Field'
|
||||
import IdDataCard from './IdDataCard'
|
||||
import PhotosCarousel from './PhotosCarousel'
|
||||
|
|
@ -17,7 +17,6 @@ export {
|
|||
CustomerSidebar,
|
||||
Field,
|
||||
EditableCard,
|
||||
NonEditableCard,
|
||||
Wizard,
|
||||
Upload
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,13 @@
|
|||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { Checkbox } from 'src/components/inputs'
|
||||
import { SecretInput, TextInput } from 'src/components/inputs/formik'
|
||||
import { P } from 'src/components/typography'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
|
||||
const SumsubSplash = ({ classes, onContinue }) => {
|
||||
const [canContinue, setCanContinue] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={classes.form}>
|
||||
<P>
|
||||
Before linking the Sumsub 3rd party service to the Lamassu Admin, make
|
||||
sure you have configured the required parameters in your personal Sumsub
|
||||
Dashboard.
|
||||
</P>
|
||||
<P>
|
||||
These parameters include the Sumsub Global Settings, Applicant Levels,
|
||||
Twilio and Webhooks.
|
||||
</P>
|
||||
<Checkbox
|
||||
value={canContinue}
|
||||
onChange={() => setCanContinue(!canContinue)}
|
||||
settings={{
|
||||
enabled: true,
|
||||
label: 'I have completed the steps needed to configure Sumsub',
|
||||
rightSideLabel: true
|
||||
}}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
<div className={classes.buttonWrapper}>
|
||||
<Button disabled={!canContinue} onClick={onContinue}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const schema = {
|
||||
code: 'sumsub',
|
||||
name: 'Sumsub',
|
||||
category: 'Compliance',
|
||||
allowMultiInstances: false,
|
||||
SplashScreenComponent: SumsubSplash,
|
||||
title: 'Sumsub (Compliance)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiToken',
|
||||
|
|
|
|||
|
|
@ -28,9 +28,8 @@ const TriggerView = ({
|
|||
config,
|
||||
toggleWizard,
|
||||
addNewTriger,
|
||||
customInfoRequests,
|
||||
emailAuth,
|
||||
additionalInfo
|
||||
customInfoRequests
|
||||
}) => {
|
||||
const currency = R.path(['fiatCurrency'])(
|
||||
fromNamespace(namespaces.LOCALE)(config)
|
||||
|
|
@ -70,12 +69,7 @@ const TriggerView = ({
|
|||
error={error?.message}
|
||||
save={save}
|
||||
validationSchema={Schema}
|
||||
elements={getElements(
|
||||
currency,
|
||||
classes,
|
||||
customInfoRequests,
|
||||
additionalInfo
|
||||
)}
|
||||
elements={getElements(currency, classes, customInfoRequests)}
|
||||
/>
|
||||
{showWizard && (
|
||||
<Wizard
|
||||
|
|
@ -86,7 +80,6 @@ const TriggerView = ({
|
|||
customInfoRequests={customInfoRequests}
|
||||
emailAuth={emailAuth}
|
||||
triggers={triggers}
|
||||
additionalInfo={additionalInfo}
|
||||
/>
|
||||
)}
|
||||
{R.isEmpty(triggers) && (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import classnames from 'classnames'
|
|||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { getAccountInstance } from 'src/utils/accounts'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
|
|
@ -19,7 +18,6 @@ import { ReactComponent as CustomInfoIcon } from 'src/styling/icons/circle butto
|
|||
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
|
||||
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
import { COMPLIANCE_SERVICES } from 'src/utils/constants'
|
||||
|
||||
import CustomInfoRequests from './CustomInfoRequests'
|
||||
import TriggerView from './TriggerView'
|
||||
|
|
@ -134,67 +132,32 @@ const Triggers = () => {
|
|||
else toggleWizard('newTrigger')()
|
||||
}
|
||||
|
||||
const accounts = data?.accounts ?? {}
|
||||
const isAnyExternalValidationAccountEnabled = () => {
|
||||
try {
|
||||
return R.any(
|
||||
it => it === true,
|
||||
R.map(
|
||||
ite => getAccountInstance(accounts[ite], ite)?.enabled,
|
||||
COMPLIANCE_SERVICES
|
||||
)
|
||||
)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = []
|
||||
const externalValidationLevels = !R.isEmpty(accounts)
|
||||
? R.reduce(
|
||||
(acc, value) => {
|
||||
const instances = accounts[value]?.instances ?? {}
|
||||
return {
|
||||
...acc,
|
||||
[value]: R.map(
|
||||
it => ({ value: it, display: it }),
|
||||
R.uniq(R.map(ite => ite.applicantLevel, instances) ?? [])
|
||||
)
|
||||
}
|
||||
},
|
||||
{},
|
||||
COMPLIANCE_SERVICES
|
||||
)
|
||||
: []
|
||||
|
||||
!isAnyExternalValidationAccountEnabled() &&
|
||||
buttons.push({
|
||||
text: 'Advanced settings',
|
||||
icon: SettingsIcon,
|
||||
inverseIcon: ReverseSettingsIcon,
|
||||
forceDisable: !(subMenu === 'advancedSettings'),
|
||||
toggle: show => {
|
||||
refetch()
|
||||
setSubMenu(show ? 'advancedSettings' : false)
|
||||
}
|
||||
})
|
||||
|
||||
buttons.push({
|
||||
text: 'Custom info requests',
|
||||
icon: CustomInfoIcon,
|
||||
inverseIcon: ReverseCustomInfoIcon,
|
||||
forceDisable: !(subMenu === 'customInfoRequests'),
|
||||
toggle: show => {
|
||||
refetch()
|
||||
setSubMenu(show ? 'customInfoRequests' : false)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection
|
||||
title="Compliance triggers"
|
||||
buttons={buttons}
|
||||
buttons={[
|
||||
{
|
||||
text: 'Advanced settings',
|
||||
icon: SettingsIcon,
|
||||
inverseIcon: ReverseSettingsIcon,
|
||||
forceDisable: !(subMenu === 'advancedSettings'),
|
||||
toggle: show => {
|
||||
refetch()
|
||||
setSubMenu(show ? 'advancedSettings' : false)
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Custom info requests',
|
||||
icon: CustomInfoIcon,
|
||||
inverseIcon: ReverseCustomInfoIcon,
|
||||
forceDisable: !(subMenu === 'customInfoRequests'),
|
||||
toggle: show => {
|
||||
refetch()
|
||||
setSubMenu(show ? 'customInfoRequests' : false)
|
||||
}
|
||||
}
|
||||
]}
|
||||
className={classnames(titleSectionWidth)}>
|
||||
{!subMenu && (
|
||||
<Box display="flex" alignItems="center">
|
||||
|
|
@ -257,10 +220,7 @@ const Triggers = () => {
|
|||
toggleWizard={toggleWizard('newTrigger')}
|
||||
addNewTriger={addNewTriger}
|
||||
emailAuth={emailAuth}
|
||||
additionalInfo={{
|
||||
customInfoRequests: enabledCustomInfoRequests,
|
||||
externalValidationLevels: externalValidationLevels
|
||||
}}
|
||||
customInfoRequests={enabledCustomInfoRequests}
|
||||
/>
|
||||
)}
|
||||
{!loading && subMenu === 'advancedSettings' && (
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ const getStep = (
|
|||
currency,
|
||||
customInfoRequests,
|
||||
emailAuth,
|
||||
triggers,
|
||||
additionalInfo
|
||||
triggers
|
||||
) => {
|
||||
switch (step) {
|
||||
// case 1:
|
||||
|
|
@ -62,13 +61,7 @@ const getStep = (
|
|||
case 1:
|
||||
return type(currency)
|
||||
case 2:
|
||||
return requirements(
|
||||
customInfoRequests,
|
||||
emailAuth,
|
||||
config,
|
||||
triggers,
|
||||
additionalInfo
|
||||
)
|
||||
return requirements(config, triggers, customInfoRequests, emailAuth)
|
||||
default:
|
||||
return Fragment
|
||||
}
|
||||
|
|
@ -226,8 +219,7 @@ const Wizard = ({
|
|||
currency,
|
||||
customInfoRequests,
|
||||
emailAuth,
|
||||
triggers,
|
||||
additionalInfo
|
||||
triggers
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -242,8 +234,7 @@ const Wizard = ({
|
|||
currency,
|
||||
customInfoRequests,
|
||||
emailAuth,
|
||||
triggers,
|
||||
additionalInfo
|
||||
triggers
|
||||
)
|
||||
|
||||
const onContinue = async it => {
|
||||
|
|
|
|||
|
|
@ -504,13 +504,6 @@ const requirementSchema = Yup.object()
|
|||
otherwise: Yup.string()
|
||||
.nullable()
|
||||
.transform(() => '')
|
||||
}),
|
||||
externalServiceApplicantLevel: Yup.string().when('requirement', {
|
||||
is: value => value === 'external',
|
||||
then: Yup.string(),
|
||||
otherwise: Yup.string()
|
||||
.nullable()
|
||||
.transform(() => '')
|
||||
})
|
||||
}).required()
|
||||
})
|
||||
|
|
@ -527,8 +520,7 @@ const requirementSchema = Yup.object()
|
|||
: true
|
||||
case 'external':
|
||||
return requirement.requirement === type
|
||||
? !R.isNil(requirement.externalService) &&
|
||||
!R.isNil(requirement.externalServiceApplicantLevel)
|
||||
? !R.isNil(requirement.externalService)
|
||||
: true
|
||||
default:
|
||||
return true
|
||||
|
|
@ -582,20 +574,13 @@ const hasCustomRequirementError = (errors, touched, values) =>
|
|||
const hasExternalRequirementError = (errors, touched, values) =>
|
||||
!!errors.requirement &&
|
||||
!!touched.requirement?.externalService &&
|
||||
!!touched.requirement?.externalServiceApplicantLevel &&
|
||||
(!values.requirement?.externalService ||
|
||||
!R.isNil(values.requirement?.externalService)) &&
|
||||
(!values.requirement?.externalServiceApplicantLevel ||
|
||||
!R.isNil(values.requirement?.externalServiceApplicantLevel))
|
||||
!values.requirement?.externalService
|
||||
|
||||
const Requirement = ({
|
||||
config = {},
|
||||
triggers,
|
||||
additionalInfo: {
|
||||
emailAuth,
|
||||
customInfoRequests = [],
|
||||
externalValidationLevels = {}
|
||||
}
|
||||
emailAuth,
|
||||
customInfoRequests = []
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const {
|
||||
|
|
@ -725,30 +710,17 @@ const Requirement = ({
|
|||
name="requirement.externalService"
|
||||
options={externalServices}
|
||||
/>
|
||||
{!R.isNil(
|
||||
externalValidationLevels[values.requirement.externalService]
|
||||
) && (
|
||||
<Field
|
||||
className={classes.dropdownField}
|
||||
component={Dropdown}
|
||||
label="Applicant level"
|
||||
name="requirement.externalServiceApplicantLevel"
|
||||
options={
|
||||
externalValidationLevels[values.requirement.externalService]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const requirements = (config, triggers, additionalInfo) => ({
|
||||
const requirements = (config, triggers, customInfoRequests, emailAuth) => ({
|
||||
schema: requirementSchema,
|
||||
options: requirementOptions,
|
||||
Component: Requirement,
|
||||
props: { config, triggers, additionalInfo },
|
||||
props: { config, triggers, customInfoRequests, emailAuth },
|
||||
hasRequirementError: hasRequirementError,
|
||||
hasCustomRequirementError: hasCustomRequirementError,
|
||||
hasExternalRequirementError: hasExternalRequirementError,
|
||||
|
|
@ -757,8 +729,7 @@ const requirements = (config, triggers, additionalInfo) => ({
|
|||
requirement: '',
|
||||
suspensionDays: '',
|
||||
customInfoRequestId: '',
|
||||
externalService: '',
|
||||
externalServiceApplicantLevel: ''
|
||||
externalService: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -788,9 +759,7 @@ const customReqIdMatches = customReqId => it => {
|
|||
return it.id === customReqId
|
||||
}
|
||||
|
||||
const RequirementInput = ({
|
||||
additionalInfo: { customInfoRequests = [], externalValidationLevels = {} }
|
||||
}) => {
|
||||
const RequirementInput = ({ customInfoRequests = [] }) => {
|
||||
const { values } = useFormikContext()
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -826,7 +795,7 @@ const RequirementView = ({
|
|||
suspensionDays,
|
||||
customInfoRequestId,
|
||||
externalService,
|
||||
additionalInfo: { customInfoRequests = [], externalValidationLevels = {} }
|
||||
customInfoRequests = []
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const display =
|
||||
|
|
@ -949,7 +918,7 @@ const ThresholdView = ({ config, currency }) => {
|
|||
return <DisplayThreshold config={config} currency={currency} />
|
||||
}
|
||||
|
||||
const getElements = (currency, classes, additionalInfo) => [
|
||||
const getElements = (currency, classes, customInfoRequests) => [
|
||||
{
|
||||
name: 'triggerType',
|
||||
size: 'sm',
|
||||
|
|
@ -970,8 +939,10 @@ const getElements = (currency, classes, additionalInfo) => [
|
|||
size: 'sm',
|
||||
width: 260,
|
||||
bypassField: true,
|
||||
input: () => <RequirementInput additionalInfo={additionalInfo} />,
|
||||
view: it => <RequirementView {...it} additionalInfo={additionalInfo} />
|
||||
input: () => <RequirementInput customInfoRequests={customInfoRequests} />,
|
||||
view: it => (
|
||||
<RequirementView {...it} customInfoRequests={customInfoRequests} />
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'threshold',
|
||||
|
|
@ -1003,7 +974,7 @@ const sortBy = [
|
|||
)
|
||||
]
|
||||
|
||||
const fromServer = (triggers, customInfoRequests) => {
|
||||
const fromServer = triggers => {
|
||||
return R.map(
|
||||
({
|
||||
requirement,
|
||||
|
|
@ -1012,15 +983,13 @@ const fromServer = (triggers, customInfoRequests) => {
|
|||
thresholdDays,
|
||||
customInfoRequestId,
|
||||
externalService,
|
||||
externalServiceApplicantLevel,
|
||||
...rest
|
||||
}) => ({
|
||||
requirement: {
|
||||
requirement,
|
||||
suspensionDays,
|
||||
customInfoRequestId,
|
||||
externalService,
|
||||
externalServiceApplicantLevel
|
||||
externalService
|
||||
},
|
||||
threshold: {
|
||||
threshold,
|
||||
|
|
@ -1039,7 +1008,6 @@ const toServer = triggers =>
|
|||
thresholdDays: threshold.thresholdDays,
|
||||
customInfoRequestId: requirement.customInfoRequestId,
|
||||
externalService: requirement.externalService,
|
||||
externalServiceApplicantLevel: requirement.externalServiceApplicantLevel,
|
||||
...rest
|
||||
}))(triggers)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ const IP_CHECK_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[
|
|||
|
||||
const SWEEPABLE_CRYPTOS = ['ETH']
|
||||
|
||||
const COMPLIANCE_SERVICES = ['sumsub']
|
||||
|
||||
export {
|
||||
CURRENCY_MAX,
|
||||
MIN_NUMBER_OF_CASSETTES,
|
||||
|
|
@ -20,6 +18,5 @@ export {
|
|||
MANUAL,
|
||||
WALLET_SCORING_DEFAULT_THRESHOLD,
|
||||
IP_CHECK_REGEX,
|
||||
SWEEPABLE_CRYPTOS,
|
||||
COMPLIANCE_SERVICES
|
||||
SWEEPABLE_CRYPTOS
|
||||
}
|
||||
|
|
|
|||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -1342,7 +1342,7 @@
|
|||
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
|
||||
},
|
||||
"bitcoinjs-message": {
|
||||
"version": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.2",
|
||||
"version": "npm:bitcoinjs-message@1.0.0-master.2",
|
||||
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-message/-/bitcoinjs-message-1.0.0-master.2.tgz",
|
||||
"integrity": "sha512-XSDGM3rA75vcDxeKqHPexika/TgWUFWdfKTv1lV8TZTb5XFHHD6ARckLdMOBiCf29eZSzbJQvF/OIWqNqMl/2A==",
|
||||
"requires": {
|
||||
|
|
@ -4714,36 +4714,6 @@
|
|||
"wif": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"bitcoinjs-message": {
|
||||
"version": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.2",
|
||||
"resolved": "https://registry.npmjs.org/@bitgo-forks/bitcoinjs-message/-/bitcoinjs-message-1.0.0-master.2.tgz",
|
||||
"integrity": "sha512-XSDGM3rA75vcDxeKqHPexika/TgWUFWdfKTv1lV8TZTb5XFHHD6ARckLdMOBiCf29eZSzbJQvF/OIWqNqMl/2A==",
|
||||
"requires": {
|
||||
"bech32": "^1.1.3",
|
||||
"bs58check": "^2.1.2",
|
||||
"buffer-equals": "^1.0.3",
|
||||
"create-hash": "^1.1.2",
|
||||
"secp256k1": "5.0.0",
|
||||
"varuint-bitcoin": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bech32": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
|
||||
"integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="
|
||||
},
|
||||
"secp256k1": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz",
|
||||
"integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==",
|
||||
"requires": {
|
||||
"elliptic": "^6.5.4",
|
||||
"node-addon-api": "^5.0.0",
|
||||
"node-gyp-build": "^4.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bitcore-lib": {
|
||||
"version": "8.25.47",
|
||||
"resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.47.tgz",
|
||||
|
|
@ -8644,12 +8614,12 @@
|
|||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
|
|
@ -12203,6 +12173,16 @@
|
|||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"get-uri": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz",
|
||||
|
|
@ -16423,6 +16403,16 @@
|
|||
"readable-stream": "^2.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"form-data": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
|
||||
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
"@haensl/subset-sum": "^3.0.5",
|
||||
"@lamassu/coins": "v1.4.10",
|
||||
"@simplewebauthn/server": "^3.0.0",
|
||||
"@sumsub/websdk-react": "^1.3.6",
|
||||
"@vonage/auth": "1.5.0",
|
||||
"@vonage/sms": "1.7.0",
|
||||
"@vonage/server-client": "1.7.0",
|
||||
|
|
@ -48,7 +47,6 @@
|
|||
"ethereumjs-wallet": "^0.6.3",
|
||||
"express": "4.17.1",
|
||||
"express-session": "^1.17.1",
|
||||
"express-ws": "^3.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"futoin-hkdf": "^1.0.2",
|
||||
"got": "^7.1.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue