Merge pull request #806 from josepfo/feat/handle-customer-tc-photos

feat: handle T&C photos and UI photos roll
This commit is contained in:
Rafael Taranto 2021-11-11 18:26:51 +00:00 committed by GitHub
commit 5570f81802
17 changed files with 604 additions and 74 deletions

View file

@ -519,7 +519,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
*/
function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
@ -528,7 +528,7 @@ function getCustomerById (id) {
select c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - now())) as days_suspended,
c.suspended_until > now() as is_suspended,
c.front_camera_path, c.front_camera_override,
c.front_camera_path, c.front_camera_at, c.front_camera_override,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created,
@ -654,6 +654,57 @@ function updateIdCardData (patch, id) {
})
}
/**
* @param {String} imageData customer t&c photo data
* @returns {Promise<Object>} new patch to be applied
*/
function updateTxCustomerPhoto (imageData) {
return Promise.resolve(imageData)
.then(imageData => {
const newPatch = {}
const directory = `${operatorDataDir}/customersphotos`
if (_.isEmpty(imageData)) {
return
}
// decode the base64 string to binary data
const decodedImageData = Buffer.from(imageData, 'base64')
// workout the image hash
// i.e. 240e85ff2e4bb931f235985dd0134e459239496d2b5af6c5665168d38ef89b50
const hash = crypto
.createHash('sha256')
.update(imageData)
.digest('hex')
// workout the image folder
// i.e. 24/0e/85
const rpath = _.join(path.sep, _.map(_.wrap(_.join, ''), _.take(3, _.chunk(2, _.split('', hash)))))
// i.e. ../<lamassu-server-home>/<operator-dir>/customersphotos/24/0e/85
const dirname = path.join(directory, rpath)
// create the directory tree if needed
_.attempt(() => makeDir.sync(dirname))
// i.e. ../<lamassu-server-home>/<operator-dir>/customersphotos/24/0e/85/240e85ff2e4bb931f235985dd01....jpg
const filename = path.join(dirname, hash + '.jpg')
// update db record patch
// i.e. {
// "idCustomerTxPhoto": "24/0e/85/240e85ff2e4bb931f235985dd01....jpg",
// "idCustomerTxPhotoAt": "now()"
// }
newPatch.txCustomerPhotoPath = path.join(rpath, hash + '.jpg')
newPatch.txCustomerPhotoAt = 'now()'
// write image file
return writeFile(filename, decodedImageData)
.then(() => newPatch)
})
}
function updateFrontCamera (id, patch) {
return Promise.resolve(patch)
.then(patch => {
@ -704,4 +755,4 @@ function updateFrontCamera (id, patch) {
})
}
module.exports = { add, get, batch, getCustomersList, getCustomerById, getById, update, updateCustomer, updatePhotoCard, updateFrontCamera, updateIdCardData }
module.exports = { add, get, batch, getCustomersList, getCustomerById, getById, update, updateCustomer, updatePhotoCard, updateFrontCamera, updateIdCardData, updateTxCustomerPhoto }

View file

@ -22,6 +22,7 @@ const { typeDefs, resolvers } = require('./graphql/schema')
const devMode = require('minimist')(process.argv.slice(2)).dev
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
const frontCameraBasedir = _.get('frontCameraDir', options)
const operatorDataBasedir = _.get('operatorDataDir', options)
const hostname = options.hostname
if (!hostname) {
@ -87,6 +88,7 @@ app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3001' })
app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, { index: false }))
app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false }))
app.use('/operator-data', serveStatic(operatorDataBasedir, { index: false }))
// Everything not on graphql or api/register is redirected to the front-end
app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))

View file

@ -7,6 +7,7 @@ const typeDef = gql`
daysSuspended: Int
isSuspended: Boolean
frontCameraPath: String
frontCameraAt: Date
frontCameraOverride: String
phone: String
isAnonymous: Boolean

View file

@ -43,6 +43,8 @@ const typeDef = gql`
expired: Boolean
machineName: String
discount: Int
txCustomerPhotoPath: String
txCustomerPhotoAt: Date
}
type Filter {

View file

@ -49,6 +49,8 @@ function batch (
array_to_string(array[c.id_card_data::json->>'firstName', c.id_card_data::json->>'lastName'], ' ') AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
txs.tx_customer_photo_at AS tx_customer_photo_at,
txs.tx_customer_photo_path AS tx_customer_photo_path,
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired
FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
@ -76,6 +78,8 @@ function batch (
array_to_string(array[c.id_card_data::json->>'firstName', c.id_card_data::json->>'lastName'], ' ') AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
txs.tx_customer_photo_at AS tx_customer_photo_at,
txs.tx_customer_photo_path AS tx_customer_photo_path,
(extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $1 AS expired
FROM (SELECT *, ${CASH_OUT_TRANSACTION_STATES} AS txStatus FROM cash_out_txs) txs
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
@ -270,4 +274,23 @@ function getTxAssociatedData (txId, txClass) {
: db.manyOrNone(actionsSql, [txId])
}
module.exports = { batch, single, cancel, getCustomerTransactionsBatch, getTx, getTxAssociatedData }
function updateTxCustomerPhoto (customerId, txId, direction, data) {
const formattedData = _.mapKeys(_.snakeCase, data)
const cashInSql = 'UPDATE cash_in_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4'
const cashOutSql = 'UPDATE cash_out_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4'
return direction === 'cashIn'
? db.oneOrNone(cashInSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId])
: db.oneOrNone(cashOutSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId])
}
module.exports = {
batch,
single,
cancel,
getCustomerTransactionsBatch,
getTx,
getTxAssociatedData,
updateTxCustomerPhoto
}

View file

@ -7,6 +7,7 @@ const compliance = require('../compliance')
const complianceTriggers = require('../compliance-triggers')
const configManager = require('../new-config-manager')
const customers = require('../customers')
const txs = require('../new-admin/services/transactions')
const httpError = require('../route-helpers').httpError
const notifier = require('../notifier')
const respond = require('../respond')
@ -99,10 +100,27 @@ function triggerSuspend (req, res, next) {
.catch(next)
}
function updateTxCustomerPhoto (req, res, next) {
const customerId = req.params.id
const txId = req.params.txId
const tcPhotoData = req.body.tcPhotoData
const direction = req.body.direction
Promise.all([customers.getById(customerId), txs.getTx(txId, direction)])
.then(([customer, tx]) => {
if (!customer || !tx) return
return customers.updateTxCustomerPhoto(tcPhotoData)
.then(newPatch => txs.updateTxCustomerPhoto(customerId, txId, direction, newPatch))
})
.then(() => respond(req, res, {}))
.catch(next)
}
router.patch('/:id', updateCustomer)
router.patch('/:id/sanctions', triggerSanctions)
router.patch('/:id/block', triggerBlock)
router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
module.exports = router