Merge pull request #830 from chaotixkilla/feat-add-multiple-cassettes-option
Add support for multiple cassettes
This commit is contained in:
commit
1c207ab250
21 changed files with 651 additions and 233 deletions
217
lib/bill-math.js
217
lib/bill-math.js
|
|
@ -1,16 +1,78 @@
|
|||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
|
||||
// Custom algorith for two cassettes. For three or more denominations, we'll need
|
||||
// to rethink this. Greedy algorithm fails to find *any* solution in some cases.
|
||||
// Dynamic programming may be too inefficient for large amounts.
|
||||
//
|
||||
// We can either require canononical denominations for 3+, or try to expand
|
||||
// this algorithm.
|
||||
exports.makeChange = function makeChange (cassettes, amount) {
|
||||
// Note: Everything here is converted to primitive numbers,
|
||||
// since they're all integers, well within JS number range,
|
||||
// and this is way more efficient in a tight loop.
|
||||
const MAX_AMOUNT_OF_SOLUTIONS = 10000
|
||||
const MAX_BRUTEFORCE_ITERATIONS = 10000000
|
||||
|
||||
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
|
||||
return [
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
|
||||
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 map = {}
|
||||
|
||||
_.forEach(it => {
|
||||
if (!map[it.denomination]) {
|
||||
map[it.denomination] = 0
|
||||
}
|
||||
map[it.denomination] += it.count
|
||||
}, cassettes)
|
||||
|
||||
return _.map(it => ({ denomination: it, count: map[it] }), _.keys(map))
|
||||
}
|
||||
|
||||
function unmergeCassettes(cassettes, output) {
|
||||
const map = {}
|
||||
|
||||
_.forEach(it => {
|
||||
if (!map[it.denomination]) {
|
||||
map[it.denomination] = 0
|
||||
}
|
||||
map[it.denomination] += it.provisioned
|
||||
}, output)
|
||||
|
||||
const response = []
|
||||
_.forEach(it => {
|
||||
const value = {
|
||||
denomination: it.denomination,
|
||||
id: uuid.v4()
|
||||
}
|
||||
|
||||
const amountNeeded = map[it.denomination]
|
||||
if (!amountNeeded) {
|
||||
return response.push({ provisioned: 0, ...value })
|
||||
}
|
||||
|
||||
if (amountNeeded < it.count) {
|
||||
map[it.denomination] = 0
|
||||
return response.push({ provisioned: amountNeeded, ...value })
|
||||
}
|
||||
|
||||
map[it.denomination] -= it.count
|
||||
return response.push({ provisioned: it.count, ...value })
|
||||
}, cassettes)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
function makeChangeDuo(cassettes, amount) {
|
||||
const small = cassettes[0]
|
||||
const large = cassettes[1]
|
||||
|
||||
|
|
@ -23,15 +85,140 @@ exports.makeChange = function makeChange (cassettes, amount) {
|
|||
const remainder = amountNum - largeDenom * i
|
||||
|
||||
if (remainder % smallDenom !== 0) continue
|
||||
|
||||
const smallCount = remainder / smallDenom
|
||||
if (smallCount > small.count) continue
|
||||
|
||||
return [
|
||||
{provisioned: smallCount, denomination: small.denomination, id: uuid.v4()},
|
||||
{provisioned: i, denomination: largeDenom, id: uuid.v4()}
|
||||
{
|
||||
provisioned: smallCount,
|
||||
denomination: small.denomination,
|
||||
id: uuid.v4()
|
||||
},
|
||||
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
|
||||
]
|
||||
}
|
||||
|
||||
return null
|
||||
return []
|
||||
}
|
||||
|
||||
function makeChange(outCassettes, amount) {
|
||||
const available = _.reduce(
|
||||
(res, val) => res + val.count * val.denomination,
|
||||
0,
|
||||
outCassettes
|
||||
)
|
||||
|
||||
if (available < amount) {
|
||||
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(cassettes)}`)
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@ function mapDispense (tx) {
|
|||
|
||||
if (_.isEmpty(bills)) return {}
|
||||
|
||||
return {
|
||||
provisioned_1: bills[0].provisioned,
|
||||
provisioned_2: bills[1].provisioned,
|
||||
dispensed_1: bills[0].dispensed,
|
||||
dispensed_2: bills[1].dispensed,
|
||||
rejected_1: bills[0].rejected,
|
||||
rejected_2: bills[1].rejected,
|
||||
denomination_1: bills[0].denomination,
|
||||
denomination_2: bills[1].denomination
|
||||
}
|
||||
const res = {}
|
||||
|
||||
_.forEach(it => {
|
||||
res[`provisioned_${it + 1}`] = bills[it].provisioned
|
||||
res[`denomination_${it + 1}`] = bills[it].denomination
|
||||
res[`dispensed_${it + 1}`] = bills[it].dispensed
|
||||
res[`rejected_${it + 1}`] = bills[it].rejected
|
||||
}, _.times(_.identity(), _.size(bills)))
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,16 +111,24 @@ function updateCassettes (t, tx) {
|
|||
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
|
||||
|
||||
const sql = `update devices set
|
||||
cassette1 = cassette1 - $1,
|
||||
cassette2 = cassette2 - $2
|
||||
where device_id = $3
|
||||
returning cassette1, cassette2`
|
||||
${_.size(tx.bills) > 0 ? `cassette1 = cassette1 - $1` : ``}
|
||||
${_.size(tx.bills) > 1 ? `, cassette2 = cassette2 - $2` : ``}
|
||||
${_.size(tx.bills) > 2 ? `, cassette3 = cassette3 - $3` : ``}
|
||||
${_.size(tx.bills) > 3 ? `, cassette4 = cassette4 - $4` : ``}
|
||||
where device_id = $${_.size(tx.bills) + 1}
|
||||
returning
|
||||
${_.size(tx.bills) > 0 ? `cassette1` : ``}
|
||||
${_.size(tx.bills) > 1 ? `, cassette2`: ``}
|
||||
${_.size(tx.bills) > 2 ? `, cassette3` : ``}
|
||||
${_.size(tx.bills) > 3 ? `, cassette4` : ``}`
|
||||
|
||||
const values = [
|
||||
tx.bills[0].dispensed + tx.bills[0].rejected,
|
||||
tx.bills[1].dispensed + tx.bills[1].rejected,
|
||||
tx.deviceId
|
||||
]
|
||||
const values = []
|
||||
|
||||
_.forEach(it => values.push(
|
||||
tx.bills[it].dispensed + tx.bills[it].rejected
|
||||
), _.times(_.identity(), _.size(tx.bills)))
|
||||
|
||||
values.push(tx.deviceId)
|
||||
|
||||
return t.one(sql, values)
|
||||
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
|
||||
|
|
|
|||
|
|
@ -39,12 +39,18 @@ function addDbBills (tx) {
|
|||
const bills = tx.bills
|
||||
if (_.isEmpty(bills)) return tx
|
||||
|
||||
return _.assign(tx, {
|
||||
provisioned1: bills[0].provisioned,
|
||||
provisioned2: bills[1].provisioned,
|
||||
denomination1: bills[0].denomination,
|
||||
denomination2: bills[1].denomination
|
||||
})
|
||||
const billsObj = {
|
||||
provisioned1: bills[0]?.provisioned ?? 0,
|
||||
provisioned2: bills[1]?.provisioned ?? 0,
|
||||
provisioned3: bills[2]?.provisioned ?? 0,
|
||||
provisioned4: bills[3]?.provisioned ?? 0,
|
||||
denomination1: bills[0]?.denomination ?? 0,
|
||||
denomination2: bills[1]?.denomination ?? 0,
|
||||
denomination3: bills[2]?.denomination ?? 0,
|
||||
denomination4: bills[3]?.denomination ?? 0
|
||||
}
|
||||
|
||||
return _.assign(tx, billsObj)
|
||||
}
|
||||
|
||||
function toDb (tx) {
|
||||
|
|
@ -76,12 +82,12 @@ function toObj (row) {
|
|||
|
||||
newObj.direction = 'cashOut'
|
||||
|
||||
const billFields = ['denomination1', 'denomination2', 'provisioned1', 'provisioned2']
|
||||
const billFields = ['denomination1', 'denomination2', 'denomination3', 'denomination4', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4']
|
||||
|
||||
if (_.every(_.isNil, _.at(billFields, newObj))) return newObj
|
||||
if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values')
|
||||
|
||||
const bills = [
|
||||
const billFieldsArr = [
|
||||
{
|
||||
denomination: newObj.denomination1,
|
||||
provisioned: newObj.provisioned1
|
||||
|
|
@ -89,9 +95,21 @@ function toObj (row) {
|
|||
{
|
||||
denomination: newObj.denomination2,
|
||||
provisioned: newObj.provisioned2
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination3,
|
||||
provisioned: newObj.provisioned3
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination4,
|
||||
provisioned: newObj.provisioned4
|
||||
}
|
||||
]
|
||||
|
||||
// There can't be bills with denomination === 0.
|
||||
// If a bill has denomination === 0, then that cassette is not set and should be filtered out.
|
||||
const bills = _.filter(it => it.denomination > 0, billFieldsArr)
|
||||
|
||||
return _.set('bills', bills, _.omit(billFields, newObj))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,17 +70,12 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
return bills
|
||||
})
|
||||
.then(bills => {
|
||||
const provisioned1 = bills[0].provisioned
|
||||
const provisioned2 = bills[1].provisioned
|
||||
const denomination1 = bills[0].denomination
|
||||
const denomination2 = bills[1].denomination
|
||||
const rec = {}
|
||||
|
||||
const rec = {
|
||||
provisioned_1: provisioned1,
|
||||
provisioned_2: provisioned2,
|
||||
denomination_1: denomination1,
|
||||
denomination_2: denomination2
|
||||
}
|
||||
_.forEach(it => {
|
||||
rec[`provisioned_${it + 1}`] = bills[it].provisioned
|
||||
rec[`denomination_${it + 1}`] = bills[it].denomination
|
||||
}, _.times(_.identity(), _.size(bills)))
|
||||
|
||||
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
||||
.then(_.constant({bills}))
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ function getMachines () {
|
|||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
cassette2: r.cassette2,
|
||||
cassette3: r.cassette3,
|
||||
cassette4: r.cassette4,
|
||||
numberOfCassettes: r.number_of_cassettes,
|
||||
version: r.version,
|
||||
model: r.model,
|
||||
pairedAt: new Date(r.created),
|
||||
|
|
@ -100,6 +103,8 @@ function getMachine (machineId, config) {
|
|||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
cassette2: r.cassette2,
|
||||
cassette3: r.cassette3,
|
||||
cassette4: r.cassette4,
|
||||
version: r.version,
|
||||
model: r.model,
|
||||
pairedAt: new Date(r.created),
|
||||
|
|
@ -123,8 +128,8 @@ function renameMachine (rec) {
|
|||
|
||||
function resetCashOutBills (rec) {
|
||||
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
|
||||
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2 WHERE device_id=$3;`
|
||||
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
|
||||
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4 WHERE device_id=$5;`
|
||||
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
|
||||
}
|
||||
|
||||
function emptyCashInBills (rec) {
|
||||
|
|
@ -133,8 +138,8 @@ function emptyCashInBills (rec) {
|
|||
}
|
||||
|
||||
function setCassetteBills (rec) {
|
||||
const sql = 'update devices set cashbox=$1, cassette1=$2, cassette2=$3 where device_id=$4'
|
||||
return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.deviceId])
|
||||
const sql = 'update devices set cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 where device_id=$6'
|
||||
return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId])
|
||||
}
|
||||
|
||||
function unpair (rec) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ const typeDefs = gql`
|
|||
cashbox: Int
|
||||
cassette1: Int
|
||||
cassette2: Int
|
||||
cassette3: Int
|
||||
cassette4: Int
|
||||
numberOfCassettes: Int
|
||||
statuses: [MachineStatus]
|
||||
latestEvent: MachineEvent
|
||||
downloadSpeed: String
|
||||
|
|
@ -333,7 +336,7 @@ const typeDefs = gql`
|
|||
}
|
||||
|
||||
type Mutation {
|
||||
machineAction(deviceId:ID!, action: MachineAction!, cashbox: Int, cassette1: Int, cassette2: Int, newName: String): Machine
|
||||
machineAction(deviceId:ID!, action: MachineAction!, cashbox: Int, cassette1: Int, cassette2: Int, cassette3: Int, cassette4: Int, newName: String): Machine
|
||||
setCustomer(customerId: ID!, customerInput: CustomerInput): Customer
|
||||
saveConfig(config: JSONObject): JSONObject
|
||||
# resetConfig(schemaVersion: Int): JSONObject
|
||||
|
|
@ -421,7 +424,7 @@ const resolvers = {
|
|||
bills: () => bills.getBills()
|
||||
},
|
||||
Mutation: {
|
||||
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }),
|
||||
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }),
|
||||
createPairingTotem: (...[, { name }]) => pairing.totem(name),
|
||||
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
||||
// resetAccounts: (...[, { schemaVersion }]) => settingsLoader.resetAccounts(schemaVersion),
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ function getMachine (machineId) {
|
|||
.then(machines => machines.find(({ deviceId }) => deviceId === machineId))
|
||||
}
|
||||
|
||||
function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, newName }) {
|
||||
function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }) {
|
||||
return getMachine(deviceId)
|
||||
.then(machine => {
|
||||
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
|
||||
return machine
|
||||
})
|
||||
.then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2], newName }))
|
||||
.then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2, cassette3, cassette4], newName }))
|
||||
.then(getMachine(deviceId))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,16 +20,16 @@ function unpair (deviceId) {
|
|||
return Promise.all([db.none(sql, [deviceId]), db.none(deleteMachinePings, [deviceId])])
|
||||
}
|
||||
|
||||
function pair (token, deviceId, machineModel) {
|
||||
function pair (token, deviceId, machineModel, numOfCassettes) {
|
||||
return pullToken(token)
|
||||
.then(r => {
|
||||
if (r.expired) return false
|
||||
|
||||
const insertSql = `insert into devices (device_id, name) values ($1, $2)
|
||||
const insertSql = `insert into devices (device_id, name, number_of_cassettes) values ($1, $2, $3)
|
||||
on conflict (device_id)
|
||||
do update set paired=TRUE, display=TRUE`
|
||||
|
||||
return db.none(insertSql, [deviceId, r.name])
|
||||
return db.none(insertSql, [deviceId, r.name, numOfCassettes])
|
||||
.then(() => true)
|
||||
})
|
||||
.catch(err => {
|
||||
|
|
|
|||
|
|
@ -108,8 +108,10 @@ function plugins (settings, deviceId) {
|
|||
if (_.isEmpty(redeemableTxs)) return cassettes
|
||||
|
||||
const sumTxs = (sum, tx) => {
|
||||
const bills = tx.bills
|
||||
const sameDenominations = a => a[0].denomination === a[1].denomination
|
||||
// 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 sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||
|
||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
||||
|
||||
if (!doDenominationsMatch) {
|
||||
|
|
@ -119,7 +121,7 @@ function plugins (settings, deviceId) {
|
|||
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
|
||||
}
|
||||
|
||||
const provisioned = _.reduce(sumTxs, [0, 0], redeemableTxs)
|
||||
const provisioned = _.reduce(sumTxs, _.times(_.constant(0), _.size(cassettes)), redeemableTxs)
|
||||
const zipped = _.zip(_.map('count', cassettes), provisioned)
|
||||
const counts = _.map(r => r[0] - r[1], zipped)
|
||||
|
||||
|
|
@ -127,16 +129,15 @@ function plugins (settings, deviceId) {
|
|||
throw new Error('Negative note count: %j', counts)
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
denomination: cassettes[0].denomination,
|
||||
count: counts[0]
|
||||
},
|
||||
{
|
||||
denomination: cassettes[1].denomination,
|
||||
count: counts[1]
|
||||
}
|
||||
]
|
||||
const computedCassettes = []
|
||||
_.forEach(it => {
|
||||
computedCassettes.push({
|
||||
denomination: cassettes[it].denomination,
|
||||
count: counts[it]
|
||||
})
|
||||
}, _.times(_.identity(), _.size(cassettes)))
|
||||
|
||||
return computedCassettes
|
||||
}
|
||||
|
||||
function buildAvailableCassettes (excludeTxId) {
|
||||
|
|
@ -144,28 +145,32 @@ function plugins (settings, deviceId) {
|
|||
|
||||
if (!cashOutConfig.active) return Promise.resolve()
|
||||
|
||||
const denominations = [cashOutConfig.top, cashOutConfig.bottom]
|
||||
|
||||
const virtualCassettes = [Math.max(cashOutConfig.top, cashOutConfig.bottom) * 2]
|
||||
|
||||
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||
.then(([rec, _redeemableTxs]) => {
|
||||
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
||||
|
||||
const denominations = []
|
||||
_.forEach(it => {
|
||||
denominations.push(cashOutConfig[`cassette${it + 1}`])
|
||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
||||
|
||||
const virtualCassettes = [Math.max(...denominations) * 2]
|
||||
|
||||
const counts = argv.cassettes
|
||||
? argv.cassettes.split(',')
|
||||
: rec.counts
|
||||
|
||||
const cassettes = [
|
||||
{
|
||||
denomination: parseInt(denominations[0], 10),
|
||||
count: parseInt(counts[0], 10)
|
||||
},
|
||||
{
|
||||
denomination: parseInt(denominations[1], 10),
|
||||
count: parseInt(counts[1], 10)
|
||||
}
|
||||
]
|
||||
if (rec.counts.length !== denominations.length) {
|
||||
throw new Error('Denominations and respective counts do not match!')
|
||||
}
|
||||
|
||||
const cassettes = []
|
||||
_.forEach(it => {
|
||||
cassettes.push({
|
||||
denomination: parseInt(denominations[it], 10),
|
||||
count: parseInt(counts[it], 10)
|
||||
})
|
||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
||||
|
||||
try {
|
||||
return {
|
||||
|
|
@ -305,7 +310,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
function dispenseAck (tx) {
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||
const cassettes = [cashOutConfig.top, cashOutConfig.bottom]
|
||||
const cassettes = [cashOutConfig.cassette1, cashOutConfig.cassette2, cashOutConfig.cassette3, cashOutConfig.cassette4]
|
||||
|
||||
return dbm.addDispense(deviceId, tx, cassettes)
|
||||
}
|
||||
|
|
@ -560,8 +565,10 @@ function plugins (settings, deviceId) {
|
|||
|
||||
function checkDeviceCashBalances (fiatCode, device) {
|
||||
const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config)
|
||||
const denomination1 = cashOutConfig.top
|
||||
const denomination2 = cashOutConfig.bottom
|
||||
const denomination1 = cashOutConfig.cassette1
|
||||
const denomination2 = cashOutConfig.cassette2
|
||||
const denomination3 = cashOutConfig.cassette3
|
||||
const denomination4 = cashOutConfig.cassette4
|
||||
const cashOutEnabled = cashOutConfig.active
|
||||
|
||||
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
||||
|
|
@ -601,7 +608,31 @@ function plugins (settings, deviceId) {
|
|||
}
|
||||
: null
|
||||
|
||||
return _.compact([cashInAlert, cassette1Alert, cassette2Alert])
|
||||
const cassette3Alert = cashOutEnabled && device.cassette3 < notifications.fiatBalanceCassette3
|
||||
? {
|
||||
code: 'LOW_CASH_OUT',
|
||||
cassette: 3,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cassette3,
|
||||
denomination: denomination3,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
const cassette4Alert = cashOutEnabled && device.cassette4 < notifications.fiatBalanceCassette4
|
||||
? {
|
||||
code: 'LOW_CASH_OUT',
|
||||
cassette: 4,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cassette4,
|
||||
denomination: denomination4,
|
||||
fiatCode
|
||||
}
|
||||
: null
|
||||
|
||||
return _.compact([cashInAlert, cassette1Alert, cassette2Alert, cassette3Alert, cassette4Alert])
|
||||
}
|
||||
|
||||
function checkCryptoBalances (fiatCode, devices) {
|
||||
|
|
|
|||
|
|
@ -26,13 +26,17 @@ exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) {
|
|||
}
|
||||
|
||||
exports.cassetteCounts = function cassetteCounts (deviceId) {
|
||||
const sql = 'SELECT cassette1, cassette2 FROM devices ' +
|
||||
const sql = 'SELECT cassette1, cassette2, cassette3, cassette4, number_of_cassettes FROM devices ' +
|
||||
'WHERE device_id=$1'
|
||||
|
||||
return db.one(sql, [deviceId])
|
||||
.then(row => {
|
||||
const counts = [row.cassette1, row.cassette2]
|
||||
return {counts}
|
||||
const counts = []
|
||||
_.forEach(it => {
|
||||
counts.push(row[`cassette${it + 1}`])
|
||||
}, _.times(_.identity(), row.number_of_cassettes))
|
||||
|
||||
return { numberOfCassettes: row.number_of_cassettes, counts }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ function checkHasLightning (settings) {
|
|||
function poll (req, res, next) {
|
||||
const machineVersion = req.query.version
|
||||
const machineModel = req.query.model
|
||||
const numOfCassettes = req.query.numOfCassettes
|
||||
const deviceId = req.deviceId
|
||||
const deviceTime = req.deviceTime
|
||||
const serialNumber = req.query.sn
|
||||
|
|
@ -417,8 +418,9 @@ function pair (req, res, next) {
|
|||
const token = req.query.token
|
||||
const deviceId = req.deviceId
|
||||
const model = req.query.model
|
||||
const numOfCassettes = req.query.numOfCassettes
|
||||
|
||||
return pairing.pair(token, deviceId, model)
|
||||
return pairing.pair(token, deviceId, model, numOfCassettes)
|
||||
.then(valid => {
|
||||
if (valid) {
|
||||
return res.json({ status: 'paired' })
|
||||
|
|
|
|||
48
migrations/1630432869178-add-more-cassette-support.js
Normal file
48
migrations/1630432869178-add-more-cassette-support.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
var db = require('./db')
|
||||
const _ = require('lodash/fp')
|
||||
const { saveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||
const { getMachines } = require('../lib/machine-loader')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
'ALTER TABLE devices ADD COLUMN cassette3 INTEGER NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE devices ADD COLUMN cassette4 INTEGER NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN provisioned_3 INTEGER',
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN provisioned_4 INTEGER',
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN denomination_3 INTEGER',
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN denomination_4 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN provisioned_3 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN provisioned_4 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN dispensed_3 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN dispensed_4 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN rejected_3 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN rejected_4 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN denomination_3 INTEGER',
|
||||
'ALTER TABLE cash_out_actions ADD COLUMN denomination_4 INTEGER',
|
||||
'ALTER TABLE devices ADD COLUMN number_of_cassettes INTEGER NOT NULL DEFAULT 2'
|
||||
]
|
||||
|
||||
return Promise.all([loadLatest(), getMachines()])
|
||||
.then(([config, machines]) => {
|
||||
const formattedMachines = _.map(it => _.pick(['deviceId'], it), machines)
|
||||
const newConfig = _.reduce((acc, value) => {
|
||||
if(_.includes(`cashOut_${value.deviceId}_top`, _.keys(config.config))) {
|
||||
acc[`cashOut_${value.deviceId}_cassette1`] = config.config[`cashOut_${value.deviceId}_top`]
|
||||
}
|
||||
|
||||
if(_.includes(`cashOut_${value.deviceId}_bottom`, _.keys(config.config))) {
|
||||
acc[`cashOut_${value.deviceId}_cassette2`] = config.config[`cashOut_${value.deviceId}_bottom`]
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {}, formattedMachines)
|
||||
|
||||
return saveConfig(newConfig)
|
||||
.then(() => db.multi(sql, next))
|
||||
.catch(err => next(err))
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -131,6 +131,7 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
|||
suffix,
|
||||
SuffixComponent = TL2,
|
||||
textStyle = it => {},
|
||||
isHidden = it => false,
|
||||
view = it => it?.toString(),
|
||||
inputProps = {}
|
||||
} = config
|
||||
|
|
@ -165,16 +166,18 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
|||
size={size}
|
||||
bold={bold}
|
||||
textAlign={textAlign}>
|
||||
{isEditing && isField && (
|
||||
{isEditing && isField && !isHidden(values) && (
|
||||
<Field name={name} component={input} {...innerProps} />
|
||||
)}
|
||||
{isEditing && !isField && <config.input name={name} />}
|
||||
{!isEditing && values && (
|
||||
{isEditing && !isField && !isHidden(values) && (
|
||||
<config.input name={name} />
|
||||
)}
|
||||
{!isEditing && values && !isHidden(values) && (
|
||||
<div style={textStyle(values, isEditing)}>
|
||||
{view(values[name], values)}
|
||||
</div>
|
||||
)}
|
||||
{suffix && (
|
||||
{suffix && !isHidden(values) && (
|
||||
<SuffixComponent
|
||||
className={classes.suffix}
|
||||
style={isEditing ? {} : textStyle(values, isEditing)}>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ const GET_INFO = gql`
|
|||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
numberOfCassettes
|
||||
}
|
||||
config
|
||||
}
|
||||
|
|
@ -62,6 +65,7 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
|||
}
|
||||
|
||||
const config = data?.config && fromNamespace(SCREEN_KEY)(data.config)
|
||||
|
||||
const fudgeFactorActive = config?.fudgeFactorActive ?? false
|
||||
const locale = data?.config && fromNamespace('locale')(data.config)
|
||||
const machines = data?.machines ?? []
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import Modal from 'src/components/Modal'
|
|||
import { Autocomplete } from 'src/components/inputs/formik'
|
||||
import denominations from 'src/utils/bill-denominations'
|
||||
import { toNamespace } from 'src/utils/config'
|
||||
import { transformNumber } from 'src/utils/number'
|
||||
|
||||
import WizardSplash from './WizardSplash'
|
||||
import WizardStep from './WizardStep'
|
||||
import { DenominationsSchema } from './helper'
|
||||
|
||||
const LAST_STEP = 4
|
||||
const MODAL_WIDTH = 554
|
||||
const MODAL_HEIGHT = 520
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ const getOptions = R.curry((locale, denomiations) => {
|
|||
})
|
||||
|
||||
const Wizard = ({ machine, locale, onClose, save, error }) => {
|
||||
const LAST_STEP = machine.numberOfCassettes + 2
|
||||
const [{ step, config }, setState] = useState({
|
||||
step: 0,
|
||||
config: { active: true }
|
||||
|
|
@ -38,7 +39,10 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
|||
const onContinue = async it => {
|
||||
if (isLastStep) {
|
||||
return save(
|
||||
toNamespace(machine.deviceId, DenominationsSchema.cast(config))
|
||||
toNamespace(
|
||||
machine.deviceId,
|
||||
DenominationsSchema.cast(config, { assert: false })
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -50,40 +54,55 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
type: 'top',
|
||||
display: 'Cassette 1 (Top)',
|
||||
component: Autocomplete,
|
||||
inputProps: {
|
||||
options: R.map(it => ({ code: it, display: it }))(options),
|
||||
labelProp: 'display',
|
||||
valueProp: 'code'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'bottom',
|
||||
display: 'Cassette 2',
|
||||
component: Autocomplete,
|
||||
inputProps: {
|
||||
options: R.map(it => ({ code: it, display: it }))(options),
|
||||
labelProp: 'display',
|
||||
valueProp: 'code'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'zeroConfLimit',
|
||||
display: '0-conf Limit',
|
||||
schema: Yup.object().shape({
|
||||
zeroConfLimit: Yup.number().required()
|
||||
const steps = []
|
||||
|
||||
R.until(
|
||||
R.gt(R.__, machine.numberOfCassettes),
|
||||
it => {
|
||||
steps.push({
|
||||
type: `cassette${it}`,
|
||||
display: `Cassette ${it}`,
|
||||
component: Autocomplete,
|
||||
inputProps: {
|
||||
options: R.map(it => ({ code: it, display: it }))(options),
|
||||
labelProp: 'display',
|
||||
valueProp: 'code'
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
return R.add(1, it)
|
||||
},
|
||||
1
|
||||
)
|
||||
|
||||
steps.push({
|
||||
type: 'zeroConfLimit',
|
||||
display: '0-conf Limit',
|
||||
schema: Yup.object().shape({
|
||||
zeroConfLimit: Yup.number().required()
|
||||
})
|
||||
})
|
||||
|
||||
const schema = () =>
|
||||
Yup.object().shape({
|
||||
top: Yup.number().required(),
|
||||
bottom: step >= 2 ? Yup.number().required() : Yup.number()
|
||||
cassette1: Yup.number().required(),
|
||||
cassette2:
|
||||
machine.numberOfCassettes > 1 && step >= 2
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
cassette3:
|
||||
machine.numberOfCassettes > 2 && step >= 3
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable(),
|
||||
cassette4:
|
||||
machine.numberOfCassettes > 3 && step >= 4
|
||||
? Yup.number().required()
|
||||
: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable()
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -100,6 +119,7 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
|||
<WizardStep
|
||||
step={step}
|
||||
name={machine.name}
|
||||
numberOfCassettes={machine.numberOfCassettes}
|
||||
error={error}
|
||||
lastStep={isLastStep}
|
||||
steps={steps}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ const WizardStep = ({
|
|||
onContinue,
|
||||
steps,
|
||||
fiatCurrency,
|
||||
options
|
||||
options,
|
||||
numberOfCassettes
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -31,22 +32,29 @@ const WizardStep = ({
|
|||
|
||||
const cassetesArtworks = {
|
||||
1: cassetteOne,
|
||||
2: cassetteTwo
|
||||
2: cassetteTwo,
|
||||
3: cassetteOne,
|
||||
4: cassetteTwo
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.content}>
|
||||
<div className={classes.titleDiv}>
|
||||
<Info2 className={classes.title}>{name}</Info2>
|
||||
<Stepper steps={4} currentStep={step} />
|
||||
<Stepper steps={steps.length + 1} currentStep={step} />
|
||||
</div>
|
||||
|
||||
{step <= 2 && (
|
||||
{step <= numberOfCassettes && (
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
onSubmit={onContinue}
|
||||
initialValues={{ top: '', bottom: '' }}
|
||||
initialValues={{
|
||||
cassette1: '',
|
||||
cassette2: '',
|
||||
cassette3: '',
|
||||
cassette4: ''
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={schema}>
|
||||
<Form>
|
||||
|
|
@ -95,7 +103,7 @@ const WizardStep = ({
|
|||
</Formik>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
{step === numberOfCassettes + 1 && (
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,28 @@
|
|||
import * as R from 'ramda'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { NumberInput } from 'src/components/inputs/formik'
|
||||
|
||||
const currencyMax = 999999999
|
||||
const DenominationsSchema = Yup.object().shape({
|
||||
top: Yup.number()
|
||||
.label('Cassette 1 (Top)')
|
||||
cassette1: Yup.number()
|
||||
.label('Cassette 1')
|
||||
.required()
|
||||
.min(1)
|
||||
.max(currencyMax),
|
||||
bottom: Yup.number()
|
||||
.label('Cassette 2 (Bottom)')
|
||||
cassette2: Yup.number()
|
||||
.label('Cassette 2')
|
||||
.required()
|
||||
.min(1)
|
||||
.max(currencyMax),
|
||||
cassette3: Yup.number()
|
||||
.label('Cassette 3')
|
||||
.min(1)
|
||||
.max(currencyMax),
|
||||
cassette4: Yup.number()
|
||||
.label('Cassette 4')
|
||||
.min(1)
|
||||
.max(currencyMax),
|
||||
zeroConfLimit: Yup.number()
|
||||
.label('0-conf Limit')
|
||||
.required()
|
||||
|
|
@ -22,7 +31,7 @@ const DenominationsSchema = Yup.object().shape({
|
|||
})
|
||||
|
||||
const getElements = (machines, { fiatCurrency } = {}) => {
|
||||
return [
|
||||
const elements = [
|
||||
{
|
||||
name: 'id',
|
||||
header: 'Machine',
|
||||
|
|
@ -30,47 +39,49 @@ const getElements = (machines, { fiatCurrency } = {}) => {
|
|||
view: it => machines.find(({ deviceId }) => deviceId === it).name,
|
||||
size: 'sm',
|
||||
editable: false
|
||||
},
|
||||
{
|
||||
name: 'top',
|
||||
header: 'Cassette 1 (Top)',
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
width: 200,
|
||||
textAlign: 'right',
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
},
|
||||
suffix: fiatCurrency
|
||||
},
|
||||
{
|
||||
name: 'bottom',
|
||||
header: 'Cassette 2 (Bottom)',
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
textAlign: 'right',
|
||||
width: 200,
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
},
|
||||
suffix: fiatCurrency
|
||||
},
|
||||
{
|
||||
name: 'zeroConfLimit',
|
||||
header: '0-conf Limit',
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
textAlign: 'right',
|
||||
width: 200,
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
},
|
||||
suffix: fiatCurrency
|
||||
}
|
||||
]
|
||||
|
||||
R.until(
|
||||
R.gt(R.__, Math.max(...R.map(it => it.numberOfCassettes, machines))),
|
||||
it => {
|
||||
elements.push({
|
||||
name: `cassette${it}`,
|
||||
header: `Cassette ${it}`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
textAlign: 'right',
|
||||
width: 200,
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
},
|
||||
suffix: fiatCurrency,
|
||||
isHidden: machine =>
|
||||
it >
|
||||
machines.find(({ deviceId }) => deviceId === machine.id)
|
||||
.numberOfCassettes
|
||||
})
|
||||
return R.add(1, it)
|
||||
},
|
||||
1
|
||||
)
|
||||
|
||||
elements.push({
|
||||
name: 'zeroConfLimit',
|
||||
header: '0-conf Limit',
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
textAlign: 'right',
|
||||
width: 200,
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
},
|
||||
suffix: fiatCurrency
|
||||
})
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
export { DenominationsSchema, getElements }
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ const SET_CASSETTE_BILLS = gql`
|
|||
$cashbox: Int!
|
||||
$cassette1: Int!
|
||||
$cassette2: Int!
|
||||
$cassette3: Int!
|
||||
$cassette4: Int!
|
||||
) {
|
||||
machineAction(
|
||||
deviceId: $deviceId
|
||||
|
|
@ -47,11 +49,15 @@ const SET_CASSETTE_BILLS = gql`
|
|||
cashbox: $cashbox
|
||||
cassette1: $cassette1
|
||||
cassette2: $cassette2
|
||||
cassette3: $cassette3
|
||||
cassette4: $cassette4
|
||||
) {
|
||||
deviceId
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -90,7 +96,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
view: (value, { deviceId }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(deviceId)?.top}
|
||||
denomination={getCashoutSettings(deviceId)?.cassette1}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
|
|
@ -109,7 +115,47 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
return (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(deviceId)?.bottom}
|
||||
denomination={getCashoutSettings(deviceId)?.cassette2}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
)
|
||||
},
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cassette3',
|
||||
header: 'Cash-out 3',
|
||||
width: 265,
|
||||
stripe: true,
|
||||
view: (value, { deviceId }) => {
|
||||
return (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(deviceId)?.cassette2}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
)
|
||||
},
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cassette4',
|
||||
header: 'Cash-out 4',
|
||||
width: 265,
|
||||
stripe: true,
|
||||
view: (value, { deviceId }) => {
|
||||
return (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(deviceId)?.cassette2}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
|
|
@ -126,14 +172,18 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
refetchQueries: () => refetchData()
|
||||
})
|
||||
|
||||
const onSave = (...[, { deviceId, cashbox, cassette1, cassette2 }]) => {
|
||||
const onSave = (
|
||||
...[, { deviceId, cashbox, cassette1, cassette2, cassette3, cassette4 }]
|
||||
) => {
|
||||
return setCassetteBills({
|
||||
variables: {
|
||||
action: 'setCassetteBills',
|
||||
deviceId: deviceId,
|
||||
cashbox,
|
||||
cassette1,
|
||||
cassette2
|
||||
cassette2,
|
||||
cassette3,
|
||||
cassette4
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,13 +26,25 @@ const ValidationSchema = Yup.object().shape({
|
|||
.min(0)
|
||||
.max(1000),
|
||||
cassette1: Yup.number()
|
||||
.label('Cassette 1 (top)')
|
||||
.label('Cassette 1')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(500),
|
||||
cassette2: Yup.number()
|
||||
.label('Cassette 2 (bottom)')
|
||||
.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)
|
||||
|
|
@ -47,6 +59,9 @@ const GET_MACHINES_AND_CONFIG = gql`
|
|||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
numberOfCassettes
|
||||
}
|
||||
config
|
||||
}
|
||||
|
|
@ -69,6 +84,8 @@ const SET_CASSETTE_BILLS = gql`
|
|||
$cashbox: Int!
|
||||
$cassette1: Int!
|
||||
$cassette2: Int!
|
||||
$cassette3: Int!
|
||||
$cassette4: Int!
|
||||
) {
|
||||
machineAction(
|
||||
deviceId: $deviceId
|
||||
|
|
@ -76,11 +93,15 @@ const SET_CASSETTE_BILLS = gql`
|
|||
cashbox: $cashbox
|
||||
cassette1: $cassette1
|
||||
cassette2: $cassette2
|
||||
cassette3: $cassette3
|
||||
cassette4: $cassette4
|
||||
) {
|
||||
deviceId
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -103,14 +124,18 @@ const CashCassettes = () => {
|
|||
const locale = data?.config && fromNamespace('locale')(data.config)
|
||||
const fiatCurrency = locale?.fiatCurrency
|
||||
|
||||
const onSave = (...[, { id, cashbox, cassette1, cassette2 }]) => {
|
||||
const onSave = (
|
||||
...[, { id, cashbox, cassette1, cassette2, cassette3, cassette4 }]
|
||||
) => {
|
||||
return setCassetteBills({
|
||||
variables: {
|
||||
action: 'setCassetteBills',
|
||||
deviceId: id,
|
||||
cashbox,
|
||||
cassette1,
|
||||
cassette2
|
||||
cassette2,
|
||||
cassette3,
|
||||
cassette4
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -136,46 +161,35 @@ const CashCassettes = () => {
|
|||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cassette1',
|
||||
header: 'Cassette 1 (Top)',
|
||||
width: 265,
|
||||
stripe: true,
|
||||
view: (value, { id }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(id)?.top}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
),
|
||||
input: CashCassetteInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cassette2',
|
||||
header: 'Cassette 2 (Bottom)',
|
||||
width: 265,
|
||||
stripe: true,
|
||||
view: (value, { id }) => {
|
||||
return (
|
||||
}
|
||||
]
|
||||
|
||||
R.until(
|
||||
R.gt(R.__, Math.max(...R.map(it => it.numberOfCassettes, machines))),
|
||||
it => {
|
||||
elements.push({
|
||||
name: `cassette${it}`,
|
||||
header: `Cassette ${it}`,
|
||||
width: 265,
|
||||
stripe: true,
|
||||
view: (value, { id }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(id)?.bottom}
|
||||
denomination={getCashoutSettings(id)?.[`cassette${it}`]}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
)
|
||||
},
|
||||
input: CashCassetteInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
),
|
||||
isHidden: ({ numberOfCassettes }) => it > numberOfCassettes,
|
||||
input: CashCassetteInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
})
|
||||
return R.add(1, it)
|
||||
},
|
||||
1
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -25,16 +25,23 @@ const CashCassettesFooter = ({
|
|||
const classes = useStyles()
|
||||
const cashout = config && fromNamespace('cashOut')(config)
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
const reducerFn = (acc, { cassette1, cassette2, id }) => {
|
||||
const topDenomination = getCashoutSettings(id).top ?? 0
|
||||
const bottomDenomination = getCashoutSettings(id).bottom ?? 0
|
||||
const reducerFn = (
|
||||
acc,
|
||||
{ cassette1, cassette2, cassette3, cassette4, id }
|
||||
) => {
|
||||
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
|
||||
const cassette2Denomination = getCashoutSettings(id).cassette2 ?? 0
|
||||
const cassette3Denomination = getCashoutSettings(id).cassette3 ?? 0
|
||||
const cassette4Denomination = getCashoutSettings(id).cassette4 ?? 0
|
||||
return [
|
||||
(acc[0] += cassette1 * topDenomination),
|
||||
(acc[1] += cassette2 * bottomDenomination)
|
||||
(acc[0] += cassette1 * cassette1Denomination),
|
||||
(acc[1] += cassette2 * cassette2Denomination),
|
||||
(acc[2] += cassette3 * cassette3Denomination),
|
||||
(acc[3] += cassette4 * cassette4Denomination)
|
||||
]
|
||||
}
|
||||
|
||||
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0], machines))
|
||||
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines))
|
||||
|
||||
/* const totalInCashBox = R.sum(
|
||||
R.flatten(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue