diff --git a/lib/compliance.js b/lib/compliance.js index 22562587..8ecd1770 100644 --- a/lib/compliance.js +++ b/lib/compliance.js @@ -72,13 +72,11 @@ function validateOfac (deviceId, sanctionsActive, customer) { function validationPatch (deviceId, sanctionsActive, customer) { return validateOfac(deviceId, sanctionsActive, customer) - .then(ofacValidation => { - if (_.isNil(customer.sanctions) || customer.sanctions !== ofacValidation) { - return {sanctions: ofacValidation} - } - - return {} - }) + .then(sactions => + _.isNil(customer.sanctions) || customer.sanctions !== sactions ? + { sanctions } : + {} + ) } module.exports = {validationPatch} diff --git a/lib/notifier/index.js b/lib/notifier/index.js index ab75d84e..06163030 100644 --- a/lib/notifier/index.js +++ b/lib/notifier/index.js @@ -166,17 +166,15 @@ function transactionNotify (tx, rec) { }) } -function complianceNotify (customer, deviceId, action, period) { - return Promise.all([ - settingsLoader.loadLatest(), - queries.getMachineName(deviceId) - ]) - .then(([settings, machineName]) => { +function complianceNotify (settings, customer, deviceId, action, period) { + return queries.getMachineName(deviceId) + .then(machineName => { const notifications = configManager.getGlobalNotifications(settings.config) const msgCore = { BLOCKED: `was blocked`, - SUSPENDED: `was suspended for ${!!period && period} days` + SUSPENDED: `was suspended for ${!!period && period} days`, + PENDING_COMPLIANCE: `is waiting for your manual approval`, } const rec = { diff --git a/lib/notifier/notificationCenter.js b/lib/notifier/notificationCenter.js index c5ac36fd..11392248 100644 --- a/lib/notifier/notificationCenter.js +++ b/lib/notifier/notificationCenter.js @@ -38,7 +38,9 @@ const customerComplianceNotify = (customer, deviceId, code, days = null) => { if (days) { date.setDate(date.getDate() + days) } - const message = code === 'SUSPENDED' ? `Customer suspended until ${date.toLocaleString()}` : `Customer blocked` + const message = code === 'SUSPENDED' ? `Customer ${customer.phone} suspended until ${date.toLocaleString()}` : + code === 'BLOCKED' ? `Customer ${customer.phone} blocked` : + `Customer ${customer.phone} has pending compliance` return clearOldCustomerSuspendedNotifications(customer.id, deviceId) .then(() => queries.getValidNotifications(COMPLIANCE, detailB)) diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js index 963cb1f1..d0e11431 100644 --- a/lib/routes/customerRoutes.js +++ b/lib/routes/customerRoutes.js @@ -4,6 +4,7 @@ const semver = require('semver') const _ = require('lodash/fp') const { zonedTimeToUtc, utcToZonedTime } = require('date-fns-tz/fp') const { add, intervalToDuration } = require('date-fns/fp') +const uuid = require('uuid') const sms = require('../sms') const BN = require('../bn') @@ -25,15 +26,54 @@ const Tx = require('../tx') const loyalty = require('../loyalty') const logger = require('../logger') -function updateCustomerCustomInfoRequest (customerId, patch, req, res) { - if (_.isNil(patch.data)) { - return customers.getById(customerId) - .then(customer => respond(req, res, { customer })) +function updateCustomerCustomInfoRequest (customerId, patch) { + const promise = _.isNil(patch.data) ? + Promise.resolve(null) : + customInfoRequestQueries.setCustomerDataViaMachine(customerId, patch.infoRequestId, patch) + return promise.then(() => customers.getById(customerId)) +} + +const createPendingManualComplianceNotifs = (settings, customer, deviceId) => { + const customInfoRequests = _.reduce( + (reqs, req) => _.set(req.info_request_id, req, reqs), + {}, + _.get(['customInfoRequestData'], customer) + ) + + const isPending = field => + uuid.validate(field) ? + _.get([field, 'override'], customInfoRequests) === 'automatic' : + customer[`${field}At`] + && (!customer[`${field}OverrideAt`] + || customer[`${field}OverrideAt`].getTime() < customer[`${field}At`].getTime()) + + const unnestCustomTriggers = triggersAutomation => { + const customTriggers = _.fromPairs(_.map(({ id, type }) => [id, type], triggersAutomation.custom)) + return _.flow( + _.unset('custom'), + _.mapKeys(k => k === 'facephoto' ? 'frontCamera' : k), + _.assign(customTriggers), + )(triggersAutomation) } - return customInfoRequestQueries.setCustomerDataViaMachine(customerId, patch.infoRequestId, patch) - .then(() => customers.getById(customerId)) - .then(customer => respond(req, res, { customer })) + const isManual = v => v === 'Manual' + + const hasManualAutomation = triggersAutomation => + _.any(isManual, _.values(triggersAutomation)) + + configManager.getTriggersAutomation(customInfoRequestQueries.getCustomInfoRequests(true), settings.config) + .then(triggersAutomation => { + triggersAutomation = unnestCustomTriggers(triggersAutomation) + if (!hasManualAutomation(triggersAutomation)) return + + const pendingFields = _.filter( + field => isManual(triggersAutomation[field]) && isPending(field), + _.keys(triggersAutomation) + ) + + if (!_.isEmpty(pendingFields)) + notifier.complianceNotify(settings, customer, deviceId, 'PENDING_COMPLIANCE') + }) } function updateCustomer (req, res, next) { @@ -43,32 +83,35 @@ function updateCustomer (req, res, next) { const patch = req.body const triggers = configManager.getTriggers(req.settings.config) const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers) + const deviceId = req.deviceId + const settings = req.settings if (patch.customRequestPatch) { - return updateCustomerCustomInfoRequest(id, patch.customRequestPatch, req, res).catch(next) + return updateCustomerCustomInfoRequest(id, patch.customRequestPatch) + .then(customer => { + createPendingManualComplianceNotifs(settings, customer, deviceId) + respond(req, res, { customer }) + }) + .catch(next) } + // BACKWARDS_COMPATIBILITY 7.5 + // machines before 7.5 expect customer with sanctions result + const isOlderMachineVersion = !machineVersion || semver.lt(machineVersion, '7.5.0-beta.0') customers.getById(id) + .then(customer => + !customer ? Promise.reject(httpError('Not Found', 404)) : + !isOlderMachineVersion ? {} : + compliance.validationPatch(deviceId, !!compatTriggers.sanctions, _.merge(customer, patch)) + ) + .then(_.merge(patch)) + .then(newPatch => customers.updatePhotoCard(id, newPatch)) + .then(newPatch => customers.updateFrontCamera(id, newPatch)) + .then(newPatch => customers.update(id, newPatch, null, txId)) .then(customer => { - if (!customer) { throw httpError('Not Found', 404) } - - const mergedCustomer = _.merge(customer, patch) - - // BACKWARDS_COMPATIBILITY 7.5 - // machines before 7.5 expect customer with sanctions result - const isOlderMachineVersion = !machineVersion || semver.lt(machineVersion, '7.5.0-beta.0') - - return Promise.resolve({}) - .then(emptyObj => { - if (!isOlderMachineVersion) return Promise.resolve(emptyObj) - return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, mergedCustomer) - }) - .then(_.merge(patch)) - .then(newPatch => customers.updatePhotoCard(id, newPatch)) - .then(newPatch => customers.updateFrontCamera(id, newPatch)) - .then(newPatch => customers.update(id, newPatch, null, txId)) + createPendingManualComplianceNotifs(settings, customer, deviceId) + respond(req, res, { customer }) }) - .then(customer => respond(req, res, { customer })) .catch(next) } @@ -100,10 +143,11 @@ function triggerSanctions (req, res, next) { function triggerBlock (req, res, next) { const id = req.params.id + const settings = req.settings customers.update(id, { authorizedOverride: 'blocked' }) .then(customer => { - notifier.complianceNotify(customer, req.deviceId, 'BLOCKED') + notifier.complianceNotify(settings, customer, req.deviceId, 'BLOCKED') return respond(req, res, { customer }) }) .catch(next) @@ -112,6 +156,7 @@ function triggerBlock (req, res, next) { function triggerSuspend (req, res, next) { const id = req.params.id const triggerId = req.body.triggerId + const settings = req.settings const triggers = configManager.getTriggers(req.settings.config) const getSuspendDays = _.compose(_.get('suspensionDays'), _.find(_.matches({ id: triggerId }))) @@ -122,7 +167,7 @@ function triggerSuspend (req, res, next) { customers.update(id, { suspendedUntil: add(suspensionDuration, new Date()) }) .then(customer => { - notifier.complianceNotify(customer, req.deviceId, 'SUSPENDED', days) + notifier.complianceNotify(settings, customer, req.deviceId, 'SUSPENDED', days) return respond(req, res, { customer }) }) .catch(next) diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js b/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js index d8ca647d..635b1c78 100644 --- a/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js +++ b/new-lamassu-admin/src/pages/Customers/components/CustomInfoRequestsData.js @@ -130,13 +130,12 @@ const CustomInfoRequestsData = ({ data }) => { ) } - const getAuthorizedStatus = it => { - return it.approved === null + const getAuthorizedStatus = it => + it.approved === null ? { label: 'Pending', type: 'neutral' } : it.approved === false ? { label: 'Rejected', type: 'error' } : { label: 'Accepted', type: 'success' } - } return ( <> diff --git a/package-lock.json b/package-lock.json index 1e3f06c7..67ebc670 100644 --- a/package-lock.json +++ b/package-lock.json @@ -770,6 +770,36 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" }, + "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" + } + } + } + }, "ethereumjs-util": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", @@ -960,6 +990,38 @@ "secp256k1": "^4.0.2", "secrets.js-grempe": "^1.1.0", "superagent": "3.8.3" + }, + "dependencies": { + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "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": { + "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" + } + } + } + } } }, "@bitgo/sdk-coin-bch": { @@ -1058,6 +1120,26 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" }, + "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==" + } + } + }, "bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -1259,6 +1341,26 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" }, + "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==" + } + } + }, "bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",