chore: use monorepo organization

This commit is contained in:
Rafael Taranto 2025-05-12 10:52:54 +01:00
parent deaf7d6ecc
commit a687827f7e
1099 changed files with 8184 additions and 11535 deletions

View file

@ -0,0 +1,40 @@
const express = require('express')
const _ = require('lodash/fp')
const router = express.Router()
const cashbox = require('../cashbox-batches')
const notifier = require('../notifier')
const { getMachine, setMachine, getMachineName } = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader')
const { getCashInSettings } = require('../new-config-manager')
const { AUTOMATIC } = require('../constants')
const logger = require('../logger')
function cashboxRemoval (req, res, next) {
const operatorId = res.locals.operatorId
notifier.cashboxNotify(req.deviceId).catch(logger.error)
return Promise.all([getMachine(req.deviceId), loadLatestConfig()])
.then(([machine, config]) => {
const cashInSettings = getCashInSettings(config)
if (cashInSettings.cashboxReset !== AUTOMATIC) {
return Promise.all([
cashbox.getMachineUnbatchedBills(req.deviceId),
getMachineName(req.deviceId)
])
}
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
.then(batch => Promise.all([
cashbox.getBatchById(batch.id),
getMachineName(batch.device_id)
]))
})
.then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' }))
.catch(next)
}
router.post('/removal', cashboxRemoval)
module.exports = router

View file

