diff --git a/lib/bill-math.js b/lib/bill-math.js
index 9cc8d47e..d4801c0f 100644
--- a/lib/bill-math.js
+++ b/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 }
diff --git a/lib/cash-out/cash-out-actions.js b/lib/cash-out/cash-out-actions.js
index 420e0ae9..bae631fb 100644
--- a/lib/cash-out/cash-out-actions.js
+++ b/lib/cash-out/cash-out-actions.js
@@ -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
}
diff --git a/lib/cash-out/cash-out-atomic.js b/lib/cash-out/cash-out-atomic.js
index 25bbc184..2fb772a7 100644
--- a/lib/cash-out/cash-out-atomic.js
+++ b/lib/cash-out/cash-out-atomic.js
@@ -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})))
diff --git a/lib/cash-out/cash-out-helper.js b/lib/cash-out/cash-out-helper.js
index dc4597fd..aecb0eb9 100644
--- a/lib/cash-out/cash-out-helper.js
+++ b/lib/cash-out/cash-out-helper.js
@@ -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))
}
diff --git a/lib/cash-out/cash-out-tx.js b/lib/cash-out/cash-out-tx.js
index c83c1dc7..e4016fa3 100644
--- a/lib/cash-out/cash-out-tx.js
+++ b/lib/cash-out/cash-out-tx.js
@@ -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}))
diff --git a/lib/machine-loader.js b/lib/machine-loader.js
index a51aa8ac..a7731533 100644
--- a/lib/machine-loader.js
+++ b/lib/machine-loader.js
@@ -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) {
diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js
index f4756ab9..a81fbc84 100644
--- a/lib/new-admin/graphql/schema.js
+++ b/lib/new-admin/graphql/schema.js
@@ -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),
diff --git a/lib/new-admin/machines.js b/lib/new-admin/machines.js
index 78e2f46a..e2d2ad5e 100644
--- a/lib/new-admin/machines.js
+++ b/lib/new-admin/machines.js
@@ -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))
}
diff --git a/lib/pairing.js b/lib/pairing.js
index 5932878c..00916abe 100644
--- a/lib/pairing.js
+++ b/lib/pairing.js
@@ -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 => {
diff --git a/lib/plugins.js b/lib/plugins.js
index 0cca0bd4..1c7b3681 100644
--- a/lib/plugins.js
+++ b/lib/plugins.js
@@ -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) {
diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js
index eb030e91..1ae15ae1 100644
--- a/lib/postgresql_interface.js
+++ b/lib/postgresql_interface.js
@@ -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 }
})
}
diff --git a/lib/routes.js b/lib/routes.js
index baee01ea..53c2fdf3 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -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' })
diff --git a/migrations/1630432869178-add-more-cassette-support.js b/migrations/1630432869178-add-more-cassette-support.js
new file mode 100644
index 00000000..d1268057
--- /dev/null
+++ b/migrations/1630432869178-add-more-cassette-support.js
@@ -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()
+}
diff --git a/new-lamassu-admin/src/components/editableTable/Row.js b/new-lamassu-admin/src/components/editableTable/Row.js
index 03bf86f4..95504cb6 100644
--- a/new-lamassu-admin/src/components/editableTable/Row.js
+++ b/new-lamassu-admin/src/components/editableTable/Row.js
@@ -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) && (