lamassu-server/lib/plugins/compliance/sumsub/sumsub.js
Sérgio Salgado 6ba0632067 feat: implement sumsub API module
feat: add 3rd party services splash screen
feat: add sumsub as a configurable 3rd party service
feat: sumsub config loader
fix: small fixes

feat: add external validation as a compliance trigger
feat: add external validation route in l-s
feat: add external validation graphql module
feat: integrate sumsub SDK

feat: improve sumsub form to allow adding multiple applicant levels with enhanced UX
feat: added support for array fields in FormRenderer
feat: allow external validation triggers to dynamically use levels setup in the services page
fix: multiple small fixes

feat: get external compliance customer info
fix: small fixes

feat: add informational card in customer profile regarding external service info

feat: send external customer data for machine trigger verification

feat: restrictions to the creation of custom info requests and external validation triggers
fix: allow for a single applicant level to be setup

fix: account instance access

fix: small fixes

fix: development-only log
2024-06-11 11:25:58 +01:00

461 lines
13 KiB
JavaScript

const _ = require('lodash/fp')
const request = require('./request')
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 createApplicantAccessToken = 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/accessTokens?userId=${opts.userId}&levelName=${opts.levelName}`,
headers: {
'Accept': 'application/json'
}
})
}
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 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 getApplicant = (opts, knowsApplicantId = true) => {
const REQUIRED_FIELDS = knowsApplicantId
? ['applicantId']
: ['externalUserId']
if (_.isEmpty(opts) || !hasRequiredFields(REQUIRED_FIELDS, opts)) {
return Promise.reject(`Missing required fields: ${getMissingRequiredFields(REQUIRED_FIELDS, opts)}`)
}
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'
}
})
}
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`
})
}
module.exports = {
CODE,
createApplicantAccessToken,
createApplicant,
getApplicant,
addIdDocument,
changeApplicantFixedInfo,
getApplicantStatus,
getApplicantIdDocsStatus,
getApplicantRejectReasons,
requestApplicantCheck,
requestApplicantCheckDiffVerificationType,
getDocumentImages,
blockApplicant,
generateShareToken,
importRawApplicant,
importApplicantFromPartnerService,
resetVerificationStep,
resetApplicant,
patchApplicantTopLevelInfo,
setApplicantRiskLevel,
addApplicantTags,
markImageAsInactive,
markImageAsActive,
getApiHealth,
changeRequiredLevel
}