@ -0,0 +1,331 @@
const express = require('express')
const router = express.Router()
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')
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')
const { getTx } = require('../new-admin/services/transactions.js')
const machineLoader = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
const T = require('../time')
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) ?
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)
}
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) {
const id = req.params.id
const patch = req.body
const deviceId = req.deviceId
const settings = req.settings
if (patch.customRequestPatch) {
return updateCustomerCustomInfoRequest(id, patch.customRequestPatch)
.then(customer => {
createPendingManualComplianceNotifs(settings, customer, deviceId)
respond(req, res, { customer })
})
.catch(next)
}
customers.getById(id)
.then(customer => !customer ? Promise.reject(httpError('Not Found', 404)) : {})
.then(_.merge(patch))
.then(newPatch => customers.updatePhotoCard(id, newPatch))
.then(newPatch => customers.updateFrontCamera(id, newPatch))
.then(newPatch => customers.update(id, newPatch, null))
.then(customer => {
createPendingManualComplianceNotifs(settings, customer, deviceId)
respond(req, res, { customer })
})
.catch(next)
}
function updateIdCardData (req, res, next) {
const id = req.params.id
const patch = req.body
customers.getById(id)
.then(customer => {
if (!customer) { throw httpError('Not Found', 404) }
return customers.updateIdCardData(patch, id)
.then(() => customer)
})
.then(customer => respond(req, res, { customer }))
.catch(next)
}
function triggerSanctions (req, res, next) {
const id = req.params.id
customers.getById(id)
.then(customer => {
if (!customer) { throw httpError('Not Found', 404) }
return compliance.validationPatch(req.deviceId, customer)
.then(patch => customers.update(id, patch))
})
.then(customer => respond(req, res, { customer }))
.catch(next)
}
function triggerBlock (req, res, next) {
const id = req.params.id
const settings = req.settings
customers.update(id, { authorizedOverride: 'blocked' })
.then(customer => {
notifier.complianceNotify(settings, customer, req.deviceId, 'BLOCKED')
return respond(req, res, { customer })
})
.catch(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 })))
const days = _.includes(triggerId, ['no-ff-camera', 'id-card-photo-disabled']) ? 1 : getSuspendDays(triggers)
const suspensionDuration = intervalToDuration({ start: 0, end: T.day * days })
customers.update(id, { suspendedUntil: add(suspensionDuration, new Date()) })
.then(customer => {
notifier.complianceNotify(settings, customer, req.deviceId, 'SUSPENDED', days)
return respond(req, res, { customer })
})
.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)
}
function buildSms (data, receiptOptions) {
return Promise.all([getTx(data.session, data.txClass), loadLatestConfig()])
.then(([tx, config]) => {
return Promise.all([customers.getCustomerById(tx.customer_id), machineLoader.getMachine(tx.device_id, config)])
.then(([customer, deviceConfig]) => {
const formattedTx = _.mapKeys(_.camelCase)(tx)
const localeConfig = configManager.getLocale(formattedTx.deviceId, config)
const timezone = localeConfig.timezone
const cashInCommission = new BN(1).plus(new BN(formattedTx.commissionPercentage))
const rate = new BN(formattedTx.rawTickerPrice).multipliedBy(cashInCommission).decimalPlaces(2)
const date = utcToZonedTime(timezone, zonedTimeToUtc(process.env.TZ, new Date()))
const dateString = `${date.toISOString().replace('T', ' ').slice(0, 19)}`
const data = {
operatorInfo: configManager.getOperatorInfo(config),
location: deviceConfig.machineLocation,
customerName: customer.name,
customerPhone: customer.phone,
session: formattedTx.id,
time: dateString,
direction: formattedTx.txClass === 'cashIn' ? 'Cash-in' : 'Cash-out',
fiat: `${formattedTx.fiat.toString()} ${formattedTx.fiatCode}`,
crypto: `${sms.toCryptoUnits(BN(formattedTx.cryptoAtoms), formattedTx.cryptoCode)} ${formattedTx.cryptoCode}`,
rate: `1 ${formattedTx.cryptoCode} = ${rate} ${formattedTx.fiatCode}`,
address: formattedTx.toAddress,
txId: formattedTx.txHash
}
return sms.formatSmsReceipt(data, receiptOptions)
})
})
}
function sendSmsReceipt (req, res, next) {
const receiptOptions = _.omit(['active', 'sms'], configManager.getReceipt(req.settings.config))
buildSms(req.body.data, receiptOptions)
.then(smsRequest => {
sms.sendMessage(req.settings, smsRequest)
.then(() => respond(req, res, {}))
.catch(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, deviceId, config, isEmailAuth) {
const triggers = configManager.getTriggers(config)
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
const customerKey = isEmailAuth ? customerData.email : customerData.phone
const getFunc = isEmailAuth ? customers.getWithEmail : customers.get
const addFunction = isEmailAuth ? customers.addWithEmail : customers.add
return getFunc(customerKey)
.then(customer => {
if (customer) return customer
return addFunction(customerData)
})
.then(customer => customers.getById(customer.id))
.then(customer => {
customers.updateLastAuthAttempt(customer.id, deviceId).catch(() => {
logger.info('failure updating last auth attempt for customer ', customer.id)
})
return customer
})
.then(customer => {
return Tx.customerHistory(customer.id, maxDaysThreshold)
.then(result => {
customer.txHistory = result
return customer
})
})
.then(customer => {
return loyalty.getCustomerActiveIndividualDiscount(customer.id)
.then(discount => ({ ...customer, discount }))
})
}
function getOrAddCustomerPhone (req, res, next) {
const deviceId = req.deviceId
const customerData = req.body
const pi = plugins(req.settings, deviceId)
const phone = req.body.phone
return pi.getPhoneCode(phone)
.then(code => {
return addOrUpdateCustomer(customerData, deviceId, req.settings.config, false)
.then(customer => respond(req, res, { code, customer }))
})
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
throw err
})
.catch(next)
}
function getOrAddCustomerEmail (req, res, next) {
const deviceId = req.deviceId
const customerData = req.body
const pi = plugins(req.settings, req.deviceId)
const email = req.body.email
return pi.getEmailCode(email)
.then(code => {
return addOrUpdateCustomer(customerData, deviceId, req.settings.config, true)
.then(customer => respond(req, res, { code, customer }))
})
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
throw err
})
.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)
router.post('/:id/smsreceipt', sendSmsReceipt)
router.get('/external', getExternalComplianceLink)
router.post('/phone_code', getOrAddCustomerPhone)
router.post('/email_code', getOrAddCustomerEmail)
module.exports = router

View file

@ -0,0 +1,14 @@
const express = require('express')
const router = express.Router()
const { updateDiagnostics } = require('../machine-loader')
function diagnostics (req, res, next) {
return updateDiagnostics(req.deviceId, req.body)
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
router.post('/', diagnostics)
module.exports = router

View file

@ -0,0 +1,14 @@
const express = require('express')
const router = express.Router()
const { updateFailedQRScans } = require('../machine-loader')
function failedQRScans (req, res, next) {
return updateFailedQRScans(req.deviceId, req.body)
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
router.post('/', failedQRScans)
module.exports = router

View file

@ -0,0 +1,34 @@
const express = require('express')
const router = express.Router()
const state = require('../middlewares/state')
const logs = require('../logs')
const THROTTLE_LOGS_QUERY = 30 * 1000
function getLastSeen (req, res, next) {
const deviceId = req.deviceId
const timestamp = Date.now()
const shouldTrigger = !state.canGetLastSeenMap[deviceId] ||
timestamp - state.canGetLastSeenMap[deviceId] >= THROTTLE_LOGS_QUERY
if (shouldTrigger) {
state.canGetLastSeenMap[deviceId] = timestamp
return logs.getLastSeen(deviceId)
.then(r => res.json(r))
.catch(next)
}
return res.status(408).json({})
}
function updateLogs (req, res, next) {
return logs.update(req.deviceId, req.body.logs)
.then(status => res.json({ success: status }))
.catch(next)
}
router.get('/', getLastSeen)
router.post('/', updateLogs)
module.exports = router

View file

@ -0,0 +1,27 @@
const express = require('express')
const router = express.Router()
const ca = require('../middlewares/ca')
const httpError = require('../route-helpers').httpError
const pairing = require('../pairing')
const populateDeviceId = require('../middlewares/populateDeviceId')
function pair (req, res, next) {
const token = req.query.token
const deviceId = req.deviceId
const model = req.query.model
const numOfCassettes = req.query.numOfCassettes
const numOfRecyclers = req.query.numOfRecyclers
return pairing.pair(token, deviceId, model, numOfCassettes, numOfRecyclers)
.then(isValid => {
if (isValid) return res.json({ status: 'paired' })
throw httpError('Pairing failed')
})
.catch(next)
}
router.post('/pair', populateDeviceId, pair)
router.get('/ca', ca)
module.exports = router

View file

@ -0,0 +1,21 @@
const express = require('express')
const router = express.Router()
const { updateNetworkHeartbeat, updateNetworkPerformance } = require('../machine-loader')
function networkHeartbeat (req, res, next) {
return updateNetworkHeartbeat(req.deviceId, req.body)
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
function networkPerformance (req, res, next) {
return updateNetworkPerformance(req.deviceId, req.body)
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
router.post('/heartbeat', networkHeartbeat)
router.post('/performance', networkPerformance)
module.exports = router

View file

@ -0,0 +1,20 @@
const express = require('express')
const router = express.Router()
const plugins = require('../plugins')
const settingsLoader = require('../new-settings-loader')
function probe (req, res, next) {
// TODO: why req.settings is undefined?
settingsLoader.loadLatest()
.then(settings => {
const pi = plugins(settings, req.deviceId)
return pi.probeLN('LN', req.body.address)
.then(r => res.status(200).send({ hardLimits: r }))
.catch(next)
})
}
router.get('/', probe)
module.exports = router

View file

@ -0,0 +1,15 @@
const express = require('express')
const router = express.Router()
const helpers = require('../route-helpers')
const respond = require('../respond')
function stateChange (req, res, next) {
helpers.stateChange(req.deviceId, req.deviceTime, req.body)
.then(() => respond(req, res))
.catch(next)
}
router.post('/', stateChange)
module.exports = router

View file

@ -0,0 +1,30 @@
const express = require('express')
const nmd = require('nano-markdown')
const router = express.Router()
const configManager = require('../new-config-manager')
const settingsLoader = require('../new-settings-loader')
const createTerms = terms => (terms.active && terms.text) ? ({
delay: terms.delay,
active: terms.active,
tcPhoto: terms.tcPhoto,
title: terms.title,
text: nmd(terms.text),
accept: terms.acceptButtonText,
cancel: terms.cancelButtonText
}) : null
function getTermsConditions (req, res, next) {
const deviceId = req.deviceId
const { config } = req.settings
const terms = configManager.getTermsConditions(config)
return settingsLoader.fetchCurrentConfigVersion()
.then(version => res.json({ terms: createTerms(terms), version }))
.catch(next)
}
router.get('/', getTermsConditions)
module.exports = router

View file

@ -0,0 +1,84 @@
const express = require('express')
const router = express.Router()
const _ = require('lodash/fp')
const dbErrorCodes = require('../db-error-codes')
const E = require('../error')
const helpers = require('../route-helpers')
const httpError = require('../route-helpers').httpError
const logger = require('../logger')
const plugins = require('../plugins')
const Tx = require('../tx')
function postTx (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
return Tx.post(_.set('deviceId', req.deviceId, req.body), pi)
.then(tx => {
if (tx.errorCode) {
logger.error(tx.error)
switch (tx.errorCode) {
case 'InsufficientFundsError':
throw httpError(tx.error, 570)
case 'scoreThresholdReached':
throw httpError(tx.error, 571)
case 'walletScoringError':
throw httpError(tx.error, 572)
default:
throw httpError(tx.error, 500)
}
}
return res.json(tx)
})
.catch(err => {
// 204 so that l-m can ignore the error
// this is fine because the request is polled and will be retried if needed.
if (err.code === dbErrorCodes.SERIALIZATION_FAILURE) {
logger.warn('Harmless DB conflict, the query will be retried.')
return res.status(204).json({})
}
if (err instanceof E.StaleTxError) return res.status(409).json({ errorType: 'stale' })
if (err instanceof E.RatchetError) return res.status(409).json({ errorType: 'ratchet' })
throw err
})
.catch(next)
}
function getTx (req, res, next) {
if (req.query.status) {
return helpers.fetchStatusTx(req.params.id, req.query.status)
.then(r => res.json(r))
.catch(next)
}
return next(httpError('Not Found', 404))
}
function getPhoneTx (req, res, next) {
if (req.query.phone) {
return helpers.fetchPhoneTx(req.query.phone)
.then(r => res.json(r))
.catch(next)
}
return next(httpError('Not Found', 404))
}
function getEmailTx (req, res, next) {
if (req.query.email) {
return helpers.fetchEmailTx(req.query.email)
.then(r => res.json(r))
.catch(next)
}
return next(httpError('Not Found', 404))
}
router.post('/', postTx)
router.get('/:id', getTx)
router.get('/', getPhoneTx)
router.get('/', getEmailTx)
module.exports = { postTx, getTx, getPhoneTx, getEmailTx, router }

View file

@ -0,0 +1,26 @@
const express = require('express')
const { emptyMachineUnits, refillMachineUnits } = require('../machine-loader')
const router = express.Router()
const emptyUnitUpdateCounts = (req, res, next) => {
const deviceId = req.deviceId
const { units: newUnits, fiatCode } = req.body
return emptyMachineUnits({ deviceId, newUnits: newUnits, fiatCode })
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
const refillUnitUpdateCounts = (req, res, next) => {
const deviceId = req.deviceId
const { units: newUnits } = req.body
return refillMachineUnits({ deviceId, newUnits: newUnits })
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
router.post('/empty', emptyUnitUpdateCounts)
router.post('/refill', refillUnitUpdateCounts)
module.exports = router

View file

@ -0,0 +1,37 @@
const express = require('express')
const router = express.Router()
const BN = require('../bn')
const commissionMath = require('../commission-math')
const configManager = require('../new-config-manager')
const loyalty = require('../loyalty')
const respond = require('../respond')
function verifyPromoCode (req, res, next) {
loyalty.getPromoCode(req.body.codeInput)
.then(promoCode => {
if (!promoCode) return next()
const transaction = req.body.tx
const commissions = configManager.getCommissions(transaction.cryptoCode, req.deviceId, req.settings.config)
const tickerRate = new BN(transaction.rawTickerPrice)
const discount = commissionMath.getDiscountRate(promoCode.discount, commissions[transaction.direction])
const rates = {
[transaction.cryptoCode]: {
[transaction.direction]: (transaction.direction === 'cashIn')
? tickerRate.times(discount).decimalPlaces(5)
: tickerRate.div(discount).decimalPlaces(5)
}
}
respond(req, res, {
promoCode: promoCode,
newRates: rates
})
})
.catch(next)
}
router.post('/', verifyPromoCode)
module.exports = router

View file

@ -0,0 +1,16 @@
const express = require('express')
const router = express.Router()
const plugins = require('../plugins')
const respond = require('../respond')
function verifyTx (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
pi.verifyTransaction(req.body)
.then(idResult => respond(req, res, idResult))
.catch(next)
}
router.post('/', verifyTx)
module.exports = router

View file

@ -0,0 +1,16 @@
const express = require('express')
const router = express.Router()
const plugins = require('../plugins')
const respond = require('../respond')
function verifyUser (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
pi.verifyUser(req.body)
.then(idResult => respond(req, res, idResult))
.catch(next)
}
router.post('/', verifyUser)
module.exports = router