feat: add bill math cassette-agnostic solution

fix: multiple generic fixes related with the recyclers
fix: slight UI data changes
This commit is contained in:
Sérgio Salgado 2023-04-26 01:53:54 +01:00
parent 2d010fc359
commit f3ab63766e
15 changed files with 173 additions and 353 deletions

View file

@ -1,225 +1,46 @@
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 getSolution = (units, amount) => {
const MAX_BRUTEFORCE_ITERATIONS = 10000000 const billList = _.reduce(
(acc, value) => {
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) { acc.push(..._.times(_.constant(value.denomination), value.count))
return [ return acc
{
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
denomination: cassettes[0].denomination
}, },
{ [],
provisioned: shouldFlip ? cassettes[1].count - c1 : c1, units
denomination: cassettes[1].denomination )
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
}, },
{ [],
provisioned: shouldFlip ? cassettes[2].count - c2 : c2, _.keys(solution)
denomination: cassettes[2].denomination )
}
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
}, },
{ [],
provisioned: shouldFlip ? cassettes[3].count - c3 : c3, _.range(0, _.size(units))
denomination: cassettes[3].denomination )
}
]
}
function mergeCassettes(cassettes) {
const map = {}
_.forEach(it => {
if (!map[it.denomination]) {
map[it.denomination] = 0
}
map[it.denomination] += it.count
}, cassettes)
return _.map(it => ({ denomination: it, count: map[it] }), _.keys(map))
}
function unmergeCassettes(cassettes, output) {
const map = {}
_.forEach(it => {
if (!map[it.denomination]) {
map[it.denomination] = 0
}
map[it.denomination] += it.provisioned
}, output)
const response = []
_.forEach(it => {
const value = {
denomination: it.denomination,
id: uuid.v4()
}
const amountNeeded = map[it.denomination]
if (!amountNeeded) {
return response.push({ provisioned: 0, ...value })
}
if (amountNeeded < it.count) {
map[it.denomination] = 0
return response.push({ provisioned: amountNeeded, ...value })
}
map[it.denomination] -= it.count
return response.push({ provisioned: it.count, ...value })
}, cassettes)
return response
}
function makeChangeDuo(cassettes, amount) {
// Initialize empty cassettes in case of undefined, due to same denomination across all cassettes results in a single merged cassette
const small = cassettes[0] ?? { denomination: 0, count: 0 }
const large = cassettes[1] ?? { denomination: 0, count: 0 }
const largeDenom = large.denomination
const smallDenom = small.denomination
const largeBills = Math.min(large.count, Math.floor(amount / largeDenom))
const amountNum = amount.toNumber()
for (let i = largeBills; i >= 0; i--) {
const remainder = amountNum - largeDenom * i
if (remainder % smallDenom !== 0) continue
const smallCount = remainder / smallDenom
if (smallCount > small.count) continue
return [
{
provisioned: smallCount,
denomination: small.denomination,
id: uuid.v4()
},
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
]
}
return []
} }
function makeChange(outCassettes, amount) { function makeChange(outCassettes, amount) {
const available = _.reduce( const solution = getSolution(outCassettes, amount)
(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 }

View file

@ -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

View file

@ -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, )(_.range(0, MAX_STACKERS))
provisioned: newObj.provisioned2 )
},
{
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])

View file

@ -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)

View file

@ -165,11 +165,13 @@ type DynamicCoinValues {
} }
type PhysicalCassette { type PhysicalCassette {
name: String!
denomination: Int! denomination: Int!
count: Int! count: Int!
} }
type PhysicalStacker { type PhysicalStacker {
name: String!
denomination: Int! denomination: Int!
count: Int! count: Int!
} }

View file

@ -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',

View file

@ -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)
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 => {

View file

@ -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]
}) })
@ -152,7 +153,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('stacker', 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(stackers, bills)) const doDenominationsMatch = _.every(sameDenominations, _.zip(stackers, bills))
@ -175,6 +176,7 @@ function plugins (settings, deviceId) {
const computedStackers = [] const computedStackers = []
_.forEach(it => { _.forEach(it => {
computedStackers.push({ computedStackers.push({
name: stackers[it].name,
denomination: stackers[it].denomination, denomination: stackers[it].denomination,
count: counts[it] count: counts[it]
}) })
@ -210,6 +212,7 @@ function plugins (settings, deviceId) {
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)
}) })
@ -241,10 +244,10 @@ function plugins (settings, deviceId) {
const denominations = [] const denominations = []
_.forEach(it => { _.forEach(it => {
denominations.push(cashOutConfig[`stacker${it + 1}f`], cashOutConfig[`stacker${it + 1}r`]) denominations.push([cashOutConfig[`stacker${it + 1}f`], cashOutConfig[`stacker${it + 1}r`]])
}, _.times(_.identity(), _stackers.numberOfStackers)) }, _.times(_.identity(), _stackers.numberOfStackers))
const virtualStackers = [Math.max(...denominations) * 2] const virtualStackers = [Math.max(..._.flatten(denominations)) * 2]
const counts = _stackers.counts const counts = _stackers.counts
@ -255,10 +258,16 @@ function plugins (settings, deviceId) {
const stackers = [] const stackers = []
_.forEach(it => { _.forEach(it => {
stackers.push({ stackers.push({
denomination: parseInt(denominations[it], 10), name: `stacker${it + 1}f`,
count: parseInt(counts[it], 10) denomination: parseInt(denominations[it][0], 10),
count: parseInt(counts[it][0], 10)
}) })
}, _.times(_.identity(), _stackers.numberOfStackers * 2)) stackers.push({
name: `stacker${it + 1}r`,
denomination: parseInt(denominations[it][1], 10),
count: parseInt(counts[it][1], 10)
})
}, _.times(_.identity(), _stackers.numberOfStackers))
try { try {
return { return {
@ -275,6 +284,11 @@ function plugins (settings, deviceId) {
}) })
} }
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
@ -1051,7 +1065,6 @@ function plugins (settings, deviceId) {
sendMessage, sendMessage,
checkBalances, checkBalances,
getMachineNames, getMachineNames,
buildAvailableCassettes,
buy, buy,
sell, sell,
getNotificationConfig, getNotificationConfig,
@ -1062,7 +1075,8 @@ function plugins (settings, deviceId) {
isValidWalletScore, isValidWalletScore,
getTransactionHash, getTransactionHash,
getInputAddresses, getInputAddresses,
isWalletScoringEnabled isWalletScoringEnabled,
buildAvailableUnits
} }
} }

View file

