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:
parent
2d010fc359
commit
f3ab63766e
15 changed files with 173 additions and 353 deletions
247
lib/bill-math.js
247
lib/bill-math.js
|
|
@ -1,225 +1,46 @@
|
|||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
const sumService = require('@haensl/subset-sum')
|
||||
|
||||
const MAX_AMOUNT_OF_SOLUTIONS = 10000
|
||||
const MAX_BRUTEFORCE_ITERATIONS = 10000000
|
||||
|
||||
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
|
||||
return [
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
|
||||
denomination: cassettes[0].denomination
|
||||
const getSolution = (units, amount) => {
|
||||
const billList = _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||
return acc
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[1].count - c1 : c1,
|
||||
denomination: cassettes[1].denomination
|
||||
[],
|
||||
units
|
||||
)
|
||||
|
||||
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,
|
||||
denomination: cassettes[2].denomination
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[3].count - c3 : c3,
|
||||
denomination: cassettes[3].denomination
|
||||
}
|
||||
]
|
||||
[],
|
||||
_.keys(solution)
|
||||
)
|
||||
}
|
||||
|
||||
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 []
|
||||
const solutionToOriginalUnits = (solution, units) => {
|
||||
const billsLeft = _.clone(_.fromPairs(_.map(it => [it.denomination, it.provisioned])(solution)))
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
const unit = units[value]
|
||||
const billsToAssign = _.clamp(0, unit.count)(_.isNaN(billsLeft[unit.denomination]) || _.isNil(billsLeft[unit.denomination]) ? 0 : billsLeft[unit.denomination])
|
||||
acc.push({ name: unit.name, denomination: unit.denomination, provisioned: billsToAssign })
|
||||
billsLeft[unit.denomination] -= billsToAssign
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
_.range(0, _.size(units))
|
||||
)
|
||||
}
|
||||
|
||||
function makeChange(outCassettes, amount) {
|
||||
const available = _.reduce(
|
||||
(res, val) => res + val.count * val.denomination,
|
||||
0,
|
||||
outCassettes
|
||||
)
|
||||
|
||||
if (available < amount) {
|
||||
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(outCassettes)}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const cassettes = mergeCassettes(outCassettes)
|
||||
const result =
|
||||
_.size(cassettes) >= 3
|
||||
? makeChangeDynamic(cassettes, amount, available)
|
||||
: makeChangeDuo(cassettes, amount)
|
||||
|
||||
if (!result.length) return null
|
||||
return unmergeCassettes(outCassettes, result)
|
||||
}
|
||||
|
||||
function makeChangeDynamicBruteForce(outCassettes, amount, available) {
|
||||
const solutions = []
|
||||
let x = 0
|
||||
|
||||
const shouldFlip = amount > _.max(_.map(it => it.denomination * it.count, outCassettes))
|
||||
const amountNum = shouldFlip ? available - amount : amount
|
||||
|
||||
const cassettes = shouldFlip ? _.reverse(outCassettes) : outCassettes
|
||||
const { denomination: denomination0, count: count0 } = cassettes[0]
|
||||
const { denomination: denomination1, count: count1 } = cassettes[1]
|
||||
const { denomination: denomination2, count: count2 } = cassettes[2]
|
||||
const { denomination: denomination3, count: count3 } = cassettes[3]
|
||||
|
||||
const startTime = new Date().getTime()
|
||||
|
||||
loop1: for (let i = 0; i <= count0; i++) {
|
||||
const firstSum = i * denomination0
|
||||
|
||||
for (let j = 0; j <= count1; j++) {
|
||||
const secondSum = firstSum + j * denomination1
|
||||
if (secondSum > amountNum) break
|
||||
|
||||
if (secondSum === amountNum) {
|
||||
solutions.push(newSolution(cassettes, i, j, 0, 0, shouldFlip))
|
||||
}
|
||||
|
||||
for (let k = 0; k <= count2; k++) {
|
||||
const thirdSum = secondSum + k * denomination2
|
||||
if (thirdSum > amountNum) break
|
||||
|
||||
if (denomination2 === 0) break
|
||||
|
||||
if (thirdSum === amountNum) {
|
||||
solutions.push(newSolution(cassettes, i, j, k, 0, shouldFlip))
|
||||
}
|
||||
|
||||
for (let l = 0; l <= count3; l++) {
|
||||
if ((x > MAX_AMOUNT_OF_SOLUTIONS && solutions.length >= 1) || x > MAX_BRUTEFORCE_ITERATIONS) break loop1
|
||||
x++
|
||||
const fourthSum = thirdSum + l * denomination3
|
||||
if (fourthSum > amountNum) break
|
||||
|
||||
if (denomination3 === 0) break
|
||||
|
||||
if (fourthSum === amountNum) {
|
||||
solutions.push(newSolution(cassettes, i, j, k, l, shouldFlip))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = new Date().getTime()
|
||||
|
||||
console.log(`Exiting bruteforce after ${x} tries. Took ${endTime - startTime} ms`)
|
||||
return solutions
|
||||
}
|
||||
|
||||
function makeChangeDynamic(cassettes, amount, available) {
|
||||
while (_.size(cassettes) < 4) {
|
||||
cassettes.push({ denomination: 0, count: 0 })
|
||||
}
|
||||
|
||||
const amountNum = amount.toNumber()
|
||||
|
||||
const solutions = makeChangeDynamicBruteForce(cassettes, amountNum, available)
|
||||
|
||||
const sortedSolutions = _.sortBy(it => {
|
||||
const arr = []
|
||||
|
||||
for (let la = 0; la < 4; la++) {
|
||||
arr.push(cassettes[la].count - it[la].provisioned)
|
||||
}
|
||||
|
||||
if (arr.length < 2) return Infinity
|
||||
return _.max(arr) - _.min(arr)
|
||||
}, solutions)
|
||||
|
||||
const cleanSolution = _.filter(
|
||||
it => it.denomination > 0,
|
||||
_.head(sortedSolutions)
|
||||
)
|
||||
|
||||
const response = cleanSolution
|
||||
|
||||
// Final sanity check
|
||||
let total = 0
|
||||
_.forEach(it => {
|
||||
total += it.provisioned * it.denomination
|
||||
}, response)
|
||||
|
||||
if (total === amountNum) return response
|
||||
|
||||
console.log(
|
||||
`Failed to find a solution for ${amountNum} with cassettes ${JSON.stringify(cassettes)}`
|
||||
)
|
||||
return []
|
||||
const solution = getSolution(outCassettes, amount)
|
||||
return solutionToOriginalUnits(solution, outCassettes)
|
||||
}
|
||||
|
||||
module.exports = { makeChange }
|
||||
|
|
|
|||
|
|
@ -40,10 +40,11 @@ function mapDispense (tx) {
|
|||
const res = {}
|
||||
|
||||
_.forEach(it => {
|
||||
res[`provisioned_${it + 1}`] = bills[it].provisioned
|
||||
res[`denomination_${it + 1}`] = bills[it].denomination
|
||||
res[`dispensed_${it + 1}`] = bills[it].dispensed
|
||||
res[`rejected_${it + 1}`] = bills[it].rejected
|
||||
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||
res[`provisioned_${suffix}`] = bills[it].provisioned
|
||||
res[`denomination_${suffix}`] = bills[it].denomination
|
||||
res[`dispensed_${suffix}`] = bills[it].dispensed
|
||||
res[`rejected_${suffix}`] = bills[it].rejected
|
||||
}, _.times(_.identity(), _.size(bills)))
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -17,6 +17,32 @@ case
|
|||
else 'Pending'
|
||||
end`
|
||||
|
||||
const MAX_CASSETTES = 4
|
||||
const MAX_STACKERS = 3
|
||||
|
||||
const BILL_FIELDS = [
|
||||
'denomination1',
|
||||
'denomination2',
|
||||
'denomination3',
|
||||
'denomination4',
|
||||
'denomination1f',
|
||||
'denomination1r',
|
||||
'denomination2f',
|
||||
'denomination2r',
|
||||
'denomination3f',
|
||||
'denomination3r',
|
||||
'provisioned1',
|
||||
'provisioned2',
|
||||
'provisioned3',
|
||||
'provisioned4',
|
||||
'provisioned1f',
|
||||
'provisioned1r',
|
||||
'provisioned2f',
|
||||
'provisioned2r',
|
||||
'provisioned3f',
|
||||
'provisioned3r'
|
||||
]
|
||||
|
||||
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
||||
|
||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||
|
|
@ -43,23 +69,37 @@ function convertBigNumFields (obj) {
|
|||
}
|
||||
|
||||
function convertField (key) {
|
||||
return _.snakeCase(key)
|
||||
return _.includes('denomination', key) || _.includes('provisioned', key) ? key : _.snakeCase(key)
|
||||
}
|
||||
|
||||
function addDbBills (tx) {
|
||||
const bills = tx.bills
|
||||
if (_.isEmpty(bills)) return tx
|
||||
|
||||
const billsObj = {
|
||||
provisioned1: bills[0]?.provisioned ?? 0,
|
||||
provisioned2: bills[1]?.provisioned ?? 0,
|
||||
provisioned3: bills[2]?.provisioned ?? 0,
|
||||
provisioned4: bills[3]?.provisioned ?? 0,
|
||||
denomination1: bills[0]?.denomination ?? 0,
|
||||
denomination2: bills[1]?.denomination ?? 0,
|
||||
denomination3: bills[2]?.denomination ?? 0,
|
||||
denomination4: bills[3]?.denomination ?? 0
|
||||
}
|
||||
const billFields = _.map(it => _.replace(/(denomination|provisioned)/g, '$1_')(it), BILL_FIELDS)
|
||||
|
||||
const billsObj = _.flow(
|
||||
_.reduce(
|
||||
(acc, value) => {
|
||||
const suffix = value.name.replace(/cassette|stacker/gi, '')
|
||||
return {
|
||||
...acc,
|
||||
[`provisioned_${suffix}`]: value.provisioned,
|
||||
[`denomination_${suffix}`]: value.denomination
|
||||
}
|
||||
},
|
||||
{}
|
||||
),
|
||||
it => {
|
||||
const missingKeys = _.reduce(
|
||||
(acc, value) => {
|
||||
return _.assign({ [value]: 0 })(acc)
|
||||
},
|
||||
{}
|
||||
)(_.difference(billFields, _.keys(it)))
|
||||
return _.assign(missingKeys, it)
|
||||
}
|
||||
)(bills)
|
||||
|
||||
return _.assign(tx, billsObj)
|
||||
}
|
||||
|
|
@ -78,7 +118,7 @@ function toObj (row) {
|
|||
let newObj = {}
|
||||
|
||||
keys.forEach(key => {
|
||||
const objKey = _.camelCase(key)
|
||||
const objKey = key.match(/denomination|provisioned/g) ? key.replace(/_/g, '') : _.camelCase(key)
|
||||
if (key === 'received_crypto_atoms' && row[key]) {
|
||||
newObj[objKey] = new BN(row[key])
|
||||
return
|
||||
|
|
@ -93,35 +133,28 @@ function toObj (row) {
|
|||
|
||||
newObj.direction = 'cashOut'
|
||||
|
||||
const billFields = ['denomination1', 'denomination2', 'denomination3', 'denomination4', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4']
|
||||
if (_.every(_.isNil, _.at(BILL_FIELDS, newObj))) return newObj
|
||||
if (_.some(_.isNil, _.at(BILL_FIELDS, newObj))) throw new Error('Missing cassette values')
|
||||
|
||||
if (_.every(_.isNil, _.at(billFields, newObj))) return newObj
|
||||
if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values')
|
||||
|
||||
const billFieldsArr = [
|
||||
{
|
||||
denomination: newObj.denomination1,
|
||||
provisioned: newObj.provisioned1
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination2,
|
||||
provisioned: newObj.provisioned2
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination3,
|
||||
provisioned: newObj.provisioned3
|
||||
},
|
||||
{
|
||||
denomination: newObj.denomination4,
|
||||
provisioned: newObj.provisioned4
|
||||
}
|
||||
]
|
||||
const billFieldsArr = _.concat(
|
||||
_.map(it => ({ name: `cassette${it + 1}`, denomination: newObj[`denomination${it + 1}`], provisioned: newObj[`provisioned${it + 1}`] }))(_.range(0, MAX_CASSETTES)),
|
||||
_.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(
|
||||
{ name: `stacker${value + 1}f`, denomination: newObj[`denomination${value + 1}f`], provisioned: newObj[`provisioned${value + 1}f`] },
|
||||
{ name: `stacker${value + 1}r`, denomination: newObj[`denomination${value + 1}r`], provisioned: newObj[`provisioned${value + 1}r`] }
|
||||
)
|
||||
return acc
|
||||
},
|
||||
[]
|
||||
)(_.range(0, MAX_STACKERS))
|
||||
)
|
||||
|
||||
// There can't be bills with denomination === 0.
|
||||
// If a bill has denomination === 0, then that cassette is not set and should be filtered out.
|
||||
const bills = _.filter(it => it.denomination > 0, billFieldsArr)
|
||||
|
||||
return _.set('bills', bills, _.omit(billFields, newObj))
|
||||
return _.set('bills', bills, _.omit(BILL_FIELDS, newObj))
|
||||
}
|
||||
|
||||
function redeemableTxs (deviceId) {
|
||||
|
|
@ -129,7 +162,10 @@ function redeemableTxs (deviceId) {
|
|||
where device_id=$1
|
||||
and redeem=$2
|
||||
and dispense=$3
|
||||
and provisioned_1 is not null
|
||||
and (
|
||||
provisioned_1 is not null or provisioned_2 is not null or provisioned_3 is not null or provisioned_4 is not null or
|
||||
provisioned_1f is not null or provisioned_1r is not null or provisioned_2f is not null or provisioned_2r is not null or provisioned_3f is not null or provisioned_3r is not null
|
||||
)
|
||||
and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
|
||||
|
||||
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
|
||||
|
|
|
|||
|
|
@ -56,14 +56,15 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
}
|
||||
|
||||
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
||||
return pi.buildAvailableCassettes(newTx.id)
|
||||
.then(cassettes => {
|
||||
return pi.buildAvailableUnits(newTx.id)
|
||||
.then(_units => {
|
||||
const units = _.concat(_units.cassettes, _units.stackers)
|
||||
logger.silly('Computing bills to dispense:', {
|
||||
txId: newTx.id,
|
||||
cassettes: cassettes.cassettes,
|
||||
units: units,
|
||||
fiat: newTx.fiat
|
||||
})
|
||||
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
|
||||
const bills = billMath.makeChange(units, newTx.fiat)
|
||||
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
||||
|
||||
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
||||
|
|
@ -73,8 +74,9 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
const rec = {}
|
||||
|
||||
_.forEach(it => {
|
||||
rec[`provisioned_${it + 1}`] = bills[it].provisioned
|
||||
rec[`denomination_${it + 1}`] = bills[it].denomination
|
||||
const suffix = bills[it].name.replace(/cassette|stacker/gi, '')
|
||||
rec[`provisioned_${suffix}`] = bills[it].provisioned
|
||||
rec[`denomination_${suffix}`] = bills[it].denomination
|
||||
}, _.times(_.identity(), _.size(bills)))
|
||||
|
||||
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
|
||||
|
|
|
|||
|
|
@ -165,11 +165,13 @@ type DynamicCoinValues {
|
|||
}
|
||||
|
||||
type PhysicalCassette {
|
||||
name: String!
|
||||
denomination: Int!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
type PhysicalStacker {
|
||||
name: String!
|
||||
denomination: Int!
|
||||
count: Int!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,9 +153,11 @@ function advancedBatch (data) {
|
|||
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
||||
'dispense', 'notified', 'redeem', 'phone', 'error',
|
||||
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
||||
'dispenseConfirmed', 'provisioned1', 'provisioned2',
|
||||
'denomination1', 'denomination2', 'errorCode', 'customerId',
|
||||
'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
||||
'provisioned1f', 'provisioned1r', 'provisioned2f', 'provisioned2r', 'provisioned3f', 'provisioned3r',
|
||||
'denomination1', 'denomination2', 'denomination3', 'denomination4',
|
||||
'denomination1f', 'denomination1r', 'denomination2f', 'denomination2r', 'denomination3f', 'denomination3r',
|
||||
'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
||||
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
||||
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const CA_PATH = process.env.CA_PATH
|
|||
// A machine on an older version (no multicassette code) could be paired with a server with multicassette code.
|
||||
// This makes sure that the server stores a default value
|
||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||
const DEFAULT_NUMBER_OF_STACKERS = 0
|
||||
|
||||
function pullToken (token) {
|
||||
const sql = `delete from pairing_tokens
|
||||
|
|
@ -36,16 +37,16 @@ function unpair (deviceId) {
|
|||
)
|
||||
}
|
||||
|
||||
function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES) {
|
||||
function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES, numOfStackers = DEFAULT_NUMBER_OF_STACKERS) {
|
||||
return pullToken(token)
|
||||
.then(r => {
|
||||
if (r.expired) return false
|
||||
|
||||
const insertSql = `insert into devices (device_id, name, number_of_cassettes) values ($1, $2, $3)
|
||||
const insertSql = `insert into devices (device_id, name, number_of_cassettes, number_of_stackers) values ($1, $2, $3)
|
||||
on conflict (device_id)
|
||||
do update set paired=TRUE, display=TRUE`
|
||||
|
||||
return db.none(insertSql, [deviceId, r.name, numOfCassettes])
|
||||
return db.none(insertSql, [deviceId, r.name, numOfCassettes, numOfStackers])
|
||||
.then(() => true)
|
||||
})
|
||||
.catch(err => {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
const sumTxs = (sum, tx) => {
|
||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
||||
const bills = _.filter(it => it.denomination > 0, tx.bills)
|
||||
const bills = _.filter(it => _.includes('cassette', it.name) && it.denomination > 0, tx.bills)
|
||||
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||
|
||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
||||
|
|
@ -139,6 +139,7 @@ function plugins (settings, deviceId) {
|
|||
const computedCassettes = []
|
||||
_.forEach(it => {
|
||||
computedCassettes.push({
|
||||
name: cassettes[it].name,
|
||||
denomination: cassettes[it].denomination,
|
||||
count: counts[it]
|
||||
})
|
||||
|
|
@ -152,7 +153,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
const sumTxs = (sum, tx) => {
|
||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
||||
const bills = _.filter(it => it.denomination > 0, tx.bills)
|
||||
const bills = _.filter(it => _.includes('stacker', it.name) && it.denomination > 0, tx.bills)
|
||||
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||
|
||||
const doDenominationsMatch = _.every(sameDenominations, _.zip(stackers, bills))
|
||||
|
|
@ -175,6 +176,7 @@ function plugins (settings, deviceId) {
|
|||
const computedStackers = []
|
||||
_.forEach(it => {
|
||||
computedStackers.push({
|
||||
name: stackers[it].name,
|
||||
denomination: stackers[it].denomination,
|
||||
count: counts[it]
|
||||
})
|
||||
|
|
@ -210,6 +212,7 @@ function plugins (settings, deviceId) {
|
|||
const cassettes = []
|
||||
_.forEach(it => {
|
||||
cassettes.push({
|
||||
name: `cassette${it + 1}`,
|
||||
denomination: parseInt(denominations[it], 10),
|
||||
count: parseInt(counts[it], 10)
|
||||
})
|
||||
|
|
@ -241,10 +244,10 @@ function plugins (settings, deviceId) {
|
|||
|
||||
const denominations = []
|
||||
_.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))
|
||||
|
||||
const virtualStackers = [Math.max(...denominations) * 2]
|
||||
const virtualStackers = [Math.max(..._.flatten(denominations)) * 2]
|
||||
|
||||
const counts = _stackers.counts
|
||||
|
||||
|
|
@ -255,10 +258,16 @@ function plugins (settings, deviceId) {
|
|||
const stackers = []
|
||||
_.forEach(it => {
|
||||
stackers.push({
|
||||
denomination: parseInt(denominations[it], 10),
|
||||
count: parseInt(counts[it], 10)
|
||||
name: `stacker${it + 1}f`,
|
||||
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 {
|
||||
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 () {
|
||||
const sql = `select id from user_config
|
||||
where type=$1
|
||||
|
|
@ -1051,7 +1065,6 @@ function plugins (settings, deviceId) {
|
|||
sendMessage,
|
||||
checkBalances,
|
||||
getMachineNames,
|
||||
buildAvailableCassettes,
|
||||
buy,
|
||||
sell,
|
||||
getNotificationConfig,
|
||||
|
|
@ -1062,7 +1075,8 @@ function plugins (settings, deviceId) {
|
|||
isValidWalletScore,
|
||||
getTransactionHash,
|
||||
getInputAddresses,
|
||||
isWalletScoringEnabled
|
||||
isWalletScoringEnabled,
|
||||
buildAvailableUnits
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ exports.stackerCounts = function stackerCounts (deviceId) {
|
|||
.then(row => {
|
||||
const counts = []
|
||||
_.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))
|
||||
|
||||
return { numberOfStackers: row.number_of_stackers, counts }
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ function pair (req, res, next) {
|
|||
const deviceId = req.deviceId
|
||||
const model = req.query.model
|
||||
const numOfCassettes = req.query.numOfCassettes
|
||||
const numOfStackers = req.query.numOfStackers
|
||||
|
||||
return pairing.pair(token, deviceId, model, numOfCassettes)
|
||||
return pairing.pair(token, deviceId, model, numOfCassettes, numOfStackers)
|
||||
.then(isValid => {
|
||||
if (isValid) return res.json({ status: 'paired' })
|
||||
throw httpError('Pairing failed')
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ exports.up = function (next) {
|
|||
ADD COLUMN stacker2f INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker2r INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker3f INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN stacker3r INTEGER NOT NULL DEFAULT 0
|
||||
ADD COLUMN stacker3r INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN number_of_stackers INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE cash_out_txs
|
||||
ADD COLUMN provisioned_1f INTEGER,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { DialogActions, makeStyles, Box } from '@material-ui/core'
|
|||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||
import Modal from 'src/components/Modal'
|
||||
|
|
@ -28,76 +27,6 @@ import helper from './helper'
|
|||
|
||||
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`
|
||||
query getData($billFilters: JSONObject) {
|
||||
machines {
|
||||
|
|
@ -327,7 +256,6 @@ const CashCassettes = () => {
|
|||
stripeWhen={isCashOutDisabled}
|
||||
elements={nonStackerElements}
|
||||
data={nonStackerMachines}
|
||||
validationSchema={ValidationSchema}
|
||||
tbodyWrapperClass={classes.tBody}
|
||||
/>
|
||||
|
||||
|
|
@ -337,7 +265,6 @@ const CashCassettes = () => {
|
|||
stripeWhen={isCashOutDisabled}
|
||||
elements={stackerElements}
|
||||
data={stackerMachines}
|
||||
validationSchema={ValidationSchema}
|
||||
tbodyWrapperClass={classes.tBody}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
|
|||
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { cashUnitCapacity } from 'src/utils/machine'
|
||||
|
||||
const widthsByCashUnits = {
|
||||
2: {
|
||||
|
|
@ -121,12 +122,13 @@ const getElements = (
|
|||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||
stripe: true,
|
||||
doubleHeader: 'Cash-out',
|
||||
view: (_, { id, cashUnits }) => (
|
||||
view: (_, { id, model, cashUnits }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(id)?.[`cassette${it}`]}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={cashUnits[`cassette${it}`]}
|
||||
capacity={cashUnitCapacity[model].cassette}
|
||||
width={
|
||||
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
||||
}
|
||||
|
|
@ -158,12 +160,13 @@ const getElements = (
|
|||
header: `Stacker ${it}F`,
|
||||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||
stripe: true,
|
||||
view: (_, { id, cashUnits }) => (
|
||||
view: (_, { id, model, cashUnits }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(id)?.[`stacker${it}f`]}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={cashUnits[`stacker${it}f`]}
|
||||
capacity={cashUnitCapacity[model].stacker}
|
||||
width={
|
||||
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
||||
}
|
||||
|
|
@ -186,12 +189,13 @@ const getElements = (
|
|||
header: `Stacker ${it}R`,
|
||||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||
stripe: true,
|
||||
view: (_, { id, cashUnits }) => (
|
||||
view: (_, { id, model, cashUnits }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(id)?.[`stacker${it}r`]}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={cashUnits[`stacker${it}r`]}
|
||||
capacity={cashUnitCapacity[model].stacker}
|
||||
width={
|
||||
widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.unitGraph
|
||||
}
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -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": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"@ethereumjs/common": "^2.6.4",
|
||||
"@ethereumjs/tx": "^3.5.1",
|
||||
"@graphql-tools/merge": "^6.2.5",
|
||||
"@haensl/subset-sum": "^3.0.5",
|
||||
"@lamassu/coins": "1.3.0",
|
||||
"@simplewebauthn/server": "^3.0.0",
|
||||
"apollo-server-express": "2.25.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue