This commit is contained in:
Josh Harvey 2018-05-01 19:35:54 +03:00
parent f7561acf3c
commit 80e851fb59
9 changed files with 131 additions and 76 deletions

9
bin/lamassu-ofac-update Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env node
const ofac = require('../lib/ofac/update')
console.log('Updating OFAC databases.')
ofac.update()
.then(() => console.log('Success.'))
.catch(console.log)

View file

@ -8,6 +8,7 @@ const logger = require('./logger')
const poller = require('./poller') const poller = require('./poller')
const settingsLoader = require('./settings-loader') const settingsLoader = require('./settings-loader')
const options = require('./options') const options = require('./options')
const ofac = require('./ofac/index')
const devMode = argv.dev || options.http const devMode = argv.dev || options.http
@ -32,7 +33,8 @@ function run () {
} }
function runOnce () { function runOnce () {
return settingsLoader.loadLatest() return ofac.load()
.then(settingsLoader.loadLatest())
.then(settings => { .then(settings => {
poller.start(settings) poller.start(settings)

23
lib/compliance.js Normal file
View file

@ -0,0 +1,23 @@
const _ = require('lodash/fp')
const ofac = require('./ofac/index')
function matchOfac (customer) {
const nameParts = _.flatMap(_.split(/\s+/), [customer.firstName, customer.lastName])
const birthDate = customer.dateOfBirth
const result = ofac.match(nameParts, birthDate)
console.log('DEBUG200: %s', result)
if (result > 0.8) throw new Error('Compliance error')
}
function validateCustomer (config, customer) {
if (config.sanctionsVerificationActive) {
matchOfac(customer)
}
return customer
}
module.exports = {validateCustomer}

View file

@ -328,4 +328,4 @@ function batch () {
}, customers))) }, customers)))
} }
module.exports = { add, get, batch, getById, update } module.exports = {add, get, batch, getById, update}

View file

@ -7,9 +7,7 @@ const nameUtils = require('./name-utils')
const options = require('../options') const options = require('../options')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const debug_log = require('../pp')(__filename) // KOSTIS TODO: remove const debugLog = require('../pp')(__filename) // KOSTIS TODO: remove
const OFAC_SOURCES_DIR = path.join(options.ofacDataDir, 'sources')
let structs = null let structs = null
@ -18,19 +16,21 @@ const readdir = util.promisify(fs.readdir)
function load () { function load () {
// NOTE: Not sure how you push code updates to existing clients. This problem // NOTE: Not sure how you push code updates to existing clients. This problem
// might pop up if new code is pushed, without re-doing setup. // might pop up if new code is pushed, without re-doing setup.
if (!OFAC_SOURCES_DIR) { if (!options.ofacDataDir) {
const message = 'The ofacDataDir option has not been set in lamassu.json' const message = 'The ofacDataDir option has not been set in lamassu.json'
return Promise.reject(new Error(message)) return Promise.reject(new Error(message))
} }
return readdir(OFAC_SOURCES_DIR) const ofacSourcesDir = path.join(options.ofacDataDir, 'sources')
.then(_.flow(
_.map(file => path.join(OFAC_SOURCES_DIR, file)), return readdir(ofacSourcesDir)
loader.load .then(_.flow(
)) _.map(file => path.join(ofacSourcesDir, file)),
.then(result => { loader.load
return (structs = result) ))
}) .then(result => {
return (structs = result)
})
} }
// nameParts should be an object like {firstName: "John", lastName: "Doe", ...} // nameParts should be an object like {firstName: "John", lastName: "Doe", ...}
@ -69,10 +69,10 @@ function match (nameParts, birthDateString, options) {
])(birthDateString) ])(birthDateString)
const candidate = {parts, fullName, words, birthDate} const candidate = {parts, fullName, words, birthDate}
debug && debug_log(candidate) debug && debugLog(candidate)
const result = matcher.match(structs, candidate, options) const result = matcher.match(structs, candidate, options)
debug && debug_log(result) debug && debugLog(result)
return result return result
} }

View file