@ -48,7 +48,7 @@ exports.stackerCounts = function stackerCounts (deviceId) {
.then(row => { .then(row => {
const counts = [] const counts = []
_.forEach(it => { _.forEach(it => {
counts.push(row[`stacker${it + 1}f`], row[`stacker${it + 1}r`]) counts.push([row[`stacker${it + 1}f`], row[`stacker${it + 1}r`]])
}, _.times(_.identity(), row.number_of_stackers)) }, _.times(_.identity(), row.number_of_stackers))
return { numberOfStackers: row.number_of_stackers, counts } return { numberOfStackers: row.number_of_stackers, counts }

View file

@ -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')

View file

@ -33,7 +33,7 @@ exports.up = function (next) {
ADD COLUMN stacker2f INTEGER NOT NULL DEFAULT 0, ADD COLUMN stacker2f INTEGER NOT NULL DEFAULT 0,
ADD COLUMN stacker2r INTEGER NOT NULL DEFAULT 0, ADD COLUMN stacker2r INTEGER NOT NULL DEFAULT 0,
ADD COLUMN stacker3f INTEGER NOT NULL DEFAULT 0, ADD COLUMN stacker3f INTEGER NOT NULL DEFAULT 0,
ADD COLUMN stacker3r INTEGER NOT NULL DEFAULT 0 ADD COLUMN stacker3r INTEGER NOT NULL DEFAULT 0,
ADD COLUMN number_of_stackers INTEGER NOT NULL DEFAULT 0`, ADD COLUMN number_of_stackers INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE cash_out_txs `ALTER TABLE cash_out_txs
ADD COLUMN provisioned_1f INTEGER, ADD COLUMN provisioned_1f INTEGER,

View file

@ -3,7 +3,6 @@ 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'
@ -28,76 +27,6 @@ import helper from './helper'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
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),
stacker1f: Yup.number()
.label('Stacker 1F')
.required('Required')
.integer()
.min(0)
.max(60),
stacker1r: Yup.number()
.label('Stacker 1R')
.required('Required')
.integer()
.min(0)
.max(60),
stacker2f: Yup.number()
.label('Stacker 2F')
.required('Required')
.integer()
.min(0)
.max(60),
stacker2r: Yup.number()
.label('Stacker 2R')
.required('Required')
.integer()
.min(0)
.max(60),
stacker3f: Yup.number()
.label('Stacker 3F')
.required('Required')
.integer()
.min(0)
.max(60),
stacker3r: Yup.number()
.label('Stacker 3R')
.required('Required')
.integer()
.min(0)
.max(60)
})
const GET_MACHINES_AND_CONFIG = gql` const GET_MACHINES_AND_CONFIG = gql`
query getData($billFilters: JSONObject) { query getData($billFilters: JSONObject) {
machines { machines {
@ -327,7 +256,6 @@ const CashCassettes = () => {
stripeWhen={isCashOutDisabled} stripeWhen={isCashOutDisabled}
elements={nonStackerElements} elements={nonStackerElements}
data={nonStackerMachines} data={nonStackerMachines}
validationSchema={ValidationSchema}
tbodyWrapperClass={classes.tBody} tbodyWrapperClass={classes.tBody}
/> />
@ -337,7 +265,6 @@ const CashCassettes = () => {
stripeWhen={isCashOutDisabled} stripeWhen={isCashOutDisabled}
elements={stackerElements} elements={stackerElements}
data={stackerMachines} data={stackerMachines}
validationSchema={ValidationSchema}
tbodyWrapperClass={classes.tBody} tbodyWrapperClass={classes.tBody}
/> />

View file

@ -5,6 +5,7 @@ import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik' import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
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 { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import { cashUnitCapacity } from 'src/utils/machine'
const widthsByCashUnits = { const widthsByCashUnits = {
2: { 2: {
@ -121,12 +122,13 @@ const getElements = (
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette, width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
stripe: true, stripe: true,
doubleHeader: 'Cash-out', doubleHeader: 'Cash-out',
view: (_, { id, cashUnits }) => ( view: (_, { id, model, cashUnits }) => (
<CashOut <CashOut
className={classes.cashbox} className={classes.cashbox}
denomination={getCashoutSettings(id)?.[`cassette${it}`]} denomination={getCashoutSettings(id)?.[`cassette${it}`]}
currency={{ code: fiatCurrency }} currency={{ code: fiatCurrency }}
notes={cashUnits[`cassette${it}`]} notes={cashUnits[`cassette${it}`]}
capacity={cashUnitCapacity[model].cassette}
width={ width={
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
} }
@ -158,12 +160,13 @@ const getElements = (
header: `Stacker ${it}F`, header: `Stacker ${it}F`,
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette, width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
stripe: true, stripe: true,
view: (_, { id, cashUnits }) => ( view: (_, { id, model, cashUnits }) => (
<CashOut <CashOut
className={classes.cashbox} className={classes.cashbox}
denomination={getCashoutSettings(id)?.[`stacker${it}f`]} denomination={getCashoutSettings(id)?.[`stacker${it}f`]}
currency={{ code: fiatCurrency }} currency={{ code: fiatCurrency }}
notes={cashUnits[`stacker${it}f`]} notes={cashUnits[`stacker${it}f`]}
capacity={cashUnitCapacity[model].stacker}
width={ width={
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
} }
@ -186,12 +189,13 @@ const getElements = (
header: `Stacker ${it}R`, header: `Stacker ${it}R`,
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette, width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
stripe: true, stripe: true,
view: (_, { id, cashUnits }) => ( view: (_, { id, model, cashUnits }) => (
<CashOut <CashOut
className={classes.cashbox} className={classes.cashbox}
denomination={getCashoutSettings(id)?.[`stacker${it}r`]} denomination={getCashoutSettings(id)?.[`stacker${it}r`]}
currency={{ code: fiatCurrency }} currency={{ code: fiatCurrency }}
notes={cashUnits[`stacker${it}r`]} notes={cashUnits[`stacker${it}r`]}
capacity={cashUnitCapacity[model].stacker}
width={ width={
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
} }

8
package-lock.json generated
View file

@ -6079,6 +6079,14 @@
} }
} }
}, },
"@haensl/subset-sum": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@haensl/subset-sum/-/subset-sum-3.0.5.tgz",
"integrity": "sha512-ySEbozvn6tzZNemM+3Sm2ZBkALuwzTQnhlIhA6Sw5Ja55QOPeEtZJMtR+TqHCvxdhfP61I9XxXpqZVlyvgvcqw==",
"requires": {
"@babel/runtime": "^7.11.2"
}
},
"@hapi/hoek": { "@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",

View file

@ -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",