Merge pull request #1552 from chaotixkilla/feat-add-aveiro-admin-ui-8.5
[LAM-849] [Release 8.5] Add Aveiro admin UI
This commit is contained in:
commit
0366cc54ed
72 changed files with 3070 additions and 926 deletions
36
bin/lamassu-update-stackers
Normal file
36
bin/lamassu-update-stackers
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const _ = require('lodash')
|
||||
const db = require('../lib/db')
|
||||
|
||||
if (process.argv.length !== 4) {
|
||||
console.log('Usage: lamassu-update-stackers <device_id> <number_of_stackers>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!_.isFinite(parseInt(process.argv[3]))) {
|
||||
console.log('Error: <number_of_stackers> is not a valid number (%s)', err)
|
||||
process.exit(3)
|
||||
}
|
||||
|
||||
if (parseInt(process.argv[3]) > 3 || parseInt(process.argv[3]) < 1) {
|
||||
console.log('Error: <number_of_stackers> is out of range. Should be a number between 1 and 3')
|
||||
process.exit(3)
|
||||
}
|
||||
|
||||
const deviceId = process.argv[2]
|
||||
const numberOfStackers = parseInt(process.argv[3])
|
||||
|
||||
const query = `UPDATE devices SET number_of_stackers = $1 WHERE device_id = $2`
|
||||
|
||||
db.none(query, [numberOfStackers, deviceId])
|
||||
.then(() => {
|
||||
console.log('Success! Device %s updated to %s stackers', deviceId, numberOfStackers)
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Error: %s', err)
|
||||
process.exit(3)
|
||||
})
|
||||
338
lib/bill-math.js
338
lib/bill-math.js
|
|
@ -1,225 +1,145 @@
|
|||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
const sumService = require('@haensl/subset-sum')
|
||||
|
||||
const MAX_AMOUNT_OF_SOLUTIONS = 10000
|
||||
const MAX_BRUTEFORCE_ITERATIONS = 10000000
|
||||
const BILL_LIST_MODES = {
|
||||
LAST_UNIT_FIRST: 0,
|
||||
FIRST_UNIT_FIRST: 1,
|
||||
LOWEST_VALUE_FIRST: 2,
|
||||
HIGHEST_VALUE_FIRST: 3,
|
||||
UNIT_ROUND_ROBIN: 4,
|
||||
VALUE_ROUND_ROBIN: 5
|
||||
}
|
||||
|
||||
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
|
||||
return [
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
|
||||
denomination: cassettes[0].denomination
|
||||
const buildBillList = (units, mode) => {
|
||||
switch (mode) {
|
||||
case BILL_LIST_MODES.LAST_UNIT_FIRST:
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||
return acc
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[1].count - c1 : c1,
|
||||
denomination: cassettes[1].denomination
|
||||
[],
|
||||
_.reverse(units)
|
||||
)
|
||||
case BILL_LIST_MODES.FIRST_UNIT_FIRST:
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||
return acc
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[2].count - c2 : c2,
|
||||
denomination: cassettes[2].denomination
|
||||
[],
|
||||
units
|
||||
)
|
||||
case BILL_LIST_MODES.LOWEST_VALUE_FIRST:
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||
return acc
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[3].count - c3 : c3,
|
||||
denomination: cassettes[3].denomination
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function mergeCassettes(cassettes) {
|
||||
const map = {}
|
||||
|
||||
_.forEach(it => {
|
||||
if (!map[it.denomination]) {
|
||||
map[it.denomination] = 0
|
||||
}
|
||||
map[it.denomination] += it.count
|
||||
}, cassettes)
|
||||
|
||||
return _.map(it => ({ denomination: it, count: map[it] }), _.keys(map))
|
||||
}
|
||||
|
||||
function unmergeCassettes(cassettes, output) {
|
||||
const map = {}
|
||||
|
||||
_.forEach(it => {
|
||||
if (!map[it.denomination]) {
|
||||
map[it.denomination] = 0
|
||||
}
|
||||
map[it.denomination] += it.provisioned
|
||||
}, output)
|
||||
|
||||
const response = []
|
||||
_.forEach(it => {
|
||||
const value = {
|
||||
denomination: it.denomination,
|
||||
id: uuid.v4()
|
||||
}
|
||||
|
||||
const amountNeeded = map[it.denomination]
|
||||
if (!amountNeeded) {
|
||||
return response.push({ provisioned: 0, ...value })
|
||||
}
|
||||
|
||||
if (amountNeeded < it.count) {
|
||||
map[it.denomination] = 0
|
||||
return response.push({ provisioned: amountNeeded, ...value })
|
||||
}
|
||||
|
||||
map[it.denomination] -= it.count
|
||||
return response.push({ provisioned: it.count, ...value })
|
||||
}, cassettes)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
function makeChangeDuo(cassettes, amount) {
|
||||
// Initialize empty cassettes in case of undefined, due to same denomination across all cassettes results in a single merged cassette
|
||||
const small = cassettes[0] ?? { denomination: 0, count: 0 }
|
||||
const large = cassettes[1] ?? { denomination: 0, count: 0 }
|
||||
|
||||
const largeDenom = large.denomination
|
||||
const smallDenom = small.denomination
|
||||
const largeBills = Math.min(large.count, Math.floor(amount / largeDenom))
|
||||
const amountNum = amount.toNumber()
|
||||
|
||||
for (let i = largeBills; i >= 0; i--) {
|
||||
const remainder = amountNum - largeDenom * i
|
||||
|
||||
if (remainder % smallDenom !== 0) continue
|
||||
const smallCount = remainder / smallDenom
|
||||
if (smallCount > small.count) continue
|
||||
return [
|
||||
{
|
||||
provisioned: smallCount,
|
||||
denomination: small.denomination,
|
||||
id: uuid.v4()
|
||||
[],
|
||||
_.orderBy(['denomination'], ['asc'])(units)
|
||||
)
|
||||
case BILL_LIST_MODES.HIGHEST_VALUE_FIRST:
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||
return acc
|
||||
},
|
||||
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
|
||||
]
|
||||
[],
|
||||
_.orderBy(['denomination'], ['desc'])(units)
|
||||
)
|
||||
case BILL_LIST_MODES.UNIT_ROUND_ROBIN:
|
||||
{
|
||||
const amountOfBills = _.reduce(
|
||||
(acc, value) => acc + value.count,
|
||||
0,
|
||||
units
|
||||
)
|
||||
|
||||
const _units = _.filter(it => it.count > 0)(_.cloneDeep(units))
|
||||
const bills = []
|
||||
|
||||
for(let i = 0; i < amountOfBills; i++) {
|
||||
const idx = i % _.size(_units)
|
||||
if (_units[idx].count > 0) {
|
||||
bills.push(_units[idx].denomination)
|
||||
_units[idx].count--
|
||||
}
|
||||
|
||||
if (_units[idx].count === 0) {
|
||||
_units.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return bills
|
||||
}
|
||||
case BILL_LIST_MODES.VALUE_ROUND_ROBIN:
|
||||
{
|
||||
const amountOfBills = _.reduce(
|
||||
(acc, value) => acc + value.count,
|
||||
0,
|
||||
units
|
||||
)
|
||||
|
||||
const _units = _.flow([_.filter(it => it.count > 0), _.orderBy(['denomination'], ['asc'])])(_.cloneDeep(units))
|
||||
const bills = []
|
||||
|
||||
for(let i = 0; i < amountOfBills; i++) {
|
||||
const idx = i % _.size(_units)
|
||||
if (_units[idx].count > 0) {
|
||||
bills.push(_units[idx].denomination)
|
||||
_units[idx].count--
|
||||
}
|
||||
|
||||
if (_units[idx].count === 0) {
|
||||
_units.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return bills
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid mode: ${mode}`)
|
||||
}
|
||||
}
|
||||
|
||||
const getSolution = (units, amount, mode) => {
|
||||
const billList = buildBillList(units, mode)
|
||||
|
||||
if (_.sum(billList) < amount.toNumber()) {
|
||||
return []
|
||||
}
|
||||
|
||||
const solver = sumService.subsetSum(billList, amount.toNumber())
|
||||
const solution = _.countBy(Math.floor, solver.next().value)
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push({ denomination: _.toNumber(value), provisioned: solution[value] })
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
_.keys(solution)
|
||||
)
|
||||
}
|
||||
|
||||
const solutionToOriginalUnits = (solution, units) => {
|
||||
const billsLeft = _.clone(_.fromPairs(_.map(it => [it.denomination, it.provisioned])(solution)))
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
const unit = units[value]
|
||||
const billsToAssign = _.clamp(0, unit.count)(_.isNaN(billsLeft[unit.denomination]) || _.isNil(billsLeft[unit.denomination]) ? 0 : billsLeft[unit.denomination])
|
||||
acc.push({ name: unit.name, denomination: unit.denomination, provisioned: billsToAssign })
|
||||
billsLeft[unit.denomination] -= billsToAssign
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
_.range(0, _.size(units))
|
||||
)
|
||||
}
|
||||
|
||||
function makeChange(outCassettes, amount) {
|
||||
const available = _.reduce(
|
||||
(res, val) => res + val.count * val.denomination,
|
||||
0,
|
||||
outCassettes
|
||||
)
|
||||
|
||||
if (available < amount) {
|
||||
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(outCassettes)}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const cassettes = mergeCassettes(outCassettes)
|
||||
const result =
|
||||
_.size(cassettes) >= 3
|
||||
? makeChangeDynamic(cassettes, amount, available)
|
||||
: makeChangeDuo(cassettes, amount)
|
||||
|
||||
if (!result.length) return null
|
||||
return unmergeCassettes(outCassettes, result)
|
||||
}
|
||||
|
||||
function makeChangeDynamicBruteForce(outCassettes, amount, available) {
|
||||
const solutions = []
|
||||
let x = 0
|
||||
|
||||
const shouldFlip = amount > _.max(_.map(it => it.denomination * it.count, outCassettes))
|
||||
const amountNum = shouldFlip ? available - amount : amount
|
||||
|
||||
const cassettes = shouldFlip ? _.reverse(outCassettes) : outCassettes
|
||||
const { denomination: denomination0, count: count0 } = cassettes[0]
|
||||
const { denomination: denomination1, count: count1 } = cassettes[1]
|
||||
const { denomination: denomination2, count: count2 } = cassettes[2]
|
||||
const { denomination: denomination3, count: count3 } = cassettes[3]
|
||||
|
||||
const startTime = new Date().getTime()
|
||||
|
||||
loop1: for (let i = 0; i <= count0; i++) {
|
||||
const firstSum = i * denomination0
|
||||
|
||||
for (let j = 0; j <= count1; j++) {
|
||||
const secondSum = firstSum + j * denomination1
|
||||
if (secondSum > amountNum) break
|
||||
|
||||
if (secondSum === amountNum) {
|
||||
solutions.push(newSolution(cassettes, i, j, 0, 0, shouldFlip))
|
||||
}
|
||||
|
||||
for (let k = 0; k <= count2; k++) {
|
||||
const thirdSum = secondSum + k * denomination2
|
||||
if (thirdSum > amountNum) break
|
||||
|
||||
if (denomination2 === 0) break
|
||||
|
||||
if (thirdSum === amountNum) {
|
||||
solutions.push(newSolution(cassettes, i, j, k, 0, shouldFlip))
|
||||
}
|
||||
|
||||
for (let l = 0; l <= count3; l++) {
|
||||
if ((x > MAX_AMOUNT_OF_SOLUTIONS && solutions.length >= 1) || x > MAX_BRUTEFORCE_ITERATIONS) break loop1
|
||||
x++
|
||||
const fourthSum = thirdSum + l * denomination3
|
||||
if (fourthSum > amountNum) break
|
||||
|
||||
if (denomination3 === 0) break
|
||||
|
||||
if (fourthSum === amountNum) {
|
||||
solutions.push(newSolution(cassettes, i, j, k, l, shouldFlip))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = new Date().getTime()
|
||||
|
||||
console.log(`Exiting bruteforce after ${x} tries. Took ${endTime - startTime} ms`)
|
||||
return solutions
|
||||
}
|
||||
|
||||
function makeChangeDynamic(cassettes, amount, available) {
|
||||
while (_.size(cassettes) < 4) {
|
||||
cassettes.push({ denomination: 0, count: 0 })
|
||||
}
|
||||
|
||||
const amountNum = amount.toNumber()
|
||||
|
||||
const solutions = makeChangeDynamicBruteForce(cassettes, amountNum, available)
|
||||
|
||||
const sortedSolutions = _.sortBy(it => {
|
||||
const arr = []
|
||||
|
||||
for (let la = 0; la < 4; la++) {
|
||||
arr.push(cassettes[la].count - it[la].provisioned)
|
||||
}
|
||||
|
||||
if (arr.length < 2) return Infinity
|
||||
return _.max(arr) - _.min(arr)
|
||||
}, solutions)
|
||||
|
||||
const cleanSolution = _.filter(
|
||||
it => it.denomination > 0,
|
||||
_.head(sortedSolutions)
|
||||
)
|
||||
|
||||
const response = cleanSolution
|
||||
|
||||
// Final sanity check
|
||||
let total = 0
|
||||
_.forEach(it => {
|
||||
total += it.provisioned * it.denomination
|
||||
}, response)
|
||||
|
||||
if (total === amountNum) return response
|
||||
|
||||
console.log(
|
||||
`Failed to find a solution for ${amountNum} with cassettes ${JSON.stringify(cassettes)}`
|
||||
)
|
||||
return []
|
||||
const solution = getSolution(outCassettes, amount, BILL_LIST_MODES.VALUE_ROUND_ROBIN)
|
||||
return solutionToOriginalUnits(solution, outCassettes)
|
||||
}
|
||||
|
||||
module.exports = { makeChange }
|
||||
|
|
|
|||
|
|
@ -41,13 +41,24 @@ function insertNewBills (t, billRows, machineTx) {
|
|||
if (_.isEmpty(bills)) return Promise.resolve([])
|
||||
|
||||
const dbBills = _.map(cashInLow.massage, bills)
|
||||
const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time']
|
||||
const getBillsByDestination = destination => _.filter(it => it.destination_unit === destination)(dbBills)
|
||||
|
||||
const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time', 'destination_unit']
|
||||
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
|
||||
const deviceID = machineTx.deviceId
|
||||
const sql2 = `update devices set cashbox = cashbox + $2
|
||||
const sql2 = `update devices set cashbox = cashbox + $2, stacker1f = stacker1f + $3, stacker1r = stacker1r + $4, stacker2f = stacker2f + $5, stacker2r = stacker2r + $6, stacker3f = stacker3f + $7, stacker3r = stacker3r + $8
|
||||
where device_id = $1`
|
||||
|
||||
return t.none(sql2, [deviceID, dbBills.length])
|
||||
return t.none(sql2, [
|
||||
deviceID,
|
||||
getBillsByDestination('cashbox').length,
|
||||
getBillsByDestination('stacker1f').length,
|
||||
getBillsByDestination('stacker1r').length,
|
||||
getBillsByDestination('stacker2f').length,
|
||||
getBillsByDestination('stacker2r').length,
|
||||
getBillsByDestination('stacker3f').length,
|
||||
getBillsByDestination('stacker3r').length
|
||||
])
|
||||
.then(() => {
|
||||
return t.none(sql)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,10 +40,11 @@ function mapDispense (tx) {
|
|||
const res = {}
|
||||
|
||||
_.forEach(it => {
|
||||
res[`provisioned_${it + 1}`] = bills[it].provisioned
|
||||
res[`denomination_${it + 1}`] = bills[it].denomination
|
||||
res[`dispensed_${it + 1}`] = bills[it].dispensed
|
||||
res[`rejected_${it + 1}`] = bills[it].rejected
|
||||
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||
res[`provisioned_${suffix}`] = bills[it].provisioned
|
||||
res[`denomination_${suffix}`] = bills[it].denomination
|
||||
res[`dispensed_${suffix}`] = bills[it].dispensed
|
||||
res[`rejected_${suffix}`] = bills[it].rejected
|
||||
}, _.times(_.identity(), _.size(bills)))
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -107,17 +107,10 @@ function nextHd (t, isHd, tx) {
|
|||
function updateCassettes (t, tx) {
|
||||
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
|
||||
|
||||
const sql = `update devices set
|
||||
${_.size(tx.bills) > 0 ? `cassette1 = cassette1 - $1` : ``}
|
||||
${_.size(tx.bills) > 1 ? `, cassette2 = cassette2 - $2` : ``}
|
||||
${_.size(tx.bills) > 2 ? `, cassette3 = cassette3 - $3` : ``}
|
||||
${_.size(tx.bills) > 3 ? `, cassette4 = cassette4 - $4` : ``}
|
||||
where device_id = $${_.size(tx.bills) + 1}
|
||||
returning
|
||||
${_.size(tx.bills) > 0 ? `cassette1` : ``}
|
||||
${_.size(tx.bills) > 1 ? `, cassette2`: ``}
|
||||
${_.size(tx.bills) > 2 ? `, cassette3` : ``}
|
||||
${_.size(tx.bills) > 3 ? `, cassette4` : ``}`
|
||||
const billsStmt = _.join(', ')(_.map(it => `${tx.bills[it].name} = ${tx.bills[it].name} - $${it + 1}`)(_.range(0, _.size(tx.bills))))
|
||||
const returnStmt = _.join(', ')(_.map(bill => `${bill.name}`)(tx.bills))
|
||||
|
||||
const sql = `UPDATE devices SET ${billsStmt} WHERE device_id = $${_.size(tx.bills) + 1} RETURNING ${returnStmt}`
|
||||
|
||||
const values = []
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,32 @@ case
|
|||
else 'Pending'
|
||||
end`
|
||||
|
||||
const MAX_CASSETTES = 4
|
||||
const MAX_STACKERS = 3
|
||||
|
||||
const BILL_FIELDS = [
|
||||
'denomination1',
|
||||
'denomination2',
|
||||
'denomination3',
|
||||
'denomination4',
|
||||
'denomination1f',
|
||||
'denomination1r',
|
||||
'denomination2f',
|
||||
'denomination2r',
|
||||
'denomination3f',
|
||||
'denomination3r',
|
||||
'provisioned1',
|
||||
'provisioned2',
|
||||
'provisioned3',
|
||||
'provisioned4',
|
||||
'provisioned1f',
|
||||
'provisioned1r',
|
||||
'provisioned2f',
|
||||
'provisioned2r',
|
||||
'provisioned3f',
|
||||
'provisioned3r'
|
||||
]
|
||||
|
||||
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
||||
|
||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||
|
|
@ -43,23 +69,37 @@ function convertBigNumFields (obj) {
|
|||
}
|
||||
|
||||
function convertField (key) {
|
||||
return _.snakeCase(key)
|
||||
return _.includes('denomination', key) || _.includes('provisioned', key) ? key : _.snakeCase(key)
|
||||
}
|
||||
|
||||
function addDbBills (tx) {
|
||||
const bills = tx.bills
|
||||
if (_.isEmpty(bills)) return tx
|
||||
|
||||
const billsObj = {
|
||||
provisioned1: bills[0]?.provisioned ?? 0,
|
||||
provisioned2: bills[1]?.provisioned ?? 0,
|
||||
provisioned3: bills[2]?.provisioned ?? 0,
|
||||
provisioned4: bills[3]?.provisioned ?? 0,
|
||||
denomination1: bills[0]?.denomination ?? 0,
|
||||
denomination2: bills[1]?.denomination ?? 0,
|
||||
denomination3: bills[2]?.denomination ?? 0,
|
||||
denomination4: bills[3]?.denomination ?? 0
|
||||
const billFields = _.map(it => _.replace(/(denomination|provisioned)/g, '$1_')(it), BILL_FIELDS)
|
||||
|
||||
const billsObj = _.flow(
|
||||
_.reduce(
|
||||
(acc, value) => {
|
||||
const suffix = value.name.replace(/cassette|stacker/gi, '')
|
||||
return {
|
||||
...acc,
|
||||
[`provisioned_${suffix}`]: value.provisioned,
|
||||
[`denomination_${suffix}`]: value.denomination
|
||||
}
|
||||
},
|
||||
{}
|
||||
),
|
||||
it => {
|
||||
const missingKeys = _.reduce(
|
||||
(acc, value) => {
|
||||
return _.assign({ [value]: 0 })(acc)
|
||||
},
|
||||
{}
|
||||
)(_.difference(billFields, _.keys(it)))
|
||||
return _.assign(missingKeys, it)
|
||||
}
|
||||
)(bills)
|
||||
|
||||
return _.assign(tx, billsObj)
|
||||
}
|
||||
|
|
@ -78,7 +118,7 @@ function toObj (row) {
|
|||
let newObj = {}
|
||||
|
||||
keys.forEach(key => {
|
||||
const objKey = _.camelCase(key)
|
||||
const objKey = key.match(/denomination|provisioned/g) ? key.replace(/_/g, '') : _.camelCase(key)
|
||||
if (key === 'received_crypto_atoms' && row[key]) {
|
||||
newObj[objKey] = new BN(row[key])
|
||||
return
|
||||
|
|
@ -93,35 +133,28 @@ function toObj (row) {
|
|||
|
||||
newObj.direction = 'cashOut'
|
||||
|
||||
const billFields = ['denomination1', 'denomination2', 'denomination3', 'denomination4', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4']
|
||||
if (_.every(_.isNil, _.at(BILL_FIELDS, newObj))) return newObj
|
||||
if (_.some(_.isNil, _.at(BILL_FIELDS, newObj))) throw new Error('Missing cassette values')
|
||||
|
||||
if (_.every(_.isNil, _.at(billFields, newObj))) return newObj
|
||||
if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values')
|
||||
|
||||
const billFieldsArr = [
|
||||
{
|
||||
denomination: newObj.denomination1,
|
||||
provisioned: newObj.provisioned1
|
||||
const billFieldsArr = _.concat(
|
||||
_.map(it => ({ name: `cassette${it + 1}`, denomination: newObj[`denomination${it + 1}`], provisioned: newObj[`provisioned${it + 1}`] }))(_.range(0, MAX_CASSETTES)),
|
||||
_.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(
|
||||
{ name: `stacker${value + 1}f`, denomination: newObj[`denomination${value + 1}f`], provisioned: newObj[`provisioned${value + 1}f`] },
|
||||
{ name: `stacker${value + 1}r`, denomination: newObj[`denomination${value + 1}r`], provisioned: newObj[`provisioned${value + 1}r`] }
|
||||
)
|
||||
return acc
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination2,
|
||||
provisioned: newObj.provisioned2
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination3,
|
||||
provisioned: newObj.provisioned3
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination4,
|
||||
provisioned: newObj.provisioned4
|
||||
}
|
||||
]
|
||||
[]
|
||||
)(_.range(0, MAX_STACKERS))
|
||||
)
|
||||
|
||||
// There can't be bills with denomination === 0.
|
||||
// If a bill has denomination === 0, then that cassette is not set and should be filtered out.
|
||||
const bills = _.filter(it => it.denomination > 0, billFieldsArr)
|
||||
|
||||
return _.set('bills', bills, _.omit(billFields, newObj))
|
||||
return _.set('bills', bills, _.omit(BILL_FIELDS, newObj))
|
||||
}
|
||||
|
||||
function redeemableTxs (deviceId) {
|
||||
|
|
@ -129,7 +162,10 @@ function redeemableTxs (deviceId) {
|
|||
where device_id=$1
|
||||
and redeem=$2
|
||||
and dispense=$3
|
||||
and provisioned_1 is not null
|
||||
and (
|
||||
provisioned_1 is not null or provisioned_2 is not null or provisioned_3 is not null or provisioned_4 is not null or
|
||||
provisioned_1f is not null or provisioned_1r is not null or provisioned_2f is not null or provisioned_2r is not null or provisioned_3f is not null or provisioned_3r is not null
|
||||
)
|
||||
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
|
||||
|
||||
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
|
||||
|
|
|
|||
|
|
@ -56,14 +56,15 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
}
|
||||
|
||||
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
||||
return pi.buildAvailableCassettes(newTx.id)
|
||||
.then(cassettes => {
|
||||
return pi.buildAvailableUnits(newTx.id)
|
||||
.then(_units => {
|
||||
const units = _.concat(_units.cassettes, _units.stackers)
|
||||
logger.silly('Computing bills to dispense:', {
|
||||
txId: newTx.id,
|
||||
cassettes: cassettes.cassettes,
|
||||
units: units,
|
||||
fiat: newTx.fiat
|
||||
})
|
||||
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
|
||||
const bills = billMath.makeChange(units, newTx.fiat)
|
||||
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
||||
|
||||
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
||||
|
|
@ -73,8 +74,9 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
const rec = {}
|
||||
|
||||
_.forEach(it => {
|
||||
rec[`provisioned_${it + 1}`] = bills[it].provisioned
|
||||
rec[`denomination_${it + 1}`] = bills[it].denomination
|
||||
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||
rec[`provisioned_${suffix}`] = bills[it].provisioned
|
||||
rec[`denomination_${suffix}`] = bills[it].denomination
|
||||
}, _.times(_.identity(), _.size(bills)))
|
||||
|
||||
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
||||
|
|
|
|||
|
|
@ -2,26 +2,37 @@ const constants = require('./constants')
|
|||
const db = require('./db')
|
||||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
const camelize = require('./utils')
|
||||
|
||||
function createCashboxBatch (deviceId, cashboxCount) {
|
||||
if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
|
||||
const sql = `INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *`
|
||||
const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *`
|
||||
const sql2 = `
|
||||
UPDATE bills SET cashbox_batch_id=$1
|
||||
FROM cash_in_txs
|
||||
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
|
||||
cash_in_txs.device_id = $2 AND
|
||||
bills.destination_unit = 'cashbox' AND
|
||||
bills.cashbox_batch_id IS NULL
|
||||
`
|
||||
return db.tx(async t => {
|
||||
const newBatch = await t.oneOrNone(sql, [uuid.v4(), deviceId])
|
||||
return t.oneOrNone(sql2, [newBatch.id, newBatch.device_id])
|
||||
const sql3 = `
|
||||
UPDATE empty_unit_bills SET cashbox_batch_id=$1
|
||||
WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`
|
||||
|
||||
return db.tx(t => {
|
||||
const batchId = uuid.v4()
|
||||
const q1 = t.none(sql, [batchId, deviceId])
|
||||
const q2 = t.none(sql2, [batchId, deviceId])
|
||||
const q3 = t.none(sql3, [batchId, deviceId])
|
||||
return t.batch([q1, q2, q3])
|
||||
})
|
||||
}
|
||||
|
||||
function updateMachineWithBatch (machineContext, oldCashboxCount) {
|
||||
const isValidContext = _.has(['deviceId', 'cashbox', 'cassettes'], machineContext)
|
||||
const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(machineContext.cassettes))
|
||||
const cashUnits = machineContext.cashUnits
|
||||
const isValidContext = _.has(['deviceId', 'cashUnits'], machineContext) && _.has(['cashbox', 'cassette1', 'cassette2', 'cassette3', 'cassette4', 'stacker1f', 'stacker1r', 'stacker2f', 'stacker2r', 'stacker3f', 'stacker3r'], cashUnits)
|
||||
const cassettes = _.filter(it => !_.isNil(it))([cashUnits.cassette1, cashUnits.cassette2, cashUnits.cassette3, cashUnits.cassette4])
|
||||
const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(cassettes))
|
||||
if (!isValidContext && !isCassetteAmountWithinRange)
|
||||
throw new Error('Insufficient info to create a new cashbox batch')
|
||||
if (_.isEqual(0, oldCashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
|
||||
|
|
@ -29,43 +40,59 @@ function updateMachineWithBatch (machineContext, oldCashboxCount) {
|
|||
return db.tx(t => {
|
||||
const deviceId = machineContext.deviceId
|
||||
const batchId = uuid.v4()
|
||||
const q1 = t.none(`INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`, [batchId, deviceId])
|
||||
const q1 = t.none(`INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`, [batchId, deviceId])
|
||||
const q2 = t.none(`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs
|
||||
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
|
||||
cash_in_txs.device_id = $2 AND
|
||||
bills.destination_unit = 'cashbox' AND
|
||||
bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
|
||||
const q3 = t.none(`UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 WHERE device_id=$6`, [
|
||||
machineContext.cashbox,
|
||||
machineContext.cassettes[0],
|
||||
machineContext.cassettes[1],
|
||||
machineContext.cassettes[2],
|
||||
machineContext.cassettes[3],
|
||||
const q3 = t.none(`UPDATE empty_unit_bills SET cashbox_batch_id=$1
|
||||
WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
|
||||
const q4 = t.none(`UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5, stacker1f=$6, stacker1r=$7, stacker2f=$8, stacker2r=$9, stacker3f=$10, stacker3r=$11 WHERE device_id=$12`, [
|
||||
cashUnits.cashbox,
|
||||
cashUnits.cassette1,
|
||||
cashUnits.cassette2,
|
||||
cashUnits.cassette3,
|
||||
cashUnits.cassette4,
|
||||
cashUnits.stacker1f,
|
||||
cashUnits.stacker1r,
|
||||
cashUnits.stacker2f,
|
||||
cashUnits.stacker2r,
|
||||
cashUnits.stacker3f,
|
||||
cashUnits.stacker3r,
|
||||
machineContext.deviceId
|
||||
])
|
||||
|
||||
return t.batch([q1, q2, q3])
|
||||
return t.batch([q1, q2, q3, q4])
|
||||
})
|
||||
}
|
||||
|
||||
function getBatches (from = new Date(0).toISOString(), until = new Date().toISOString()) {
|
||||
const sql = `
|
||||
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
|
||||
FROM cashbox_batches AS cb
|
||||
LEFT JOIN bills AS b ON cb.id = b.cashbox_batch_id
|
||||
WHERE cb.created >= $1 AND cb.created <= $2
|
||||
GROUP BY cb.id
|
||||
ORDER BY cb.created DESC
|
||||
SELECT cuo.id, cuo.device_id, cuo.created, cuo.operation_type, cuo.bill_count_override, cuo.performed_by, json_agg(bi.*) AS bills
|
||||
FROM cash_unit_operation AS cuo
|
||||
LEFT JOIN (
|
||||
SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (SELECT id, device_id FROM cash_in_txs) AS cit ON cit.id = b.cash_in_txs_id UNION
|
||||
SELECT id, fiat, fiat_code, created, cashbox_batch_id, device_id FROM empty_unit_bills
|
||||
) AS bi ON cuo.id = bi.cashbox_batch_id
|
||||
WHERE cuo.created >= $1 AND cuo.created <= $2 AND cuo.operation_type = 'cash-box-empty'
|
||||
GROUP BY cuo.id
|
||||
ORDER BY cuo.created DESC
|
||||
`
|
||||
return db.any(sql, [from, until]).then(res => _.map(it => _.mapKeys(ite => _.camelCase(ite), it), res))
|
||||
|
||||
return db.any(sql, [from, until]).then(camelize)
|
||||
}
|
||||
|
||||
function editBatchById (id, performedBy) {
|
||||
const sql = `UPDATE cashbox_batches SET performed_by=$1 WHERE id=$2`
|
||||
const sql = `UPDATE cash_unit_operation SET performed_by=$1 WHERE id=$2 AND cuo.operation_type = 'cash-box-empty'`
|
||||
return db.none(sql, [performedBy, id])
|
||||
}
|
||||
|
||||
function getBillsByBatchId (id) {
|
||||
const sql = `SELECT * FROM bills WHERE cashbox_batch_id=$1`
|
||||
const sql = `SELECT bi.* FROM (
|
||||
SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (SELECT id, device_id FROM cash_in_txs) AS cit ON cit.id = b.cash_in_txs_id UNION
|
||||
SELECT id, fiat, fiat_code, created, cashbox_batch_id, device_id FROM empty_unit_bills
|
||||
) AS bi WHERE bi.cashbox_batch_id=$1`
|
||||
return db.any(sql, [id])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,29 @@ const anonymousCustomer = {
|
|||
name: 'anonymous'
|
||||
}
|
||||
|
||||
const CASSETTE_MAX_CAPACITY = 500
|
||||
const CASH_UNIT_CAPACITY = {
|
||||
grandola: {
|
||||
cashbox: 2000,
|
||||
recycler: 2800
|
||||
},
|
||||
aveiro: {
|
||||
cashbox: 1500,
|
||||
stacker: 60,
|
||||
cassette: 500
|
||||
},
|
||||
tejo: {
|
||||
// TODO: add support for the different cashbox configuration in Tejo
|
||||
cashbox: 1000,
|
||||
cassette: 500
|
||||
},
|
||||
gaia: {
|
||||
cashbox: 600
|
||||
},
|
||||
sintra: {
|
||||
cashbox: 1000,
|
||||
cassette: 500
|
||||
}
|
||||
}
|
||||
|
||||
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
|
||||
const CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES = 4
|
||||
|
|
@ -39,7 +61,7 @@ const BALANCE_FETCH_SPEED_MULTIPLIER = {
|
|||
|
||||
module.exports = {
|
||||
anonymousCustomer,
|
||||
CASSETTE_MAX_CAPACITY,
|
||||
CASH_UNIT_CAPACITY,
|
||||
AUTHENTICATOR_ISSUER_ENTITY,
|
||||
AUTH_TOKEN_EXPIRATION_TIME,
|
||||
REGISTRATION_TOKEN_EXPIRATION_TIME,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const plugins = require('../plugins')
|
|||
const configManager = require('../new-config-manager')
|
||||
const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests')
|
||||
const state = require('../middlewares/state')
|
||||
const { getMachine } = require('../machine-loader')
|
||||
|
||||
const VERSION = require('../../package.json').version
|
||||
|
||||
|
|
@ -114,6 +115,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
configManager.getOperatorInfo(settings.config),
|
||||
configManager.getReceipt(settings.config),
|
||||
!!configManager.getCashOut(deviceId, settings.config).active,
|
||||
getMachine(deviceId, currentConfigVersion),
|
||||
])
|
||||
.then(([
|
||||
enablePaperWalletOnly,
|
||||
|
|
@ -124,6 +126,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
operatorInfo,
|
||||
receiptInfo,
|
||||
twoWayMode,
|
||||
{ numberOfCassettes, numberOfStackers },
|
||||
]) =>
|
||||
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
||||
null :
|
||||
|
|
@ -138,7 +141,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
languages: localeInfo.languages,
|
||||
fiatCode: localeInfo.fiatCurrency
|
||||
},
|
||||
machineInfo: { deviceId, deviceName },
|
||||
machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfStackers },
|
||||
twoWayMode,
|
||||
speedtestFiles,
|
||||
urlsToPing,
|
||||
|
|
@ -167,13 +170,25 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
|||
)(cassettes) :
|
||||
null
|
||||
|
||||
const massageStackers = stackers =>
|
||||
stackers ?
|
||||
_.flow(
|
||||
stackers => _.set('physical', _.get('stackers', stackers), stackers),
|
||||
stackers => _.set('virtual', _.get('virtualStackers', stackers), stackers),
|
||||
_.unset('stackers'),
|
||||
_.unset('virtualStackers')
|
||||
)(stackers) :
|
||||
null
|
||||
|
||||
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
||||
|
||||
return _.flow(
|
||||
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'coins', 'rates']),
|
||||
const res = _.flow(
|
||||
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'stackers', 'coins', 'rates']),
|
||||
|
||||
_.update('cassettes', massageCassettes),
|
||||
|
||||
_.update('stackers', massageStackers),
|
||||
|
||||
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
||||
_.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])),
|
||||
|
||||
|
|
@ -185,9 +200,10 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
|||
|
||||
/* Group the separate objects by cryptoCode */
|
||||
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
||||
({ areThereAvailablePromoCodes, balances, cassettes, coins, rates }) => ({
|
||||
({ areThereAvailablePromoCodes, balances, cassettes, stackers, coins, rates }) => ({
|
||||
areThereAvailablePromoCodes,
|
||||
cassettes,
|
||||
stackers,
|
||||
coins: _.flow(
|
||||
_.reduce(
|
||||
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
|
||||
|
|
@ -209,7 +225,20 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
|||
_.set('reboot', !!pid && state.reboots?.[operatorId]?.[deviceId] === pid),
|
||||
_.set('shutdown', !!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid),
|
||||
_.set('restartServices', !!pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid),
|
||||
_.set('emptyUnit', !!pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid),
|
||||
_.set('refillUnit', !!pid && state.refillUnit?.[operatorId]?.[deviceId] === pid),
|
||||
)(pq)
|
||||
|
||||
// Clean up the state middleware and prevent commands from being issued more than once
|
||||
if (!_.isNil(state.emptyUnit?.[operatorId]?.[deviceId])) {
|
||||
delete state.emptyUnit?.[operatorId]?.[deviceId]
|
||||
}
|
||||
|
||||
if (!_.isNil(state.refillUnit?.[operatorId]?.[deviceId])) {
|
||||
delete state.refillUnit?.[operatorId]?.[deviceId]
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ type OperatorInfo {
|
|||
type MachineInfo {
|
||||
deviceId: String!
|
||||
deviceName: String
|
||||
numberOfCassettes: Int
|
||||
numberOfStackers: Int
|
||||
}
|
||||
|
||||
type ReceiptInfo {
|
||||
|
|
@ -165,6 +167,13 @@ type DynamicCoinValues {
|
|||
}
|
||||
|
||||
type PhysicalCassette {
|
||||
name: String!
|
||||
denomination: Int!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
type PhysicalStacker {
|
||||
name: String!
|
||||
denomination: Int!
|
||||
count: Int!
|
||||
}
|
||||
|
|
@ -174,13 +183,21 @@ type Cassettes {
|
|||
virtual: [Int!]!
|
||||
}
|
||||
|
||||
type Stackers {
|
||||
physical: [PhysicalStacker!]!
|
||||
virtual: [Int!]!
|
||||
}
|
||||
|
||||
type DynamicConfig {
|
||||
areThereAvailablePromoCodes: Boolean!
|
||||
cassettes: Cassettes
|
||||
stackers: Stackers
|
||||
coins: [DynamicCoinValues!]!
|
||||
reboot: Boolean!
|
||||
shutdown: Boolean!
|
||||
restartServices: Boolean!
|
||||
emptyUnit: Boolean!
|
||||
refillUnit: Boolean!
|
||||
}
|
||||
|
||||
type Configs {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
const axios = require('axios')
|
||||
const uuid = require('uuid')
|
||||
|
||||
const batching = require('./cashbox-batches')
|
||||
|
|
@ -13,6 +12,7 @@ const settingsLoader = require('./new-settings-loader')
|
|||
const notifierUtils = require('./notifier/utils')
|
||||
const notifierQueries = require('./notifier/queries')
|
||||
const { ApolloError } = require('apollo-server-errors');
|
||||
const { loadLatestConfig } = require('./new-settings-loader')
|
||||
|
||||
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||
|
|
@ -21,12 +21,21 @@ const stuckStatus = { label: 'Stuck', type: 'error' }
|
|||
function toMachineObject (r) {
|
||||
return {
|
||||
deviceId: r.device_id,
|
||||
cashUnits: {
|
||||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
cassette2: r.cassette2,
|
||||
cassette3: r.cassette3,
|
||||
cassette4: r.cassette4,
|
||||
stacker1f: r.stacker1f,
|
||||
stacker1r: r.stacker1r,
|
||||
stacker2f: r.stacker2f,
|
||||
stacker2r: r.stacker2r,
|
||||
stacker3f: r.stacker3f,
|
||||
stacker3r: r.stacker3r
|
||||
},
|
||||
numberOfCassettes: r.number_of_cassettes,
|
||||
numberOfStackers: r.number_of_stackers,
|
||||
version: r.version,
|
||||
model: r.model,
|
||||
pairedAt: new Date(r.created),
|
||||
|
|
@ -141,8 +150,9 @@ function renameMachine (rec) {
|
|||
|
||||
function resetCashOutBills (rec) {
|
||||
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
|
||||
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4 WHERE device_id=$5;`
|
||||
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
|
||||
const { cassette1, cassette2, cassette3, cassette4, stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r } = rec.cashUnits
|
||||
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, stacker1f=$5, stacker1r=$6, stacker2f=$7, stacker2r=$8, stacker3f=$9, stacker3r=$10 WHERE device_id=$11;`
|
||||
return db.none(sql, [cassette1, cassette2, cassette3, cassette4, stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r, rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
|
||||
}
|
||||
|
||||
function emptyCashInBills (rec) {
|
||||
|
|
@ -151,17 +161,138 @@ function emptyCashInBills (rec) {
|
|||
}
|
||||
|
||||
function setCassetteBills (rec) {
|
||||
const { cashbox, cassette1, cassette2, cassette3, cassette4, stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r } = rec.cashUnits
|
||||
return db.oneOrNone(`SELECT cashbox FROM devices WHERE device_id=$1 LIMIT 1`, [rec.deviceId])
|
||||
.then(oldCashboxValue => {
|
||||
if (_.isNil(oldCashboxValue) || rec.cashbox === oldCashboxValue.cashbox) {
|
||||
const sql = 'UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 WHERE device_id=$6'
|
||||
return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId])
|
||||
if (_.isNil(oldCashboxValue) || cashbox === oldCashboxValue.cashbox) {
|
||||
const sql = 'UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5, stacker1f=$6, stacker1r=$7, stacker2f=$8, stacker2r=$9, stacker3f=$10, stacker3r=$11 WHERE device_id=$12'
|
||||
return db.none(sql, [cashbox, cassette1, cassette2, cassette3, cassette4, stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r, rec.deviceId])
|
||||
}
|
||||
|
||||
return batching.updateMachineWithBatch({ ...rec, oldCashboxValue })
|
||||
})
|
||||
}
|
||||
|
||||
function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
|
||||
return loadLatestConfig()
|
||||
.then(config => Promise.all([getMachine(deviceId), configManager.getCashOut(deviceId, config)]))
|
||||
.then(([machine, cashoutSettings]) => {
|
||||
const movedBills = _.reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[value]: {
|
||||
operationName: `cash-${_.replace(/(cassette|stacker)/g, '$1-')(value)}-empty`,
|
||||
delta: newUnits[value] - machine.cashUnits[value],
|
||||
denomination: value !== 'cashbox' ? cashoutSettings[value] : null
|
||||
}
|
||||
}),
|
||||
{},
|
||||
_.keys(newUnits)
|
||||
)
|
||||
|
||||
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cashbox'], movedBills)))
|
||||
const operationsToCreate = _.map(it => ({
|
||||
id: uuid.v4(),
|
||||
device_id: deviceId,
|
||||
operation_type: it
|
||||
}))(operationNames)
|
||||
|
||||
const billArr = _.reduce(
|
||||
(acc, value) => {
|
||||
const unit = movedBills[value]
|
||||
return _.concat(acc, _.times(() => ({
|
||||
id: uuid.v4(),
|
||||
fiat: unit.denomination,
|
||||
fiat_code: fiatCode,
|
||||
device_id: deviceId
|
||||
// TODO: Uncomment this if we decide to keep track of bills across multiple operations. For now, we'll just create the emptying operations for each unit affected, but not relate these events with individual bills and just use the field for the cashbox batch event
|
||||
// cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|stacker)/g, '$1-')(value)}-empty`, operationsToCreate).id
|
||||
}), Math.abs(unit.delta)))
|
||||
},
|
||||
[],
|
||||
_.keys(_.omit(['cashbox'], movedBills))
|
||||
)
|
||||
|
||||
// This occurs when an empty unit is called when the units are already empty, hence, no bills moved around
|
||||
if (_.isEmpty(billArr) && _.isEmpty(operationsToCreate)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return db.tx(t => {
|
||||
const q1Cols = ['id', 'device_id', 'operation_type']
|
||||
const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'))
|
||||
const q2Cols = ['id', 'fiat', 'fiat_code', 'device_id']
|
||||
const q2 = t.none(pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills'))
|
||||
const q3 = t.none(`UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5, stacker1f=$6, stacker1r=$7, stacker2f=$8, stacker2r=$9, stacker3f=$10, stacker3r=$11 WHERE device_id=$12`, [
|
||||
_.defaultTo(machine.cashUnits.cashbox, newUnits.cashbox),
|
||||
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
||||
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
||||
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
||||
_.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4),
|
||||
_.defaultTo(machine.cashUnits.stacker1f, newUnits.stacker1f),
|
||||
_.defaultTo(machine.cashUnits.stacker1r, newUnits.stacker1r),
|
||||
_.defaultTo(machine.cashUnits.stacker2f, newUnits.stacker2f),
|
||||
_.defaultTo(machine.cashUnits.stacker2r, newUnits.stacker2r),
|
||||
_.defaultTo(machine.cashUnits.stacker3f, newUnits.stacker3f),
|
||||
_.defaultTo(machine.cashUnits.stacker3r, newUnits.stacker3r),
|
||||
deviceId
|
||||
])
|
||||
|
||||
return t.batch([q1, q2, q3])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function refillMachineUnits ({ deviceId, newUnits }) {
|
||||
return getMachine(deviceId)
|
||||
.then(machine => {
|
||||
const movedBills = _.reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[value]: {
|
||||
operationName: `cash-${_.replace(/(stacker)/g, '$1-')(value)}-refill`,
|
||||
delta: newUnits[value] - machine.cashUnits[value]
|
||||
}
|
||||
}),
|
||||
{},
|
||||
_.keys(newUnits)
|
||||
)
|
||||
|
||||
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cassette1', 'cassette2', 'cassette3', 'cassette4'], movedBills)))
|
||||
const operationsToCreate = _.map(it => ({
|
||||
id: uuid.v4(),
|
||||
device_id: deviceId,
|
||||
operation_type: it
|
||||
}))(operationNames)
|
||||
|
||||
// This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around
|
||||
if (_.isEmpty(operationsToCreate)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return db.tx(t => {
|
||||
const q1Cols = ['id', 'device_id', 'operation_type']
|
||||
const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'))
|
||||
const q2 = t.none(`UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5, stacker1f=$6, stacker1r=$7, stacker2f=$8, stacker2r=$9, stacker3f=$10, stacker3r=$11 WHERE device_id=$12`, [
|
||||
_.defaultTo(machine.cashUnits.cashbox, newUnits.cashbox),
|
||||
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
||||
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
||||
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
||||
_.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4),
|
||||
_.defaultTo(machine.cashUnits.stacker1f, newUnits.stacker1f),
|
||||
_.defaultTo(machine.cashUnits.stacker1r, newUnits.stacker1r),
|
||||
_.defaultTo(machine.cashUnits.stacker2f, newUnits.stacker2f),
|
||||
_.defaultTo(machine.cashUnits.stacker2r, newUnits.stacker2r),
|
||||
_.defaultTo(machine.cashUnits.stacker3f, newUnits.stacker3f),
|
||||
_.defaultTo(machine.cashUnits.stacker3r, newUnits.stacker3r),
|
||||
deviceId
|
||||
])
|
||||
|
||||
return t.batch([q1, q2])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function unpair (rec) {
|
||||
return pairing.unpair(rec.deviceId)
|
||||
}
|
||||
|
|
@ -193,6 +324,24 @@ function restartServices (rec) {
|
|||
)])
|
||||
}
|
||||
|
||||
function emptyUnit (rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'emptyUnit',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)])
|
||||
}
|
||||
|
||||
function refillUnit (rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'refillUnit',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)])
|
||||
}
|
||||
|
||||
function setMachine (rec, operatorId) {
|
||||
rec.operatorId = operatorId
|
||||
switch (rec.action) {
|
||||
|
|
@ -204,6 +353,8 @@ function setMachine (rec, operatorId) {
|
|||
case 'reboot': return reboot(rec)
|
||||
case 'shutdown': return shutdown(rec)
|
||||
case 'restartServices': return restartServices(rec)
|
||||
case 'emptyUnit': return emptyUnit(rec)
|
||||
case 'refillUnit': return refillUnit(rec)
|
||||
default: throw new Error('No such action: ' + rec.action)
|
||||
}
|
||||
}
|
||||
|
|
@ -277,5 +428,7 @@ module.exports = {
|
|||
updateNetworkHeartbeat,
|
||||
getNetworkPerformance,
|
||||
getNetworkHeartbeat,
|
||||
getConfig
|
||||
getConfig,
|
||||
emptyMachineUnits,
|
||||
refillMachineUnits
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ function machineAction (type, value) {
|
|||
logger.debug(`Restarting services of machine '${deviceId}' from operator ${operatorId}`)
|
||||
state.restartServicesMap[operatorId] = { [deviceId]: pid }
|
||||
break
|
||||
case 'emptyUnit':
|
||||
logger.debug(`Emptying units from machine '${deviceId}' from operator ${operatorId}`)
|
||||
state.emptyUnit[operatorId] = { [deviceId]: pid }
|
||||
break
|
||||
case 'refillUnit':
|
||||
logger.debug(`Refilling stackers from machine '${deviceId}' from operator ${operatorId}`)
|
||||
state.refillUnit[operatorId] = { [deviceId]: pid }
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ module.exports = (function () {
|
|||
reboots: {},
|
||||
shutdowns: {},
|
||||
restartServicesMap: {},
|
||||
emptyUnit: {},
|
||||
refillUnit: {},
|
||||
mnemonic: null
|
||||
}
|
||||
}())
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ const resolvers = {
|
|||
unpairedMachines: () => machineLoader.getUnpairedMachines()
|
||||
},
|
||||
Mutation: {
|
||||
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context]) =>
|
||||
machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context)
|
||||
machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) =>
|
||||
machineAction({ deviceId, action, cashUnits, newName }, context)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ const typeDef = gql`
|
|||
type Bill {
|
||||
id: ID
|
||||
fiat: Int
|
||||
fiatCode: String
|
||||
deviceId: ID
|
||||
created: Date
|
||||
cashboxBatchId: ID
|
||||
cashUnitOperationId: ID
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
|
|
|||
|
|
@ -14,12 +14,9 @@ const typeDef = gql`
|
|||
pairedAt: Date
|
||||
version: String
|
||||
model: String
|
||||
cashbox: Int
|
||||
cassette1: Int
|
||||
cassette2: Int
|
||||
cassette3: Int
|
||||
cassette4: Int
|
||||
cashUnits: CashUnits
|
||||
numberOfCassettes: Int
|
||||
numberOfStackers: Int
|
||||
statuses: [MachineStatus]
|
||||
latestEvent: MachineEvent
|
||||
downloadSpeed: String
|
||||
|
|
@ -27,6 +24,34 @@ const typeDef = gql`
|
|||
packetLoss: String
|
||||
}
|
||||
|
||||
type CashUnits {
|
||||
cashbox: Int
|
||||
cassette1: Int
|
||||
cassette2: Int
|
||||
cassette3: Int
|
||||
cassette4: Int
|
||||
stacker1f: Int
|
||||
stacker1r: Int
|
||||
stacker2f: Int
|
||||
stacker2r: Int
|
||||
stacker3f: Int
|
||||
stacker3r: Int
|
||||
}
|
||||
|
||||
input CashUnitsInput {
|
||||
cashbox: Int
|
||||
cassette1: Int
|
||||
cassette2: Int
|
||||
cassette3: Int
|
||||
cassette4: Int
|
||||
stacker1f: Int
|
||||
stacker1r: Int
|
||||
stacker2f: Int
|
||||
stacker2r: Int
|
||||
stacker3f: Int
|
||||
stacker3r: Int
|
||||
}
|
||||
|
||||
type UnpairedMachine {
|
||||
id: ID!
|
||||
deviceId: ID!
|
||||
|
|
@ -55,6 +80,8 @@ const typeDef = gql`
|
|||
reboot
|
||||
shutdown
|
||||
restartServices
|
||||
emptyUnit
|
||||
refillUnit
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
|
@ -64,7 +91,7 @@ const typeDef = gql`
|
|||
}
|
||||
|
||||
type Mutation {
|
||||
machineAction(deviceId:ID!, action: MachineAction!, cashbox: Int, cassette1: Int, cassette2: Int, cassette3: Int, cassette4: Int, newName: String): Machine @auth
|
||||
machineAction(deviceId:ID!, action: MachineAction!, cashUnits: CashUnitsInput, newName: String): Machine @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ const getBills = filters => {
|
|||
|
||||
const sql = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (
|
||||
SELECT id, device_id FROM cash_in_txs ${deviceStatement}
|
||||
) AS cit ON cit.id = b.cash_in_txs_id ${batchStatement(filters.batch)}`
|
||||
) AS cit ON cit.id = b.cash_in_txs_id ${batchStatement(filters.batch)} ${_.isNil(batchStatement(filters.batch)) ? `WHERE` : `AND`} b.destination_unit = 'cashbox'`
|
||||
|
||||
return db.any(sql)
|
||||
.then(res => _.map(_.mapKeys(_.camelCase), res))
|
||||
const sql2 = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, b.device_id FROM empty_unit_bills b ${deviceStatement} ${!_.isNil(filters.deviceId) && !_.isNil(filters.batch) ? `AND ${_.replace('WHERE', '', batchStatement(filters.batch))}` : `${batchStatement(filters.batch)}`}`
|
||||
|
||||
return Promise.all([db.any(sql), db.any(sql2)])
|
||||
.then(([bills, operationalBills]) => _.map(_.mapKeys(_.camelCase), _.concat(bills, operationalBills)))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ function getMachine (machineId) {
|
|||
.then(machines => machines.find(({ deviceId }) => deviceId === machineId))
|
||||
}
|
||||
|
||||
function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context) {
|
||||
function machineAction ({ deviceId, action, cashUnits, newName }, context) {
|
||||
const operatorId = context.res.locals.operatorId
|
||||
return getMachine(deviceId)
|
||||
.then(machine => {
|
||||
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
|
||||
return machine
|
||||
})
|
||||
.then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2, cassette3, cassette4], newName }, operatorId))
|
||||
.then(machineLoader.setMachine({ deviceId, action, cashUnits, newName }, operatorId))
|
||||
.then(getMachine(deviceId))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -153,9 +153,11 @@ function advancedBatch (data) {
|
|||
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
||||
'dispense', 'notified', 'redeem', 'phone', 'error',
|
||||
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
||||
'dispenseConfirmed', 'provisioned1', 'provisioned2',
|
||||
'denomination1', 'denomination2', 'errorCode', 'customerId',
|
||||
'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
||||
'provisioned1f', 'provisioned1r', 'provisioned2f', 'provisioned2r', 'provisioned3f', 'provisioned3r',
|
||||
'denomination1', 'denomination2', 'denomination3', 'denomination4',
|
||||
'denomination1f', 'denomination1r', 'denomination2f', 'denomination2r', 'denomination3f', 'denomination3r',
|
||||
'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
||||
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
||||
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ const CODES_DISPLAY = {
|
|||
HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
|
||||
CASH_BOX_FULL: 'Cash box full',
|
||||
LOW_CASH_OUT: 'Low Cash-out',
|
||||
LOW_RECYCLER_STACKER: 'Low Recycler Stacker',
|
||||
HIGH_RECYCLER_STACKER: 'High Recycler Stacker',
|
||||
CASHBOX_REMOVED: 'Cashbox removed'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const CA_PATH = process.env.CA_PATH
|
|||
// A machine on an older version (no multicassette code) could be paired with a server with multicassette code.
|
||||
// This makes sure that the server stores a default value
|
||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||
const DEFAULT_NUMBER_OF_STACKERS = 0
|
||||
|
||||
function pullToken (token) {
|
||||
const sql = `delete from pairing_tokens
|
||||
|
|
@ -36,16 +37,16 @@ function unpair (deviceId) {
|
|||
)
|
||||
}
|
||||
|
||||
function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES) {
|
||||
function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES, numOfStackers = DEFAULT_NUMBER_OF_STACKERS) {
|
||||
return pullToken(token)
|
||||
.then(r => {
|
||||
if (r.expired) return false
|
||||
|
||||
const insertSql = `insert into devices (device_id, name, number_of_cassettes) values ($1, $2, $3)
|
||||
const insertSql = `insert into devices (device_id, name, number_of_cassettes, number_of_stackers) values ($1, $2, $3, $4)
|
||||
on conflict (device_id)
|
||||
do update set paired=TRUE, display=TRUE`
|
||||
|
||||
return db.none(insertSql, [deviceId, r.name, numOfCassettes])
|
||||
return db.none(insertSql, [deviceId, r.name, numOfCassettes, numOfStackers])
|
||||
.then(() => true)
|
||||
})
|
||||
.catch(err => {
|
||||
|
|
|
|||
233
lib/plugins.js
233
lib/plugins.js
|
|
@ -27,7 +27,7 @@ const loyalty = require('./loyalty')
|
|||
const transactionBatching = require('./tx-batching')
|
||||
const state = require('./middlewares/state')
|
||||
|
||||
const { CASSETTE_MAX_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants')
|
||||
const { CASH_UNIT_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants')
|
||||
|
||||
const notifier = require('./notifier')
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
const sumTxs = (sum, tx) => {
|
||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
||||
const bills = _.filter(it => it.denomination > 0, tx.bills)
|
||||
const bills = _.filter(it => _.includes('cassette', it.name) && it.denomination > 0, tx.bills)
|
||||
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||
|
||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
||||
|
|
@ -139,6 +139,7 @@ function plugins (settings, deviceId) {
|
|||
const computedCassettes = []
|
||||
_.forEach(it => {
|
||||
computedCassettes.push({
|
||||
name: cassettes[it].name,
|
||||
denomination: cassettes[it].denomination,
|
||||
count: counts[it]
|
||||
})
|
||||
|
|
@ -147,37 +148,75 @@ function plugins (settings, deviceId) {
|
|||
return computedCassettes
|
||||
}
|
||||
|
||||
function computeAvailableStackers (stackers, redeemableTxs) {
|
||||
if (_.isEmpty(redeemableTxs)) return stackers
|
||||
|
||||
const sumTxs = (sum, tx) => {
|
||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
||||
const bills = _.filter(it => _.includes('stacker', it.name) && it.denomination > 0, tx.bills)
|
||||
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||
|
||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(stackers, bills))
|
||||
|
||||
if (!doDenominationsMatch) {
|
||||
throw new Error('Denominations don\'t add up, stackers were changed.')
|
||||
}
|
||||
|
||||
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
|
||||
}
|
||||
|
||||
const provisioned = _.reduce(sumTxs, _.times(_.constant(0), _.size(stackers)), redeemableTxs)
|
||||
const zipped = _.zip(_.map('count', stackers), provisioned)
|
||||
const counts = _.map(r => r[0] - r[1], zipped)
|
||||
|
||||
if (_.some(_.lt(_, 0), counts)) {
|
||||
throw new Error('Negative note count: %j', counts)
|
||||
}
|
||||
|
||||
const computedStackers = []
|
||||
_.forEach(it => {
|
||||
computedStackers.push({
|
||||
name: stackers[it].name,
|
||||
denomination: stackers[it].denomination,
|
||||
count: counts[it]
|
||||
})
|
||||
}, _.times(_.identity(), _.size(stackers)))
|
||||
|
||||
return computedStackers
|
||||
}
|
||||
|
||||
function buildAvailableCassettes (excludeTxId) {
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||
|
||||
if (!cashOutConfig.active) return Promise.resolve()
|
||||
|
||||
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||
.then(([rec, _redeemableTxs]) => {
|
||||
.then(([_cassettes, _redeemableTxs]) => {
|
||||
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
||||
|
||||
const denominations = []
|
||||
_.forEach(it => {
|
||||
denominations.push(cashOutConfig[`cassette${it + 1}`])
|
||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
||||
}, _.times(_.identity(), _cassettes.numberOfCassettes))
|
||||
|
||||
const virtualCassettes = [Math.max(...denominations) * 2]
|
||||
|
||||
const counts = argv.cassettes
|
||||
? argv.cassettes.split(',')
|
||||
: rec.counts
|
||||
: _cassettes.counts
|
||||
|
||||
if (rec.counts.length !== denominations.length) {
|
||||
if (_cassettes.counts.length !== denominations.length) {
|
||||
throw new Error('Denominations and respective counts do not match!')
|
||||
}
|
||||
|
||||
const cassettes = []
|
||||
_.forEach(it => {
|
||||
cassettes.push({
|
||||
name: `cassette${it + 1}`,
|
||||
denomination: parseInt(denominations[it], 10),
|
||||
count: parseInt(counts[it], 10)
|
||||
})
|
||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
||||
}, _.times(_.identity(), _cassettes.numberOfCassettes))
|
||||
|
||||
try {
|
||||
return {
|
||||
|
|
@ -194,6 +233,62 @@ function plugins (settings, deviceId) {
|
|||
})
|
||||
}
|
||||
|
||||
function buildAvailableStackers (excludeTxId) {
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||
|
||||
if (!cashOutConfig.active) return Promise.resolve()
|
||||
|
||||
return Promise.all([dbm.stackerCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||
.then(([_stackers, _redeemableTxs]) => {
|
||||
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
||||
|
||||
const denominations = []
|
||||
_.forEach(it => {
|
||||
denominations.push([cashOutConfig[`stacker${it + 1}f`], cashOutConfig[`stacker${it + 1}r`]])
|
||||
}, _.times(_.identity(), _stackers.numberOfStackers))
|
||||
|
||||
const virtualStackers = [Math.max(..._.flatten(denominations)) * 2]
|
||||
|
||||
const counts = _stackers.counts
|
||||
|
||||
if (counts.length !== denominations.length) {
|
||||
throw new Error('Denominations and respective counts do not match!')
|
||||
}
|
||||
|
||||
const stackers = []
|
||||
_.forEach(it => {
|
||||
stackers.push({
|
||||
name: `stacker${it + 1}f`,
|
||||
denomination: parseInt(denominations[it][0], 10),
|
||||
count: parseInt(counts[it][0], 10)
|
||||
})
|
||||
stackers.push({
|
||||
name: `stacker${it + 1}r`,
|
||||
denomination: parseInt(denominations[it][1], 10),
|
||||
count: parseInt(counts[it][1], 10)
|
||||
})
|
||||
}, _.times(_.identity(), _stackers.numberOfStackers))
|
||||
|
||||
try {
|
||||
return {
|
||||
stackers: computeAvailableStackers(stackers, redeemableTxs),
|
||||
virtualStackers
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return {
|
||||
stackers,
|
||||
virtualStackers
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildAvailableUnits (excludeTxId) {
|
||||
return Promise.all([buildAvailableCassettes(excludeTxId), buildAvailableStackers(excludeTxId)])
|
||||
.then(([cassettes, stackers]) => ({ cassettes: cassettes.cassettes, stackers: stackers.stackers }))
|
||||
}
|
||||
|
||||
function fetchCurrentConfigVersion () {
|
||||
const sql = `select id from user_config
|
||||
where type=$1
|
||||
|
|
@ -240,6 +335,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
return Promise.all([
|
||||
buildAvailableCassettes(),
|
||||
buildAvailableStackers(),
|
||||
fetchCurrentConfigVersion(),
|
||||
millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)),
|
||||
loyalty.getNumberOfAvailablePromoCodes(),
|
||||
|
|
@ -250,6 +346,7 @@ function plugins (settings, deviceId) {
|
|||
])
|
||||
.then(([
|
||||
cassettes,
|
||||
stackers,
|
||||
configVersion,
|
||||
timezone,
|
||||
numberOfAvailablePromoCodes,
|
||||
|
|
@ -273,6 +370,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
return {
|
||||
cassettes,
|
||||
stackers,
|
||||
rates: buildRates(tickers),
|
||||
balances: buildBalances(balances),
|
||||
coins,
|
||||
|
|
@ -645,71 +743,164 @@ function plugins (settings, deviceId) {
|
|||
const denomination2 = cashOutConfig.cassette2
|
||||
const denomination3 = cashOutConfig.cassette3
|
||||
const denomination4 = cashOutConfig.cassette4
|
||||
const denomination1f = cashOutConfig.stacker1f
|
||||
const denomination1r = cashOutConfig.stacker1r
|
||||
const denomination2f = cashOutConfig.stacker2f
|
||||
const denomination2r = cashOutConfig.stacker2r
|
||||
const denomination3f = cashOutConfig.stacker3f
|
||||
const denomination3r = cashOutConfig.stacker3r
|
||||
const cashOutEnabled = cashOutConfig.active
|
||||
const isCassetteLow = (have, max, limit) => cashOutEnabled && ((have / max) * 100) < limit
|
||||
const isUnitLow = (have, max, limit) => cashOutEnabled && ((have / max) * 100) < limit
|
||||
// const isUnitHigh = (have, max, limit) => cashOutEnabled && ((have / max) * 100) > limit
|
||||
|
||||
// const isUnitOutOfBounds = (have, max, lowerBound, upperBound) => isUnitLow(have, max, lowerBound) || isUnitHigh(have, max, upperBound)
|
||||
|
||||
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
||||
|
||||
const machineName = device.name
|
||||
|
||||
const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
|
||||
const cashInAlert = device.cashUnits.cashbox > notifications.cashInAlertThreshold
|
||||
? {
|
||||
code: 'CASH_BOX_FULL',
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cashbox
|
||||
notes: device.cashUnits.cashbox
|
||||
}
|
||||
: null
|
||||
|
||||
const cassette1Alert = device.numberOfCassettes >= 1 && isCassetteLow(device.cassette1, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette1)
|
||||
const cassette1Alert = device.numberOfCassettes >= 1 && isUnitLow(device.cashUnits.cassette1, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette1)
|
||||
? {
|
||||
code: 'LOW_CASH_OUT',
|
||||
cassette: 1,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cassette1,
|
||||
notes: device.cashUnits.cassette1,
|
||||
denomination: denomination1,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const cassette2Alert = device.numberOfCassettes >= 2 && isCassetteLow(device.cassette2, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette2)
|
||||
const cassette2Alert = device.numberOfCassettes >= 2 && isUnitLow(device.cashUnits.cassette2, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette2)
|
||||
? {
|
||||
code: 'LOW_CASH_OUT',
|
||||
cassette: 2,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cassette2,
|
||||
notes: device.cashUnits.cassette2,
|
||||
denomination: denomination2,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const cassette3Alert = device.numberOfCassettes >= 3 && isCassetteLow(device.cassette3, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette3)
|
||||
const cassette3Alert = device.numberOfCassettes >= 3 && isUnitLow(device.cashUnits.cassette3, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette3)
|
||||
? {
|
||||
code: 'LOW_CASH_OUT',
|
||||
cassette: 3,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cassette3,
|
||||
notes: device.cashUnits.cassette3,
|
||||
denomination: denomination3,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const cassette4Alert = device.numberOfCassettes >= 4 && isCassetteLow(device.cassette4, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette4)
|
||||
const cassette4Alert = device.numberOfCassettes >= 4 && isUnitLow(device.cashUnits.cassette4, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette4)
|
||||
? {
|
||||
code: 'LOW_CASH_OUT',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cassette4,
|
||||
notes: device.cashUnits.cassette4,
|
||||
denomination: denomination4,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
return _.compact([cashInAlert, cassette1Alert, cassette2Alert, cassette3Alert, cassette4Alert])
|
||||
const stacker1fAlert = device.numberOfStackers >= 1 && isUnitLow(device.cashUnits.stacker1f, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker1f)
|
||||
? {
|
||||
code: 'LOW_RECYCLER_STACKER',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cashUnits.stacker1f,
|
||||
denomination: denomination1f,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const stacker1rAlert = device.numberOfStackers >= 1 && isUnitLow(device.cashUnits.stacker1r, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker1r)
|
||||
? {
|
||||
code: 'LOW_RECYCLER_STACKER',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cashUnits.stacker1r,
|
||||
denomination: denomination1r,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const stacker2fAlert = device.numberOfStackers >= 2 && isUnitLow(device.cashUnits.stacker2f, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker2f)
|
||||
? {
|
||||
code: 'LOW_RECYCLER_STACKER',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cashUnits.stacker2f,
|
||||
denomination: denomination2f,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const stacker2rAlert = device.numberOfStackers >= 2 && isUnitLow(device.cashUnits.stacker2r, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker2r)
|
||||
? {
|
||||
code: 'LOW_RECYCLER_STACKER',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cashUnits.stacker2r,
|
||||
denomination: denomination2r,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const stacker3fAlert = device.numberOfStackers >= 3 && isUnitLow(device.cashUnits.stacker3f, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker3f)
|
||||
? {
|
||||
code: 'LOW_RECYCLER_STACKER',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cashUnits.stacker3f,
|
||||
denomination: denomination3f,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const stacker3rAlert = device.numberOfStackers >= 3 && isUnitLow(device.cashUnits.stacker3r, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker3r)
|
||||
? {
|
||||
code: 'LOW_RECYCLER_STACKER',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cashUnits.stacker3r,
|
||||
denomination: denomination3r,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
return _.compact([
|
||||
cashInAlert,
|
||||
cassette1Alert,
|
||||
cassette2Alert,
|
||||
cassette3Alert,
|
||||
cassette4Alert,
|
||||
stacker1fAlert,
|
||||
stacker1rAlert,
|
||||
stacker2fAlert,
|
||||
stacker2rAlert,
|
||||
stacker3fAlert,
|
||||
stacker3rAlert
|
||||
])
|
||||
}
|
||||
|
||||
function checkCryptoBalances (fiatCode, devices) {
|
||||
|
|
@ -874,7 +1065,6 @@ function plugins (settings, deviceId) {
|
|||
sendMessage,
|
||||
checkBalances,
|
||||
getMachineNames,
|
||||
buildAvailableCassettes,
|
||||
buy,
|
||||
sell,
|
||||
getNotificationConfig,
|
||||
|
|
@ -885,7 +1075,8 @@ function plugins (settings, deviceId) {
|
|||
isValidWalletScore,
|
||||
getTransactionHash,
|
||||
getInputAddresses,
|
||||
isWalletScoringEnabled
|
||||
isWalletScoringEnabled,
|
||||
buildAvailableUnits
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,21 @@ exports.cassetteCounts = function cassetteCounts (deviceId) {
|
|||
})
|
||||
}
|
||||
|
||||
exports.stackerCounts = function stackerCounts (deviceId) {
|
||||
const sql = 'SELECT stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r, number_of_stackers FROM devices ' +
|
||||
'WHERE device_id=$1'
|
||||
|
||||
return db.one(sql, [deviceId])
|
||||
.then(row => {
|
||||
const counts = []
|
||||
_.forEach(it => {
|
||||
counts.push([row[`stacker${it + 1}f`], row[`stacker${it + 1}r`]])
|
||||
}, _.times(_.identity(), row.number_of_stackers))
|
||||
|
||||
return { numberOfStackers: row.number_of_stackers, counts }
|
||||
})
|
||||
}
|
||||
|
||||
// Note: since we only prune on insert, we'll always have
|
||||
// last known state.
|
||||
exports.machineEvent = function machineEvent (rec) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const populateDeviceId = require('./middlewares/populateDeviceId')
|
|||
const populateSettings = require('./middlewares/populateSettings')
|
||||
const recordPing = require('./middlewares/recordPing')
|
||||
|
||||
const unitsRoutes = require('./routes/unitsRoutes')
|
||||
const cashboxRoutes = require('./routes/cashboxRoutes')
|
||||
const customerRoutes = require('./routes/customerRoutes')
|
||||
const logsRoutes = require('./routes/logsRoutes')
|
||||
|
|
@ -82,6 +83,7 @@ app.use('/customer', customerRoutes)
|
|||
app.use('/tx', txRoutes)
|
||||
|
||||
app.use('/logs', logsRoutes)
|
||||
app.use('/units', unitsRoutes)
|
||||
|
||||
graphQLServer.applyMiddleware({ app })
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function notifyCashboxRemoval (req, res, next) {
|
|||
}
|
||||
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to automatic. A cashbox batch WILL be created')
|
||||
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...')
|
||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashUnits.cashbox)
|
||||
.then(() => {
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Finished creating the new cashbox batch`)
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Resetting the cashbox counter on device ${req.deviceId}`)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ function pair (req, res, next) {
|
|||
const deviceId = req.deviceId
|
||||
const model = req.query.model
|
||||
const numOfCassettes = req.query.numOfCassettes
|
||||
const numOfStackers = req.query.numOfStackers
|
||||
|
||||
return pairing.pair(token, deviceId, model, numOfCassettes)
|
||||
return pairing.pair(token, deviceId, model, numOfCassettes, numOfStackers)
|
||||
.then(isValid => {
|
||||
if (isValid) return res.json({ status: 'paired' })
|
||||
throw httpError('Pairing failed')
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ function poll (req, res, next) {
|
|||
const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid
|
||||
const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid
|
||||
const restartServices = pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid
|
||||
const emptyUnit = pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid
|
||||
const refillUnit = pid && state.refillUnit?.[operatorId]?.[deviceId] === pid
|
||||
const langs = localeConfig.languages
|
||||
|
||||
const locale = {
|
||||
|
|
@ -119,6 +121,8 @@ function poll (req, res, next) {
|
|||
reboot,
|
||||
shutdown,
|
||||
restartServices,
|
||||
emptyUnit,
|
||||
refillUnit,
|
||||
hasLightning,
|
||||
receipt,
|
||||
operatorInfo,
|
||||
|
|
|
|||
26
lib/routes/unitsRoutes.js
Normal file
26
lib/routes/unitsRoutes.js
Normal 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
|
||||
9
lib/utils.js
Normal file
9
lib/utils.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const _ = require('lodash')
|
||||
|
||||
const camelize = obj =>
|
||||
_.transform(obj, (acc, value, key, target) => {
|
||||
const camelKey = _.isArray(target) ? key : _.camelCase(key.toString())
|
||||
acc[camelKey] = _.isObject(value) && !(value instanceof Date) ? camelize(value) : value
|
||||
})
|
||||
|
||||
module.exports = camelize
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const _ = require('lodash/fp')
|
||||
const { migrationSaveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||
const { CASSETTE_MAX_CAPACITY } = require('../lib/constants')
|
||||
const CASSETTE_MAX_CAPACITY = 500
|
||||
|
||||
exports.up = function (next) {
|
||||
return loadLatest()
|
||||
|
|
|
|||
117
migrations/1681428616990-aveiro-recycler-settings.js
Normal file
117
migrations/1681428616990-aveiro-recycler-settings.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`ALTER TABLE cash_out_actions
|
||||
ADD COLUMN provisioned_1f INTEGER,
|
||||
ADD COLUMN provisioned_1r INTEGER,
|
||||
ADD COLUMN provisioned_2f INTEGER,
|
||||
ADD COLUMN provisioned_2r INTEGER,
|
||||
ADD COLUMN provisioned_3f INTEGER,
|
||||
ADD COLUMN provisioned_3r INTEGER,
|
||||
ADD COLUMN dispensed_1f INTEGER,
|
||||
ADD COLUMN dispensed_1r INTEGER,
|
||||
ADD COLUMN dispensed_2f INTEGER,
|
||||
ADD COLUMN dispensed_2r INTEGER,
|
||||
ADD COLUMN dispensed_3f INTEGER,
|
||||
ADD COLUMN dispensed_3r INTEGER,
|
||||
ADD COLUMN rejected_1f INTEGER,
|
||||
ADD COLUMN rejected_1r INTEGER,
|
||||
ADD COLUMN rejected_2f INTEGER,
|
||||
ADD COLUMN rejected_2r INTEGER,
|
||||
ADD COLUMN rejected_3f INTEGER,
|
||||
ADD COLUMN rejected_3r INTEGER,
|
||||
ADD COLUMN denomination_1f INTEGER,
|
||||
ADD COLUMN denomination_1r INTEGER,
|
||||
ADD COLUMN denomination_2f INTEGER,
|
||||
ADD COLUMN denomination_2r INTEGER,
|
||||
ADD COLUMN denomination_3f INTEGER,
|
||||
ADD COLUMN denomination_3r INTEGER`,
|
||||
`ALTER TABLE devices
|
||||
ADD COLUMN stacker1f INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker1r INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker2f INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker2r INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker3f INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker3r INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN number_of_stackers INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE cash_out_txs
|
||||
ADD COLUMN provisioned_1f INTEGER,
|
||||
ADD COLUMN provisioned_1r INTEGER,
|
||||
ADD COLUMN provisioned_2f INTEGER,
|
||||
ADD COLUMN provisioned_2r INTEGER,
|
||||
ADD COLUMN provisioned_3f INTEGER,
|
||||
ADD COLUMN provisioned_3r INTEGER,
|
||||
ADD COLUMN denomination_1f INTEGER,
|
||||
ADD COLUMN denomination_1r INTEGER,
|
||||
ADD COLUMN denomination_2f INTEGER,
|
||||
ADD COLUMN denomination_2r INTEGER,
|
||||
ADD COLUMN denomination_3f INTEGER,
|
||||
ADD COLUMN denomination_3r INTEGER`,
|
||||
`CREATE TYPE cash_unit AS ENUM (
|
||||
'cashbox',
|
||||
'cassette1',
|
||||
'cassette2',
|
||||
'cassette3',
|
||||
'cassette4',
|
||||
'stacker1f',
|
||||
'stacker1r',
|
||||
'stacker2f',
|
||||
'stacker2r',
|
||||
'stacker3f',
|
||||
'stacker3r'
|
||||
)`,
|
||||
`ALTER TABLE bills ADD COLUMN destination_unit cash_unit NOT NULL DEFAULT 'cashbox'`,
|
||||
`CREATE TYPE cash_unit_operation_type AS ENUM(
|
||||
'cash-box-empty',
|
||||
'cash-box-refill',
|
||||
'cash-cassette-1-refill',
|
||||
'cash-cassette-1-empty',
|
||||
'cash-cassette-1-count-change',
|
||||
'cash-cassette-2-refill',
|
||||
'cash-cassette-2-empty',
|
||||
'cash-cassette-2-count-change',
|
||||
'cash-cassette-3-refill',
|
||||
'cash-cassette-3-empty',
|
||||
'cash-cassette-3-count-change',
|
||||
'cash-cassette-4-refill',
|
||||
'cash-cassette-4-empty',
|
||||
'cash-cassette-4-count-change',
|
||||
'cash-stacker-1f-refill',
|
||||
'cash-stacker-1f-empty',
|
||||
'cash-stacker-1f-count-change',
|
||||
'cash-stacker-1r-refill',
|
||||
'cash-stacker-1r-empty',
|
||||
'cash-stacker-1r-count-change',
|
||||
'cash-stacker-2f-refill',
|
||||
'cash-stacker-2f-empty',
|
||||
'cash-stacker-2f-count-change',
|
||||
'cash-stacker-2r-refill',
|
||||
'cash-stacker-2r-empty',
|
||||
'cash-stacker-2r-count-change',
|
||||
'cash-stacker-3f-refill',
|
||||
'cash-stacker-3f-empty',
|
||||
'cash-stacker-3f-count-change',
|
||||
'cash-stacker-3r-refill',
|
||||
'cash-stacker-3r-empty',
|
||||
'cash-stacker-3r-count-change'
|
||||
)`,
|
||||
`ALTER TABLE cashbox_batches ALTER COLUMN operation_type TYPE cash_unit_operation_type USING operation_type::text::cash_unit_operation_type`,
|
||||
`ALTER TABLE cashbox_batches RENAME TO cash_unit_operation`,
|
||||
`DROP TYPE cashbox_batch_type`,
|
||||
`CREATE TABLE empty_unit_bills (
|
||||
id UUID PRIMARY KEY,
|
||||
fiat INTEGER NOT NULL,
|
||||
fiat_code TEXT NOT NULL,
|
||||
created TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
device_id TEXT REFERENCES devices (device_id),
|
||||
cashbox_batch_id UUID REFERENCES cash_unit_operation (id)
|
||||
)`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -120,6 +120,7 @@ const ActionCol = ({ disabled, editing }) => {
|
|||
const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
||||
const {
|
||||
name,
|
||||
names,
|
||||
bypassField,
|
||||
input,
|
||||
editable = true,
|
||||
|
|
@ -128,6 +129,8 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
|||
width,
|
||||
textAlign,
|
||||
editingAlign = textAlign,
|
||||
prefix,
|
||||
PrefixComponent = Label2,
|
||||
suffix,
|
||||
SuffixComponent = Label2,
|
||||
textStyle = it => {},
|
||||
|
|
@ -136,6 +139,8 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
|||
inputProps = {}
|
||||
} = config
|
||||
|
||||
const fields = names ?? [name]
|
||||
|
||||
const { values } = useFormikContext()
|
||||
const isEditable = editable => {
|
||||
if (typeof editable === 'function') return editable(values)
|
||||
|
|
@ -159,25 +164,35 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classes.fields}>
|
||||
{R.map(f => (
|
||||
<Td
|
||||
className={{
|
||||
[classes.extraPaddingRight]: extraPaddingRight,
|
||||
[classes.extraPadding]: extraPadding,
|
||||
[classes.withSuffix]: suffix
|
||||
[classes.withSuffix]: suffix,
|
||||
[classes.withPrefix]: prefix
|
||||
}}
|
||||
width={width}
|
||||
size={size}
|
||||
bold={bold}
|
||||
textAlign={textAlign}>
|
||||
{prefix && !isHidden(values) && (
|
||||
<PrefixComponent
|
||||
className={classes.prefix}
|
||||
style={isEditing ? {} : textStyle(values, isEditing)}>
|
||||
{typeof prefix === 'function' ? prefix(f) : prefix}
|
||||
</PrefixComponent>
|
||||
)}
|
||||
{isEditing && isField && !isHidden(values) && (
|
||||
<Field name={name} component={input} {...innerProps} />
|
||||
<Field name={f} component={input} {...innerProps} />
|
||||
)}
|
||||
{isEditing && !isField && !isHidden(values) && (
|
||||
<config.input name={name} />
|
||||
<config.input name={f} />
|
||||
)}
|
||||
{!isEditing && values && !isHidden(values) && (
|
||||
<div style={textStyle(values, isEditing)}>
|
||||
{view(values[name], values)}
|
||||
{view(values[f], values)}
|
||||
</div>
|
||||
)}
|
||||
{suffix && !isHidden(values) && (
|
||||
|
|
@ -189,6 +204,8 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
|||
)}
|
||||
{isHidden(values) && <StripesSvg />}
|
||||
</Td>
|
||||
))(fields)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,21 @@ export default {
|
|||
suffix: {
|
||||
margin: [[0, 0, 0, 7]]
|
||||
},
|
||||
size: ({ size }) => bySize(size),
|
||||
bold
|
||||
withPrefix: ({ textAlign }) => {
|
||||
const justifyContent = textAlign === 'right' ? 'flex-end' : textAlign
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent
|
||||
}
|
||||
},
|
||||
prefix: {
|
||||
margin: [[0, 7, 0, 0]]
|
||||
},
|
||||
size: ({ size }) => bySize(size),
|
||||
bold,
|
||||
fields: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const Cashbox = ({
|
|||
percent = 0,
|
||||
cashOut = false,
|
||||
width,
|
||||
height,
|
||||
className,
|
||||
emptyPartClassName,
|
||||
labelClassName,
|
||||
|
|
@ -27,6 +28,7 @@ const Cashbox = ({
|
|||
percent,
|
||||
cashOut,
|
||||
width,
|
||||
height,
|
||||
applyColorVariant,
|
||||
isLow
|
||||
})
|
||||
|
|
@ -55,35 +57,17 @@ const Cashbox = ({
|
|||
|
||||
// https://support.lamassu.is/hc/en-us/articles/360025595552-Installing-the-Sintra-Forte
|
||||
// Sintra and Sintra Forte can have up to 500 notes per cashOut box and up to 1000 per cashIn box
|
||||
const CashIn = ({ currency, notes, total }) => {
|
||||
const classes = gridClasses()
|
||||
return (
|
||||
<>
|
||||
<div className={classes.row}>
|
||||
<div>
|
||||
<div className={classes.innerRow}>
|
||||
<Info2 className={classes.noMarginText}>{notes} notes</Info2>
|
||||
</div>
|
||||
<div className={classes.innerRow}>
|
||||
<Label1 className={classes.noMarginText}>
|
||||
{total} {currency.code}
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CashOut = ({
|
||||
const CashIn = ({
|
||||
capacity = 500,
|
||||
denomination = 0,
|
||||
currency,
|
||||
notes,
|
||||
className,
|
||||
editingMode = false,
|
||||
threshold,
|
||||
width
|
||||
width,
|
||||
height,
|
||||
total,
|
||||
omitInnerPercentage
|
||||
}) => {
|
||||
const percent = (100 * notes) / capacity
|
||||
const isLow = percent < threshold
|
||||
|
|
@ -98,6 +82,54 @@ const CashOut = ({
|
|||
cashOut
|
||||
isLow={isLow}
|
||||
width={width}
|
||||
height={height}
|
||||
omitInnerPercentage={omitInnerPercentage}
|
||||
/>
|
||||
</div>
|
||||
{!editingMode && (
|
||||
<div className={classes.col2}>
|
||||
<div className={classes.innerRow}>
|
||||
<Info2 className={classes.noMarginText}>{notes} notes</Info2>
|
||||
</div>
|
||||
<div className={classes.innerRow}>
|
||||
<Label1 className={classes.noMarginText}>
|
||||
{total} {currency.code}
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CashOut = ({
|
||||
capacity = 500,
|
||||
denomination = 0,
|
||||
currency,
|
||||
notes,
|
||||
className,
|
||||
editingMode = false,
|
||||
threshold,
|
||||
width,
|
||||
height,
|
||||
omitInnerPercentage
|
||||
}) => {
|
||||
const percent = (100 * notes) / capacity
|
||||
const isLow = percent < threshold
|
||||
const classes = gridClasses()
|
||||
return (
|
||||
<>
|
||||
<div className={classes.row}>
|
||||
<div className={classes.col}>
|
||||
<Cashbox
|
||||
className={className}
|
||||
percent={percent}
|
||||
cashOut
|
||||
isLow={isLow}
|
||||
width={width}
|
||||
height={height}
|
||||
omitInnerPercentage={omitInnerPercentage}
|
||||
/>
|
||||
</div>
|
||||
{!editingMode && (
|
||||
|
|
@ -121,4 +153,30 @@ const CashOut = ({
|
|||
)
|
||||
}
|
||||
|
||||
export { Cashbox, CashIn, CashOut }
|
||||
const CashOutLite = ({
|
||||
capacity = 500,
|
||||
denomination = 0,
|
||||
currency,
|
||||
notes,
|
||||
threshold,
|
||||
width
|
||||
}) => {
|
||||
const percent = (100 * notes) / capacity
|
||||
const isLow = percent < threshold
|
||||
const classes = gridClasses()
|
||||
return (
|
||||
<div className={classes.col}>
|
||||
<Cashbox
|
||||
percent={percent}
|
||||
cashOut
|
||||
isLow={isLow}
|
||||
width={width}
|
||||
height={15}
|
||||
omitInnerPercentage
|
||||
/>
|
||||
<Chip label={`${denomination} ${currency.code}`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Cashbox, CashIn, CashOut, CashOutLite }
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const cashboxStyles = {
|
|||
cashbox: {
|
||||
borderColor: colorPicker,
|
||||
backgroundColor: colorPicker,
|
||||
height: 118,
|
||||
height: ({ height }) => height ?? 118,
|
||||
width: ({ width }) => width ?? 80,
|
||||
border: '2px solid',
|
||||
textAlign: 'end',
|
||||
|
|
@ -58,7 +58,13 @@ const cashboxStyles = {
|
|||
|
||||
const gridStyles = {
|
||||
row: {
|
||||
display: 'flex'
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
col: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
},
|
||||
innerRow: {
|
||||
display: 'flex',
|
||||
|
|
|
|||
|
|
@ -185,6 +185,42 @@ const MachineActions = memo(({ machine, onActionSuccess }) => {
|
|||
}}>
|
||||
Restart Services
|
||||
</ActionButton>
|
||||
{machine.model === 'aveiro' && (
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.inlineChip}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setAction({
|
||||
command: 'emptyUnit',
|
||||
display: 'Empty',
|
||||
message:
|
||||
"Triggering this action will move all cash inside the machine towards its cashbox (if possible), allowing for the collection of cash from the machine via only its cashbox. Depending on how full the cash units are, it's possible that this action will need to be used more than once to ensure that the unit is left completely empty."
|
||||
})
|
||||
}}>
|
||||
Empty Unit
|
||||
</ActionButton>
|
||||
)}
|
||||
{machine.model === 'aveiro' && (
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.inlineChip}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setAction({
|
||||
command: 'refillUnit',
|
||||
display: 'Refill',
|
||||
message:
|
||||
'Triggering this action will refill the stackers in this machine, by using bills present in its cassettes. This action may require manual operation of the cassettes and close attention to make sure that the denominations in the cassettes match the denominations in the stackers.'
|
||||
})
|
||||
}}>
|
||||
Refill Unit
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
disabled={disabled}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const Row = ({
|
|||
)}
|
||||
</Tr>
|
||||
</div>
|
||||
{expandable && expanded && (
|
||||
{expanded && (
|
||||
<div className={classes.after}>
|
||||
<Tr className={classnames({ [classes.expanded]: expanded })}>
|
||||
<Td width={width}>
|
||||
|
|
@ -99,6 +99,7 @@ const DataTable = ({
|
|||
data = [],
|
||||
Details,
|
||||
className,
|
||||
tableClassName,
|
||||
expandable,
|
||||
initialExpanded,
|
||||
onClick,
|
||||
|
|
@ -169,7 +170,7 @@ const DataTable = ({
|
|||
|
||||
return (
|
||||
<Box display="flex" flex="1" flexDirection="column">
|
||||
<Table className={classes.table}>
|
||||
<Table className={classnames(classes.table, tableClassName)}>
|
||||
<THead>
|
||||
{elements.map(({ width, className, textAlign, header }, idx) => (
|
||||
<Th
|
||||
|
|
|
|||
|
|
@ -40,12 +40,21 @@ const GET_INFO = gql`
|
|||
machines {
|
||||
name
|
||||
deviceId
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
stacker1f
|
||||
stacker1r
|
||||
stacker2f
|
||||
stacker2r
|
||||
stacker3f
|
||||
stacker3r
|
||||
}
|
||||
numberOfCassettes
|
||||
numberOfStackers
|
||||
}
|
||||
config
|
||||
}
|
||||
|
|
@ -113,9 +122,9 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
|||
data={config}
|
||||
stripeWhen={wasNeverEnabled}
|
||||
enableEdit
|
||||
editWidth={134}
|
||||
editWidth={95}
|
||||
enableToggle
|
||||
toggleWidth={109}
|
||||
toggleWidth={100}
|
||||
onToggle={onToggle}
|
||||
save={save}
|
||||
error={error?.message}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ const MODAL_WIDTH = 554
|
|||
const MODAL_HEIGHT = 520
|
||||
|
||||
const Wizard = ({ machine, locale, onClose, save, error }) => {
|
||||
const LAST_STEP = machine.numberOfCassettes + 1
|
||||
// Each stacker counts as two steps, one for front and another for rear
|
||||
const LAST_STEP = machine.numberOfCassettes + machine.numberOfStackers * 2 + 1
|
||||
const [{ step, config }, setState] = useState({
|
||||
step: 0,
|
||||
config: { active: true }
|
||||
|
|
@ -46,7 +47,8 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const steps = R.map(
|
||||
const steps = R.concat(
|
||||
R.map(
|
||||
it => ({
|
||||
type: `cassette${it}`,
|
||||
display: `Cassette ${it}`,
|
||||
|
|
@ -58,25 +60,92 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
|||
}
|
||||
}),
|
||||
R.range(1, machine.numberOfCassettes + 1)
|
||||
),
|
||||
R.chain(
|
||||
it => [
|
||||
{
|
||||
type: `stacker${it}f`,
|
||||
display: `Stacker ${it}F`,
|
||||
component: Autocomplete,
|
||||
inputProps: {
|
||||
options: options,
|
||||
labelProp: 'display',
|
||||
valueProp: 'code'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: `stacker${it}r`,
|
||||
display: `Stacker ${it}R`,
|
||||
component: Autocomplete,
|
||||
inputProps: {
|
||||
options: options,
|
||||
labelProp: 'display',
|
||||
valueProp: 'code'
|
||||
}
|
||||
}
|
||||
],
|
||||
R.range(1, machine.numberOfStackers + 1)
|
||||
)
|
||||
)
|
||||
|
||||
const schema = () =>
|
||||
Yup.object().shape({
|
||||
cassette1: Yup.number().required(),
|
||||
cassette1:
|
||||
machine.numberOfCassettes >= 1 && step >= 1
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
cassette2:
|
||||
machine.numberOfCassettes > 1 && step >= 2
|
||||
machine.numberOfCassettes >= 2 && step >= 2
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
cassette3:
|
||||
machine.numberOfCassettes > 2 && step >= 3
|
||||
machine.numberOfCassettes >= 3 && step >= 3
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
cassette4:
|
||||
machine.numberOfCassettes > 3 && step >= 4
|
||||
machine.numberOfCassettes >= 4 && step >= 4
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
stacker1f:
|
||||
machine.numberOfStackers >= 1 && step >= machine.numberOfCassettes + 1
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
stacker1r:
|
||||
machine.numberOfStackers >= 1 && step >= machine.numberOfCassettes + 2
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
stacker2f:
|
||||
machine.numberOfStackers >= 2 && step >= machine.numberOfCassettes + 3
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
stacker2r:
|
||||
machine.numberOfStackers >= 2 && step >= machine.numberOfCassettes + 4
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
stacker3f:
|
||||
machine.numberOfStackers >= 3 && step >= machine.numberOfCassettes + 5
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
stacker3r:
|
||||
machine.numberOfStackers >= 3 && step >= machine.numberOfCassettes + 6
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import styles from './WizardStep.styles'
|
|||
const useStyles = makeStyles(styles)
|
||||
|
||||
const getCassetesArtworks = () => ({
|
||||
1: {
|
||||
1: cassetteOne
|
||||
},
|
||||
2: {
|
||||
1: cassetteOne,
|
||||
2: cassetteTwo
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ import { getBillOptions } from 'src/utils/bill-options'
|
|||
import { CURRENCY_MAX } from 'src/utils/constants'
|
||||
import { transformNumber } from 'src/utils/number'
|
||||
|
||||
const widthsByNumberOfCassettes = {
|
||||
2: { machine: 300, cassette: 225, zeroConf: 200 },
|
||||
3: { machine: 210, cassette: 180, zeroConf: 200 },
|
||||
4: { machine: 200, cassette: 150, zeroConf: 150 }
|
||||
const widthsByNumberOfUnits = {
|
||||
2: { machine: 325, cassette: 340 },
|
||||
3: { machine: 300, cassette: 235 },
|
||||
4: { machine: 205, cassette: 200 },
|
||||
5: { machine: 180, cassette: 165 },
|
||||
6: { machine: 165, cassette: 140 },
|
||||
7: { machine: 130, cassette: 125 }
|
||||
}
|
||||
|
||||
const DenominationsSchema = Yup.object().shape({
|
||||
|
|
@ -22,9 +25,10 @@ const DenominationsSchema = Yup.object().shape({
|
|||
.max(CURRENCY_MAX),
|
||||
cassette2: Yup.number()
|
||||
.label('Cassette 2')
|
||||
.required()
|
||||
.min(1)
|
||||
.max(CURRENCY_MAX),
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
cassette3: Yup.number()
|
||||
.label('Cassette 3')
|
||||
.min(1)
|
||||
|
|
@ -36,6 +40,42 @@ const DenominationsSchema = Yup.object().shape({
|
|||
.min(1)
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
stacker1f: Yup.number()
|
||||
.label('Stacker 1')
|
||||
.min(1)
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
stacker1r: Yup.number()
|
||||
.label('Stacker 1')
|
||||
.min(1)
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
stacker2f: Yup.number()
|
||||
.label('Stacker 2')
|
||||
.min(1)
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
stacker2r: Yup.number()
|
||||
.label('Stacker 2')
|
||||
.min(1)
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
stacker3f: Yup.number()
|
||||
.label('Stacker 3')
|
||||
.min(1)
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
stacker3r: Yup.number()
|
||||
.label('Stacker 3')
|
||||
.min(1)
|
||||
.max(CURRENCY_MAX)
|
||||
.nullable()
|
||||
.transform(transformNumber)
|
||||
})
|
||||
|
||||
|
|
@ -45,6 +85,11 @@ const getElements = (machines, locale = {}, classes) => {
|
|||
...R.map(it => it.numberOfCassettes, machines),
|
||||
0
|
||||
)
|
||||
const maxNumberOfStackers = Math.max(
|
||||
...R.map(it => it.numberOfStackers, machines),
|
||||
0
|
||||
)
|
||||
const numberOfCashUnits = maxNumberOfCassettes + maxNumberOfStackers
|
||||
|
||||
const options = getBillOptions(locale, denominations)
|
||||
const cassetteProps =
|
||||
|
|
@ -61,7 +106,7 @@ const getElements = (machines, locale = {}, classes) => {
|
|||
{
|
||||
name: 'id',
|
||||
header: 'Machine',
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine,
|
||||
width: widthsByNumberOfUnits[numberOfCashUnits]?.machine,
|
||||
view: it => machines.find(({ deviceId }) => deviceId === it).name,
|
||||
size: 'sm',
|
||||
editable: false
|
||||
|
|
@ -77,13 +122,13 @@ const getElements = (machines, locale = {}, classes) => {
|
|||
size: 'sm',
|
||||
stripe: true,
|
||||
textAlign: 'right',
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassette,
|
||||
width: widthsByNumberOfUnits[numberOfCashUnits]?.cassette,
|
||||
suffix: fiatCurrency,
|
||||
bold: bold,
|
||||
view: it => it,
|
||||
input: options?.length > 0 ? Autocomplete : NumberInput,
|
||||
inputProps: cassetteProps,
|
||||
doubleHeader: 'Denominations',
|
||||
doubleHeader: 'Denominations of Cassettes & Recyclers',
|
||||
isHidden: machine =>
|
||||
it >
|
||||
machines.find(({ deviceId }) => deviceId === machine.id)
|
||||
|
|
@ -94,6 +139,32 @@ const getElements = (machines, locale = {}, classes) => {
|
|||
1
|
||||
)
|
||||
|
||||
R.until(
|
||||
R.gt(R.__, maxNumberOfStackers),
|
||||
it => {
|
||||
elements.push({
|
||||
names: [`stacker${it}f`, `stacker${it}r`],
|
||||
header: `Stacker ${it}`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
textAlign: 'right',
|
||||
width: widthsByNumberOfUnits[numberOfCashUnits]?.cassette,
|
||||
prefix: it => (R.last(it) === 'f' ? 'F' : 'R'),
|
||||
suffix: fiatCurrency,
|
||||
bold: bold,
|
||||
input: options?.length > 0 ? Autocomplete : NumberInput,
|
||||
inputProps: cassetteProps,
|
||||
doubleHeader: 'Denominations of Cassettes & Recyclers',
|
||||
isHidden: machine =>
|
||||
it >
|
||||
machines.find(({ deviceId }) => deviceId === machine.id)
|
||||
.numberOfStackers
|
||||
})
|
||||
return R.add(1, it)
|
||||
},
|
||||
1
|
||||
)
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,10 @@ const MachinesTable = ({ machines = [], numToRender }) => {
|
|||
it =>
|
||||
machine.numberOfCassettes >= it ? (
|
||||
<StyledCell align="left">
|
||||
{makePercentageText(it, machine[`cassette${it}`])}
|
||||
{makePercentageText(
|
||||
it,
|
||||
machine.cashUnits[`cassette${it}`]
|
||||
)}
|
||||
</StyledCell>
|
||||
) : (
|
||||
<StyledCell align="left">
|
||||
|
|
|
|||
|
|
@ -24,12 +24,21 @@ const GET_DATA = gql`
|
|||
machines {
|
||||
name
|
||||
deviceId
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
stacker1f
|
||||
stacker1r
|
||||
stacker2f
|
||||
stacker2r
|
||||
stacker3f
|
||||
stacker3r
|
||||
}
|
||||
numberOfCassettes
|
||||
numberOfStackers
|
||||
statuses {
|
||||
label
|
||||
type
|
||||
|
|
|
|||
|
|
@ -3,85 +3,49 @@ import { makeStyles } from '@material-ui/core'
|
|||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import { Table as EditableTable } from 'src/components/editableTable'
|
||||
import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
|
||||
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import CashUnitDetails from 'src/pages/Maintenance/CashUnitDetails'
|
||||
import Wizard from 'src/pages/Maintenance/Wizard/Wizard'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import helper from 'src/pages/Maintenance/helper'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
|
||||
import styles from './Cassettes.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const widthsByNumberOfCassettes = {
|
||||
2: { cashbox: 203, cassette: 280, cassetteGraph: 80, editWidth: 87 },
|
||||
3: { cashbox: 164, cassette: 200, cassetteGraph: 60, editWidth: 87 },
|
||||
4: { cashbox: 131, cassette: 158, cassetteGraph: 40, editWidth: 87 }
|
||||
}
|
||||
|
||||
const ValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required('Required'),
|
||||
cashbox: Yup.number()
|
||||
.label('Cash box')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(1000),
|
||||
cassette1: Yup.number()
|
||||
.required('Required')
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500),
|
||||
cassette2: Yup.number()
|
||||
.required('Required')
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500),
|
||||
cassette3: Yup.number()
|
||||
.required('Required')
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500),
|
||||
cassette4: Yup.number()
|
||||
.required('Required')
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500)
|
||||
})
|
||||
|
||||
const SET_CASSETTE_BILLS = gql`
|
||||
mutation MachineAction(
|
||||
$deviceId: ID!
|
||||
$action: MachineAction!
|
||||
$cashbox: Int!
|
||||
$cassette1: Int!
|
||||
$cassette2: Int!
|
||||
$cassette3: Int!
|
||||
$cassette4: Int!
|
||||
) {
|
||||
machineAction(
|
||||
deviceId: $deviceId
|
||||
action: $action
|
||||
cashbox: $cashbox
|
||||
cassette1: $cassette1
|
||||
cassette2: $cassette2
|
||||
cassette3: $cassette3
|
||||
cassette4: $cassette4
|
||||
$cashUnits: CashUnitsInput
|
||||
) {
|
||||
machineAction(deviceId: $deviceId, action: $action, cashUnits: $cashUnits) {
|
||||
deviceId
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
stacker1f
|
||||
stacker1r
|
||||
stacker2f
|
||||
stacker2r
|
||||
stacker3f
|
||||
stacker3r
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const widths = {
|
||||
name: 0,
|
||||
cashbox: 175,
|
||||
cassettes: 585,
|
||||
edit: 90
|
||||
}
|
||||
|
||||
const CashCassettes = ({ machine, config, refetchData, bills }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -89,115 +53,47 @@ const CashCassettes = ({ machine, config, refetchData, bills }) => {
|
|||
|
||||
const cashout = config && fromNamespace('cashOut')(config)
|
||||
const locale = config && fromNamespace('locale')(config)
|
||||
const fillingPercentageSettings =
|
||||
config && fromNamespace('notifications', config)
|
||||
const fiatCurrency = locale?.fiatCurrency
|
||||
const numberOfCassettes = machine.numberOfCassettes
|
||||
|
||||
const getCashoutSettings = deviceId => fromNamespace(deviceId)(cashout)
|
||||
const isCashOutDisabled = ({ deviceId }) =>
|
||||
!getCashoutSettings(deviceId).active
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'cashbox',
|
||||
header: 'Cash box',
|
||||
width: widthsByNumberOfCassettes[numberOfCassettes].cashbox,
|
||||
stripe: false,
|
||||
view: value => (
|
||||
<CashIn
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
total={R.sum(R.map(it => it.fiat)(bills))}
|
||||
/>
|
||||
),
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
R.until(
|
||||
R.gt(R.__, numberOfCassettes),
|
||||
it => {
|
||||
elements.push({
|
||||
name: `cassette${it}`,
|
||||
header: `Cash cassette ${it}`,
|
||||
width: widthsByNumberOfCassettes[numberOfCassettes].cassette,
|
||||
stripe: true,
|
||||
doubleHeader: 'Cash-out',
|
||||
view: value => {
|
||||
return (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={
|
||||
getCashoutSettings(machine.deviceId)?.[`cassette${it}`]
|
||||
}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
width={widthsByNumberOfCassettes[numberOfCassettes].cassetteGraph}
|
||||
threshold={
|
||||
fillingPercentageSettings[`fillingPercentageCassette${it}`]
|
||||
}
|
||||
/>
|
||||
const elements = R.filter(it => it.name !== 'name')(
|
||||
helper.getElements(classes, config, bills, setWizard, widths)
|
||||
)
|
||||
},
|
||||
isHidden: ({ numberOfCassettes }) => it > numberOfCassettes,
|
||||
input: CashCassetteInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0,
|
||||
width: widthsByNumberOfCassettes[numberOfCassettes].cassetteGraph,
|
||||
inputClassName: classes.cashbox
|
||||
}
|
||||
})
|
||||
return R.add(1, it)
|
||||
},
|
||||
1
|
||||
)
|
||||
|
||||
elements.push({
|
||||
name: 'edit',
|
||||
header: 'Edit',
|
||||
width: widthsByNumberOfCassettes[numberOfCassettes].editWidth,
|
||||
view: () => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setWizard(true)
|
||||
}}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
|
||||
refetchQueries: () => refetchData()
|
||||
})
|
||||
|
||||
const onSave = (_, cashbox, cassettes) =>
|
||||
const onSave = (_, cashUnits) =>
|
||||
setCassetteBills({
|
||||
variables: {
|
||||
action: 'setCassetteBills',
|
||||
deviceId: machine.deviceId,
|
||||
cashbox,
|
||||
...cassettes
|
||||
cashUnits
|
||||
}
|
||||
})
|
||||
|
||||
const InnerCashUnitDetails = ({ it }) => (
|
||||
<CashUnitDetails
|
||||
machine={it}
|
||||
bills={bills[it.deviceId] ?? []}
|
||||
currency={fiatCurrency}
|
||||
config={config}
|
||||
hideMachineData
|
||||
widths
|
||||
/>
|
||||
)
|
||||
|
||||
return machine.name ? (
|
||||
<>
|
||||
<EditableTable
|
||||
error={error?.message}
|
||||
editWidth={widthsByNumberOfCassettes[numberOfCassettes].editWidth}
|
||||
stripeWhen={isCashOutDisabled}
|
||||
disableRowEdit={isCashOutDisabled}
|
||||
name="cashboxes"
|
||||
<DataTable
|
||||
elements={elements}
|
||||
data={[machine]}
|
||||
save={onSave}
|
||||
validationSchema={ValidationSchema}
|
||||
Details={InnerCashUnitDetails}
|
||||
emptyText="No machines so far"
|
||||
initialExpanded={0}
|
||||
tableClassName={classes.dataTable}
|
||||
/>
|
||||
{wizard && (
|
||||
<Wizard
|
||||
|
|
|
|||
|
|
@ -1,6 +1,34 @@
|
|||
import { offDarkColor } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
cashbox: {
|
||||
height: 36
|
||||
unitsRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [[10, 0]],
|
||||
'& > *': {
|
||||
marginRight: 30
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginRight: 0
|
||||
}
|
||||
},
|
||||
units: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'& > *': {
|
||||
marginRight: 10
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginRight: 0
|
||||
}
|
||||
},
|
||||
verticalLine: {
|
||||
height: '100%',
|
||||
width: 1,
|
||||
backgroundColor: offDarkColor
|
||||
},
|
||||
dataTable: {
|
||||
minHeight: 290
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,12 +29,21 @@ const GET_INFO = gql`
|
|||
pairedAt
|
||||
version
|
||||
model
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
stacker1f
|
||||
stacker1r
|
||||
stacker2f
|
||||
stacker2r
|
||||
stacker3f
|
||||
stacker3r
|
||||
}
|
||||
numberOfCassettes
|
||||
numberOfStackers
|
||||
statuses {
|
||||
label
|
||||
type
|
||||
|
|
@ -100,7 +109,7 @@ const Machines = ({ data, refetch, reload }) => {
|
|||
|
||||
const machine = R.path(['machine'])(data) ?? {}
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
const bills = R.path(['bills'])(data) ?? []
|
||||
const bills = R.groupBy(bill => bill.deviceId)(R.path(['bills'])(data) ?? [])
|
||||
|
||||
const machineName = R.path(['name'])(machine) ?? null
|
||||
const machineID = R.path(['deviceId'])(machine) ?? null
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import { offColor } from 'src/styling/variables'
|
||||
|
||||
export default {
|
||||
cashbox: {
|
||||
height: 36
|
||||
},
|
||||
tBody: {
|
||||
maxHeight: '65vh',
|
||||
overflow: 'auto'
|
||||
},
|
||||
tableWidth: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: 1
|
||||
},
|
||||
descriptions: {
|
||||
color: offColor,
|
||||
marginTop: 0
|
||||
},
|
||||
cashboxReset: {
|
||||
color: offColor,
|
||||
margin: [[13, 0, -5, 20]]
|
||||
},
|
||||
selection: {
|
||||
marginRight: 12
|
||||
},
|
||||
downloadLogsButton: {
|
||||
marginLeft: 13
|
||||
}
|
||||
}
|
||||
229
new-lamassu-admin/src/pages/Maintenance/CashUnitDetails.js
Normal file
229
new-lamassu-admin/src/pages/Maintenance/CashUnitDetails.js
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import Chip from 'src/components/Chip'
|
||||
import { CashOut } from 'src/components/inputs'
|
||||
import { Label1, TL2 } from 'src/components/typography'
|
||||
import { offDarkColor } from 'src/styling/variables'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { cashUnitCapacity, modelPrettifier } from 'src/utils/machine'
|
||||
|
||||
const styles = {
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginTop: 12,
|
||||
marginBottom: 16,
|
||||
'& > *': {
|
||||
marginRight: 40
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginRight: 0
|
||||
},
|
||||
minHeight: 120
|
||||
},
|
||||
row: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
col: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
machineData: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: 210
|
||||
},
|
||||
billList: ({ hideMachineData }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: hideMachineData ? 60 : 160,
|
||||
'& > span': {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
'& > p': {
|
||||
minWidth: 30
|
||||
}
|
||||
}
|
||||
}),
|
||||
unitList: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'& > *': {
|
||||
marginRight: 20
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginRight: 0
|
||||
},
|
||||
marginTop: 10
|
||||
},
|
||||
verticalLine: {
|
||||
height: '100%',
|
||||
width: 1,
|
||||
backgroundColor: offDarkColor
|
||||
},
|
||||
label: {
|
||||
marginBottom: 10
|
||||
},
|
||||
loadingBoxes: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& > *': {
|
||||
marginBottom: 20
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginBottom: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CashUnitDetails = ({
|
||||
machine,
|
||||
bills,
|
||||
currency,
|
||||
config,
|
||||
hideMachineData = false
|
||||
}) => {
|
||||
const classes = useStyles({ hideMachineData })
|
||||
const billCount = R.countBy(it => it.fiat)(bills)
|
||||
const fillingPercentageSettings = fromNamespace('notifications', config)
|
||||
const cashout = fromNamespace('cashOut')(config)
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
{!hideMachineData && (
|
||||
<div className={classes.machineData}>
|
||||
<Label1>Machine Model</Label1>
|
||||
<span>{modelPrettifier[machine.model]}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.billList}>
|
||||
<Label1>Cash box</Label1>
|
||||
{R.isEmpty(billCount) && <TL2 noMargin>Empty</TL2>}
|
||||
{R.map(it => (
|
||||
<span>
|
||||
<TL2 noMargin>{billCount[it]}</TL2>
|
||||
<Chip label={`${it} ${currency}`} />
|
||||
</span>
|
||||
))(R.keys(billCount))}
|
||||
</div>
|
||||
<div className={classes.unitList}>
|
||||
{machine.numberOfStackers === 0 &&
|
||||
R.map(it => (
|
||||
<>
|
||||
<div className={classes.col}>
|
||||
<Label1
|
||||
noMargin
|
||||
className={classes.label}>{`Cassette ${it}`}</Label1>
|
||||
<CashOut
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`cassette${it}`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`cassette${it}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[`fillingPercentageCassette${it}`]
|
||||
}
|
||||
capacity={cashUnitCapacity[machine.model].cassette}
|
||||
/>
|
||||
</div>
|
||||
{it !== machine.numberOfCassettes && (
|
||||
<span className={classes.verticalLine} />
|
||||
)}
|
||||
</>
|
||||
))(R.range(1, machine.numberOfCassettes + 1))}
|
||||
{machine.numberOfStackers > 0 && (
|
||||
<>
|
||||
<div className={classes.col}>
|
||||
<Label1
|
||||
noMargin
|
||||
className={classes.label}>{`Loading boxes`}</Label1>
|
||||
<div className={classes.loadingBoxes}>
|
||||
{R.map(it => (
|
||||
<CashOut
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`cassette${it}`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`cassette${it}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageCassette${it}`
|
||||
]
|
||||
}
|
||||
capacity={cashUnitCapacity[machine.model].cassette}
|
||||
/>
|
||||
))(R.range(1, machine.numberOfCassettes + 1))}
|
||||
</div>
|
||||
</div>
|
||||
<span className={classes.verticalLine} />
|
||||
{R.map(it => (
|
||||
<>
|
||||
<div className={classes.col}>
|
||||
<Label1
|
||||
noMargin
|
||||
className={classes.label}>{`Stacker ${it}`}</Label1>
|
||||
<div className={classes.loadingBoxes}>
|
||||
<CashOut
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`stacker${it}f`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`stacker${it}f`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageStacker${it}f`
|
||||
]
|
||||
}
|
||||
capacity={cashUnitCapacity[machine.model].stacker}
|
||||
/>
|
||||
<CashOut
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`stacker${it}r`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`stacker${it}r`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageStacker${it}r`
|
||||
]
|
||||
}
|
||||
capacity={cashUnitCapacity[machine.model].stacker}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{it !== machine.numberOfStackers && (
|
||||
<span className={classes.verticalLine} />
|
||||
)}
|
||||
</>
|
||||
))(R.range(1, machine.numberOfStackers + 1))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CashUnitDetails
|
||||
|
|
@ -3,101 +3,52 @@ import { DialogActions, makeStyles, Box } from '@material-ui/core'
|
|||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { IconButton, Button } from 'src/components/buttons'
|
||||
import { Table as EditableTable } from 'src/components/editableTable'
|
||||
import { RadioGroup } from 'src/components/inputs'
|
||||
import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
|
||||
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { EmptyTable } from 'src/components/table'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { P, Label1 } from 'src/components/typography'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { ReactComponent as ReverseHistoryIcon } from 'src/styling/icons/circle buttons/history/white.svg'
|
||||
import { ReactComponent as HistoryIcon } from 'src/styling/icons/circle buttons/history/zodiac.svg'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
import { MANUAL, AUTOMATIC } from 'src/utils/constants.js'
|
||||
import { MANUAL, AUTOMATIC } from 'src/utils/constants'
|
||||
import { onlyFirstToUpper } from 'src/utils/string'
|
||||
|
||||
import styles from './CashCassettes.styles.js'
|
||||
import CashCassettesFooter from './CashCassettesFooter'
|
||||
import CashUnitDetails from './CashUnitDetails'
|
||||
import styles from './CashUnits.styles'
|
||||
import CashCassettesFooter from './CashUnitsFooter'
|
||||
import CashboxHistory from './CashboxHistory'
|
||||
import Wizard from './Wizard/Wizard'
|
||||
import helper from './helper'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const widthsByNumberOfCassettes = {
|
||||
2: {
|
||||
machine: 250,
|
||||
cashbox: 260,
|
||||
cassette: 300,
|
||||
cassetteGraph: 80,
|
||||
editWidth: 90
|
||||
},
|
||||
3: {
|
||||
machine: 220,
|
||||
cashbox: 215,
|
||||
cassette: 225,
|
||||
cassetteGraph: 60,
|
||||
editWidth: 90
|
||||
},
|
||||
4: {
|
||||
machine: 190,
|
||||
cashbox: 180,
|
||||
cassette: 185,
|
||||
cassetteGraph: 50,
|
||||
editWidth: 90
|
||||
}
|
||||
}
|
||||
|
||||
const ValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
cashbox: Yup.number()
|
||||
.label('Cash box')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(1000),
|
||||
cassette1: Yup.number()
|
||||
.label('Cassette 1')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500),
|
||||
cassette2: Yup.number()
|
||||
.label('Cassette 2')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500),
|
||||
cassette3: Yup.number()
|
||||
.label('Cassette 3')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500),
|
||||
cassette4: Yup.number()
|
||||
.label('Cassette 4')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500)
|
||||
})
|
||||
|
||||
const GET_MACHINES_AND_CONFIG = gql`
|
||||
query getData($billFilters: JSONObject) {
|
||||
machines {
|
||||
name
|
||||
id: deviceId
|
||||
model
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
stacker1f
|
||||
stacker1r
|
||||
stacker2f
|
||||
stacker2r
|
||||
stacker3f
|
||||
stacker3r
|
||||
}
|
||||
numberOfCassettes
|
||||
numberOfStackers
|
||||
}
|
||||
unpairedMachines {
|
||||
id: deviceId
|
||||
|
|
@ -123,27 +74,23 @@ const SET_CASSETTE_BILLS = gql`
|
|||
mutation MachineAction(
|
||||
$deviceId: ID!
|
||||
$action: MachineAction!
|
||||
$cashbox: Int!
|
||||
$cassette1: Int!
|
||||
$cassette2: Int!
|
||||
$cassette3: Int!
|
||||
$cassette4: Int!
|
||||
) {
|
||||
machineAction(
|
||||
deviceId: $deviceId
|
||||
action: $action
|
||||
cashbox: $cashbox
|
||||
cassette1: $cassette1
|
||||
cassette2: $cassette2
|
||||
cassette3: $cassette3
|
||||
cassette4: $cassette4
|
||||
$cashUnits: CashUnitsInput
|
||||
) {
|
||||
machineAction(deviceId: $deviceId, action: $action, cashUnits: $cashUnits) {
|
||||
deviceId
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
stacker1f
|
||||
stacker1r
|
||||
stacker2f
|
||||
stacker2r
|
||||
stacker3f
|
||||
stacker3r
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -154,6 +101,13 @@ const GET_BATCHES_CSV = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const widths = {
|
||||
name: 250,
|
||||
cashbox: 200,
|
||||
cassettes: 575,
|
||||
edit: 90
|
||||
}
|
||||
|
||||
const CashCassettes = () => {
|
||||
const classes = useStyles()
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
|
|
@ -173,7 +127,6 @@ const CashCassettes = () => {
|
|||
const machines = R.path(['machines'])(data) ?? []
|
||||
const unpairedMachines = R.path(['unpairedMachines'])(data) ?? []
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
const fillingPercentageSettings = fromNamespace('notifications', config)
|
||||
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
|
||||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
|
@ -191,21 +144,15 @@ const CashCassettes = () => {
|
|||
const cashout = data?.config && fromNamespace('cashOut')(data.config)
|
||||
const locale = data?.config && fromNamespace('locale')(data.config)
|
||||
const fiatCurrency = locale?.fiatCurrency
|
||||
const maxNumberOfCassettes = Math.max(
|
||||
...R.map(it => it.numberOfCassettes, machines),
|
||||
0
|
||||
)
|
||||
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
const isCashOutDisabled = ({ id }) => !getCashoutSettings(id).active
|
||||
|
||||
const onSave = (id, cashbox, cassettes) => {
|
||||
const onSave = (id, cashUnits) => {
|
||||
return setCassetteBills({
|
||||
variables: {
|
||||
action: 'setCassetteBills',
|
||||
deviceId: id,
|
||||
cashbox,
|
||||
...cassettes
|
||||
cashUnits
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -235,85 +182,23 @@ const CashCassettes = () => {
|
|||
setSelectedRadio(selectedRadio)
|
||||
}
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'name',
|
||||
header: 'Machine',
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine,
|
||||
view: name => <>{name}</>,
|
||||
input: ({ field: { value: name } }) => <>{name}</>
|
||||
},
|
||||
{
|
||||
name: 'cashbox',
|
||||
header: 'Cash box',
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cashbox,
|
||||
view: (value, { id }) => (
|
||||
<CashIn
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
total={R.sum(R.map(it => it.fiat, bills[id] ?? []))}
|
||||
/>
|
||||
),
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
R.until(
|
||||
R.gt(R.__, maxNumberOfCassettes),
|
||||
it => {
|
||||
elements.push({
|
||||
name: `cassette${it}`,
|
||||
header: `Cassette ${it}`,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassette,
|
||||
stripe: true,
|
||||
doubleHeader: 'Cash-out',
|
||||
view: (value, { id }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(id)?.[`cassette${it}`]}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
width={
|
||||
widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassetteGraph
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[`fillingPercentageCassette${it}`]
|
||||
}
|
||||
/>
|
||||
),
|
||||
isHidden: ({ numberOfCassettes }) => it > numberOfCassettes,
|
||||
input: CashCassetteInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassetteGraph,
|
||||
inputClassName: classes.cashbox
|
||||
}
|
||||
})
|
||||
return R.add(1, it)
|
||||
},
|
||||
1
|
||||
const elements = helper.getElements(
|
||||
classes,
|
||||
config,
|
||||
bills,
|
||||
setWizard,
|
||||
widths,
|
||||
setMachineId
|
||||
)
|
||||
|
||||
elements.push({
|
||||
name: 'edit',
|
||||
header: 'Edit',
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.editWidth,
|
||||
textAlign: 'center',
|
||||
view: (value, { id }) => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setMachineId(id)
|
||||
setWizard(true)
|
||||
}}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
const InnerCashUnitDetails = ({ it }) => (
|
||||
<CashUnitDetails
|
||||
machine={it}
|
||||
bills={bills[it.id] ?? []}
|
||||
currency={fiatCurrency}
|
||||
config={config}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
!dataLoading && (
|
||||
|
|
@ -369,14 +254,13 @@ const CashCassettes = () => {
|
|||
</TitleSection>
|
||||
{!showHistory && (
|
||||
<>
|
||||
<EditableTable
|
||||
error={error?.message}
|
||||
name="cashboxes"
|
||||
stripeWhen={isCashOutDisabled}
|
||||
<DataTable
|
||||
loading={dataLoading}
|
||||
elements={elements}
|
||||
data={machines}
|
||||
validationSchema={ValidationSchema}
|
||||
tbodyWrapperClass={classes.tBody}
|
||||
Details={InnerCashUnitDetails}
|
||||
emptyText="No machines so far"
|
||||
expandable
|
||||
/>
|
||||
|
||||
{data && R.isEmpty(machines) && (
|
||||
56
new-lamassu-admin/src/pages/Maintenance/CashUnits.styles.js
Normal file
56
new-lamassu-admin/src/pages/Maintenance/CashUnits.styles.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { offColor, offDarkColor } from 'src/styling/variables'
|
||||
|
||||
export default {
|
||||
cashbox: {
|
||||
height: 36
|
||||
},
|
||||
tBody: {
|
||||
maxHeight: '65vh',
|
||||
overflow: 'auto'
|
||||
},
|
||||
tableWidth: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: 1
|
||||
},
|
||||
descriptions: {
|
||||
color: offColor,
|
||||
marginTop: 0
|
||||
},
|
||||
cashboxReset: {
|
||||
color: offColor,
|
||||
margin: [[13, 0, -5, 20]]
|
||||
},
|
||||
selection: {
|
||||
marginRight: 12
|
||||
},
|
||||
downloadLogsButton: {
|
||||
marginLeft: 13
|
||||
},
|
||||
unitsRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [[10, 0]],
|
||||
'& > *': {
|
||||
marginRight: 30
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginRight: 0
|
||||
}
|
||||
},
|
||||
units: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'& > *': {
|
||||
marginRight: 10
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginRight: 0
|
||||
}
|
||||
},
|
||||
verticalLine: {
|
||||
height: '100%',
|
||||
width: 1,
|
||||
backgroundColor: offDarkColor
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@ import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-ou
|
|||
import { fromNamespace } from 'src/utils/config'
|
||||
import { numberToFiatAmount } from 'src/utils/number.js'
|
||||
|
||||
import styles from './CashCassettesFooter.styles.js'
|
||||
import styles from './CashUnitsFooter.styles.js'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CashCassettesFooter = ({
|
||||
|
|
@ -22,9 +23,9 @@ const CashCassettesFooter = ({
|
|||
const classes = useStyles()
|
||||
const cashout = config && fromNamespace('cashOut')(config)
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
const reducerFn = (
|
||||
const cashoutReducerFn = (
|
||||
acc,
|
||||
{ cassette1, cassette2, cassette3, cassette4, id }
|
||||
{ cashUnits: { cassette1, cassette2, cassette3, cassette4 }, id }
|
||||
) => {
|
||||
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
|
||||
const cassette2Denomination = getCashoutSettings(id).cassette2 ?? 0
|
||||
|
|
@ -38,11 +39,49 @@ const CashCassettesFooter = ({
|
|||
]
|
||||
}
|
||||
|
||||
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines))
|
||||
const recyclerReducerFn = (
|
||||
acc,
|
||||
{
|
||||
cashUnits: {
|
||||
stacker1f,
|
||||
stacker1r,
|
||||
stacker2f,
|
||||
stacker2r,
|
||||
stacker3f,
|
||||
stacker3r
|
||||
},
|
||||
id
|
||||
}
|
||||
) => {
|
||||
const stacker1fDenomination = getCashoutSettings(id).stacker1f ?? 0
|
||||
const stacker1rDenomination = getCashoutSettings(id).stacker1r ?? 0
|
||||
const stacker2fDenomination = getCashoutSettings(id).stacker2f ?? 0
|
||||
const stacker2rDenomination = getCashoutSettings(id).stacker2r ?? 0
|
||||
const stacker3fDenomination = getCashoutSettings(id).stacker3f ?? 0
|
||||
const stacker3rDenomination = getCashoutSettings(id).stacker3r ?? 0
|
||||
return [
|
||||
(acc[0] += stacker1f * stacker1fDenomination),
|
||||
(acc[1] += stacker1r * stacker1rDenomination),
|
||||
(acc[0] += stacker2f * stacker2fDenomination),
|
||||
(acc[1] += stacker2r * stacker2rDenomination),
|
||||
(acc[0] += stacker3f * stacker3fDenomination),
|
||||
(acc[1] += stacker3r * stacker3rDenomination)
|
||||
]
|
||||
}
|
||||
|
||||
const totalInRecyclers = R.sum(
|
||||
R.reduce(recyclerReducerFn, [0, 0, 0, 0, 0, 0], machines)
|
||||
)
|
||||
|
||||
const totalInCassettes = R.sum(
|
||||
R.reduce(cashoutReducerFn, [0, 0, 0, 0], machines)
|
||||
)
|
||||
|
||||
const totalInCashBox = R.sum(R.map(it => it.fiat)(bills))
|
||||
|
||||
const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0)
|
||||
const total = new BigNumber(
|
||||
totalInCassettes + totalInCashBox + totalInRecyclers
|
||||
).toFormat(0)
|
||||
|
||||
return (
|
||||
<div className={classes.footerContainer}>
|
||||
|
|
@ -62,6 +101,13 @@ const CashCassettesFooter = ({
|
|||
{numberToFiatAmount(totalInCassettes)} {currencyCode}
|
||||
</Info1>
|
||||
</div>
|
||||
<div className={classes.flex}>
|
||||
<TxOutIcon className={classes.icon} />
|
||||
<Info2 className={classes.iconLabel}>Recycle:</Info2>
|
||||
<Info1 className={classes.valueDisplay}>
|
||||
{numberToFiatAmount(totalInRecyclers)} {currencyCode}
|
||||
</Info1>
|
||||
</div>
|
||||
<div className={classes.flex}>
|
||||
<Info2 className={classes.iconLabel}>Total:</Info2>
|
||||
<Info1 className={classes.valueDisplay}>
|
||||
|
|
@ -26,8 +26,7 @@ const GET_BATCHES = gql`
|
|||
performedBy
|
||||
bills {
|
||||
fiat
|
||||
deviceId
|
||||
created
|
||||
fiatCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,19 @@ const GET_MACHINES = gql`
|
|||
pairedAt
|
||||
version
|
||||
paired
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
stacker1f
|
||||
stacker1r
|
||||
stacker2f
|
||||
stacker2r
|
||||
stacker3f
|
||||
stacker3r
|
||||
}
|
||||
version
|
||||
model
|
||||
statuses {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as Yup from 'yup'
|
|||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { MAX_NUMBER_OF_CASSETTES } from 'src/utils/constants'
|
||||
import { cashUnitCapacity, modelPrettifier } from 'src/utils/machine'
|
||||
import { defaultToZero } from 'src/utils/number'
|
||||
|
||||
import WizardSplash from './WizardSplash'
|
||||
|
|
@ -11,13 +12,21 @@ import WizardStep from './WizardStep'
|
|||
|
||||
const MODAL_WIDTH = 554
|
||||
const MODAL_HEIGHT = 535
|
||||
const CASHBOX_DEFAULT_CAPACITY = 500
|
||||
|
||||
const CASSETTE_FIELDS = R.map(
|
||||
it => `cassette${it}`,
|
||||
R.range(1, MAX_NUMBER_OF_CASSETTES + 1)
|
||||
)
|
||||
|
||||
const STACKER_FIELDS = [
|
||||
'stacker1f',
|
||||
'stacker1r',
|
||||
'stacker2f',
|
||||
'stacker2r',
|
||||
'stacker3f',
|
||||
'stacker3r'
|
||||
]
|
||||
|
||||
const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||
const [{ step, config }, setState] = useState({
|
||||
step: 0,
|
||||
|
|
@ -28,7 +37,9 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
R.isEmpty(cashoutSettings) || !cashoutSettings?.active
|
||||
|
||||
const numberOfCassettes = isCashOutDisabled ? 0 : machine.numberOfCassettes
|
||||
const numberOfStackers = machine.numberOfStackers
|
||||
|
||||
// const LAST_STEP = numberOfCassettes + numberOfStackers * 2 + 1
|
||||
const LAST_STEP = numberOfCassettes + 1
|
||||
|
||||
const title = `Update counts`
|
||||
|
|
@ -45,6 +56,18 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const buildStackerObj = cassetteInput => {
|
||||
return R.reduce(
|
||||
(acc, value) => {
|
||||
acc[value] = machine.cashUnits[value]
|
||||
// acc[value] = defaultToZero(cassetteInput[value])
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
STACKER_FIELDS
|
||||
)
|
||||
}
|
||||
|
||||
const onContinue = it => {
|
||||
const newConfig = R.merge(config, it)
|
||||
if (isLastStep) {
|
||||
|
|
@ -53,10 +76,16 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
it?.wasCashboxEmptied
|
||||
].includes('YES')
|
||||
|
||||
const cashbox = wasCashboxEmptied ? 0 : machine?.cashbox
|
||||
const cassettes = buildCassetteObj(it)
|
||||
const stackers = buildStackerObj(it)
|
||||
|
||||
save(machine.id, cashbox, cassettes)
|
||||
const cashUnits = {
|
||||
cashbox: wasCashboxEmptied ? 0 : machine?.cashUnits.cashbox,
|
||||
...cassettes,
|
||||
...stackers
|
||||
}
|
||||
|
||||
save(machine.id, cashUnits)
|
||||
return onClose()
|
||||
}
|
||||
|
||||
|
|
@ -78,12 +107,69 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
.integer()
|
||||
.required()
|
||||
.min(0)
|
||||
.max(CASHBOX_DEFAULT_CAPACITY)
|
||||
.max(
|
||||
cashUnitCapacity[machine.model].cassette,
|
||||
`${modelPrettifier[machine.model]} maximum cassette capacity is ${
|
||||
cashUnitCapacity[machine.model].cassette
|
||||
} bills`
|
||||
)
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
const makeInitialValues = () =>
|
||||
// const makeStackerSteps = R.pipe(
|
||||
// R.add(1),
|
||||
// R.range(1),
|
||||
// R.chain(i => [
|
||||
// {
|
||||
// type: `stacker ${i}f`,
|
||||
// schema: Yup.object().shape({
|
||||
// [`stacker${i}f`]: Yup.number()
|
||||
// .label('Bill count')
|
||||
// .positive()
|
||||
// .integer()
|
||||
// .required()
|
||||
// .min(0)
|
||||
// .max(
|
||||
// i === 1
|
||||
// ? cashUnitCapacity[machine.model].stacker -
|
||||
// cashUnitCapacity[machine.model].escrow
|
||||
// : cashUnitCapacity[machine.model].stacker,
|
||||
// i === 1
|
||||
// ? `${
|
||||
// modelPrettifier[machine.model]
|
||||
// } maximum stacker capacity for the escrow unit is ${cashUnitCapacity[
|
||||
// machine.model
|
||||
// ].stacker - cashUnitCapacity[machine.model].escrow} bills`
|
||||
// : `${
|
||||
// modelPrettifier[machine.model]
|
||||
// } maximum stacker capacity is ${
|
||||
// cashUnitCapacity[machine.model].stacker
|
||||
// } bills`
|
||||
// )
|
||||
// })
|
||||
// },
|
||||
// {
|
||||
// type: `stacker ${i}r`,
|
||||
// schema: Yup.object().shape({
|
||||
// [`stacker${i}r`]: Yup.number()
|
||||
// .label('Bill count')
|
||||
// .positive()
|
||||
// .integer()
|
||||
// .required()
|
||||
// .min(0)
|
||||
// .max(
|
||||
// cashUnitCapacity[machine.model].stacker,
|
||||
// `${modelPrettifier[machine.model]} maximum stacker capacity is ${
|
||||
// cashUnitCapacity[machine.model].stacker
|
||||
// } bills`
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
// ])
|
||||
// )
|
||||
|
||||
const makeCassettesInitialValues = () =>
|
||||
!R.isEmpty(cashoutSettings)
|
||||
? R.reduce(
|
||||
(acc, value) => {
|
||||
|
|
@ -95,16 +181,35 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
)
|
||||
: {}
|
||||
|
||||
const steps = R.prepend(
|
||||
const makeStackersInitialValues = () =>
|
||||
!R.isEmpty(cashoutSettings)
|
||||
? R.reduce(
|
||||
(acc, value) => {
|
||||
acc[`stacker${value}f`] = ''
|
||||
acc[`stacker${value}r`] = ''
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
R.range(1, numberOfStackers + 1)
|
||||
)
|
||||
: {}
|
||||
|
||||
const makeInitialValues = () =>
|
||||
R.merge(makeCassettesInitialValues(), makeStackersInitialValues())
|
||||
|
||||
const steps = R.pipe(
|
||||
// R.concat(makeStackerSteps(numberOfStackers)),
|
||||
R.concat(makeCassetteSteps(numberOfCassettes)),
|
||||
R.concat([
|
||||
{
|
||||
type: 'cashbox',
|
||||
schema: Yup.object().shape({
|
||||
wasCashboxEmptied: Yup.string().required('Select one option.')
|
||||
}),
|
||||
cashoutRequired: false
|
||||
},
|
||||
makeCassetteSteps(numberOfCassettes)
|
||||
)
|
||||
}
|
||||
])
|
||||
)([])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -122,7 +227,6 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
name={machine?.name}
|
||||
machine={machine}
|
||||
cashoutSettings={cashoutSettings}
|
||||
cassetteCapacity={CASHBOX_DEFAULT_CAPACITY}
|
||||
error={error}
|
||||
lastStep={isLastStep}
|
||||
steps={steps}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import tejo4CassetteThree from 'src/styling/icons/cassettes/tejo/4-cassettes/4-c
|
|||
import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { comet, errorColor } from 'src/styling/variables'
|
||||
import { cashUnitCapacity } from 'src/utils/machine'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { startCase } from 'src/utils/string'
|
||||
|
||||
const styles = {
|
||||
content: {
|
||||
|
|
@ -104,19 +106,41 @@ const styles = {
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const cassetesArtworks = (numberOfCassettes, step) =>
|
||||
[
|
||||
const CASHBOX_STEP = 1
|
||||
|
||||
const cassetesArtworks = (step, numberOfCassettes, numberOfStackers) => {
|
||||
const cassetteStepsStart = CASHBOX_STEP + 1
|
||||
return [
|
||||
[cassetteOne],
|
||||
[cassetteOne, cassetteTwo],
|
||||
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
|
||||
[tejo4CassetteOne, tejo4CassetteTwo, tejo4CassetteThree, tejo4CassetteFour]
|
||||
][numberOfCassettes - 2][step - 2]
|
||||
][numberOfCassettes - cassetteStepsStart + 1][step - cassetteStepsStart]
|
||||
}
|
||||
|
||||
const getCashUnitFieldName = (step, numberOfCassettes, numberOfStackers) => {
|
||||
if (step === CASHBOX_STEP) return { name: 'cashbox', category: 'cashbox' }
|
||||
const cassetteStepsStart = CASHBOX_STEP + 1
|
||||
if (step < cassetteStepsStart + numberOfCassettes)
|
||||
return {
|
||||
name: `cassette${step - cassetteStepsStart + 1}`,
|
||||
category: 'cassette'
|
||||
}
|
||||
const stackerStepsStart = CASHBOX_STEP + numberOfCassettes + 1
|
||||
if (step < stackerStepsStart + numberOfStackers * 2)
|
||||
return {
|
||||
name: `stacker${Math.ceil((step - stackerStepsStart + 1) / 2)}${
|
||||
(step - stackerStepsStart) % 2 === 0 ? 'f' : 'r'
|
||||
}`,
|
||||
category: 'stacker'
|
||||
}
|
||||
}
|
||||
|
||||
const WizardStep = ({
|
||||
step,
|
||||
name,
|
||||
machine,
|
||||
cashoutSettings,
|
||||
cassetteCapacity,
|
||||
error,
|
||||
lastStep,
|
||||
steps,
|
||||
|
|
@ -133,16 +157,20 @@ const WizardStep = ({
|
|||
{ display: 'No', code: 'NO' }
|
||||
]
|
||||
|
||||
const cassetteField = `cassette${step - 1}`
|
||||
const numberOfCassettes = machine.numberOfCassettes
|
||||
const originalCassetteCount = machine?.[cassetteField]
|
||||
const cassetteDenomination = cashoutSettings?.[cassetteField]
|
||||
const numberOfStackers = machine.numberOfStackers
|
||||
const {
|
||||
name: cashUnitField,
|
||||
category: cashUnitCategory
|
||||
} = getCashUnitFieldName(step, numberOfCassettes, numberOfStackers)
|
||||
const originalCashUnitCount = machine?.cashUnits?.[cashUnitField]
|
||||
const cashUnitDenomination = cashoutSettings?.[cashUnitField]
|
||||
|
||||
const cassetteCount = values => values[cassetteField] || originalCassetteCount
|
||||
const cassetteTotal = values => cassetteCount(values) * cassetteDenomination
|
||||
const cassetteCount = values => values[cashUnitField] || originalCashUnitCount
|
||||
const cassetteTotal = values => cassetteCount(values) * cashUnitDenomination
|
||||
const getPercentage = R.pipe(
|
||||
cassetteCount,
|
||||
count => 100 * (count / cassetteCapacity),
|
||||
count => 100 * (count / cashUnitCapacity[machine.model][cashUnitCategory]),
|
||||
R.clamp(0, 100)
|
||||
)
|
||||
|
||||
|
|
@ -161,7 +189,7 @@ const WizardStep = ({
|
|||
initialValues={{ wasCashboxEmptied: '' }}
|
||||
enableReinitialize
|
||||
validationSchema={steps[0].schema}>
|
||||
{({ values, errors }) => (
|
||||
{({ errors }) => (
|
||||
<Form>
|
||||
<div
|
||||
className={classnames(classes.horizontalAlign, classes.form)}>
|
||||
|
|
@ -206,7 +234,7 @@ const WizardStep = ({
|
|||
classes.lineAlignment
|
||||
)}>
|
||||
<Info1 noMargin className={classes.cashboxBills}>
|
||||
{machine?.cashbox}
|
||||
{machine?.cashUnits.cashbox}
|
||||
</Info1>
|
||||
<P noMargin>accepted bills</P>
|
||||
</div>
|
||||
|
|
@ -236,7 +264,11 @@ const WizardStep = ({
|
|||
<img
|
||||
className={classes.stepImage}
|
||||
alt="cassette"
|
||||
src={cassetesArtworks(numberOfCassettes, step)}></img>
|
||||
src={cassetesArtworks(
|
||||
step,
|
||||
numberOfCassettes,
|
||||
numberOfStackers
|
||||
)}></img>
|
||||
<div className={classes.formWrapper}>
|
||||
<div
|
||||
className={classnames(
|
||||
|
|
@ -257,7 +289,13 @@ const WizardStep = ({
|
|||
<H4
|
||||
className={classes.cassetteFormTitleContent}
|
||||
noMargin>
|
||||
Cash cassette {step - 1} (dispenser)
|
||||
{startCase(cashUnitField)} (
|
||||
{R.includes('cassette', cashUnitField)
|
||||
? `dispenser`
|
||||
: R.includes('stacker', cashUnitField)
|
||||
? `recycler`
|
||||
: ``}
|
||||
)
|
||||
</H4>
|
||||
</div>
|
||||
<Cashbox
|
||||
|
|
@ -276,13 +314,13 @@ const WizardStep = ({
|
|||
component={NumberInput}
|
||||
decimalPlaces={0}
|
||||
width={50}
|
||||
placeholder={originalCassetteCount.toString()}
|
||||
name={cassetteField}
|
||||
placeholder={originalCashUnitCount.toString()}
|
||||
name={cashUnitField}
|
||||
className={classes.cashboxBills}
|
||||
autoFocus
|
||||
/>
|
||||
<P>
|
||||
{cassetteDenomination} {fiatCurrency} bills loaded
|
||||
{cashUnitDenomination} {fiatCurrency} bills loaded
|
||||
</P>
|
||||
</div>
|
||||
<P noMargin className={classes.fiatTotal}>
|
||||
|
|
|
|||
141
new-lamassu-admin/src/pages/Maintenance/helper.js
Normal file
141
new-lamassu-admin/src/pages/Maintenance/helper.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import * as R from 'ramda'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import { CashIn, CashOutLite } from 'src/components/inputs/cashbox/Cashbox'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { cashUnitCapacity } from 'src/utils/machine'
|
||||
|
||||
const getElements = (
|
||||
classes,
|
||||
config,
|
||||
bills,
|
||||
setWizard,
|
||||
widths,
|
||||
setMachineId
|
||||
) => {
|
||||
console.log(config)
|
||||
const fillingPercentageSettings = fromNamespace('notifications', config)
|
||||
const locale = fromNamespace('locale')(config)
|
||||
const cashout = fromNamespace('cashOut')(config)
|
||||
const fiatCurrency = locale?.fiatCurrency
|
||||
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'name',
|
||||
header: 'Machine',
|
||||
width: widths.name,
|
||||
view: m => <>{m.name}</>,
|
||||
input: ({ field: { value: name } }) => <>{name}</>
|
||||
},
|
||||
{
|
||||
name: 'cashbox',
|
||||
header: 'Cashbox',
|
||||
width: widths.cashbox,
|
||||
view: m => (
|
||||
<CashIn
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits.cashbox}
|
||||
total={R.sum(R.map(it => it.fiat, bills[m.id ?? m.deviceId] ?? []))}
|
||||
width={25}
|
||||
height={45}
|
||||
omitInnerPercentage
|
||||
className={classes.padding}
|
||||
/>
|
||||
),
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cassettes',
|
||||
header: 'Cassettes & Recyclers',
|
||||
width: widths.cassettes,
|
||||
view: m => {
|
||||
return (
|
||||
<div className={classes.unitsRow}>
|
||||
<div className={classes.units}>
|
||||
{R.map(it => (
|
||||
<CashOutLite
|
||||
width={'100%'}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits[`cassette${it}`]}
|
||||
denomination={
|
||||
getCashoutSettings(m.id ?? m.deviceId)[`cassette${it}`]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[`fillingPercentageCassette${it}`]
|
||||
}
|
||||
capacity={cashUnitCapacity[m.model].cassette}
|
||||
/>
|
||||
))(R.range(1, m.numberOfCassettes + 1))}
|
||||
</div>
|
||||
<div className={classes.units}>
|
||||
{R.map(it => (
|
||||
<>
|
||||
<CashOutLite
|
||||
width={'100%'}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits[`stacker${it}f`]}
|
||||
denomination={
|
||||
getCashoutSettings(m.id ?? m.deviceId)[`stacker${it}f`]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageStacker${it}f`
|
||||
]
|
||||
}
|
||||
capacity={cashUnitCapacity[m.model].stacker}
|
||||
/>
|
||||
<CashOutLite
|
||||
width={'100%'}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits[`stacker${it}r`]}
|
||||
denomination={
|
||||
getCashoutSettings(m.id ?? m.deviceId)[`stacker${it}r`]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageStacker${it}r`
|
||||
]
|
||||
}
|
||||
capacity={cashUnitCapacity[m.model].stacker}
|
||||
/>
|
||||
{it !== m.numberOfStackers && (
|
||||
<span className={classes.verticalLine} />
|
||||
)}
|
||||
</>
|
||||
))(R.range(1, m.numberOfStackers + 1))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
header: 'Edit',
|
||||
width: widths.edit,
|
||||
textAlign: 'center',
|
||||
view: m => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
!R.isNil(setMachineId) && setMachineId(m.id ?? m.deviceId)
|
||||
setWizard(true)
|
||||
}}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
export default { getElements }
|
||||
|
|
@ -28,6 +28,7 @@ const GET_INFO = gql`
|
|||
name
|
||||
deviceId
|
||||
numberOfCassettes
|
||||
numberOfStackers
|
||||
}
|
||||
cryptoCurrencies {
|
||||
code
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
const CASH_IN_KEY = 'fiatBalanceAlertsCashIn'
|
||||
const CASH_OUT_KEY = 'fiatBalanceAlertsCashOut'
|
||||
const RECYCLER_STACKER_KEY = 'fiatBalanceAlertsRecyclerStacker'
|
||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||
const DEFAULT_NUMBER_OF_STACKERS = 0
|
||||
const notesMin = 0
|
||||
const notesMax = 9999999
|
||||
|
||||
|
|
@ -39,6 +41,11 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
|||
DEFAULT_NUMBER_OF_CASSETTES
|
||||
)
|
||||
|
||||
const maxNumberOfStackers = Math.max(
|
||||
...R.map(it => it.numberOfStackers, machines),
|
||||
DEFAULT_NUMBER_OF_STACKERS
|
||||
)
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
cashInAlertThreshold: Yup.number()
|
||||
.transform(transformNumber)
|
||||
|
|
@ -160,6 +167,76 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
|||
)}
|
||||
</div>
|
||||
</Form>
|
||||
<Form className={classes.form}>
|
||||
<PromptWhenDirty />
|
||||
<Header
|
||||
title="Cash recycling (stackers)"
|
||||
editing={isEditing(RECYCLER_STACKER_KEY)}
|
||||
disabled={isDisabled(RECYCLER_STACKER_KEY)}
|
||||
setEditing={it => setEditing(RECYCLER_STACKER_KEY, it)}
|
||||
/>
|
||||
<div className={classes.wrapper}>
|
||||
{R.chain(
|
||||
it => [
|
||||
<>
|
||||
<div className={classes.row}>
|
||||
<Cashbox
|
||||
labelClassName={classes.cashboxLabel}
|
||||
emptyPartClassName={classes.cashboxEmptyPart}
|
||||
percent={
|
||||
values[`fillingPercentageStacker${it + 1}f`] ??
|
||||
data[`stacker${it + 1}f`]
|
||||
}
|
||||
applyColorVariant
|
||||
applyFiatBalanceAlertsStyling
|
||||
omitInnerPercentage
|
||||
cashOut
|
||||
/>
|
||||
<div className={classes.col2}>
|
||||
<TL2 className={classes.title}>Stacker {it + 1}F</TL2>
|
||||
<EditableNumber
|
||||
label="Alert me under"
|
||||
name={`fillingPercentageStacker${it + 1}f`}
|
||||
editing={isEditing(RECYCLER_STACKER_KEY)}
|
||||
displayValue={x => (x === '' ? '-' : x)}
|
||||
decoration="%"
|
||||
width={fieldWidth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
<>
|
||||
<div className={classes.row}>
|
||||
<Cashbox
|
||||
labelClassName={classes.cashboxLabel}
|
||||
emptyPartClassName={classes.cashboxEmptyPart}
|
||||
percent={
|
||||
values[`fillingPercentageStacker${it + 1}r`] ??
|
||||
data[`stacker${it + 1}r`]
|
||||
}
|
||||
applyColorVariant
|
||||
applyFiatBalanceAlertsStyling
|
||||
omitInnerPercentage
|
||||
cashOut
|
||||
/>
|
||||
<div className={classes.col2}>
|
||||
<TL2 className={classes.title}>Stacker {it + 1}R</TL2>
|
||||
<EditableNumber
|
||||
label="Alert me under"
|
||||
name={`fillingPercentageStacker${it + 1}r`}
|
||||
editing={isEditing(RECYCLER_STACKER_KEY)}
|
||||
displayValue={x => (x === '' ? '-' : x)}
|
||||
decoration="%"
|
||||
width={fieldWidth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
],
|
||||
R.times(R.identity, maxNumberOfStackers)
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import Locales from 'src/pages/Locales'
|
|||
import IndividualDiscounts from 'src/pages/LoyaltyPanel/IndividualDiscounts'
|
||||
import PromoCodes from 'src/pages/LoyaltyPanel/PromoCodes'
|
||||
import MachineLogs from 'src/pages/MachineLogs'
|
||||
import CashCassettes from 'src/pages/Maintenance/CashCassettes'
|
||||
import CashUnits from 'src/pages/Maintenance/CashUnits'
|
||||
import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||
import Notifications from 'src/pages/Notifications/Notifications'
|
||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||
|
|
@ -48,11 +48,11 @@ const getLamassuRoutes = () => [
|
|||
},
|
||||
children: [
|
||||
{
|
||||
key: 'cash_cassettes',
|
||||
label: 'Cash Cassettes',
|
||||
route: '/maintenance/cash-cassettes',
|
||||
key: 'cash_units',
|
||||
label: 'Cash Units',
|
||||
route: '/maintenance/cash-units',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CashCassettes
|
||||
component: CashUnits
|
||||
},
|
||||
{
|
||||
key: 'funding',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import Locales from 'src/pages/Locales'
|
|||
import IndividualDiscounts from 'src/pages/LoyaltyPanel/IndividualDiscounts'
|
||||
import PromoCodes from 'src/pages/LoyaltyPanel/PromoCodes'
|
||||
import MachineLogs from 'src/pages/MachineLogs'
|
||||
import CashCassettes from 'src/pages/Maintenance/CashCassettes'
|
||||
import CashUnits from 'src/pages/Maintenance/CashUnits'
|
||||
import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||
import Notifications from 'src/pages/Notifications/Notifications'
|
||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||
|
|
@ -48,11 +48,11 @@ const getPazuzRoutes = () => [
|
|||
},
|
||||
children: [
|
||||
{
|
||||
key: 'cash_cassettes',
|
||||
label: 'Cash Cassettes',
|
||||
route: '/maintenance/cash-cassettes',
|
||||
key: 'cash_units',
|
||||
label: 'Cash Units',
|
||||
route: '/maintenance/cash-units',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CashCassettes
|
||||
component: CashUnits
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,34 @@ const modelPrettifier = {
|
|||
douro1: 'Douro',
|
||||
sintra: 'Sintra',
|
||||
gaia: 'Gaia',
|
||||
tejo: 'Tejo'
|
||||
tejo: 'Tejo',
|
||||
aveiro: 'Aveiro',
|
||||
grandola: 'Grândola'
|
||||
}
|
||||
|
||||
export { modelPrettifier }
|
||||
const cashUnitCapacity = {
|
||||
grandola: {
|
||||
cashbox: 2000,
|
||||
recycler: 2800
|
||||
},
|
||||
aveiro: {
|
||||
cashbox: 1500,
|
||||
stacker: 60,
|
||||
escrow: 20,
|
||||
cassette: 500
|
||||
},
|
||||
tejo: {
|
||||
// TODO: add support for the different cashbox configuration in Tejo
|
||||
cashbox: 1000,
|
||||
cassette: 500
|
||||
},
|
||||
gaia: {
|
||||
cashbox: 600
|
||||
},
|
||||
sintra: {
|
||||
cashbox: 1000,
|
||||
cassette: 500
|
||||
}
|
||||
}
|
||||
|
||||
export { modelPrettifier, cashUnitCapacity }
|
||||
|
|
|
|||
745
package-lock.json
generated
745
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,7 @@
|
|||
"@ethereumjs/common": "^2.6.4",
|
||||
"@ethereumjs/tx": "^3.5.1",
|
||||
"@graphql-tools/merge": "^6.2.5",
|
||||
"@haensl/subset-sum": "^3.0.5",
|
||||
"@lamassu/coins": "1.3.0",
|
||||
"@simplewebauthn/server": "^3.0.0",
|
||||
"apollo-server-express": "2.25.1",
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
"main.js.map": "/static/js/main.900511f9.chunk.js.map",
|
||||
"runtime-main.js": "/static/js/runtime-main.5b925903.js",
|
||||
"runtime-main.js.map": "/static/js/runtime-main.5b925903.js.map",
|
||||
"static/js/2.4b3df17b.chunk.js": "/static/js/2.4b3df17b.chunk.js",
|
||||
"static/js/2.4b3df17b.chunk.js.map": "/static/js/2.4b3df17b.chunk.js.map",
|
||||
"static/js/2.01291e3c.chunk.js": "/static/js/2.01291e3c.chunk.js",
|
||||
"static/js/2.01291e3c.chunk.js.map": "/static/js/2.01291e3c.chunk.js.map",
|
||||
"index.html": "/index.html",
|
||||
"static/js/2.4b3df17b.chunk.js.LICENSE.txt": "/static/js/2.4b3df17b.chunk.js.LICENSE.txt",
|
||||
"static/js/2.01291e3c.chunk.js.LICENSE.txt": "/static/js/2.01291e3c.chunk.js.LICENSE.txt",
|
||||
"static/media/3-cassettes-open-1-left.d6d9aa73.svg": "/static/media/3-cassettes-open-1-left.d6d9aa73.svg",
|
||||
"static/media/3-cassettes-open-2-left.a9ee8d4c.svg": "/static/media/3-cassettes-open-2-left.a9ee8d4c.svg",
|
||||
"static/media/3-cassettes-open-3-left.08fed660.svg": "/static/media/3-cassettes-open-3-left.08fed660.svg",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/static/js/main.dabea017.chunk.js
Normal file
2
public/static/js/main.dabea017.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
public/static/js/main.dabea017.chunk.js.map
Normal file
1
public/static/js/main.dabea017.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue