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

View file

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

View file

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

View file

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

View file

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

View file

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

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.
// 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 => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
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": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",

View file

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