@ -1,7 +1,7 @@
const jaro = require('talisman/metrics/distance/jaro') const jaro = require('talisman/metrics/distance/jaro')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const debug_log = require('../pp')(__filename) // KOSTIS TODO: remove const debugLog = require('../pp')(__filename) // KOSTIS TODO: remove
const stringSimilarity = _.curry(jaro) const stringSimilarity = _.curry(jaro)
@ -44,7 +44,6 @@ function match (structs, candidate, options) {
_.map(_.get('id')) _.map(_.get('id'))
)(aliases) )(aliases)
const phoneticWeight = ratio const phoneticWeight = ratio
const stringWeight = 1 - phoneticWeight const stringWeight = 1 - phoneticWeight
@ -90,8 +89,8 @@ function match (structs, candidate, options) {
_.map(_.first) _.map(_.first)
)(matches) )(matches)
debug && debug_log(aliasIdsFromFullName) debug && debugLog(aliasIdsFromFullName)
debug && debug_log(aliasIdsFromNamePart) debug && debugLog(aliasIdsFromNamePart)
// Get the full record for each matched id // Get the full record for each matched id
const getIndividual = aliasId => { const getIndividual = aliasId => {

View file

@ -7,23 +7,28 @@ const util = require('util')
const options = require('../options') const options = require('../options')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const OFAC_DATA_DIR = options.ofacDataDir
const OFAC_SOURCES_DIR = path.join(OFAC_DATA_DIR, 'sources')
const OFAC_ETAGS_FILE = path.join(OFAC_DATA_DIR, 'etags.json')
const DOWNLOAD_DIR = path.resolve('/tmp') const DOWNLOAD_DIR = path.resolve('/tmp')
function mkdir (path) {
return new Promise((resolve, reject) => {
fs.mkdir(path, err => {
if (!err) return resolve()
if (err.code === 'EEXIST') return resolve()
reject(err)
})
})
}
const mkdir = util.promisify(fs.mkdir)
const readFile = util.promisify(fs.readFile) const readFile = util.promisify(fs.readFile)
const writeFile = util.promisify(fs.writeFile) const writeFile = util.promisify(fs.writeFile)
const rename = util.promisify(fs.rename) const rename = util.promisify(fs.rename)
const unlink = util.promisify(fs.unlink) const unlink = util.promisify(fs.unlink)
const remove = file => { const remove = file => {
console.log("remove", file) console.log('remove', file)
return unlink(file) return unlink(file)
} }
const promiseGetEtag = (source) => { const promiseGetEtag = (source) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const {url: sourceUrl} = source const {url: sourceUrl} = source
@ -46,14 +51,14 @@ const promiseGetEtag = (source) => {
} }
const download = _.curry((dstDir, source) => { const download = _.curry((dstDir, source) => {
console.log("download", source) console.log('download', source)
const {name, url: sourceUrl} = source const {name, url: sourceUrl} = source
const dstFile = path.join(dstDir, name + '.xml') const dstFile = path.join(dstDir, name + '.xml')
const file = fs.createWriteStream(dstFile) const file = fs.createWriteStream(dstFile)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = https.get(sourceUrl, response => { const request = https.get(sourceUrl, response => {
response.pipe(file); response.pipe(file)
file.on('finish', () => file.close(() => resolve(dstFile))) file.on('finish', () => file.close(() => resolve(dstFile)))
}) })
@ -62,14 +67,14 @@ const download = _.curry((dstDir, source) => {
}) })
const parseToJson = srcFile => { const parseToJson = srcFile => {
console.log("parseToJson", srcFile) console.log('parseToJson', srcFile)
const dstFile = srcFile.replace(/\.xml$/, '.json') const dstFile = srcFile.replace(/\.xml$/, '.json')
const writeStream = fs.createWriteStream(dstFile) const writeStream = fs.createWriteStream(dstFile)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
parser.parse(srcFile, (err, profile) => { parser.parse(srcFile, (err, profile) => {
console.log("callback", err, profile) console.log('callback', err, profile)
if (err) { if (err) {
reject(err) reject(err)
@ -90,65 +95,78 @@ const parseToJson = srcFile => {
}) })
} }
const moveToSourcesDir = srcFile => { const moveToSourcesDir = (srcFile, ofacSourcesDir) => {
console.log("moveToSourcesDir", srcFile) console.log('moveToSourcesDir', srcFile)
const name = path.basename(srcFile) const name = path.basename(srcFile)
const dstFile = path.join(OFAC_SOURCES_DIR, name) const dstFile = path.join(ofacSourcesDir, name)
return rename(srcFile, dstFile) return rename(srcFile, dstFile)
} }
function update () {
const OFAC_DATA_DIR = options.ofacDataDir
const update = () => mkdir(OFAC_DATA_DIR).catch(err => null) if (!OFAC_DATA_DIR) {
.then(() => mkdir(OFAC_SOURCES_DIR)).catch(err => null) throw new Error('ofacDataDir must be defined in lamassu.json')
.then(() => writeFile(OFAC_ETAGS_FILE, '{}', {encoding: 'utf-8', flag: 'wx'})) }
.catch(err => null)
.then(() => {
const promiseOldEtags = readFile(OFAC_ETAGS_FILE, {encoding: 'utf-8'})
.then(json => JSON.parse(json) || {})
const promiseNewEtags = Promise.resolve(options.ofacSources || []) const OFAC_SOURCES_DIR = path.join(OFAC_DATA_DIR, 'sources')
.then(sources => Promise.all(_.map(promiseGetEtag, sources)) const OFAC_ETAGS_FILE = path.join(OFAC_DATA_DIR, 'etags.json')
.then(etags => _.map(
([source, etag]) => ({...source, etag}),
_.zip(sources, etags)
))
)
return Promise.all([promiseOldEtags, promiseNewEtags]) return mkdir(OFAC_DATA_DIR)
.then(([oldEtags, newEtags]) => { .then(() => mkdir(OFAC_SOURCES_DIR))
console.log("OLD", JSON.stringify(oldEtags, null, 4)) .then(() => writeFile(OFAC_ETAGS_FILE, '{}', {encoding: 'utf-8', flag: 'wx'}))
console.log("NEW", JSON.stringify(newEtags, null, 4)) .catch(err => {
if (err.code === 'EEXIST') return
throw err
})
.then(() => {
const promiseOldEtags = readFile(OFAC_ETAGS_FILE, {encoding: 'utf-8'})
.then(json => JSON.parse(json) || {})
const hasNotChanged = ({name, etag}) => oldEtags[name] === etag const promiseNewEtags = Promise.resolve(options.ofacSources || [])
.then(sources => Promise.all(_.map(promiseGetEtag, sources))
.then(etags => _.map(
([source, etag]) => ({...source, etag}),
_.zip(sources, etags)
))
)
const downloads = _.flow( return Promise.all([promiseOldEtags, promiseNewEtags])
_.reject(hasNotChanged), .then(([oldEtags, newEtags]) => {
_.map(file => download(DOWNLOAD_DIR, file).then(parseToJson)) console.log('OLD', JSON.stringify(oldEtags, null, 4))
)(newEtags) console.log('NEW', JSON.stringify(newEtags, null, 4))
const oldFileNames = _.keys(oldEtags) const hasNotChanged = ({name, etag}) => oldEtags[name] === etag
const newFileNames = _.map(_.get('name'), newEtags)
const missingFileNames = _.difference(oldFileNames, newFileNames)
const resolve = name => path.join(OFAC_SOURCES_DIR, name + '.json')
const missing = _.map(resolve, missingFileNames)
const etagsJson = _.flow( const downloads = _.flow(
_.map(source => [source.name, source.etag]), _.reject(hasNotChanged),
_.fromPairs, _.map(file => download(DOWNLOAD_DIR, file).then(parseToJson))
obj => JSON.stringify(obj, null, 4) )(newEtags)
)(newEtags)
return Promise.all(downloads) const oldFileNames = _.keys(oldEtags)
.then(parsed => { const newFileNames = _.map(_.get('name'), newEtags)
console.log("finished", parsed) const missingFileNames = _.difference(oldFileNames, newFileNames)
const resolve = name => path.join(OFAC_SOURCES_DIR, name + '.json')
const missing = _.map(resolve, missingFileNames)
const moves = _.map(moveToSourcesDir, parsed) const etagsJson = _.flow(
const deletions = _.map(remove, missing) _.map(source => [source.name, source.etag]),
const updateEtags = writeFile(OFAC_ETAGS_FILE, etagsJson) _.fromPairs,
obj => JSON.stringify(obj, null, 4)
)(newEtags)
return Promise.all([updateEtags, ...moves, ...deletions]) return Promise.all(downloads)
.then(parsed => {
console.log('finished', parsed)
const moves = _.map(src => moveToSourcesDir(src, OFAC_SOURCES_DIR), parsed)
const deletions = _.map(remove, missing)
const updateEtags = writeFile(OFAC_ETAGS_FILE, etagsJson)
return Promise.all([updateEtags, ...moves, ...deletions])
})
}) })
}) })
}) }
module.exports = {update} module.exports = {update}

View file

@ -186,11 +186,14 @@ function getCustomerWithPhoneCode (req, res, next) {
function updateCustomer (req, res, next) { function updateCustomer (req, res, next) {
const id = req.params.id const id = req.params.id
const patch = req.body const patch = req.body
const config = configManager.unscoped(req.settings.config)
customers.getById(id) customers.getById(id)
.then(customer => { .then(customer => {
if (!customer) { throw httpError('Not Found', 404) } if (!customer) { throw httpError('Not Found', 404) }
return customers.update(id, patch) return customers.update(id, patch)
}) })
.then(customer => customers.validate(config, customer))
.then(customer => respond(req, res, {customer})) .then(customer => respond(req, res, {customer}))
.catch(next) .catch(next)
} }

View file

@ -75,7 +75,8 @@
"lamassu-cancel": "./bin/lamassu-cancel", "lamassu-cancel": "./bin/lamassu-cancel",
"lamassu-nuke-db": "./bin/lamassu-nuke-db", "lamassu-nuke-db": "./bin/lamassu-nuke-db",
"lamassu-coins": "./bin/lamassu-coins", "lamassu-coins": "./bin/lamassu-coins",
"lamassu-update": "./bin/lamassu-update" "lamassu-update": "./bin/lamassu-update",
"lamassu-ofac-update": "./bin/lamassu-ofac-update"
}, },
"scripts": { "scripts": {
"start": "node bin/lamassu-server", "start": "node bin/lamassu-server",