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)
|
||||||
|
})
|
||||||
344
lib/bill-math.js
344
lib/bill-math.js
|
|
@ -1,225 +1,145 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const uuid = require('uuid')
|
const sumService = require('@haensl/subset-sum')
|
||||||
|
|
||||||
const MAX_AMOUNT_OF_SOLUTIONS = 10000
|
const BILL_LIST_MODES = {
|
||||||
const MAX_BRUTEFORCE_ITERATIONS = 10000000
|
LAST_UNIT_FIRST: 0,
|
||||||
|
FIRST_UNIT_FIRST: 1,
|
||||||
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
|
LOWEST_VALUE_FIRST: 2,
|
||||||
return [
|
HIGHEST_VALUE_FIRST: 3,
|
||||||
{
|
UNIT_ROUND_ROBIN: 4,
|
||||||
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
|
VALUE_ROUND_ROBIN: 5
|
||||||
denomination: cassettes[0].denomination
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: shouldFlip ? cassettes[1].count - c1 : c1,
|
|
||||||
denomination: cassettes[1].denomination
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: shouldFlip ? cassettes[2].count - c2 : c2,
|
|
||||||
denomination: cassettes[2].denomination
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: shouldFlip ? cassettes[3].count - c3 : c3,
|
|
||||||
denomination: cassettes[3].denomination
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeCassettes(cassettes) {
|
const buildBillList = (units, mode) => {
|
||||||
const map = {}
|
switch (mode) {
|
||||||
|
case BILL_LIST_MODES.LAST_UNIT_FIRST:
|
||||||
_.forEach(it => {
|
return _.reduce(
|
||||||
if (!map[it.denomination]) {
|
(acc, value) => {
|
||||||
map[it.denomination] = 0
|
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||||
}
|
return acc
|
||||||
map[it.denomination] += it.count
|
},
|
||||||
}, cassettes)
|
[],
|
||||||
|
_.reverse(units)
|
||||||
return _.map(it => ({ denomination: it, count: map[it] }), _.keys(map))
|
)
|
||||||
}
|
case BILL_LIST_MODES.FIRST_UNIT_FIRST:
|
||||||
|
return _.reduce(
|
||||||
function unmergeCassettes(cassettes, output) {
|
(acc, value) => {
|
||||||
const map = {}
|
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||||
|
return acc
|
||||||
_.forEach(it => {
|
},
|
||||||
if (!map[it.denomination]) {
|
[],
|
||||||
map[it.denomination] = 0
|
units
|
||||||
}
|
)
|
||||||
map[it.denomination] += it.provisioned
|
case BILL_LIST_MODES.LOWEST_VALUE_FIRST:
|
||||||
}, output)
|
return _.reduce(
|
||||||
|
(acc, value) => {
|
||||||
const response = []
|
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||||
_.forEach(it => {
|
return acc
|
||||||
const value = {
|
},
|
||||||
denomination: it.denomination,
|
[],
|
||||||
id: uuid.v4()
|
_.orderBy(['denomination'], ['asc'])(units)
|
||||||
}
|
)
|
||||||
|
case BILL_LIST_MODES.HIGHEST_VALUE_FIRST:
|
||||||
const amountNeeded = map[it.denomination]
|
return _.reduce(
|
||||||
if (!amountNeeded) {
|
(acc, value) => {
|
||||||
return response.push({ provisioned: 0, ...value })
|
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||||
}
|
return acc
|
||||||
|
},
|
||||||
if (amountNeeded < it.count) {
|
[],
|
||||||
map[it.denomination] = 0
|
_.orderBy(['denomination'], ['desc'])(units)
|
||||||
return response.push({ provisioned: amountNeeded, ...value })
|
)
|
||||||
}
|
case BILL_LIST_MODES.UNIT_ROUND_ROBIN:
|
||||||
|
|
||||||
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,
|
const amountOfBills = _.reduce(
|
||||||
denomination: small.denomination,
|
(acc, value) => acc + value.count,
|
||||||
id: uuid.v4()
|
0,
|
||||||
},
|
units
|
||||||
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
|
)
|
||||||
]
|
|
||||||
|
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 []
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function makeChange(outCassettes, amount) {
|
||||||
const available = _.reduce(
|
const solution = getSolution(outCassettes, amount, BILL_LIST_MODES.VALUE_ROUND_ROBIN)
|
||||||
(res, val) => res + val.count * val.denomination,
|
return solutionToOriginalUnits(solution, outCassettes)
|
||||||
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 []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { makeChange }
|
module.exports = { makeChange }
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,24 @@ function insertNewBills (t, billRows, machineTx) {
|
||||||
if (_.isEmpty(bills)) return Promise.resolve([])
|
if (_.isEmpty(bills)) return Promise.resolve([])
|
||||||
|
|
||||||
const dbBills = _.map(cashInLow.massage, bills)
|
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 sql = pgp.helpers.insert(dbBills, columns, 'bills')
|
||||||
const deviceID = machineTx.deviceId
|
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`
|
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(() => {
|
.then(() => {
|
||||||
return t.none(sql)
|
return t.none(sql)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,11 @@ function mapDispense (tx) {
|
||||||
const res = {}
|
const res = {}
|
||||||
|
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
res[`provisioned_${it + 1}`] = bills[it].provisioned
|
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||||
res[`denomination_${it + 1}`] = bills[it].denomination
|
res[`provisioned_${suffix}`] = bills[it].provisioned
|
||||||
res[`dispensed_${it + 1}`] = bills[it].dispensed
|
res[`denomination_${suffix}`] = bills[it].denomination
|
||||||
res[`rejected_${it + 1}`] = bills[it].rejected
|
res[`dispensed_${suffix}`] = bills[it].dispensed
|
||||||
|
res[`rejected_${suffix}`] = bills[it].rejected
|
||||||
}, _.times(_.identity(), _.size(bills)))
|
}, _.times(_.identity(), _.size(bills)))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
||||||
|
|
@ -107,17 +107,10 @@ function nextHd (t, isHd, tx) {
|
||||||
function updateCassettes (t, tx) {
|
function updateCassettes (t, tx) {
|
||||||
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
|
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
|
||||||
|
|
||||||
const sql = `update devices set
|
const billsStmt = _.join(', ')(_.map(it => `${tx.bills[it].name} = ${tx.bills[it].name} - $${it + 1}`)(_.range(0, _.size(tx.bills))))
|
||||||
${_.size(tx.bills) > 0 ? `cassette1 = cassette1 - $1` : ``}
|
const returnStmt = _.join(', ')(_.map(bill => `${bill.name}`)(tx.bills))
|
||||||
${_.size(tx.bills) > 1 ? `, cassette2 = cassette2 - $2` : ``}
|
|
||||||
${_.size(tx.bills) > 2 ? `, cassette3 = cassette3 - $3` : ``}
|
const sql = `UPDATE devices SET ${billsStmt} WHERE device_id = $${_.size(tx.bills) + 1} RETURNING ${returnStmt}`
|
||||||
${_.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 values = []
|
const values = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,32 @@ case
|
||||||
else 'Pending'
|
else 'Pending'
|
||||||
end`
|
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 }
|
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
||||||
|
|
||||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||||
|
|
@ -43,23 +69,37 @@ function convertBigNumFields (obj) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertField (key) {
|
function convertField (key) {
|
||||||
return _.snakeCase(key)
|
return _.includes('denomination', key) || _.includes('provisioned', key) ? key : _.snakeCase(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDbBills (tx) {
|
function addDbBills (tx) {
|
||||||
const bills = tx.bills
|
const bills = tx.bills
|
||||||
if (_.isEmpty(bills)) return tx
|
if (_.isEmpty(bills)) return tx
|
||||||
|
|
||||||
const billsObj = {
|
const billFields = _.map(it => _.replace(/(denomination|provisioned)/g, '$1_')(it), BILL_FIELDS)
|
||||||
provisioned1: bills[0]?.provisioned ?? 0,
|
|
||||||
provisioned2: bills[1]?.provisioned ?? 0,
|
const billsObj = _.flow(
|
||||||
provisioned3: bills[2]?.provisioned ?? 0,
|
_.reduce(
|
||||||
provisioned4: bills[3]?.provisioned ?? 0,
|
(acc, value) => {
|
||||||
denomination1: bills[0]?.denomination ?? 0,
|
const suffix = value.name.replace(/cassette|stacker/gi, '')
|
||||||
denomination2: bills[1]?.denomination ?? 0,
|
return {
|
||||||
denomination3: bills[2]?.denomination ?? 0,
|
...acc,
|
||||||
denomination4: bills[3]?.denomination ?? 0
|
[`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)
|
return _.assign(tx, billsObj)
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +118,7 @@ function toObj (row) {
|
||||||
let newObj = {}
|
let newObj = {}
|
||||||
|
|
||||||
keys.forEach(key => {
|
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]) {
|
if (key === 'received_crypto_atoms' && row[key]) {
|
||||||
newObj[objKey] = new BN(row[key])
|
newObj[objKey] = new BN(row[key])
|
||||||
return
|
return
|
||||||
|
|
@ -93,35 +133,28 @@ function toObj (row) {
|
||||||
|
|
||||||
newObj.direction = 'cashOut'
|
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
|
const billFieldsArr = _.concat(
|
||||||
if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values')
|
_.map(it => ({ name: `cassette${it + 1}`, denomination: newObj[`denomination${it + 1}`], provisioned: newObj[`provisioned${it + 1}`] }))(_.range(0, MAX_CASSETTES)),
|
||||||
|
_.reduce(
|
||||||
const billFieldsArr = [
|
(acc, value) => {
|
||||||
{
|
acc.push(
|
||||||
denomination: newObj.denomination1,
|
{ name: `stacker${value + 1}f`, denomination: newObj[`denomination${value + 1}f`], provisioned: newObj[`provisioned${value + 1}f`] },
|
||||||
provisioned: newObj.provisioned1
|
{ 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
|
[]
|
||||||
},
|
)(_.range(0, MAX_STACKERS))
|
||||||
{
|
)
|
||||||
denomination: newObj.denomination3,
|
|
||||||
provisioned: newObj.provisioned3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
denomination: newObj.denomination4,
|
|
||||||
provisioned: newObj.provisioned4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// There can't be bills with denomination === 0.
|
// 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.
|
// 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)
|
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) {
|
function redeemableTxs (deviceId) {
|
||||||
|
|
@ -129,7 +162,10 @@ function redeemableTxs (deviceId) {
|
||||||
where device_id=$1
|
where device_id=$1
|
||||||
and redeem=$2
|
and redeem=$2
|
||||||
and dispense=$3
|
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`
|
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
|
||||||
|
|
||||||
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
|
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)) {
|
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
||||||
return pi.buildAvailableCassettes(newTx.id)
|
return pi.buildAvailableUnits(newTx.id)
|
||||||
.then(cassettes => {
|
.then(_units => {
|
||||||
|
const units = _.concat(_units.cassettes, _units.stackers)
|
||||||
logger.silly('Computing bills to dispense:', {
|
logger.silly('Computing bills to dispense:', {
|
||||||
txId: newTx.id,
|
txId: newTx.id,
|
||||||
cassettes: cassettes.cassettes,
|
units: units,
|
||||||
fiat: newTx.fiat
|
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))
|
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
||||||
|
|
||||||
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
||||||
|
|
@ -73,8 +74,9 @@ function postProcess (txVector, justAuthorized, pi) {
|
||||||
const rec = {}
|
const rec = {}
|
||||||
|
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
rec[`provisioned_${it + 1}`] = bills[it].provisioned
|
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||||
rec[`denomination_${it + 1}`] = bills[it].denomination
|
rec[`provisioned_${suffix}`] = bills[it].provisioned
|
||||||
|
rec[`denomination_${suffix}`] = bills[it].denomination
|
||||||
}, _.times(_.identity(), _.size(bills)))
|
}, _.times(_.identity(), _.size(bills)))
|
||||||
|
|
||||||
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,37 @@ const constants = require('./constants')
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const uuid = require('uuid')
|
const uuid = require('uuid')
|
||||||
|
const camelize = require('./utils')
|
||||||
|
|
||||||
function createCashboxBatch (deviceId, cashboxCount) {
|
function createCashboxBatch (deviceId, cashboxCount) {
|
||||||
if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
|
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 = `
|
const sql2 = `
|
||||||
UPDATE bills SET cashbox_batch_id=$1
|
UPDATE bills SET cashbox_batch_id=$1
|
||||||
FROM cash_in_txs
|
FROM cash_in_txs
|
||||||
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
|
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
|
||||||
cash_in_txs.device_id = $2 AND
|
cash_in_txs.device_id = $2 AND
|
||||||
|
bills.destination_unit = 'cashbox' AND
|
||||||
bills.cashbox_batch_id IS NULL
|
bills.cashbox_batch_id IS NULL
|
||||||
`
|
`
|
||||||
return db.tx(async t => {
|
const sql3 = `
|
||||||
const newBatch = await t.oneOrNone(sql, [uuid.v4(), deviceId])
|
UPDATE empty_unit_bills SET cashbox_batch_id=$1
|
||||||
return t.oneOrNone(sql2, [newBatch.id, newBatch.device_id])
|
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) {
|
function updateMachineWithBatch (machineContext, oldCashboxCount) {
|
||||||
const isValidContext = _.has(['deviceId', 'cashbox', 'cassettes'], machineContext)
|
const cashUnits = machineContext.cashUnits
|
||||||
const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(machineContext.cassettes))
|
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)
|
if (!isValidContext && !isCassetteAmountWithinRange)
|
||||||
throw new Error('Insufficient info to create a new cashbox batch')
|
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.')
|
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 => {
|
return db.tx(t => {
|
||||||
const deviceId = machineContext.deviceId
|
const deviceId = machineContext.deviceId
|
||||||
const batchId = uuid.v4()
|
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
|
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
|
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
|
||||||
cash_in_txs.device_id = $2 AND
|
cash_in_txs.device_id = $2 AND
|
||||||
|
bills.destination_unit = 'cashbox' AND
|
||||||
bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
|
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`, [
|
const q3 = t.none(`UPDATE empty_unit_bills SET cashbox_batch_id=$1
|
||||||
machineContext.cashbox,
|
WHERE empty_unit_bills.device_id = $2 AND empty_unit_bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
|
||||||
machineContext.cassettes[0],
|
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`, [
|
||||||
machineContext.cassettes[1],
|
cashUnits.cashbox,
|
||||||
machineContext.cassettes[2],
|
cashUnits.cassette1,
|
||||||
machineContext.cassettes[3],
|
cashUnits.cassette2,
|
||||||
|
cashUnits.cassette3,
|
||||||
|
cashUnits.cassette4,
|
||||||
|
cashUnits.stacker1f,
|
||||||
|
cashUnits.stacker1r,
|
||||||
|
cashUnits.stacker2f,
|
||||||
|
cashUnits.stacker2r,
|
||||||
|
cashUnits.stacker3f,
|
||||||
|
cashUnits.stacker3r,
|
||||||
machineContext.deviceId
|
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()) {
|
function getBatches (from = new Date(0).toISOString(), until = new Date().toISOString()) {
|
||||||
const sql = `
|
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
|
SELECT cuo.id, cuo.device_id, cuo.created, cuo.operation_type, cuo.bill_count_override, cuo.performed_by, json_agg(bi.*) AS bills
|
||||||
FROM cashbox_batches AS cb
|
FROM cash_unit_operation AS cuo
|
||||||
LEFT JOIN bills AS b ON cb.id = b.cashbox_batch_id
|
LEFT JOIN (
|
||||||
WHERE cb.created >= $1 AND cb.created <= $2
|
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
|
||||||
GROUP BY cb.id
|
SELECT id, fiat, fiat_code, created, cashbox_batch_id, device_id FROM empty_unit_bills
|
||||||
ORDER BY cb.created DESC
|
) 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) {
|
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])
|
return db.none(sql, [performedBy, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBillsByBatchId (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])
|
return db.any(sql, [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,29 @@ const anonymousCustomer = {
|
||||||
name: 'anonymous'
|
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_MINIMUM_AMOUNT_OF_CASSETTES = 2
|
||||||
const CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES = 4
|
const CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES = 4
|
||||||
|
|
@ -39,7 +61,7 @@ const BALANCE_FETCH_SPEED_MULTIPLIER = {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
anonymousCustomer,
|
anonymousCustomer,
|
||||||
CASSETTE_MAX_CAPACITY,
|
CASH_UNIT_CAPACITY,
|
||||||
AUTHENTICATOR_ISSUER_ENTITY,
|
AUTHENTICATOR_ISSUER_ENTITY,
|
||||||
AUTH_TOKEN_EXPIRATION_TIME,
|
AUTH_TOKEN_EXPIRATION_TIME,
|
||||||
REGISTRATION_TOKEN_EXPIRATION_TIME,
|
REGISTRATION_TOKEN_EXPIRATION_TIME,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const plugins = require('../plugins')
|
||||||
const configManager = require('../new-config-manager')
|
const configManager = require('../new-config-manager')
|
||||||
const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests')
|
const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests')
|
||||||
const state = require('../middlewares/state')
|
const state = require('../middlewares/state')
|
||||||
|
const { getMachine } = require('../machine-loader')
|
||||||
|
|
||||||
const VERSION = require('../../package.json').version
|
const VERSION = require('../../package.json').version
|
||||||
|
|
||||||
|
|
@ -114,6 +115,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
configManager.getOperatorInfo(settings.config),
|
configManager.getOperatorInfo(settings.config),
|
||||||
configManager.getReceipt(settings.config),
|
configManager.getReceipt(settings.config),
|
||||||
!!configManager.getCashOut(deviceId, settings.config).active,
|
!!configManager.getCashOut(deviceId, settings.config).active,
|
||||||
|
getMachine(deviceId, currentConfigVersion),
|
||||||
])
|
])
|
||||||
.then(([
|
.then(([
|
||||||
enablePaperWalletOnly,
|
enablePaperWalletOnly,
|
||||||
|
|
@ -124,6 +126,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
operatorInfo,
|
operatorInfo,
|
||||||
receiptInfo,
|
receiptInfo,
|
||||||
twoWayMode,
|
twoWayMode,
|
||||||
|
{ numberOfCassettes, numberOfStackers },
|
||||||
]) =>
|
]) =>
|
||||||
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
|
||||||
null :
|
null :
|
||||||
|
|
@ -138,7 +141,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
languages: localeInfo.languages,
|
languages: localeInfo.languages,
|
||||||
fiatCode: localeInfo.fiatCurrency
|
fiatCode: localeInfo.fiatCurrency
|
||||||
},
|
},
|
||||||
machineInfo: { deviceId, deviceName },
|
machineInfo: { deviceId, deviceName, numberOfCassettes, numberOfStackers },
|
||||||
twoWayMode,
|
twoWayMode,
|
||||||
speedtestFiles,
|
speedtestFiles,
|
||||||
urlsToPing,
|
urlsToPing,
|
||||||
|
|
@ -167,13 +170,25 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
||||||
)(cassettes) :
|
)(cassettes) :
|
||||||
null
|
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)
|
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
||||||
|
|
||||||
return _.flow(
|
const res = _.flow(
|
||||||
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'coins', 'rates']),
|
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'stackers', 'coins', 'rates']),
|
||||||
|
|
||||||
_.update('cassettes', massageCassettes),
|
_.update('cassettes', massageCassettes),
|
||||||
|
|
||||||
|
_.update('stackers', massageStackers),
|
||||||
|
|
||||||
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
||||||
_.update('coins', _.map(({ 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 */
|
/* Group the separate objects by cryptoCode */
|
||||||
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
||||||
({ areThereAvailablePromoCodes, balances, cassettes, coins, rates }) => ({
|
({ areThereAvailablePromoCodes, balances, cassettes, stackers, coins, rates }) => ({
|
||||||
areThereAvailablePromoCodes,
|
areThereAvailablePromoCodes,
|
||||||
cassettes,
|
cassettes,
|
||||||
|
stackers,
|
||||||
coins: _.flow(
|
coins: _.flow(
|
||||||
_.reduce(
|
_.reduce(
|
||||||
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
|
(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('reboot', !!pid && state.reboots?.[operatorId]?.[deviceId] === pid),
|
||||||
_.set('shutdown', !!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid),
|
_.set('shutdown', !!pid && state.shutdowns?.[operatorId]?.[deviceId] === pid),
|
||||||
_.set('restartServices', !!pid && state.restartServicesMap?.[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)
|
)(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 {
|
type MachineInfo {
|
||||||
deviceId: String!
|
deviceId: String!
|
||||||
deviceName: String
|
deviceName: String
|
||||||
|
numberOfCassettes: Int
|
||||||
|
numberOfStackers: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceiptInfo {
|
type ReceiptInfo {
|
||||||
|
|
@ -165,6 +167,13 @@ type DynamicCoinValues {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhysicalCassette {
|
type PhysicalCassette {
|
||||||
|
name: String!
|
||||||
|
denomination: Int!
|
||||||
|
count: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhysicalStacker {
|
||||||
|
name: String!
|
||||||
denomination: Int!
|
denomination: Int!
|
||||||
count: Int!
|
count: Int!
|
||||||
}
|
}
|
||||||
|
|
@ -174,13 +183,21 @@ type Cassettes {
|
||||||
virtual: [Int!]!
|
virtual: [Int!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Stackers {
|
||||||
|
physical: [PhysicalStacker!]!
|
||||||
|
virtual: [Int!]!
|
||||||
|
}
|
||||||
|
|
||||||
type DynamicConfig {
|
type DynamicConfig {
|
||||||
areThereAvailablePromoCodes: Boolean!
|
areThereAvailablePromoCodes: Boolean!
|
||||||
cassettes: Cassettes
|
cassettes: Cassettes
|
||||||
|
stackers: Stackers
|
||||||
coins: [DynamicCoinValues!]!
|
coins: [DynamicCoinValues!]!
|
||||||
reboot: Boolean!
|
reboot: Boolean!
|
||||||
shutdown: Boolean!
|
shutdown: Boolean!
|
||||||
restartServices: Boolean!
|
restartServices: Boolean!
|
||||||
|
emptyUnit: Boolean!
|
||||||
|
refillUnit: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Configs {
|
type Configs {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const pgp = require('pg-promise')()
|
const pgp = require('pg-promise')()
|
||||||
const axios = require('axios')
|
|
||||||
const uuid = require('uuid')
|
const uuid = require('uuid')
|
||||||
|
|
||||||
const batching = require('./cashbox-batches')
|
const batching = require('./cashbox-batches')
|
||||||
|
|
@ -13,6 +12,7 @@ const settingsLoader = require('./new-settings-loader')
|
||||||
const notifierUtils = require('./notifier/utils')
|
const notifierUtils = require('./notifier/utils')
|
||||||
const notifierQueries = require('./notifier/queries')
|
const notifierQueries = require('./notifier/queries')
|
||||||
const { ApolloError } = require('apollo-server-errors');
|
const { ApolloError } = require('apollo-server-errors');
|
||||||
|
const { loadLatestConfig } = require('./new-settings-loader')
|
||||||
|
|
||||||
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||||
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||||
|
|
@ -21,12 +21,21 @@ const stuckStatus = { label: 'Stuck', type: 'error' }
|
||||||
function toMachineObject (r) {
|
function toMachineObject (r) {
|
||||||
return {
|
return {
|
||||||
deviceId: r.device_id,
|
deviceId: r.device_id,
|
||||||
cashbox: r.cashbox,
|
cashUnits: {
|
||||||
cassette1: r.cassette1,
|
cashbox: r.cashbox,
|
||||||
cassette2: r.cassette2,
|
cassette1: r.cassette1,
|
||||||
cassette3: r.cassette3,
|
cassette2: r.cassette2,
|
||||||
cassette4: r.cassette4,
|
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,
|
numberOfCassettes: r.number_of_cassettes,
|
||||||
|
numberOfStackers: r.number_of_stackers,
|
||||||
version: r.version,
|
version: r.version,
|
||||||
model: r.model,
|
model: r.model,
|
||||||
pairedAt: new Date(r.created),
|
pairedAt: new Date(r.created),
|
||||||
|
|
@ -141,8 +150,9 @@ function renameMachine (rec) {
|
||||||
|
|
||||||
function resetCashOutBills (rec) {
|
function resetCashOutBills (rec) {
|
||||||
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
|
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
|
||||||
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4 WHERE device_id=$5;`
|
const { cassette1, cassette2, cassette3, cassette4, stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r } = rec.cashUnits
|
||||||
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
|
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) {
|
function emptyCashInBills (rec) {
|
||||||
|
|
@ -151,17 +161,138 @@ function emptyCashInBills (rec) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCassetteBills (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])
|
return db.oneOrNone(`SELECT cashbox FROM devices WHERE device_id=$1 LIMIT 1`, [rec.deviceId])
|
||||||
.then(oldCashboxValue => {
|
.then(oldCashboxValue => {
|
||||||
if (_.isNil(oldCashboxValue) || rec.cashbox === oldCashboxValue.cashbox) {
|
if (_.isNil(oldCashboxValue) || cashbox === oldCashboxValue.cashbox) {
|
||||||
const sql = 'UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 WHERE device_id=$6'
|
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, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId])
|
return db.none(sql, [cashbox, cassette1, cassette2, cassette3, cassette4, stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r, rec.deviceId])
|
||||||
}
|
}
|
||||||
|
|
||||||
return batching.updateMachineWithBatch({ ...rec, oldCashboxValue })
|
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) {
|
function unpair (rec) {
|
||||||
return pairing.unpair(rec.deviceId)
|
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) {
|
function setMachine (rec, operatorId) {
|
||||||
rec.operatorId = operatorId
|
rec.operatorId = operatorId
|
||||||
switch (rec.action) {
|
switch (rec.action) {
|
||||||
|
|
@ -204,6 +353,8 @@ function setMachine (rec, operatorId) {
|
||||||
case 'reboot': return reboot(rec)
|
case 'reboot': return reboot(rec)
|
||||||
case 'shutdown': return shutdown(rec)
|
case 'shutdown': return shutdown(rec)
|
||||||
case 'restartServices': return restartServices(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)
|
default: throw new Error('No such action: ' + rec.action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -277,5 +428,7 @@ module.exports = {
|
||||||
updateNetworkHeartbeat,
|
updateNetworkHeartbeat,
|
||||||
getNetworkPerformance,
|
getNetworkPerformance,
|
||||||
getNetworkHeartbeat,
|
getNetworkHeartbeat,
|
||||||
getConfig
|
getConfig,
|
||||||
|
emptyMachineUnits,
|
||||||
|
refillMachineUnits
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,14 @@ function machineAction (type, value) {
|
||||||
logger.debug(`Restarting services of machine '${deviceId}' from operator ${operatorId}`)
|
logger.debug(`Restarting services of machine '${deviceId}' from operator ${operatorId}`)
|
||||||
state.restartServicesMap[operatorId] = { [deviceId]: pid }
|
state.restartServicesMap[operatorId] = { [deviceId]: pid }
|
||||||
break
|
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:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ module.exports = (function () {
|
||||||
reboots: {},
|
reboots: {},
|
||||||
shutdowns: {},
|
shutdowns: {},
|
||||||
restartServicesMap: {},
|
restartServicesMap: {},
|
||||||
|
emptyUnit: {},
|
||||||
|
refillUnit: {},
|
||||||
mnemonic: null
|
mnemonic: null
|
||||||
}
|
}
|
||||||
}())
|
}())
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ const resolvers = {
|
||||||
unpairedMachines: () => machineLoader.getUnpairedMachines()
|
unpairedMachines: () => machineLoader.getUnpairedMachines()
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context]) =>
|
machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) =>
|
||||||
machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context)
|
machineAction({ deviceId, action, cashUnits, newName }, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ const typeDef = gql`
|
||||||
type Bill {
|
type Bill {
|
||||||
id: ID
|
id: ID
|
||||||
fiat: Int
|
fiat: Int
|
||||||
|
fiatCode: String
|
||||||
deviceId: ID
|
deviceId: ID
|
||||||
created: Date
|
created: Date
|
||||||
cashboxBatchId: ID
|
cashUnitOperationId: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,9 @@ const typeDef = gql`
|
||||||
pairedAt: Date
|
pairedAt: Date
|
||||||
version: String
|
version: String
|
||||||
model: String
|
model: String
|
||||||
cashbox: Int
|
cashUnits: CashUnits
|
||||||
cassette1: Int
|
|
||||||
cassette2: Int
|
|
||||||
cassette3: Int
|
|
||||||
cassette4: Int
|
|
||||||
numberOfCassettes: Int
|
numberOfCassettes: Int
|
||||||
|
numberOfStackers: Int
|
||||||
statuses: [MachineStatus]
|
statuses: [MachineStatus]
|
||||||
latestEvent: MachineEvent
|
latestEvent: MachineEvent
|
||||||
downloadSpeed: String
|
downloadSpeed: String
|
||||||
|
|
@ -27,6 +24,34 @@ const typeDef = gql`
|
||||||
packetLoss: String
|
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 {
|
type UnpairedMachine {
|
||||||
id: ID!
|
id: ID!
|
||||||
deviceId: ID!
|
deviceId: ID!
|
||||||
|
|
@ -55,6 +80,8 @@ const typeDef = gql`
|
||||||
reboot
|
reboot
|
||||||
shutdown
|
shutdown
|
||||||
restartServices
|
restartServices
|
||||||
|
emptyUnit
|
||||||
|
refillUnit
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
|
@ -64,7 +91,7 @@ const typeDef = gql`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
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 (
|
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}
|
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)
|
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)}`}`
|
||||||
.then(res => _.map(_.mapKeys(_.camelCase), res))
|
|
||||||
|
return Promise.all([db.any(sql), db.any(sql2)])
|
||||||
|
.then(([bills, operationalBills]) => _.map(_.mapKeys(_.camelCase), _.concat(bills, operationalBills)))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ function getMachine (machineId) {
|
||||||
.then(machines => machines.find(({ deviceId }) => deviceId === 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
|
const operatorId = context.res.locals.operatorId
|
||||||
return getMachine(deviceId)
|
return getMachine(deviceId)
|
||||||
.then(machine => {
|
.then(machine => {
|
||||||
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
|
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
|
||||||
return machine
|
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))
|
.then(getMachine(deviceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,11 @@ function advancedBatch (data) {
|
||||||
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
||||||
'dispense', 'notified', 'redeem', 'phone', 'error',
|
'dispense', 'notified', 'redeem', 'phone', 'error',
|
||||||
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
||||||
'dispenseConfirmed', 'provisioned1', 'provisioned2',
|
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
||||||
'denomination1', 'denomination2', 'errorCode', 'customerId',
|
'provisioned1f', 'provisioned1r', 'provisioned2f', 'provisioned2r', 'provisioned3f', 'provisioned3r',
|
||||||
'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
'denomination1', 'denomination2', 'denomination3', 'denomination4',
|
||||||
|
'denomination1f', 'denomination1r', 'denomination2f', 'denomination2r', 'denomination3f', 'denomination3r',
|
||||||
|
'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||||
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
||||||
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
||||||
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ const CODES_DISPLAY = {
|
||||||
HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
|
HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
|
||||||
CASH_BOX_FULL: 'Cash box full',
|
CASH_BOX_FULL: 'Cash box full',
|
||||||
LOW_CASH_OUT: 'Low Cash-out',
|
LOW_CASH_OUT: 'Low Cash-out',
|
||||||
|
LOW_RECYCLER_STACKER: 'Low Recycler Stacker',
|
||||||
|
HIGH_RECYCLER_STACKER: 'High Recycler Stacker',
|
||||||
CASHBOX_REMOVED: 'Cashbox removed'
|
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.
|
// 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
|
// This makes sure that the server stores a default value
|
||||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||||
|
const DEFAULT_NUMBER_OF_STACKERS = 0
|
||||||
|
|
||||||
function pullToken (token) {
|
function pullToken (token) {
|
||||||
const sql = `delete from pairing_tokens
|
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)
|
return pullToken(token)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r.expired) return false
|
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)
|
on conflict (device_id)
|
||||||
do update set paired=TRUE, display=TRUE`
|
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)
|
.then(() => true)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
|
||||||
233
lib/plugins.js
233
lib/plugins.js
|
|
@ -27,7 +27,7 @@ const loyalty = require('./loyalty')
|
||||||
const transactionBatching = require('./tx-batching')
|
const transactionBatching = require('./tx-batching')
|
||||||
const state = require('./middlewares/state')
|
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')
|
const notifier = require('./notifier')
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
const sumTxs = (sum, tx) => {
|
const sumTxs = (sum, tx) => {
|
||||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
// 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 sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||||
|
|
||||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
||||||
|
|
@ -139,6 +139,7 @@ function plugins (settings, deviceId) {
|
||||||
const computedCassettes = []
|
const computedCassettes = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
computedCassettes.push({
|
computedCassettes.push({
|
||||||
|
name: cassettes[it].name,
|
||||||
denomination: cassettes[it].denomination,
|
denomination: cassettes[it].denomination,
|
||||||
count: counts[it]
|
count: counts[it]
|
||||||
})
|
})
|
||||||
|
|
@ -147,37 +148,75 @@ function plugins (settings, deviceId) {
|
||||||
return computedCassettes
|
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) {
|
function buildAvailableCassettes (excludeTxId) {
|
||||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||||
|
|
||||||
if (!cashOutConfig.active) return Promise.resolve()
|
if (!cashOutConfig.active) return Promise.resolve()
|
||||||
|
|
||||||
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||||
.then(([rec, _redeemableTxs]) => {
|
.then(([_cassettes, _redeemableTxs]) => {
|
||||||
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
||||||
|
|
||||||
const denominations = []
|
const denominations = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
denominations.push(cashOutConfig[`cassette${it + 1}`])
|
denominations.push(cashOutConfig[`cassette${it + 1}`])
|
||||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
}, _.times(_.identity(), _cassettes.numberOfCassettes))
|
||||||
|
|
||||||
const virtualCassettes = [Math.max(...denominations) * 2]
|
const virtualCassettes = [Math.max(...denominations) * 2]
|
||||||
|
|
||||||
const counts = argv.cassettes
|
const counts = argv.cassettes
|
||||||
? argv.cassettes.split(',')
|
? 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!')
|
throw new Error('Denominations and respective counts do not match!')
|
||||||
}
|
}
|
||||||
|
|
||||||
const cassettes = []
|
const cassettes = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
cassettes.push({
|
cassettes.push({
|
||||||
|
name: `cassette${it + 1}`,
|
||||||
denomination: parseInt(denominations[it], 10),
|
denomination: parseInt(denominations[it], 10),
|
||||||
count: parseInt(counts[it], 10)
|
count: parseInt(counts[it], 10)
|
||||||
})
|
})
|
||||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
}, _.times(_.identity(), _cassettes.numberOfCassettes))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return {
|
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 () {
|
function fetchCurrentConfigVersion () {
|
||||||
const sql = `select id from user_config
|
const sql = `select id from user_config
|
||||||
where type=$1
|
where type=$1
|
||||||
|
|
@ -240,6 +335,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
buildAvailableCassettes(),
|
buildAvailableCassettes(),
|
||||||
|
buildAvailableStackers(),
|
||||||
fetchCurrentConfigVersion(),
|
fetchCurrentConfigVersion(),
|
||||||
millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)),
|
millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)),
|
||||||
loyalty.getNumberOfAvailablePromoCodes(),
|
loyalty.getNumberOfAvailablePromoCodes(),
|
||||||
|
|
@ -250,6 +346,7 @@ function plugins (settings, deviceId) {
|
||||||
])
|
])
|
||||||
.then(([
|
.then(([
|
||||||
cassettes,
|
cassettes,
|
||||||
|
stackers,
|
||||||
configVersion,
|
configVersion,
|
||||||
timezone,
|
timezone,
|
||||||
numberOfAvailablePromoCodes,
|
numberOfAvailablePromoCodes,
|
||||||
|
|
@ -273,6 +370,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cassettes,
|
cassettes,
|
||||||
|
stackers,
|
||||||
rates: buildRates(tickers),
|
rates: buildRates(tickers),
|
||||||
balances: buildBalances(balances),
|
balances: buildBalances(balances),
|
||||||
coins,
|
coins,
|
||||||
|
|
@ -645,71 +743,164 @@ function plugins (settings, deviceId) {
|
||||||
const denomination2 = cashOutConfig.cassette2
|
const denomination2 = cashOutConfig.cassette2
|
||||||
const denomination3 = cashOutConfig.cassette3
|
const denomination3 = cashOutConfig.cassette3
|
||||||
const denomination4 = cashOutConfig.cassette4
|
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 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 notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
||||||
|
|
||||||
const machineName = device.name
|
const machineName = device.name
|
||||||
|
|
||||||
const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
|
const cashInAlert = device.cashUnits.cashbox > notifications.cashInAlertThreshold
|
||||||
? {
|
? {
|
||||||
code: 'CASH_BOX_FULL',
|
code: 'CASH_BOX_FULL',
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
notes: device.cashbox
|
notes: device.cashUnits.cashbox
|
||||||
}
|
}
|
||||||
: null
|
: 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',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 1,
|
cassette: 1,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
notes: device.cassette1,
|
notes: device.cashUnits.cassette1,
|
||||||
denomination: denomination1,
|
denomination: denomination1,
|
||||||
fiatCode
|
fiatCode
|
||||||
}
|
}
|
||||||
: null
|
: 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',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 2,
|
cassette: 2,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
notes: device.cassette2,
|
notes: device.cashUnits.cassette2,
|
||||||
denomination: denomination2,
|
denomination: denomination2,
|
||||||
fiatCode
|
fiatCode
|
||||||
}
|
}
|
||||||
: null
|
: 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',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 3,
|
cassette: 3,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
notes: device.cassette3,
|
notes: device.cashUnits.cassette3,
|
||||||
denomination: denomination3,
|
denomination: denomination3,
|
||||||
fiatCode
|
fiatCode
|
||||||
}
|
}
|
||||||
: null
|
: 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',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
notes: device.cassette4,
|
notes: device.cashUnits.cassette4,
|
||||||
denomination: denomination4,
|
denomination: denomination4,
|
||||||
fiatCode
|
fiatCode
|
||||||
}
|
}
|
||||||
: null
|
: 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) {
|
function checkCryptoBalances (fiatCode, devices) {
|
||||||
|
|
@ -874,7 +1065,6 @@ function plugins (settings, deviceId) {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
checkBalances,
|
checkBalances,
|
||||||
getMachineNames,
|
getMachineNames,
|
||||||
buildAvailableCassettes,
|
|
||||||
buy,
|
buy,
|
||||||
sell,
|
sell,
|
||||||
getNotificationConfig,
|
getNotificationConfig,
|
||||||
|
|
@ -885,7 +1075,8 @@ function plugins (settings, deviceId) {
|
||||||
isValidWalletScore,
|
isValidWalletScore,
|
||||||
getTransactionHash,
|
getTransactionHash,
|
||||||
getInputAddresses,
|
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
|
// Note: since we only prune on insert, we'll always have
|
||||||
// last known state.
|
// last known state.
|
||||||
exports.machineEvent = function machineEvent (rec) {
|
exports.machineEvent = function machineEvent (rec) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const populateDeviceId = require('./middlewares/populateDeviceId')
|
||||||
const populateSettings = require('./middlewares/populateSettings')
|
const populateSettings = require('./middlewares/populateSettings')
|
||||||
const recordPing = require('./middlewares/recordPing')
|
const recordPing = require('./middlewares/recordPing')
|
||||||
|
|
||||||
|
const unitsRoutes = require('./routes/unitsRoutes')
|
||||||
const cashboxRoutes = require('./routes/cashboxRoutes')
|
const cashboxRoutes = require('./routes/cashboxRoutes')
|
||||||
const customerRoutes = require('./routes/customerRoutes')
|
const customerRoutes = require('./routes/customerRoutes')
|
||||||
const logsRoutes = require('./routes/logsRoutes')
|
const logsRoutes = require('./routes/logsRoutes')
|
||||||
|
|
@ -82,6 +83,7 @@ app.use('/customer', customerRoutes)
|
||||||
app.use('/tx', txRoutes)
|
app.use('/tx', txRoutes)
|
||||||
|
|
||||||
app.use('/logs', logsRoutes)
|
app.use('/logs', logsRoutes)
|
||||||
|
app.use('/units', unitsRoutes)
|
||||||
|
|
||||||
graphQLServer.applyMiddleware({ app })
|
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 - Cashbox reset is set to automatic. A cashbox batch WILL be created')
|
||||||
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...')
|
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...')
|
||||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
return cashbox.createCashboxBatch(req.deviceId, machine.cashUnits.cashbox)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`** DEBUG ** - Cashbox removal - Finished creating the new cashbox batch`)
|
logger.info(`** DEBUG ** - Cashbox removal - Finished creating the new cashbox batch`)
|
||||||
logger.info(`** DEBUG ** - Cashbox removal - Resetting the cashbox counter on device ${req.deviceId}`)
|
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 deviceId = req.deviceId
|
||||||
const model = req.query.model
|
const model = req.query.model
|
||||||
const numOfCassettes = req.query.numOfCassettes
|
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 => {
|
.then(isValid => {
|
||||||
if (isValid) return res.json({ status: 'paired' })
|
if (isValid) return res.json({ status: 'paired' })
|
||||||
throw httpError('Pairing failed')
|
throw httpError('Pairing failed')
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ function poll (req, res, next) {
|
||||||
const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid
|
const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid
|
||||||
const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid
|
const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid
|
||||||
const restartServices = pid && state.restartServicesMap?.[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 langs = localeConfig.languages
|
||||||
|
|
||||||
const locale = {
|
const locale = {
|
||||||
|
|
@ -119,6 +121,8 @@ function poll (req, res, next) {
|
||||||
reboot,
|
reboot,
|
||||||
shutdown,
|
shutdown,
|
||||||
restartServices,
|
restartServices,
|
||||||
|
emptyUnit,
|
||||||
|
refillUnit,
|
||||||
hasLightning,
|
hasLightning,
|
||||||
receipt,
|
receipt,
|
||||||
operatorInfo,
|
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 _ = require('lodash/fp')
|
||||||
const { migrationSaveConfig, loadLatest } = require('../lib/new-settings-loader')
|
const { migrationSaveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||||
const { CASSETTE_MAX_CAPACITY } = require('../lib/constants')
|
const CASSETTE_MAX_CAPACITY = 500
|
||||||
|
|
||||||
exports.up = function (next) {
|
exports.up = function (next) {
|
||||||
return loadLatest()
|
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 ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
names,
|
||||||
bypassField,
|
bypassField,
|
||||||
input,
|
input,
|
||||||
editable = true,
|
editable = true,
|
||||||
|
|
@ -128,6 +129,8 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
||||||
width,
|
width,
|
||||||
textAlign,
|
textAlign,
|
||||||
editingAlign = textAlign,
|
editingAlign = textAlign,
|
||||||
|
prefix,
|
||||||
|
PrefixComponent = Label2,
|
||||||
suffix,
|
suffix,
|
||||||
SuffixComponent = Label2,
|
SuffixComponent = Label2,
|
||||||
textStyle = it => {},
|
textStyle = it => {},
|
||||||
|
|
@ -136,6 +139,8 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
||||||
inputProps = {}
|
inputProps = {}
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
|
const fields = names ?? [name]
|
||||||
|
|
||||||
const { values } = useFormikContext()
|
const { values } = useFormikContext()
|
||||||
const isEditable = editable => {
|
const isEditable = editable => {
|
||||||
if (typeof editable === 'function') return editable(values)
|
if (typeof editable === 'function') return editable(values)
|
||||||
|
|
@ -159,36 +164,48 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Td
|
<div className={classes.fields}>
|
||||||
className={{
|
{R.map(f => (
|
||||||
[classes.extraPaddingRight]: extraPaddingRight,
|
<Td
|
||||||
[classes.extraPadding]: extraPadding,
|
className={{
|
||||||
[classes.withSuffix]: suffix
|
[classes.extraPaddingRight]: extraPaddingRight,
|
||||||
}}
|
[classes.extraPadding]: extraPadding,
|
||||||
width={width}
|
[classes.withSuffix]: suffix,
|
||||||
size={size}
|
[classes.withPrefix]: prefix
|
||||||
bold={bold}
|
}}
|
||||||
textAlign={textAlign}>
|
width={width}
|
||||||
{isEditing && isField && !isHidden(values) && (
|
size={size}
|
||||||
<Field name={name} component={input} {...innerProps} />
|
bold={bold}
|
||||||
)}
|
textAlign={textAlign}>
|
||||||
{isEditing && !isField && !isHidden(values) && (
|
{prefix && !isHidden(values) && (
|
||||||
<config.input name={name} />
|
<PrefixComponent
|
||||||
)}
|
className={classes.prefix}
|
||||||
{!isEditing && values && !isHidden(values) && (
|
style={isEditing ? {} : textStyle(values, isEditing)}>
|
||||||
<div style={textStyle(values, isEditing)}>
|
{typeof prefix === 'function' ? prefix(f) : prefix}
|
||||||
{view(values[name], values)}
|
</PrefixComponent>
|
||||||
</div>
|
)}
|
||||||
)}
|
{isEditing && isField && !isHidden(values) && (
|
||||||
{suffix && !isHidden(values) && (
|
<Field name={f} component={input} {...innerProps} />
|
||||||
<SuffixComponent
|
)}
|
||||||
className={classes.suffix}
|
{isEditing && !isField && !isHidden(values) && (
|
||||||
style={isEditing ? {} : textStyle(values, isEditing)}>
|
<config.input name={f} />
|
||||||
{suffix}
|
)}
|
||||||
</SuffixComponent>
|
{!isEditing && values && !isHidden(values) && (
|
||||||
)}
|
<div style={textStyle(values, isEditing)}>
|
||||||
{isHidden(values) && <StripesSvg />}
|
{view(values[f], values)}
|
||||||
</Td>
|
</div>
|
||||||
|
)}
|
||||||
|
{suffix && !isHidden(values) && (
|
||||||
|
<SuffixComponent
|
||||||
|
className={classes.suffix}
|
||||||
|
style={isEditing ? {} : textStyle(values, isEditing)}>
|
||||||
|
{suffix}
|
||||||
|
</SuffixComponent>
|
||||||
|
)}
|
||||||
|
{isHidden(values) && <StripesSvg />}
|
||||||
|
</Td>
|
||||||
|
))(fields)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,21 @@ export default {
|
||||||
suffix: {
|
suffix: {
|
||||||
margin: [[0, 0, 0, 7]]
|
margin: [[0, 0, 0, 7]]
|
||||||
},
|
},
|
||||||
|
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),
|
size: ({ size }) => bySize(size),
|
||||||
bold
|
bold,
|
||||||
|
fields: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const Cashbox = ({
|
||||||
percent = 0,
|
percent = 0,
|
||||||
cashOut = false,
|
cashOut = false,
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
className,
|
className,
|
||||||
emptyPartClassName,
|
emptyPartClassName,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
|
|
@ -27,6 +28,7 @@ const Cashbox = ({
|
||||||
percent,
|
percent,
|
||||||
cashOut,
|
cashOut,
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
applyColorVariant,
|
applyColorVariant,
|
||||||
isLow
|
isLow
|
||||||
})
|
})
|
||||||
|
|
@ -55,35 +57,17 @@ const Cashbox = ({
|
||||||
|
|
||||||
// https://support.lamassu.is/hc/en-us/articles/360025595552-Installing-the-Sintra-Forte
|
// 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
|
// 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 CashIn = ({
|
||||||
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 = ({
|
|
||||||
capacity = 500,
|
capacity = 500,
|
||||||
denomination = 0,
|
|
||||||
currency,
|
currency,
|
||||||
notes,
|
notes,
|
||||||
className,
|
className,
|
||||||
editingMode = false,
|
editingMode = false,
|
||||||
threshold,
|
threshold,
|
||||||
width
|
width,
|
||||||
|
height,
|
||||||
|
total,
|
||||||
|
omitInnerPercentage
|
||||||
}) => {
|
}) => {
|
||||||
const percent = (100 * notes) / capacity
|
const percent = (100 * notes) / capacity
|
||||||
const isLow = percent < threshold
|
const isLow = percent < threshold
|
||||||
|
|
@ -98,6 +82,54 @@ const CashOut = ({
|
||||||
cashOut
|
cashOut
|
||||||
isLow={isLow}
|
isLow={isLow}
|
||||||
width={width}
|
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>
|
</div>
|
||||||
{!editingMode && (
|
{!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: {
|
cashbox: {
|
||||||
borderColor: colorPicker,
|
borderColor: colorPicker,
|
||||||
backgroundColor: colorPicker,
|
backgroundColor: colorPicker,
|
||||||
height: 118,
|
height: ({ height }) => height ?? 118,
|
||||||
width: ({ width }) => width ?? 80,
|
width: ({ width }) => width ?? 80,
|
||||||
border: '2px solid',
|
border: '2px solid',
|
||||||
textAlign: 'end',
|
textAlign: 'end',
|
||||||
|
|
@ -58,7 +58,13 @@ const cashboxStyles = {
|
||||||
|
|
||||||
const gridStyles = {
|
const gridStyles = {
|
||||||
row: {
|
row: {
|
||||||
display: 'flex'
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
innerRow: {
|
innerRow: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,42 @@ const MachineActions = memo(({ machine, onActionSuccess }) => {
|
||||||
}}>
|
}}>
|
||||||
Restart Services
|
Restart Services
|
||||||
</ActionButton>
|
</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>
|
</div>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ const Row = ({
|
||||||
)}
|
)}
|
||||||
</Tr>
|
</Tr>
|
||||||
</div>
|
</div>
|
||||||
{expandable && expanded && (
|
{expanded && (
|
||||||
<div className={classes.after}>
|
<div className={classes.after}>
|
||||||
<Tr className={classnames({ [classes.expanded]: expanded })}>
|
<Tr className={classnames({ [classes.expanded]: expanded })}>
|
||||||
<Td width={width}>
|
<Td width={width}>
|
||||||
|
|
@ -99,6 +99,7 @@ const DataTable = ({
|
||||||
data = [],
|
data = [],
|
||||||
Details,
|
Details,
|
||||||
className,
|
className,
|
||||||
|
tableClassName,
|
||||||
expandable,
|
expandable,
|
||||||
initialExpanded,
|
initialExpanded,
|
||||||
onClick,
|
onClick,
|
||||||
|
|
@ -169,7 +170,7 @@ const DataTable = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flex="1" flexDirection="column">
|
<Box display="flex" flex="1" flexDirection="column">
|
||||||
<Table className={classes.table}>
|
<Table className={classnames(classes.table, tableClassName)}>
|
||||||
<THead>
|
<THead>
|
||||||
{elements.map(({ width, className, textAlign, header }, idx) => (
|
{elements.map(({ width, className, textAlign, header }, idx) => (
|
||||||
<Th
|
<Th
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,21 @@ const GET_INFO = gql`
|
||||||
machines {
|
machines {
|
||||||
name
|
name
|
||||||
deviceId
|
deviceId
|
||||||
cashbox
|
cashUnits {
|
||||||
cassette1
|
cashbox
|
||||||
cassette2
|
cassette1
|
||||||
cassette3
|
cassette2
|
||||||
cassette4
|
cassette3
|
||||||
|
cassette4
|
||||||
|
stacker1f
|
||||||
|
stacker1r
|
||||||
|
stacker2f
|
||||||
|
stacker2r
|
||||||
|
stacker3f
|
||||||
|
stacker3r
|
||||||
|
}
|
||||||
numberOfCassettes
|
numberOfCassettes
|
||||||
|
numberOfStackers
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
@ -113,9 +122,9 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
||||||
data={config}
|
data={config}
|
||||||
stripeWhen={wasNeverEnabled}
|
stripeWhen={wasNeverEnabled}
|
||||||
enableEdit
|
enableEdit
|
||||||
editWidth={134}
|
editWidth={95}
|
||||||
enableToggle
|
enableToggle
|
||||||
toggleWidth={109}
|
toggleWidth={100}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
save={save}
|
save={save}
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ const MODAL_WIDTH = 554
|
||||||
const MODAL_HEIGHT = 520
|
const MODAL_HEIGHT = 520
|
||||||
|
|
||||||
const Wizard = ({ machine, locale, onClose, save, error }) => {
|
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({
|
const [{ step, config }, setState] = useState({
|
||||||
step: 0,
|
step: 0,
|
||||||
config: { active: true }
|
config: { active: true }
|
||||||
|
|
@ -46,37 +47,105 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = R.map(
|
const steps = R.concat(
|
||||||
it => ({
|
R.map(
|
||||||
type: `cassette${it}`,
|
it => ({
|
||||||
display: `Cassette ${it}`,
|
type: `cassette${it}`,
|
||||||
component: Autocomplete,
|
display: `Cassette ${it}`,
|
||||||
inputProps: {
|
component: Autocomplete,
|
||||||
options: options,
|
inputProps: {
|
||||||
labelProp: 'display',
|
options: options,
|
||||||
valueProp: 'code'
|
labelProp: 'display',
|
||||||
}
|
valueProp: 'code'
|
||||||
}),
|
}
|
||||||
R.range(1, machine.numberOfCassettes + 1)
|
}),
|
||||||
|
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 = () =>
|
const schema = () =>
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
cassette1: Yup.number().required(),
|
cassette1:
|
||||||
|
machine.numberOfCassettes >= 1 && step >= 1
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
cassette2:
|
cassette2:
|
||||||
machine.numberOfCassettes > 1 && step >= 2
|
machine.numberOfCassettes >= 2 && step >= 2
|
||||||
? Yup.number().required()
|
? Yup.number().required()
|
||||||
: Yup.number()
|
: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
.nullable(),
|
.nullable(),
|
||||||
cassette3:
|
cassette3:
|
||||||
machine.numberOfCassettes > 2 && step >= 3
|
machine.numberOfCassettes >= 3 && step >= 3
|
||||||
? Yup.number().required()
|
? Yup.number().required()
|
||||||
: Yup.number()
|
: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
.nullable(),
|
.nullable(),
|
||||||
cassette4:
|
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().required()
|
||||||
: Yup.number()
|
: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import styles from './WizardStep.styles'
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const getCassetesArtworks = () => ({
|
const getCassetesArtworks = () => ({
|
||||||
|
1: {
|
||||||
|
1: cassetteOne
|
||||||
|
},
|
||||||
2: {
|
2: {
|
||||||
1: cassetteOne,
|
1: cassetteOne,
|
||||||
2: cassetteTwo
|
2: cassetteTwo
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@ import { getBillOptions } from 'src/utils/bill-options'
|
||||||
import { CURRENCY_MAX } from 'src/utils/constants'
|
import { CURRENCY_MAX } from 'src/utils/constants'
|
||||||
import { transformNumber } from 'src/utils/number'
|
import { transformNumber } from 'src/utils/number'
|
||||||
|
|
||||||
const widthsByNumberOfCassettes = {
|
const widthsByNumberOfUnits = {
|
||||||
2: { machine: 300, cassette: 225, zeroConf: 200 },
|
2: { machine: 325, cassette: 340 },
|
||||||
3: { machine: 210, cassette: 180, zeroConf: 200 },
|
3: { machine: 300, cassette: 235 },
|
||||||
4: { machine: 200, cassette: 150, zeroConf: 150 }
|
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({
|
const DenominationsSchema = Yup.object().shape({
|
||||||
|
|
@ -22,9 +25,10 @@ const DenominationsSchema = Yup.object().shape({
|
||||||
.max(CURRENCY_MAX),
|
.max(CURRENCY_MAX),
|
||||||
cassette2: Yup.number()
|
cassette2: Yup.number()
|
||||||
.label('Cassette 2')
|
.label('Cassette 2')
|
||||||
.required()
|
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(CURRENCY_MAX),
|
.max(CURRENCY_MAX)
|
||||||
|
.nullable()
|
||||||
|
.transform(transformNumber),
|
||||||
cassette3: Yup.number()
|
cassette3: Yup.number()
|
||||||
.label('Cassette 3')
|
.label('Cassette 3')
|
||||||
.min(1)
|
.min(1)
|
||||||
|
|
@ -36,6 +40,42 @@ const DenominationsSchema = Yup.object().shape({
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(CURRENCY_MAX)
|
.max(CURRENCY_MAX)
|
||||||
.nullable()
|
.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)
|
.transform(transformNumber)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -45,6 +85,11 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
...R.map(it => it.numberOfCassettes, machines),
|
...R.map(it => it.numberOfCassettes, machines),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
const maxNumberOfStackers = Math.max(
|
||||||
|
...R.map(it => it.numberOfStackers, machines),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const numberOfCashUnits = maxNumberOfCassettes + maxNumberOfStackers
|
||||||
|
|
||||||
const options = getBillOptions(locale, denominations)
|
const options = getBillOptions(locale, denominations)
|
||||||
const cassetteProps =
|
const cassetteProps =
|
||||||
|
|
@ -61,7 +106,7 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
{
|
{
|
||||||
name: 'id',
|
name: 'id',
|
||||||
header: 'Machine',
|
header: 'Machine',
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine,
|
width: widthsByNumberOfUnits[numberOfCashUnits]?.machine,
|
||||||
view: it => machines.find(({ deviceId }) => deviceId === it).name,
|
view: it => machines.find(({ deviceId }) => deviceId === it).name,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
editable: false
|
editable: false
|
||||||
|
|
@ -77,13 +122,13 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassette,
|
width: widthsByNumberOfUnits[numberOfCashUnits]?.cassette,
|
||||||
suffix: fiatCurrency,
|
suffix: fiatCurrency,
|
||||||
bold: bold,
|
bold: bold,
|
||||||
view: it => it,
|
view: it => it,
|
||||||
input: options?.length > 0 ? Autocomplete : NumberInput,
|
input: options?.length > 0 ? Autocomplete : NumberInput,
|
||||||
inputProps: cassetteProps,
|
inputProps: cassetteProps,
|
||||||
doubleHeader: 'Denominations',
|
doubleHeader: 'Denominations of Cassettes & Recyclers',
|
||||||
isHidden: machine =>
|
isHidden: machine =>
|
||||||
it >
|
it >
|
||||||
machines.find(({ deviceId }) => deviceId === machine.id)
|
machines.find(({ deviceId }) => deviceId === machine.id)
|
||||||
|
|
@ -94,6 +139,32 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
1
|
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
|
return elements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,10 @@ const MachinesTable = ({ machines = [], numToRender }) => {
|
||||||
it =>
|
it =>
|
||||||
machine.numberOfCassettes >= it ? (
|
machine.numberOfCassettes >= it ? (
|
||||||
<StyledCell align="left">
|
<StyledCell align="left">
|
||||||
{makePercentageText(it, machine[`cassette${it}`])}
|
{makePercentageText(
|
||||||
|
it,
|
||||||
|
machine.cashUnits[`cassette${it}`]
|
||||||
|
)}
|
||||||
</StyledCell>
|
</StyledCell>
|
||||||
) : (
|
) : (
|
||||||
<StyledCell align="left">
|
<StyledCell align="left">
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,21 @@ const GET_DATA = gql`
|
||||||
machines {
|
machines {
|
||||||
name
|
name
|
||||||
deviceId
|
deviceId
|
||||||
cashbox
|
cashUnits {
|
||||||
cassette1
|
cashbox
|
||||||
cassette2
|
cassette1
|
||||||
cassette3
|
cassette2
|
||||||
cassette4
|
cassette3
|
||||||
|
cassette4
|
||||||
|
stacker1f
|
||||||
|
stacker1r
|
||||||
|
stacker2f
|
||||||
|
stacker2r
|
||||||
|
stacker3f
|
||||||
|
stacker3r
|
||||||
|
}
|
||||||
numberOfCassettes
|
numberOfCassettes
|
||||||
|
numberOfStackers
|
||||||
statuses {
|
statuses {
|
||||||
label
|
label
|
||||||
type
|
type
|
||||||
|
|
|
||||||
|
|
@ -3,85 +3,49 @@ import { makeStyles } from '@material-ui/core'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import * as Yup from 'yup'
|
|
||||||
|
|
||||||
import { IconButton } from 'src/components/buttons'
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
import { Table as EditableTable } from 'src/components/editableTable'
|
import CashUnitDetails from 'src/pages/Maintenance/CashUnitDetails'
|
||||||
import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
|
|
||||||
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
|
|
||||||
import Wizard from 'src/pages/Maintenance/Wizard/Wizard'
|
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 { fromNamespace } from 'src/utils/config'
|
||||||
|
|
||||||
import styles from './Cassettes.styles'
|
import styles from './Cassettes.styles'
|
||||||
|
|
||||||
const useStyles = makeStyles(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`
|
const SET_CASSETTE_BILLS = gql`
|
||||||
mutation MachineAction(
|
mutation MachineAction(
|
||||||
$deviceId: ID!
|
$deviceId: ID!
|
||||||
$action: MachineAction!
|
$action: MachineAction!
|
||||||
$cashbox: Int!
|
$cashUnits: CashUnitsInput
|
||||||
$cassette1: Int!
|
|
||||||
$cassette2: Int!
|
|
||||||
$cassette3: Int!
|
|
||||||
$cassette4: Int!
|
|
||||||
) {
|
) {
|
||||||
machineAction(
|
machineAction(deviceId: $deviceId, action: $action, cashUnits: $cashUnits) {
|
||||||
deviceId: $deviceId
|
|
||||||
action: $action
|
|
||||||
cashbox: $cashbox
|
|
||||||
cassette1: $cassette1
|
|
||||||
cassette2: $cassette2
|
|
||||||
cassette3: $cassette3
|
|
||||||
cassette4: $cassette4
|
|
||||||
) {
|
|
||||||
deviceId
|
deviceId
|
||||||
cashbox
|
cashUnits {
|
||||||
cassette1
|
cashbox
|
||||||
cassette2
|
cassette1
|
||||||
cassette3
|
cassette2
|
||||||
cassette4
|
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 CashCassettes = ({ machine, config, refetchData, bills }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
|
@ -89,115 +53,47 @@ const CashCassettes = ({ machine, config, refetchData, bills }) => {
|
||||||
|
|
||||||
const cashout = config && fromNamespace('cashOut')(config)
|
const cashout = config && fromNamespace('cashOut')(config)
|
||||||
const locale = config && fromNamespace('locale')(config)
|
const locale = config && fromNamespace('locale')(config)
|
||||||
const fillingPercentageSettings =
|
|
||||||
config && fromNamespace('notifications', config)
|
|
||||||
const fiatCurrency = locale?.fiatCurrency
|
const fiatCurrency = locale?.fiatCurrency
|
||||||
const numberOfCassettes = machine.numberOfCassettes
|
|
||||||
|
|
||||||
const getCashoutSettings = deviceId => fromNamespace(deviceId)(cashout)
|
const getCashoutSettings = deviceId => fromNamespace(deviceId)(cashout)
|
||||||
const isCashOutDisabled = ({ deviceId }) =>
|
|
||||||
!getCashoutSettings(deviceId).active
|
|
||||||
|
|
||||||
const elements = [
|
const elements = R.filter(it => it.name !== 'name')(
|
||||||
{
|
helper.getElements(classes, config, bills, setWizard, widths)
|
||||||
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}`]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
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, {
|
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
|
||||||
refetchQueries: () => refetchData()
|
refetchQueries: () => refetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSave = (_, cashbox, cassettes) =>
|
const onSave = (_, cashUnits) =>
|
||||||
setCassetteBills({
|
setCassetteBills({
|
||||||
variables: {
|
variables: {
|
||||||
action: 'setCassetteBills',
|
action: 'setCassetteBills',
|
||||||
deviceId: machine.deviceId,
|
deviceId: machine.deviceId,
|
||||||
cashbox,
|
cashUnits
|
||||||
...cassettes
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const InnerCashUnitDetails = ({ it }) => (
|
||||||
|
<CashUnitDetails
|
||||||
|
machine={it}
|
||||||
|
bills={bills[it.deviceId] ?? []}
|
||||||
|
currency={fiatCurrency}
|
||||||
|
config={config}
|
||||||
|
hideMachineData
|
||||||
|
widths
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
return machine.name ? (
|
return machine.name ? (
|
||||||
<>
|
<>
|
||||||
<EditableTable
|
<DataTable
|
||||||
error={error?.message}
|
|
||||||
editWidth={widthsByNumberOfCassettes[numberOfCassettes].editWidth}
|
|
||||||
stripeWhen={isCashOutDisabled}
|
|
||||||
disableRowEdit={isCashOutDisabled}
|
|
||||||
name="cashboxes"
|
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={[machine]}
|
data={[machine]}
|
||||||
save={onSave}
|
Details={InnerCashUnitDetails}
|
||||||
validationSchema={ValidationSchema}
|
emptyText="No machines so far"
|
||||||
|
initialExpanded={0}
|
||||||
|
tableClassName={classes.dataTable}
|
||||||
/>
|
/>
|
||||||
{wizard && (
|
{wizard && (
|
||||||
<Wizard
|
<Wizard
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,34 @@
|
||||||
|
import { offDarkColor } from 'src/styling/variables'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
cashbox: {
|
unitsRow: {
|
||||||
height: 36
|
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
|
pairedAt
|
||||||
version
|
version
|
||||||
model
|
model
|
||||||
cashbox
|
cashUnits {
|
||||||
cassette1
|
cashbox
|
||||||
cassette2
|
cassette1
|
||||||
cassette3
|
cassette2
|
||||||
cassette4
|
cassette3
|
||||||
|
cassette4
|
||||||
|
stacker1f
|
||||||
|
stacker1r
|
||||||
|
stacker2f
|
||||||
|
stacker2r
|
||||||
|
stacker3f
|
||||||
|
stacker3r
|
||||||
|
}
|
||||||
numberOfCassettes
|
numberOfCassettes
|
||||||
|
numberOfStackers
|
||||||
statuses {
|
statuses {
|
||||||
label
|
label
|
||||||
type
|
type
|
||||||
|
|
@ -100,7 +109,7 @@ const Machines = ({ data, refetch, reload }) => {
|
||||||
|
|
||||||
const machine = R.path(['machine'])(data) ?? {}
|
const machine = R.path(['machine'])(data) ?? {}
|
||||||
const config = R.path(['config'])(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 machineName = R.path(['name'])(machine) ?? null
|
||||||
const machineID = R.path(['deviceId'])(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 gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import * as Yup from 'yup'
|
|
||||||
|
|
||||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { IconButton, Button } from 'src/components/buttons'
|
import { IconButton, Button } from 'src/components/buttons'
|
||||||
import { Table as EditableTable } from 'src/components/editableTable'
|
|
||||||
import { RadioGroup } from 'src/components/inputs'
|
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 TitleSection from 'src/components/layout/TitleSection'
|
||||||
import { EmptyTable } from 'src/components/table'
|
import { EmptyTable } from 'src/components/table'
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
import { P, Label1 } from 'src/components/typography'
|
import { P, Label1 } from 'src/components/typography'
|
||||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
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 ReverseHistoryIcon } from 'src/styling/icons/circle buttons/history/white.svg'
|
||||||
import { ReactComponent as HistoryIcon } from 'src/styling/icons/circle buttons/history/zodiac.svg'
|
import { ReactComponent as HistoryIcon } from 'src/styling/icons/circle buttons/history/zodiac.svg'
|
||||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
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 { onlyFirstToUpper } from 'src/utils/string'
|
||||||
|
|
||||||
import styles from './CashCassettes.styles.js'
|
import CashUnitDetails from './CashUnitDetails'
|
||||||
import CashCassettesFooter from './CashCassettesFooter'
|
import styles from './CashUnits.styles'
|
||||||
|
import CashCassettesFooter from './CashUnitsFooter'
|
||||||
import CashboxHistory from './CashboxHistory'
|
import CashboxHistory from './CashboxHistory'
|
||||||
import Wizard from './Wizard/Wizard'
|
import Wizard from './Wizard/Wizard'
|
||||||
|
import helper from './helper'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
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`
|
const GET_MACHINES_AND_CONFIG = gql`
|
||||||
query getData($billFilters: JSONObject) {
|
query getData($billFilters: JSONObject) {
|
||||||
machines {
|
machines {
|
||||||
name
|
name
|
||||||
id: deviceId
|
id: deviceId
|
||||||
cashbox
|
model
|
||||||
cassette1
|
cashUnits {
|
||||||
cassette2
|
cashbox
|
||||||
cassette3
|
cassette1
|
||||||
cassette4
|
cassette2
|
||||||
|
cassette3
|
||||||
|
cassette4
|
||||||
|
stacker1f
|
||||||
|
stacker1r
|
||||||
|
stacker2f
|
||||||
|
stacker2r
|
||||||
|
stacker3f
|
||||||
|
stacker3r
|
||||||
|
}
|
||||||
numberOfCassettes
|
numberOfCassettes
|
||||||
|
numberOfStackers
|
||||||
}
|
}
|
||||||
unpairedMachines {
|
unpairedMachines {
|
||||||
id: deviceId
|
id: deviceId
|
||||||
|
|
@ -123,27 +74,23 @@ const SET_CASSETTE_BILLS = gql`
|
||||||
mutation MachineAction(
|
mutation MachineAction(
|
||||||
$deviceId: ID!
|
$deviceId: ID!
|
||||||
$action: MachineAction!
|
$action: MachineAction!
|
||||||
$cashbox: Int!
|
$cashUnits: CashUnitsInput
|
||||||
$cassette1: Int!
|
|
||||||
$cassette2: Int!
|
|
||||||
$cassette3: Int!
|
|
||||||
$cassette4: Int!
|
|
||||||
) {
|
) {
|
||||||
machineAction(
|
machineAction(deviceId: $deviceId, action: $action, cashUnits: $cashUnits) {
|
||||||
deviceId: $deviceId
|
|
||||||
action: $action
|
|
||||||
cashbox: $cashbox
|
|
||||||
cassette1: $cassette1
|
|
||||||
cassette2: $cassette2
|
|
||||||
cassette3: $cassette3
|
|
||||||
cassette4: $cassette4
|
|
||||||
) {
|
|
||||||
deviceId
|
deviceId
|
||||||
cashbox
|
cashUnits {
|
||||||
cassette1
|
cashbox
|
||||||
cassette2
|
cassette1
|
||||||
cassette3
|
cassette2
|
||||||
cassette4
|
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 CashCassettes = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
|
|
@ -173,7 +127,6 @@ const CashCassettes = () => {
|
||||||
const machines = R.path(['machines'])(data) ?? []
|
const machines = R.path(['machines'])(data) ?? []
|
||||||
const unpairedMachines = R.path(['unpairedMachines'])(data) ?? []
|
const unpairedMachines = R.path(['unpairedMachines'])(data) ?? []
|
||||||
const config = R.path(['config'])(data) ?? {}
|
const config = R.path(['config'])(data) ?? {}
|
||||||
const fillingPercentageSettings = fromNamespace('notifications', config)
|
|
||||||
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
|
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
|
||||||
refetchQueries: () => ['getData']
|
refetchQueries: () => ['getData']
|
||||||
})
|
})
|
||||||
|
|
@ -191,21 +144,15 @@ const CashCassettes = () => {
|
||||||
const cashout = data?.config && fromNamespace('cashOut')(data.config)
|
const cashout = data?.config && fromNamespace('cashOut')(data.config)
|
||||||
const locale = data?.config && fromNamespace('locale')(data.config)
|
const locale = data?.config && fromNamespace('locale')(data.config)
|
||||||
const fiatCurrency = locale?.fiatCurrency
|
const fiatCurrency = locale?.fiatCurrency
|
||||||
const maxNumberOfCassettes = Math.max(
|
|
||||||
...R.map(it => it.numberOfCassettes, machines),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||||
const isCashOutDisabled = ({ id }) => !getCashoutSettings(id).active
|
|
||||||
|
|
||||||
const onSave = (id, cashbox, cassettes) => {
|
const onSave = (id, cashUnits) => {
|
||||||
return setCassetteBills({
|
return setCassetteBills({
|
||||||
variables: {
|
variables: {
|
||||||
action: 'setCassetteBills',
|
action: 'setCassetteBills',
|
||||||
deviceId: id,
|
deviceId: id,
|
||||||
cashbox,
|
cashUnits
|
||||||
...cassettes
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -235,85 +182,23 @@ const CashCassettes = () => {
|
||||||
setSelectedRadio(selectedRadio)
|
setSelectedRadio(selectedRadio)
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = [
|
const elements = helper.getElements(
|
||||||
{
|
classes,
|
||||||
name: 'name',
|
config,
|
||||||
header: 'Machine',
|
bills,
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine,
|
setWizard,
|
||||||
view: name => <>{name}</>,
|
widths,
|
||||||
input: ({ field: { value: name } }) => <>{name}</>
|
setMachineId
|
||||||
},
|
|
||||||
{
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elements.push({
|
const InnerCashUnitDetails = ({ it }) => (
|
||||||
name: 'edit',
|
<CashUnitDetails
|
||||||
header: 'Edit',
|
machine={it}
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.editWidth,
|
bills={bills[it.id] ?? []}
|
||||||
textAlign: 'center',
|
currency={fiatCurrency}
|
||||||
view: (value, { id }) => {
|
config={config}
|
||||||
return (
|
/>
|
||||||
<IconButton
|
)
|
||||||
onClick={() => {
|
|
||||||
setMachineId(id)
|
|
||||||
setWizard(true)
|
|
||||||
}}>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!dataLoading && (
|
!dataLoading && (
|
||||||
|
|
@ -369,14 +254,13 @@ const CashCassettes = () => {
|
||||||
</TitleSection>
|
</TitleSection>
|
||||||
{!showHistory && (
|
{!showHistory && (
|
||||||
<>
|
<>
|
||||||
<EditableTable
|
<DataTable
|
||||||
error={error?.message}
|
loading={dataLoading}
|
||||||
name="cashboxes"
|
|
||||||
stripeWhen={isCashOutDisabled}
|
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={machines}
|
data={machines}
|
||||||
validationSchema={ValidationSchema}
|
Details={InnerCashUnitDetails}
|
||||||
tbodyWrapperClass={classes.tBody}
|
emptyText="No machines so far"
|
||||||
|
expandable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{data && R.isEmpty(machines) && (
|
{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 { fromNamespace } from 'src/utils/config'
|
||||||
import { numberToFiatAmount } from 'src/utils/number.js'
|
import { numberToFiatAmount } from 'src/utils/number.js'
|
||||||
|
|
||||||
import styles from './CashCassettesFooter.styles.js'
|
import styles from './CashUnitsFooter.styles.js'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const CashCassettesFooter = ({
|
const CashCassettesFooter = ({
|
||||||
|
|
@ -22,9 +23,9 @@ const CashCassettesFooter = ({
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const cashout = config && fromNamespace('cashOut')(config)
|
const cashout = config && fromNamespace('cashOut')(config)
|
||||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||||
const reducerFn = (
|
const cashoutReducerFn = (
|
||||||
acc,
|
acc,
|
||||||
{ cassette1, cassette2, cassette3, cassette4, id }
|
{ cashUnits: { cassette1, cassette2, cassette3, cassette4 }, id }
|
||||||
) => {
|
) => {
|
||||||
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
|
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
|
||||||
const cassette2Denomination = getCashoutSettings(id).cassette2 ?? 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 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 (
|
return (
|
||||||
<div className={classes.footerContainer}>
|
<div className={classes.footerContainer}>
|
||||||
|
|
@ -62,6 +101,13 @@ const CashCassettesFooter = ({
|
||||||
{numberToFiatAmount(totalInCassettes)} {currencyCode}
|
{numberToFiatAmount(totalInCassettes)} {currencyCode}
|
||||||
</Info1>
|
</Info1>
|
||||||
</div>
|
</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}>
|
<div className={classes.flex}>
|
||||||
<Info2 className={classes.iconLabel}>Total:</Info2>
|
<Info2 className={classes.iconLabel}>Total:</Info2>
|
||||||
<Info1 className={classes.valueDisplay}>
|
<Info1 className={classes.valueDisplay}>
|
||||||
|
|
@ -26,8 +26,7 @@ const GET_BATCHES = gql`
|
||||||
performedBy
|
performedBy
|
||||||
bills {
|
bills {
|
||||||
fiat
|
fiat
|
||||||
deviceId
|
fiatCode
|
||||||
created
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,19 @@ const GET_MACHINES = gql`
|
||||||
pairedAt
|
pairedAt
|
||||||
version
|
version
|
||||||
paired
|
paired
|
||||||
cashbox
|
cashUnits {
|
||||||
cassette1
|
cashbox
|
||||||
cassette2
|
cassette1
|
||||||
|
cassette2
|
||||||
|
cassette3
|
||||||
|
cassette4
|
||||||
|
stacker1f
|
||||||
|
stacker1r
|
||||||
|
stacker2f
|
||||||
|
stacker2r
|
||||||
|
stacker3f
|
||||||
|
stacker3r
|
||||||
|
}
|
||||||
version
|
version
|
||||||
model
|
model
|
||||||
statuses {
|
statuses {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as Yup from 'yup'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { MAX_NUMBER_OF_CASSETTES } from 'src/utils/constants'
|
import { MAX_NUMBER_OF_CASSETTES } from 'src/utils/constants'
|
||||||
|
import { cashUnitCapacity, modelPrettifier } from 'src/utils/machine'
|
||||||
import { defaultToZero } from 'src/utils/number'
|
import { defaultToZero } from 'src/utils/number'
|
||||||
|
|
||||||
import WizardSplash from './WizardSplash'
|
import WizardSplash from './WizardSplash'
|
||||||
|
|
@ -11,13 +12,21 @@ import WizardStep from './WizardStep'
|
||||||
|
|
||||||
const MODAL_WIDTH = 554
|
const MODAL_WIDTH = 554
|
||||||
const MODAL_HEIGHT = 535
|
const MODAL_HEIGHT = 535
|
||||||
const CASHBOX_DEFAULT_CAPACITY = 500
|
|
||||||
|
|
||||||
const CASSETTE_FIELDS = R.map(
|
const CASSETTE_FIELDS = R.map(
|
||||||
it => `cassette${it}`,
|
it => `cassette${it}`,
|
||||||
R.range(1, MAX_NUMBER_OF_CASSETTES + 1)
|
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 Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
const [{ step, config }, setState] = useState({
|
const [{ step, config }, setState] = useState({
|
||||||
step: 0,
|
step: 0,
|
||||||
|
|
@ -28,7 +37,9 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
R.isEmpty(cashoutSettings) || !cashoutSettings?.active
|
R.isEmpty(cashoutSettings) || !cashoutSettings?.active
|
||||||
|
|
||||||
const numberOfCassettes = isCashOutDisabled ? 0 : machine.numberOfCassettes
|
const numberOfCassettes = isCashOutDisabled ? 0 : machine.numberOfCassettes
|
||||||
|
const numberOfStackers = machine.numberOfStackers
|
||||||
|
|
||||||
|
// const LAST_STEP = numberOfCassettes + numberOfStackers * 2 + 1
|
||||||
const LAST_STEP = numberOfCassettes + 1
|
const LAST_STEP = numberOfCassettes + 1
|
||||||
|
|
||||||
const title = `Update counts`
|
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 onContinue = it => {
|
||||||
const newConfig = R.merge(config, it)
|
const newConfig = R.merge(config, it)
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
|
|
@ -53,10 +76,16 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
it?.wasCashboxEmptied
|
it?.wasCashboxEmptied
|
||||||
].includes('YES')
|
].includes('YES')
|
||||||
|
|
||||||
const cashbox = wasCashboxEmptied ? 0 : machine?.cashbox
|
|
||||||
const cassettes = buildCassetteObj(it)
|
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()
|
return onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,12 +107,69 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
.integer()
|
.integer()
|
||||||
.required()
|
.required()
|
||||||
.min(0)
|
.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.isEmpty(cashoutSettings)
|
||||||
? R.reduce(
|
? R.reduce(
|
||||||
(acc, value) => {
|
(acc, value) => {
|
||||||
|
|
@ -95,16 +181,35 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
)
|
)
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const steps = R.prepend(
|
const makeStackersInitialValues = () =>
|
||||||
{
|
!R.isEmpty(cashoutSettings)
|
||||||
type: 'cashbox',
|
? R.reduce(
|
||||||
schema: Yup.object().shape({
|
(acc, value) => {
|
||||||
wasCashboxEmptied: Yup.string().required('Select one option.')
|
acc[`stacker${value}f`] = ''
|
||||||
}),
|
acc[`stacker${value}r`] = ''
|
||||||
cashoutRequired: false
|
return acc
|
||||||
},
|
},
|
||||||
makeCassetteSteps(numberOfCassettes)
|
{},
|
||||||
)
|
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
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)([])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -122,7 +227,6 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
name={machine?.name}
|
name={machine?.name}
|
||||||
machine={machine}
|
machine={machine}
|
||||||
cashoutSettings={cashoutSettings}
|
cashoutSettings={cashoutSettings}
|
||||||
cassetteCapacity={CASHBOX_DEFAULT_CAPACITY}
|
|
||||||
error={error}
|
error={error}
|
||||||
lastStep={isLastStep}
|
lastStep={isLastStep}
|
||||||
steps={steps}
|
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 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 { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
import { comet, errorColor } from 'src/styling/variables'
|
import { comet, errorColor } from 'src/styling/variables'
|
||||||
|
import { cashUnitCapacity } from 'src/utils/machine'
|
||||||
import { numberToFiatAmount } from 'src/utils/number'
|
import { numberToFiatAmount } from 'src/utils/number'
|
||||||
|
import { startCase } from 'src/utils/string'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
content: {
|
content: {
|
||||||
|
|
@ -104,19 +106,41 @@ const styles = {
|
||||||
|
|
||||||
const useStyles = makeStyles(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],
|
[cassetteOne, cassetteTwo],
|
||||||
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
|
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
|
||||||
[tejo4CassetteOne, tejo4CassetteTwo, tejo4CassetteThree, tejo4CassetteFour]
|
[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 = ({
|
const WizardStep = ({
|
||||||
step,
|
step,
|
||||||
name,
|
name,
|
||||||
machine,
|
machine,
|
||||||
cashoutSettings,
|
cashoutSettings,
|
||||||
cassetteCapacity,
|
|
||||||
error,
|
error,
|
||||||
lastStep,
|
lastStep,
|
||||||
steps,
|
steps,
|
||||||
|
|
@ -133,16 +157,20 @@ const WizardStep = ({
|
||||||
{ display: 'No', code: 'NO' }
|
{ display: 'No', code: 'NO' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const cassetteField = `cassette${step - 1}`
|
|
||||||
const numberOfCassettes = machine.numberOfCassettes
|
const numberOfCassettes = machine.numberOfCassettes
|
||||||
const originalCassetteCount = machine?.[cassetteField]
|
const numberOfStackers = machine.numberOfStackers
|
||||||
const cassetteDenomination = cashoutSettings?.[cassetteField]
|
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 cassetteCount = values => values[cashUnitField] || originalCashUnitCount
|
||||||
const cassetteTotal = values => cassetteCount(values) * cassetteDenomination
|
const cassetteTotal = values => cassetteCount(values) * cashUnitDenomination
|
||||||
const getPercentage = R.pipe(
|
const getPercentage = R.pipe(
|
||||||
cassetteCount,
|
cassetteCount,
|
||||||
count => 100 * (count / cassetteCapacity),
|
count => 100 * (count / cashUnitCapacity[machine.model][cashUnitCategory]),
|
||||||
R.clamp(0, 100)
|
R.clamp(0, 100)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -161,7 +189,7 @@ const WizardStep = ({
|
||||||
initialValues={{ wasCashboxEmptied: '' }}
|
initialValues={{ wasCashboxEmptied: '' }}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
validationSchema={steps[0].schema}>
|
validationSchema={steps[0].schema}>
|
||||||
{({ values, errors }) => (
|
{({ errors }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div
|
<div
|
||||||
className={classnames(classes.horizontalAlign, classes.form)}>
|
className={classnames(classes.horizontalAlign, classes.form)}>
|
||||||
|
|
@ -206,7 +234,7 @@ const WizardStep = ({
|
||||||
classes.lineAlignment
|
classes.lineAlignment
|
||||||
)}>
|
)}>
|
||||||
<Info1 noMargin className={classes.cashboxBills}>
|
<Info1 noMargin className={classes.cashboxBills}>
|
||||||
{machine?.cashbox}
|
{machine?.cashUnits.cashbox}
|
||||||
</Info1>
|
</Info1>
|
||||||
<P noMargin>accepted bills</P>
|
<P noMargin>accepted bills</P>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -236,7 +264,11 @@ const WizardStep = ({
|
||||||
<img
|
<img
|
||||||
className={classes.stepImage}
|
className={classes.stepImage}
|
||||||
alt="cassette"
|
alt="cassette"
|
||||||
src={cassetesArtworks(numberOfCassettes, step)}></img>
|
src={cassetesArtworks(
|
||||||
|
step,
|
||||||
|
numberOfCassettes,
|
||||||
|
numberOfStackers
|
||||||
|
)}></img>
|
||||||
<div className={classes.formWrapper}>
|
<div className={classes.formWrapper}>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
|
|
@ -257,7 +289,13 @@ const WizardStep = ({
|
||||||
<H4
|
<H4
|
||||||
className={classes.cassetteFormTitleContent}
|
className={classes.cassetteFormTitleContent}
|
||||||
noMargin>
|
noMargin>
|
||||||
Cash cassette {step - 1} (dispenser)
|
{startCase(cashUnitField)} (
|
||||||
|
{R.includes('cassette', cashUnitField)
|
||||||
|
? `dispenser`
|
||||||
|
: R.includes('stacker', cashUnitField)
|
||||||
|
? `recycler`
|
||||||
|
: ``}
|
||||||
|
)
|
||||||
</H4>
|
</H4>
|
||||||
</div>
|
</div>
|
||||||
<Cashbox
|
<Cashbox
|
||||||
|
|
@ -276,13 +314,13 @@ const WizardStep = ({
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
decimalPlaces={0}
|
decimalPlaces={0}
|
||||||
width={50}
|
width={50}
|
||||||
placeholder={originalCassetteCount.toString()}
|
placeholder={originalCashUnitCount.toString()}
|
||||||
name={cassetteField}
|
name={cashUnitField}
|
||||||
className={classes.cashboxBills}
|
className={classes.cashboxBills}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<P>
|
<P>
|
||||||
{cassetteDenomination} {fiatCurrency} bills loaded
|
{cashUnitDenomination} {fiatCurrency} bills loaded
|
||||||
</P>
|
</P>
|
||||||
</div>
|
</div>
|
||||||
<P noMargin className={classes.fiatTotal}>
|
<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
|
name
|
||||||
deviceId
|
deviceId
|
||||||
numberOfCassettes
|
numberOfCassettes
|
||||||
|
numberOfStackers
|
||||||
}
|
}
|
||||||
cryptoCurrencies {
|
cryptoCurrencies {
|
||||||
code
|
code
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const CASH_IN_KEY = 'fiatBalanceAlertsCashIn'
|
const CASH_IN_KEY = 'fiatBalanceAlertsCashIn'
|
||||||
const CASH_OUT_KEY = 'fiatBalanceAlertsCashOut'
|
const CASH_OUT_KEY = 'fiatBalanceAlertsCashOut'
|
||||||
|
const RECYCLER_STACKER_KEY = 'fiatBalanceAlertsRecyclerStacker'
|
||||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||||
|
const DEFAULT_NUMBER_OF_STACKERS = 0
|
||||||
const notesMin = 0
|
const notesMin = 0
|
||||||
const notesMax = 9999999
|
const notesMax = 9999999
|
||||||
|
|
||||||
|
|
@ -39,6 +41,11 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
DEFAULT_NUMBER_OF_CASSETTES
|
DEFAULT_NUMBER_OF_CASSETTES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxNumberOfStackers = Math.max(
|
||||||
|
...R.map(it => it.numberOfStackers, machines),
|
||||||
|
DEFAULT_NUMBER_OF_STACKERS
|
||||||
|
)
|
||||||
|
|
||||||
const schema = Yup.object().shape({
|
const schema = Yup.object().shape({
|
||||||
cashInAlertThreshold: Yup.number()
|
cashInAlertThreshold: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
|
|
@ -160,6 +167,76 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</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>
|
</Formik>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import Locales from 'src/pages/Locales'
|
||||||
import IndividualDiscounts from 'src/pages/LoyaltyPanel/IndividualDiscounts'
|
import IndividualDiscounts from 'src/pages/LoyaltyPanel/IndividualDiscounts'
|
||||||
import PromoCodes from 'src/pages/LoyaltyPanel/PromoCodes'
|
import PromoCodes from 'src/pages/LoyaltyPanel/PromoCodes'
|
||||||
import MachineLogs from 'src/pages/MachineLogs'
|
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 MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||||
import Notifications from 'src/pages/Notifications/Notifications'
|
import Notifications from 'src/pages/Notifications/Notifications'
|
||||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||||
|
|
@ -48,11 +48,11 @@ const getLamassuRoutes = () => [
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'cash_cassettes',
|
key: 'cash_units',
|
||||||
label: 'Cash Cassettes',
|
label: 'Cash Units',
|
||||||
route: '/maintenance/cash-cassettes',
|
route: '/maintenance/cash-units',
|
||||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: CashCassettes
|
component: CashUnits
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'funding',
|
key: 'funding',
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import Locales from 'src/pages/Locales'
|
||||||
import IndividualDiscounts from 'src/pages/LoyaltyPanel/IndividualDiscounts'
|
import IndividualDiscounts from 'src/pages/LoyaltyPanel/IndividualDiscounts'
|
||||||
import PromoCodes from 'src/pages/LoyaltyPanel/PromoCodes'
|
import PromoCodes from 'src/pages/LoyaltyPanel/PromoCodes'
|
||||||
import MachineLogs from 'src/pages/MachineLogs'
|
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 MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||||
import Notifications from 'src/pages/Notifications/Notifications'
|
import Notifications from 'src/pages/Notifications/Notifications'
|
||||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||||
|
|
@ -48,11 +48,11 @@ const getPazuzRoutes = () => [
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'cash_cassettes',
|
key: 'cash_units',
|
||||||
label: 'Cash Cassettes',
|
label: 'Cash Units',
|
||||||
route: '/maintenance/cash-cassettes',
|
route: '/maintenance/cash-units',
|
||||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: CashCassettes
|
component: CashUnits
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'logs',
|
key: 'logs',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,34 @@ const modelPrettifier = {
|
||||||
douro1: 'Douro',
|
douro1: 'Douro',
|
||||||
sintra: 'Sintra',
|
sintra: 'Sintra',
|
||||||
gaia: 'Gaia',
|
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/common": "^2.6.4",
|
||||||
"@ethereumjs/tx": "^3.5.1",
|
"@ethereumjs/tx": "^3.5.1",
|
||||||
"@graphql-tools/merge": "^6.2.5",
|
"@graphql-tools/merge": "^6.2.5",
|
||||||
|
"@haensl/subset-sum": "^3.0.5",
|
||||||
"@lamassu/coins": "1.3.0",
|
"@lamassu/coins": "1.3.0",
|
||||||
"@simplewebauthn/server": "^3.0.0",
|
"@simplewebauthn/server": "^3.0.0",
|
||||||
"apollo-server-express": "2.25.1",
|
"apollo-server-express": "2.25.1",
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
"main.js.map": "/static/js/main.900511f9.chunk.js.map",
|
"main.js.map": "/static/js/main.900511f9.chunk.js.map",
|
||||||
"runtime-main.js": "/static/js/runtime-main.5b925903.js",
|
"runtime-main.js": "/static/js/runtime-main.5b925903.js",
|
||||||
"runtime-main.js.map": "/static/js/runtime-main.5b925903.js.map",
|
"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.01291e3c.chunk.js": "/static/js/2.01291e3c.chunk.js",
|
||||||
"static/js/2.4b3df17b.chunk.js.map": "/static/js/2.4b3df17b.chunk.js.map",
|
"static/js/2.01291e3c.chunk.js.map": "/static/js/2.01291e3c.chunk.js.map",
|
||||||
"index.html": "/index.html",
|
"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-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-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",
|
"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