diff --git a/lib/compliance-external.js b/lib/compliance-external.js
new file mode 100644
index 00000000..466b77e9
--- /dev/null
+++ b/lib/compliance-external.js
@@ -0,0 +1,80 @@
+const _ = require('lodash/fp')
+
+const logger = require('./logger')
+const configManager = require('./new-config-manager')
+const ph = require('./plugin-helper')
+
+const getPlugin = (settings, pluginCode) => {
+ const account = settings.accounts[pluginCode]
+ const plugin = ph.load(ph.COMPLIANCE, pluginCode)
+
+ return ({ plugin, account })
+}
+
+const getStatus = (settings, service, customerId) => {
+ try {
+ const { plugin, account } = getPlugin(settings, service)
+
+ return plugin.getApplicantStatus(account, customerId)
+ .then((status) => ({
+ service,
+ status
+ }))
+ .catch((error) => {
+ if (error.response.status !== 404) logger.error(`Error getting applicant for service ${service}:`, error.message)
+ return {
+ service: service,
+ status: null,
+ }
+ })
+ } catch (error) {
+ logger.error(`Error loading plugin for service ${service}:`, error)
+ return Promise.resolve({
+ service: service,
+ status: null,
+ })
+ }
+
+}
+
+const getStatusMap = (settings, customerExternalCompliance) => {
+ const triggers = configManager.getTriggers(settings.config)
+ 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) => {
+ if (result.status) 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)
+}
+
+module.exports = {
+ getStatusMap,
+ getStatus,
+ createApplicant,
+ createLink
+}
diff --git a/lib/customers.js b/lib/customers.js
index 62ef15a3..cb7c588f 100644
--- a/lib/customers.js
+++ b/lib/customers.js
@@ -17,6 +17,9 @@ const NUM_RESULTS = 1000
const sms = require('./sms')
const settingsLoader = require('./new-settings-loader')
const logger = require('./logger')
+const externalCompliance = require('./compliance-external')
+
+const { APPROVED, RETRY } = require('./plugins/compliance/consts')
const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel', 'scoreThresholdReached', 'walletScoringError']
@@ -243,7 +246,7 @@ function deleteEditedData (id, data) {
'id_card_data',
'id_card_photo',
'us_ssn',
- 'subcriber_info',
+ 'subscriber_info',
'name'
]
const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, data))
@@ -322,6 +325,7 @@ function getById (id) {
return db.oneOrNone(sql, [id])
.then(assignCustomerData)
.then(getCustomInfoRequestsData)
+ .then(getExternalComplianceMachine)
.then(camelize)
}
@@ -342,7 +346,11 @@ function camelize (customer) {
function camelizeDeep (customer) {
return _.flow(
camelize,
- it => ({ ...it, notes: (it.notes ?? []).map(camelize) })
+ it => ({
+ ...it,
+ notes: (it.notes ?? []).map(camelize),
+ externalCompliance: (it.externalCompliance ?? []).map(camelize)
+ })
)(customer)
}
@@ -587,6 +595,7 @@ function getCustomerById (id) {
return db.oneOrNone(sql, [passableErrorCodes, id])
.then(assignCustomerData)
.then(getCustomInfoRequestsData)
+ .then(getExternalCompliance)
.then(camelizeDeep)
.then(formatSubscriberInfo)
}
@@ -927,6 +936,95 @@ function updateLastAuthAttempt (customerId) {
return db.none(sql, [customerId])
}
+function getExternalComplianceMachine (customer) {
+ return settingsLoader.loadLatest()
+ .then(settings => externalCompliance.getStatusMap(settings, customer.id))
+ .then(statusMap => {
+ return updateExternalComplianceByMap(customer.id, statusMap)
+ .then(() => customer.externalCompliance = statusMap)
+ .then(() => customer)
+ })
+}
+
+function updateExternalCompliance(customerId, service, status) {
+ const sql = `
+ UPDATE customer_external_compliance SET last_known_status = $1, last_updated = now()
+ WHERE customer_id=$2 AND service=$3
+ `
+ return db.none(sql, [status, customerId, service])
+}
+
+function updateExternalComplianceByMap(customerId, serviceMap) {
+ const sql = `
+ UPDATE customer_external_compliance SET last_known_status = $1, last_updated = now()
+ WHERE customer_id=$2 AND service=$3
+ `
+ const pairs = _.toPairs(serviceMap)
+ const promises = _.map(([service, status]) => db.none(sql, [status.answer, customerId, service]))(pairs)
+ return Promise.all(promises)
+}
+
+function getExternalCompliance(customer) {
+ const sql = `SELECT external_id, service, last_known_status, last_updated
+ FROM customer_external_compliance where customer_id=$1`
+ return db.manyOrNone(sql, [customer.id])
+ .then(compliance => {
+ customer.externalCompliance = compliance
+ })
+ .then(() => customer)
+}
+
+function getOpenExternalCompliance() {
+ const sql = `SELECT customer_id, service, last_known_status FROM customer_external_compliance where last_known_status in ('PENDING', 'RETRY') or last_known_status is null`
+ return db.manyOrNone(sql)
+}
+
+function notifyRetryExternalCompliance(settings, customerId, service) {
+ const sql = 'SELECT phone FROM customers WHERE id=$1'
+ const promises = [db.one(sql, [customerId]), externalCompliance.createLink(settings, service, customerId)]
+
+ return Promise.all(promises)
+ .then(([toNumber, link]) => {
+ const body = `Your external compliance verification has failed. Please try again. Link for retry: ${link}`
+
+ return sms.sendMessage(settings, { toNumber, body })
+ })
+}
+
+function notifyApprovedExternalCompliance(settings, customerId) {
+ const sql = 'SELECT phone FROM customers WHERE id=$1'
+ return db.one(sql, [customerId])
+ .then((toNumber) => {
+ const body = 'Your external compliance verification has been approved.'
+
+ return sms.sendMessage(settings, { toNumber, body })
+ })
+}
+
+function checkExternalCompliance(settings) {
+ return getOpenExternalCompliance()
+ .then(externals => {
+ console.log(externals)
+ const promises = _.map(external => {
+ return externalCompliance.getStatus(settings, external.service, external.customer_id)
+ .then(status => {
+ console.log('status', status, external.customer_id, external.service)
+ if (status.status.answer === RETRY) notifyRetryExternalCompliance(settings, external.customer_id, status.service)
+ if (status.status.answer === APPROVED) notifyApprovedExternalCompliance(settings, external.customer_id)
+
+ return updateExternalCompliance(external.customer_id, external.service, status.status.answer)
+ })
+ }, externals)
+ return Promise.all(promises)
+ })
+}
+
+function addExternalCompliance(customerId, service, id) {
+ const sql = `INSERT INTO customer_external_compliance (customer_id, external_id, service) VALUES ($1, $2, $3)`
+ return db.none(sql, [customerId, id, service])
+}
+
+
module.exports = {
add,
addWithEmail,
@@ -950,5 +1048,7 @@ module.exports = {
updateTxCustomerPhoto,
enableTestCustomer,
disableTestCustomer,
- updateLastAuthAttempt
+ updateLastAuthAttempt,
+ addExternalCompliance,
+ checkExternalCompliance
}
diff --git a/lib/graphql/types.js b/lib/graphql/types.js
index 7977e522..26622c37 100644
--- a/lib/graphql/types.js
+++ b/lib/graphql/types.js
@@ -109,6 +109,7 @@ type Trigger {
thresholdDays: Int
customInfoRequestId: String
customInfoRequest: CustomInfoRequest
+ externalService: String
}
type TermsDetails {
diff --git a/lib/new-admin/config/accounts.js b/lib/new-admin/config/accounts.js
index e5e5231e..ae60a507 100644
--- a/lib/new-admin/config/accounts.js
+++ b/lib/new-admin/config/accounts.js
@@ -15,6 +15,7 @@ const ID_VERIFIER = 'idVerifier'
const EMAIL = 'email'
const ZERO_CONF = 'zeroConf'
const WALLET_SCORING = 'wallet_scoring'
+const COMPLIANCE = 'compliance'
const ALL_ACCOUNTS = [
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
@@ -61,7 +62,9 @@ const ALL_ACCOUNTS = [
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDT_TRON, TRX] },
- { code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true }
+ { code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
+ { code: 'sumsub', display: 'Sumsub', class: COMPLIANCE },
+ { code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true },
]
const devMode = require('minimist')(process.argv.slice(2)).dev
diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js
index d238fda6..fe140032 100644
--- a/lib/new-admin/graphql/types/customer.type.js
+++ b/lib/new-admin/graphql/types/customer.type.js
@@ -40,6 +40,7 @@ const typeDef = gql`
customInfoRequests: [CustomRequestData]
notes: [CustomerNote]
isTestCustomer: Boolean
+ externalCompliance: [JSONObject]
}
input CustomerInput {
diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js
index a87c25cd..302c35df 100644
--- a/lib/new-settings-loader.js
+++ b/lib/new-settings-loader.js
@@ -29,7 +29,9 @@ const SECRET_FIELDS = [
'inforu.apiKey',
'galoy.walletId',
'galoy.apiSecret',
- 'bitfinex.secret'
+ 'bitfinex.secret',
+ 'sumsub.apiToken',
+ 'sumsub.privateKey'
]
/*
diff --git a/lib/plugin-helper.js b/lib/plugin-helper.js
index 6af93c74..de189979 100644
--- a/lib/plugin-helper.js
+++ b/lib/plugin-helper.js
@@ -11,7 +11,8 @@ const pluginCodes = {
LAYER2: 'layer2',
SMS: 'sms',
EMAIL: 'email',
- ZERO_CONF: 'zero-conf'
+ ZERO_CONF: 'zero-conf',
+ COMPLIANCE: 'compliance'
}
module.exports = _.assign({load}, pluginCodes)
diff --git a/lib/plugins/compliance/consts.js b/lib/plugins/compliance/consts.js
new file mode 100644
index 00000000..c6291189
--- /dev/null
+++ b/lib/plugins/compliance/consts.js
@@ -0,0 +1,6 @@
+module.exports = {
+ PENDING: 'PENDING',
+ RETRY: 'RETRY',
+ APPROVED: 'APPROVED',
+ REJECTED: 'REJECTED'
+}
\ No newline at end of file
diff --git a/lib/plugins/compliance/mock-compliance/mock-compliance.js b/lib/plugins/compliance/mock-compliance/mock-compliance.js
new file mode 100644
index 00000000..954b8072
--- /dev/null
+++ b/lib/plugins/compliance/mock-compliance/mock-compliance.js
@@ -0,0 +1,31 @@
+const uuid = require('uuid')
+
+const {APPROVED} = require('../consts')
+
+const CODE = 'mock-compliance'
+
+const createLink = (settings, userId, level) => {
+ return `this is a mock external link, ${userId}, ${level}`
+}
+
+const getApplicantStatus = (account, userId) => {
+ return Promise.resolve({
+ service: CODE,
+ status: {
+ level: account.applicantLevel, answer: APPROVED
+ }
+ })
+}
+
+const createApplicant = () => {
+ return Promise.resolve({
+ id: uuid.v4()
+ })
+}
+
+module.exports = {
+ CODE,
+ createApplicant,
+ getApplicantStatus,
+ createLink
+}
diff --git a/lib/plugins/compliance/sumsub/request.js b/lib/plugins/compliance/sumsub/request.js
new file mode 100644
index 00000000..f102f996
--- /dev/null
+++ b/lib/plugins/compliance/sumsub/request.js
@@ -0,0 +1,34 @@
+const axios = require('axios')
+const crypto = require('crypto')
+const _ = require('lodash/fp')
+const FormData = require('form-data')
+
+const axiosConfig = {
+ baseURL: 'https://api.sumsub.com'
+}
+
+const getSigBuilder = (apiToken, secretKey) => config => {
+ 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(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
+
+ return config
+}
+
+const request = ((account, config) => {
+ const instance = axios.create(axiosConfig)
+ instance.interceptors.request.use(getSigBuilder(account.apiToken, account.secretKey), Promise.reject)
+ return instance(config)
+})
+
+module.exports = request
diff --git a/lib/plugins/compliance/sumsub/sumsub.api.js b/lib/plugins/compliance/sumsub/sumsub.api.js
new file mode 100644
index 00000000..a7d7557a
--- /dev/null
+++ b/lib/plugins/compliance/sumsub/sumsub.api.js
@@ -0,0 +1,98 @@
+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) => {
+ if (!id) {
+ return Promise.reject('Missing required fields: id')
+ }
+
+ const config = {
+ method: 'GET',
+ url: `/resources/applicants/-;externalUserId=${id}/one`,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+const getApplicantStatus = (account, id) => {
+ if (!id) {
+ return Promise.reject(`Missing required fields: id`)
+ }
+
+ const config = {
+ method: 'GET',
+ url: `/resources/applicants/${id}/status`,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+const getApplicantById = (account, id) => {
+ if (!id) {
+ return Promise.reject(`Missing required fields: id`)
+ }
+
+ const config = {
+ method: 'GET',
+ url: `/resources/applicants/${id}/one`,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }
+
+ return request(account, config)
+}
+
+module.exports = {
+ createLink,
+ createApplicant,
+ getApplicantByExternalId,
+ getApplicantById,
+ getApplicantStatus
+}
diff --git a/lib/plugins/compliance/sumsub/sumsub.js b/lib/plugins/compliance/sumsub/sumsub.js
new file mode 100644
index 00000000..80e2d2ea
--- /dev/null
+++ b/lib/plugins/compliance/sumsub/sumsub.js
@@ -0,0 +1,52 @@
+const _ = require('lodash/fp')
+
+const sumsubApi = require('./sumsub.api')
+const { PENDING, RETRY, APPROVED, REJECTED } = require('../consts')
+
+const CODE = 'sumsub'
+
+const getApplicantByExternalId = (account, userId) => {
+ return sumsubApi.getApplicantByExternalId(account, userId)
+ .then(r => r.data)
+}
+
+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 createLink = (account, userId, level) => {
+ return sumsubApi.createLink(account, userId, level)
+ .then(r => r.data.url)
+}
+
+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 last review was from a different level, return the current level and RETRY
+ if (levelName !== account.applicantLevel) return { level: account.applicantLevel, answer: RETRY }
+
+ let answer = PENDING
+ if (reviewAnswer === 'GREEN' && reviewStatus === 'completed') answer = APPROVED
+ if (reviewAnswer === 'RED' && reviewRejectType === 'RETRY') answer = RETRY
+ if (reviewAnswer === 'RED' && reviewRejectType === 'FINAL') answer = REJECTED
+
+ return { level: levelName, answer }
+ })
+}
+
+module.exports = {
+ CODE,
+ createApplicant,
+ getApplicantStatus,
+ createLink
+}
\ No newline at end of file
diff --git a/lib/poller.js b/lib/poller.js
index ca002612..3c33c93b 100644
--- a/lib/poller.js
+++ b/lib/poller.js
@@ -6,6 +6,7 @@ const T = require('./time')
const logger = require('./logger')
const cashOutTx = require('./cash-out/cash-out-tx')
const cashInTx = require('./cash-in/cash-in-tx')
+const customers = require('./customers')
const sanctionsUpdater = require('./ofac/update')
const sanctions = require('./ofac/index')
const coinAtmRadar = require('./coinatmradar/coinatmradar')
@@ -31,6 +32,7 @@ const RADAR_UPDATE_INTERVAL = 5 * T.minutes
const PRUNE_MACHINES_HEARTBEAT = 1 * T.day
const TRANSACTION_BATCH_LIFECYCLE = 20 * T.minutes
const TICKER_RATES_INTERVAL = 59 * T.seconds
+const EXTERNAL_COMPLIANCE_INTERVAL = 1 * T.minutes
const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds
const PENDING_INTERVAL = 10 * T.seconds
@@ -127,6 +129,10 @@ function updateCoinAtmRadar () {
.then(rates => coinAtmRadar.update(rates, settings()))
}
+// function checkExternalCompliance (settings) {
+// return customers.checkExternalCompliance(settings)
+// }
+
function initializeEachSchema (schemas = ['public']) {
// for each schema set "thread variables" and do polling
return _.forEach(schema => {
@@ -190,6 +196,7 @@ function doPolling (schema) {
pi().sweepHd()
notifier.checkNotification(pi())
updateCoinAtmRadar()
+ // checkExternalCompliance(settings())
addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST)
addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST)
@@ -206,6 +213,7 @@ function doPolling (schema) {
addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, schema, QUEUE.SLOW)
addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, schema, QUEUE.SLOW)
addToQueue(pi().pruneMachinesHeartbeat, PRUNE_MACHINES_HEARTBEAT, schema, QUEUE.SLOW, settings)
+ // addToQueue(checkExternalCompliance, EXTERNAL_COMPLIANCE_INTERVAL, schema, QUEUE.SLOW, settings)
}
function setup (schemasToAdd = [], schemasToRemove = []) {
diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js
index d0e11431..0ebb4128 100644
--- a/lib/routes/customerRoutes.js
+++ b/lib/routes/customerRoutes.js
@@ -25,6 +25,7 @@ const plugins = require('../plugins')
const Tx = require('../tx')
const loyalty = require('../loyalty')
const logger = require('../logger')
+const externalCompliance = require('../compliance-external')
function updateCustomerCustomInfoRequest (customerId, patch) {
const promise = _.isNil(patch.data) ?
@@ -234,6 +235,28 @@ function sendSmsReceipt (req, res, next) {
})
}
+function getExternalComplianceLink (req, res, next) {
+ const customerId = req.query.customer
+ const triggerId = req.query.trigger
+ const isRetry = req.query.isRetry
+ if (_.isNil(customerId) || _.isNil(triggerId)) return next(httpError('Not Found', 404))
+
+ const settings = req.settings
+ const triggers = configManager.getTriggers(settings.config)
+ const trigger = _.find(it => it.id === triggerId)(triggers)
+ const externalService = trigger.externalService
+
+ if (isRetry) {
+ return externalCompliance.createLink(settings, externalService, customerId)
+ .then(url => respond(req, res, { url }))
+ }
+
+ return externalCompliance.createApplicant(settings, externalService, customerId)
+ .then(applicant => customers.addExternalCompliance(customerId, externalService, applicant.id))
+ .then(() => externalCompliance.createLink(settings, externalService, customerId))
+ .then(url => respond(req, res, { url }))
+}
+
function addOrUpdateCustomer (customerData, config, isEmailAuth) {
const triggers = configManager.getTriggers(config)
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
@@ -311,6 +334,7 @@ router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
router.post('/:id/smsreceipt', sendSmsReceipt)
+router.get('/external', getExternalComplianceLink)
router.post('/phone_code', getOrAddCustomerPhone)
router.post('/email_code', getOrAddCustomerEmail)
diff --git a/migrations/1718464437502-integrate-sumsub.js b/migrations/1718464437502-integrate-sumsub.js
new file mode 100644
index 00000000..e47348c3
--- /dev/null
+++ b/migrations/1718464437502-integrate-sumsub.js
@@ -0,0 +1,21 @@
+const db = require('./db')
+
+exports.up = function (next) {
+ let sql = [
+ `CREATE TYPE EXTERNAL_COMPLIANCE_STATUS AS ENUM('PENDING', 'APPROVED', 'REJECTED', 'RETRY')`,
+ `CREATE TABLE CUSTOMER_EXTERNAL_COMPLIANCE (
+ customer_id UUID NOT NULL REFERENCES customers(id),
+ service TEXT NOT NULL,
+ external_id TEXT NOT NULL,
+ last_known_status EXTERNAL_COMPLIANCE_STATUS,
+ last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE (customer_id, service)
+ )`
+ ]
+
+ db.multi(sql, next)
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/new-lamassu-admin/src/components/buttons/DeleteButton.js b/new-lamassu-admin/src/components/buttons/DeleteButton.js
new file mode 100644
index 00000000..c1c31082
--- /dev/null
+++ b/new-lamassu-admin/src/components/buttons/DeleteButton.js
@@ -0,0 +1,53 @@
+import { makeStyles } from '@material-ui/core/styles'
+import classnames from 'classnames'
+import React, { memo } from 'react'
+
+import typographyStyles from 'src/components/typography/styles'
+import { ReactComponent as DeleteIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
+import { zircon, zircon2, comet, fontColor, white } from 'src/styling/variables'
+
+const { p } = typographyStyles
+
+const styles = {
+ button: {
+ extend: p,
+ border: 'none',
+ backgroundColor: zircon,
+ cursor: 'pointer',
+ outline: 0,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 167,
+ height: 48,
+ color: fontColor,
+ '&:hover': {
+ backgroundColor: zircon2
+ },
+ '&:active': {
+ backgroundColor: comet,
+ color: white,
+ '& svg g *': {
+ stroke: white
+ }
+ },
+ '& svg': {
+ marginRight: 8
+ }
+ }
+}
+
+const useStyles = makeStyles(styles)
+
+const SimpleButton = memo(({ className, children, ...props }) => {
+ const classes = useStyles()
+
+ return (
+
+ )
+})
+
+export default SimpleButton
diff --git a/new-lamassu-admin/src/components/buttons/index.js b/new-lamassu-admin/src/components/buttons/index.js
index 42530ee3..e0e34f7d 100644
--- a/new-lamassu-admin/src/components/buttons/index.js
+++ b/new-lamassu-admin/src/components/buttons/index.js
@@ -1,6 +1,7 @@
import ActionButton from './ActionButton'
import AddButton from './AddButton'
import Button from './Button'
+import DeleteButton from './DeleteButton'
import FeatureButton from './FeatureButton'
import IDButton from './IDButton'
import IconButton from './IconButton'
@@ -19,5 +20,6 @@ export {
IDButton,
AddButton,
SupportLinkButton,
- SubpageButton
+ SubpageButton,
+ DeleteButton
}
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js
index a2aa9a81..049c0227 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomerData.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js
@@ -64,7 +64,7 @@ const Photo = ({ show, src }) => {
const CustomerData = ({
locale,
- customer,
+ customer = {},
updateCustomer,
replacePhoto,
editCustomer,
@@ -399,6 +399,33 @@ const CustomerData = ({
})
}, R.keys(smsData) ?? [])
+ const externalCompliance = R.map(it => ({
+ fields: [
+ {
+ name: 'externalId',
+ label: 'Third Party ID',
+ editable: false
+ },
+ {
+ name: 'lastKnownStatus',
+ label: 'Last Known Status',
+ editable: false
+ },
+ {
+ name: 'lastUpdated',
+ label: 'Last Updated',
+ editable: false
+ }
+ ],
+ titleIcon: