fix: multiple fixes related with recyclers/stackers
feat: add bill destination unit for cash-in txs feat: l-m communication regarding cash unit state
This commit is contained in:
parent
2638bd1717
commit
2967ad3a75
17 changed files with 573 additions and 102 deletions
|
|
@ -41,7 +41,7 @@ function insertNewBills (t, billRows, machineTx) {
|
||||||
if (_.isEmpty(bills)) return Promise.resolve([])
|
if (_.isEmpty(bills)) return Promise.resolve([])
|
||||||
|
|
||||||
const dbBills = _.map(cashInLow.massage, bills)
|
const dbBills = _.map(cashInLow.massage, bills)
|
||||||
const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time']
|
const columns = ['id', 'fiat', 'fiat_code', 'crypto_code', 'cash_in_fee', 'cash_in_txs_id', 'device_time', 'destination_unit']
|
||||||
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
|
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
|
||||||
const deviceID = machineTx.deviceId
|
const deviceID = machineTx.deviceId
|
||||||
const sql2 = `update devices set cashbox = cashbox + $2
|
const sql2 = `update devices set cashbox = cashbox + $2
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,29 @@ const anonymousCustomer = {
|
||||||
name: 'anonymous'
|
name: 'anonymous'
|
||||||
}
|
}
|
||||||
|
|
||||||
const CASSETTE_MAX_CAPACITY = 500
|
const CASH_UNIT_CAPACITY = {
|
||||||
|
grandola: {
|
||||||
|
cashbox: 2000,
|
||||||
|
recycler: 2800
|
||||||
|
},
|
||||||
|
aveiro: {
|
||||||
|
cashbox: 1500,
|
||||||
|
stacker: 60,
|
||||||
|
cassette: 500
|
||||||
|
},
|
||||||
|
tejo: {
|
||||||
|
// TODO: add support for the different cashbox configuration in Tejo
|
||||||
|
cashbox: 1000,
|
||||||
|
cassette: 500
|
||||||
|
},
|
||||||
|
gaia: {
|
||||||
|
cashbox: 600
|
||||||
|
},
|
||||||
|
sintra: {
|
||||||
|
cashbox: 1000,
|
||||||
|
cassette: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
|
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
|
||||||
const CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES = 4
|
const CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES = 4
|
||||||
|
|
@ -39,7 +61,7 @@ const BALANCE_FETCH_SPEED_MULTIPLIER = {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
anonymousCustomer,
|
anonymousCustomer,
|
||||||
CASSETTE_MAX_CAPACITY,
|
CASH_UNIT_CAPACITY,
|
||||||
AUTHENTICATOR_ISSUER_ENTITY,
|
AUTHENTICATOR_ISSUER_ENTITY,
|
||||||
AUTH_TOKEN_EXPIRATION_TIME,
|
AUTH_TOKEN_EXPIRATION_TIME,
|
||||||
REGISTRATION_TOKEN_EXPIRATION_TIME,
|
REGISTRATION_TOKEN_EXPIRATION_TIME,
|
||||||
|
|
|
||||||
|
|
@ -167,13 +167,25 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
||||||
)(cassettes) :
|
)(cassettes) :
|
||||||
null
|
null
|
||||||
|
|
||||||
|
const massageStackers = stackers =>
|
||||||
|
stackers ?
|
||||||
|
_.flow(
|
||||||
|
stackers => _.set('physical', _.get('stackers', stackers), stackers),
|
||||||
|
stackers => _.set('virtual', _.get('virtualStackers', stackers), stackers),
|
||||||
|
_.unset('stackers'),
|
||||||
|
_.unset('virtualStackers')
|
||||||
|
)(stackers) :
|
||||||
|
null
|
||||||
|
|
||||||
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
||||||
|
|
||||||
return _.flow(
|
return _.flow(
|
||||||
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'coins', 'rates']),
|
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'stackers', 'coins', 'rates']),
|
||||||
|
|
||||||
_.update('cassettes', massageCassettes),
|
_.update('cassettes', massageCassettes),
|
||||||
|
|
||||||
|
_.update('stackers', massageStackers),
|
||||||
|
|
||||||
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
/* [{ cryptoCode, rates }, ...] => [[cryptoCode, rates], ...] */
|
||||||
_.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])),
|
_.update('coins', _.map(({ cryptoCode, rates }) => [cryptoCode, rates])),
|
||||||
|
|
||||||
|
|
@ -185,9 +197,10 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
||||||
|
|
||||||
/* Group the separate objects by cryptoCode */
|
/* Group the separate objects by cryptoCode */
|
||||||
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
||||||
({ areThereAvailablePromoCodes, balances, cassettes, coins, rates }) => ({
|
({ areThereAvailablePromoCodes, balances, cassettes, stackers, coins, rates }) => ({
|
||||||
areThereAvailablePromoCodes,
|
areThereAvailablePromoCodes,
|
||||||
cassettes,
|
cassettes,
|
||||||
|
stackers,
|
||||||
coins: _.flow(
|
coins: _.flow(
|
||||||
_.reduce(
|
_.reduce(
|
||||||
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
|
(ret, [cryptoCode, obj]) => _.update(cryptoCode, _.assign(obj), ret),
|
||||||
|
|
@ -216,22 +229,25 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
||||||
const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, operatorId, pid, settings }, info) =>
|
const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, operatorId, pid, settings }, info) =>
|
||||||
plugins(settings, deviceId)
|
plugins(settings, deviceId)
|
||||||
.pollQueries()
|
.pollQueries()
|
||||||
.then(pq => ({
|
.then(pq => {
|
||||||
static: staticConfig({
|
console.log(pq)
|
||||||
currentConfigVersion,
|
return {
|
||||||
deviceId,
|
static: staticConfig({
|
||||||
deviceName,
|
currentConfigVersion,
|
||||||
pq,
|
deviceId,
|
||||||
settings,
|
deviceName,
|
||||||
}),
|
pq,
|
||||||
dynamic: dynamicConfig({
|
settings,
|
||||||
deviceId,
|
}),
|
||||||
operatorId,
|
dynamic: dynamicConfig({
|
||||||
pid,
|
deviceId,
|
||||||
pq,
|
operatorId,
|
||||||
settings,
|
pid,
|
||||||
}),
|
pq,
|
||||||
}))
|
settings,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const massageTerms = terms => (terms.active && terms.text) ? ({
|
const massageTerms = terms => (terms.active && terms.text) ? ({
|
||||||
|
|
|
||||||
|
|
@ -169,14 +169,25 @@ type PhysicalCassette {
|
||||||
count: Int!
|
count: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PhysicalStacker {
|
||||||
|
denomination: Int!
|
||||||
|
count: Int!
|
||||||
|
}
|
||||||
|
|
||||||
type Cassettes {
|
type Cassettes {
|
||||||
physical: [PhysicalCassette!]!
|
physical: [PhysicalCassette!]!
|
||||||
virtual: [Int!]!
|
virtual: [Int!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Stackers {
|
||||||
|
physical: [PhysicalStacker!]!
|
||||||
|
virtual: [Int!]!
|
||||||
|
}
|
||||||
|
|
||||||
type DynamicConfig {
|
type DynamicConfig {
|
||||||
areThereAvailablePromoCodes: Boolean!
|
areThereAvailablePromoCodes: Boolean!
|
||||||
cassettes: Cassettes
|
cassettes: Cassettes
|
||||||
|
stackers: Stackers
|
||||||
coins: [DynamicCoinValues!]!
|
coins: [DynamicCoinValues!]!
|
||||||
reboot: Boolean!
|
reboot: Boolean!
|
||||||
shutdown: Boolean!
|
shutdown: Boolean!
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ const CODES_DISPLAY = {
|
||||||
HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
|
HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
|
||||||
CASH_BOX_FULL: 'Cash box full',
|
CASH_BOX_FULL: 'Cash box full',
|
||||||
LOW_CASH_OUT: 'Low Cash-out',
|
LOW_CASH_OUT: 'Low Cash-out',
|
||||||
|
LOW_RECYCLER_STACKER: 'Low Recycler Stacker',
|
||||||
|
HIGH_RECYCLER_STACKER: 'High Recycler Stacker',
|
||||||
CASHBOX_REMOVED: 'Cashbox removed'
|
CASHBOX_REMOVED: 'Cashbox removed'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
137
lib/plugins.js
137
lib/plugins.js
|
|
@ -27,7 +27,7 @@ const loyalty = require('./loyalty')
|
||||||
const transactionBatching = require('./tx-batching')
|
const transactionBatching = require('./tx-batching')
|
||||||
const state = require('./middlewares/state')
|
const state = require('./middlewares/state')
|
||||||
|
|
||||||
const { CASSETTE_MAX_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants')
|
const { CASH_UNIT_CAPACITY, CASH_OUT_DISPENSE_READY, CONFIRMATION_CODE } = require('./constants')
|
||||||
|
|
||||||
const notifier = require('./notifier')
|
const notifier = require('./notifier')
|
||||||
|
|
||||||
|
|
@ -147,27 +147,63 @@ function plugins (settings, deviceId) {
|
||||||
return computedCassettes
|
return computedCassettes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeAvailableStackers (stackers, redeemableTxs) {
|
||||||
|
if (_.isEmpty(redeemableTxs)) return stackers
|
||||||
|
|
||||||
|
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 sameDenominations = a => a[0]?.denomination === a[1]?.denomination
|
||||||
|
|
||||||
|
const doDenominationsMatch = _.every(sameDenominations, _.zip(stackers, bills))
|
||||||
|
|
||||||
|
if (!doDenominationsMatch) {
|
||||||
|
throw new Error('Denominations don\'t add up, stackers were changed.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
|
||||||
|
}
|
||||||
|
|
||||||
|
const provisioned = _.reduce(sumTxs, _.times(_.constant(0), _.size(stackers)), redeemableTxs)
|
||||||
|
const zipped = _.zip(_.map('count', stackers), provisioned)
|
||||||
|
const counts = _.map(r => r[0] - r[1], zipped)
|
||||||
|
|
||||||
|
if (_.some(_.lt(_, 0), counts)) {
|
||||||
|
throw new Error('Negative note count: %j', counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedStackers = []
|
||||||
|
_.forEach(it => {
|
||||||
|
computedStackers.push({
|
||||||
|
denomination: stackers[it].denomination,
|
||||||
|
count: counts[it]
|
||||||
|
})
|
||||||
|
}, _.times(_.identity(), _.size(stackers)))
|
||||||
|
|
||||||
|
return computedStackers
|
||||||
|
}
|
||||||
|
|
||||||
function buildAvailableCassettes (excludeTxId) {
|
function buildAvailableCassettes (excludeTxId) {
|
||||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||||
|
|
||||||
if (!cashOutConfig.active) return Promise.resolve()
|
if (!cashOutConfig.active) return Promise.resolve()
|
||||||
|
|
||||||
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||||
.then(([rec, _redeemableTxs]) => {
|
.then(([_cassettes, _redeemableTxs]) => {
|
||||||
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
||||||
|
|
||||||
const denominations = []
|
const denominations = []
|
||||||
_.forEach(it => {
|
_.forEach(it => {
|
||||||
denominations.push(cashOutConfig[`cassette${it + 1}`])
|
denominations.push(cashOutConfig[`cassette${it + 1}`])
|
||||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
}, _.times(_.identity(), _cassettes.numberOfCassettes))
|
||||||
|
|
||||||
const virtualCassettes = [Math.max(...denominations) * 2]
|
const virtualCassettes = [Math.max(...denominations) * 2]
|
||||||
|
|
||||||
const counts = argv.cassettes
|
const counts = argv.cassettes
|
||||||
? argv.cassettes.split(',')
|
? argv.cassettes.split(',')
|
||||||
: rec.counts
|
: _cassettes.counts
|
||||||
|
|
||||||
if (rec.counts.length !== denominations.length) {
|
if (_cassettes.counts.length !== denominations.length) {
|
||||||
throw new Error('Denominations and respective counts do not match!')
|
throw new Error('Denominations and respective counts do not match!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +213,7 @@ function plugins (settings, deviceId) {
|
||||||
denomination: parseInt(denominations[it], 10),
|
denomination: parseInt(denominations[it], 10),
|
||||||
count: parseInt(counts[it], 10)
|
count: parseInt(counts[it], 10)
|
||||||
})
|
})
|
||||||
}, _.times(_.identity(), rec.numberOfCassettes))
|
}, _.times(_.identity(), _cassettes.numberOfCassettes))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
|
|
@ -194,6 +230,51 @@ function plugins (settings, deviceId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAvailableStackers (excludeTxId) {
|
||||||
|
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||||
|
|
||||||
|
if (!cashOutConfig.active) return Promise.resolve()
|
||||||
|
|
||||||
|
return Promise.all([dbm.stackerCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||||
|
.then(([_stackers, _redeemableTxs]) => {
|
||||||
|
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
||||||
|
|
||||||
|
const denominations = []
|
||||||
|
_.forEach(it => {
|
||||||
|
denominations.push(cashOutConfig[`stacker${it + 1}f`], cashOutConfig[`stacker${it + 1}r`])
|
||||||
|
}, _.times(_.identity(), _stackers.numberOfStackers))
|
||||||
|
|
||||||
|
const virtualStackers = [Math.max(...denominations) * 2]
|
||||||
|
|
||||||
|
const counts = _stackers.counts
|
||||||
|
|
||||||
|
if (counts.length !== denominations.length) {
|
||||||
|
throw new Error('Denominations and respective counts do not match!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const stackers = []
|
||||||
|
_.forEach(it => {
|
||||||
|
stackers.push({
|
||||||
|
denomination: parseInt(denominations[it], 10),
|
||||||
|
count: parseInt(counts[it], 10)
|
||||||
|
})
|
||||||
|
}, _.times(_.identity(), _stackers.numberOfStackers * 2))
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
stackers: computeAvailableStackers(stackers, redeemableTxs),
|
||||||
|
virtualStackers
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err)
|
||||||
|
return {
|
||||||
|
stackers,
|
||||||
|
virtualStackers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function fetchCurrentConfigVersion () {
|
function fetchCurrentConfigVersion () {
|
||||||
const sql = `select id from user_config
|
const sql = `select id from user_config
|
||||||
where type=$1
|
where type=$1
|
||||||
|
|
@ -240,6 +321,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
buildAvailableCassettes(),
|
buildAvailableCassettes(),
|
||||||
|
buildAvailableStackers(),
|
||||||
fetchCurrentConfigVersion(),
|
fetchCurrentConfigVersion(),
|
||||||
millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)),
|
millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)),
|
||||||
loyalty.getNumberOfAvailablePromoCodes(),
|
loyalty.getNumberOfAvailablePromoCodes(),
|
||||||
|
|
@ -250,6 +332,7 @@ function plugins (settings, deviceId) {
|
||||||
])
|
])
|
||||||
.then(([
|
.then(([
|
||||||
cassettes,
|
cassettes,
|
||||||
|
stackers,
|
||||||
configVersion,
|
configVersion,
|
||||||
timezone,
|
timezone,
|
||||||
numberOfAvailablePromoCodes,
|
numberOfAvailablePromoCodes,
|
||||||
|
|
@ -273,6 +356,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cassettes,
|
cassettes,
|
||||||
|
stackers,
|
||||||
rates: buildRates(tickers),
|
rates: buildRates(tickers),
|
||||||
balances: buildBalances(balances),
|
balances: buildBalances(balances),
|
||||||
coins,
|
coins,
|
||||||
|
|
@ -652,7 +736,10 @@ function plugins (settings, deviceId) {
|
||||||
const denomination3f = cashOutConfig.stacker3f
|
const denomination3f = cashOutConfig.stacker3f
|
||||||
const denomination3r = cashOutConfig.stacker3r
|
const denomination3r = cashOutConfig.stacker3r
|
||||||
const cashOutEnabled = cashOutConfig.active
|
const cashOutEnabled = cashOutConfig.active
|
||||||
const isCassetteLow = (have, max, limit) => cashOutEnabled && ((have / max) * 100) < limit
|
const isUnitLow = (have, max, limit) => cashOutEnabled && ((have / max) * 100) < limit
|
||||||
|
// const isUnitHigh = (have, max, limit) => cashOutEnabled && ((have / max) * 100) > limit
|
||||||
|
|
||||||
|
// const isUnitOutOfBounds = (have, max, lowerBound, upperBound) => isUnitLow(have, max, lowerBound) || isUnitHigh(have, max, upperBound)
|
||||||
|
|
||||||
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
||||||
|
|
||||||
|
|
@ -667,7 +754,7 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const cassette1Alert = device.numberOfCassettes >= 1 && isCassetteLow(device.cashUnits.cassette1, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette1)
|
const cassette1Alert = device.numberOfCassettes >= 1 && isUnitLow(device.cashUnits.cassette1, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette1)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 1,
|
cassette: 1,
|
||||||
|
|
@ -679,7 +766,7 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const cassette2Alert = device.numberOfCassettes >= 2 && isCassetteLow(device.cashUnits.cassette2, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette2)
|
const cassette2Alert = device.numberOfCassettes >= 2 && isUnitLow(device.cashUnits.cassette2, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette2)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 2,
|
cassette: 2,
|
||||||
|
|
@ -691,7 +778,7 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const cassette3Alert = device.numberOfCassettes >= 3 && isCassetteLow(device.cashUnits.cassette3, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette3)
|
const cassette3Alert = device.numberOfCassettes >= 3 && isUnitLow(device.cashUnits.cassette3, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette3)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 3,
|
cassette: 3,
|
||||||
|
|
@ -703,7 +790,7 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const cassette4Alert = device.numberOfCassettes >= 4 && isCassetteLow(device.cashUnits.cassette4, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageCassette4)
|
const cassette4Alert = device.numberOfCassettes >= 4 && isUnitLow(device.cashUnits.cassette4, CASH_UNIT_CAPACITY[device.model]['cassette'], notifications.fillingPercentageCassette4)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_CASH_OUT',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
|
|
@ -715,9 +802,9 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const stacker1fAlert = device.numberOfStackers >= 1 && isCassetteLow(device.cashUnits.stacker1f, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageStacker1f)
|
const stacker1fAlert = device.numberOfStackers >= 1 && isUnitLow(device.cashUnits.stacker1f, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker1f)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_RECYCLER_STACKER',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
|
|
@ -727,9 +814,9 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const stacker1rAlert = device.numberOfStackers >= 1 && isCassetteLow(device.cashUnits.stacker1r, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageStacker1r)
|
const stacker1rAlert = device.numberOfStackers >= 1 && isUnitLow(device.cashUnits.stacker1r, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker1r)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_RECYCLER_STACKER',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
|
|
@ -739,21 +826,21 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const stacker2fAlert = device.numberOfStackers >= 2 && isCassetteLow(device.cashUnits.stacker2f, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageStacker2f)
|
const stacker2fAlert = device.numberOfStackers >= 2 && isUnitLow(device.cashUnits.stacker2f, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker2f)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_RECYCLER_STACKER',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
notes: device.cashUnits.stacker1f,
|
notes: device.cashUnits.stacker2f,
|
||||||
denomination: denomination1f,
|
denomination: denomination2f,
|
||||||
fiatCode
|
fiatCode
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const stacker2rAlert = device.numberOfStackers >= 2 && isCassetteLow(device.cashUnits.stacker2r, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageStacker2r)
|
const stacker2rAlert = device.numberOfStackers >= 2 && isUnitLow(device.cashUnits.stacker2r, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker2r)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_RECYCLER_STACKER',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
|
|
@ -763,9 +850,9 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const stacker3fAlert = device.numberOfStackers >= 3 && isCassetteLow(device.cashUnits.stacker3f, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageStacker3f)
|
const stacker3fAlert = device.numberOfStackers >= 3 && isUnitLow(device.cashUnits.stacker3f, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker3f)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_RECYCLER_STACKER',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
|
|
@ -775,9 +862,9 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const stacker3rAlert = device.numberOfStackers >= 3 && isCassetteLow(device.cashUnits.stacker3r, CASSETTE_MAX_CAPACITY, notifications.fillingPercentageStacker3r)
|
const stacker3rAlert = device.numberOfStackers >= 3 && isUnitLow(device.cashUnits.stacker3r, CASH_UNIT_CAPACITY[device.model]['stacker'], notifications.fillingPercentageStacker3r)
|
||||||
? {
|
? {
|
||||||
code: 'LOW_CASH_OUT',
|
code: 'LOW_RECYCLER_STACKER',
|
||||||
cassette: 4,
|
cassette: 4,
|
||||||
machineName,
|
machineName,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,21 @@ exports.cassetteCounts = function cassetteCounts (deviceId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.stackerCounts = function stackerCounts (deviceId) {
|
||||||
|
const sql = 'SELECT stacker1f, stacker1r, stacker2f, stacker2r, stacker3f, stacker3r, number_of_stackers FROM devices ' +
|
||||||
|
'WHERE device_id=$1'
|
||||||
|
|
||||||
|
return db.one(sql, [deviceId])
|
||||||
|
.then(row => {
|
||||||
|
const counts = []
|
||||||
|
_.forEach(it => {
|
||||||
|
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 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Note: since we only prune on insert, we'll always have
|
// Note: since we only prune on insert, we'll always have
|
||||||
// last known state.
|
// last known state.
|
||||||
exports.machineEvent = function machineEvent (rec) {
|
exports.machineEvent = function machineEvent (rec) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const { migrationSaveConfig, loadLatest } = require('../lib/new-settings-loader')
|
const { migrationSaveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||||
const { CASSETTE_MAX_CAPACITY } = require('../lib/constants')
|
const CASSETTE_MAX_CAPACITY = 500
|
||||||
|
|
||||||
exports.up = function (next) {
|
exports.up = function (next) {
|
||||||
return loadLatest()
|
return loadLatest()
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,9 @@ exports.up = function (next) {
|
||||||
ADD COLUMN denomination_2f INTEGER,
|
ADD COLUMN denomination_2f INTEGER,
|
||||||
ADD COLUMN denomination_2r INTEGER,
|
ADD COLUMN denomination_2r INTEGER,
|
||||||
ADD COLUMN denomination_3f INTEGER,
|
ADD COLUMN denomination_3f INTEGER,
|
||||||
ADD COLUMN denomination_3r INTEGER`
|
ADD COLUMN denomination_3r INTEGER`,
|
||||||
|
`CREATE TYPE bill_destination_unit AS ENUM ('cashbox', 'stacker1f', 'stacker1r', 'stacker2f', 'stacker2r', 'stacker3f', 'stacker3r')`,
|
||||||
|
`ALTER TABLE bills ADD COLUMN destination_unit bill_destination_unit NOT NULL DEFAULT 'cashbox'`
|
||||||
]
|
]
|
||||||
|
|
||||||
db.multi(sql, next)
|
db.multi(sql, next)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ const MODAL_WIDTH = 554
|
||||||
const MODAL_HEIGHT = 520
|
const MODAL_HEIGHT = 520
|
||||||
|
|
||||||
const Wizard = ({ machine, locale, onClose, save, error }) => {
|
const Wizard = ({ machine, locale, onClose, save, error }) => {
|
||||||
const LAST_STEP = machine.numberOfCassettes + machine.numberOfStackers + 1
|
// Each stacker counts as two steps, one for front and another for rear
|
||||||
|
const LAST_STEP = machine.numberOfCassettes + machine.numberOfStackers * 2 + 1
|
||||||
const [{ step, config }, setState] = useState({
|
const [{ step, config }, setState] = useState({
|
||||||
step: 0,
|
step: 0,
|
||||||
config: { active: true }
|
config: { active: true }
|
||||||
|
|
@ -83,30 +84,68 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
R.range(
|
R.range(1, machine.numberOfStackers + 1)
|
||||||
machine.numberOfCassettes + 1,
|
|
||||||
machine.numberOfCassettes + machine.numberOfStackers + 1
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const schema = () =>
|
const schema = () =>
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
cassette1: Yup.number().required(),
|
cassette1:
|
||||||
|
machine.numberOfCassettes >= 1 && step >= 1
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
cassette2:
|
cassette2:
|
||||||
machine.numberOfCassettes > 1 && step >= 2
|
machine.numberOfCassettes >= 2 && step >= 2
|
||||||
? Yup.number().required()
|
? Yup.number().required()
|
||||||
: Yup.number()
|
: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
.nullable(),
|
.nullable(),
|
||||||
cassette3:
|
cassette3:
|
||||||
machine.numberOfCassettes > 2 && step >= 3
|
machine.numberOfCassettes >= 3 && step >= 3
|
||||||
? Yup.number().required()
|
? Yup.number().required()
|
||||||
: Yup.number()
|
: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
.nullable(),
|
.nullable(),
|
||||||
cassette4:
|
cassette4:
|
||||||
machine.numberOfCassettes > 3 && step >= 4
|
machine.numberOfCassettes >= 4 && step >= 4
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
|
stacker1f:
|
||||||
|
machine.numberOfStackers >= 1 && step >= machine.numberOfCassettes + 1
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
|
stacker1r:
|
||||||
|
machine.numberOfStackers >= 1 && step >= machine.numberOfCassettes + 2
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
|
stacker2f:
|
||||||
|
machine.numberOfStackers >= 2 && step >= machine.numberOfCassettes + 3
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
|
stacker2r:
|
||||||
|
machine.numberOfStackers >= 2 && step >= machine.numberOfCassettes + 4
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
|
stacker3f:
|
||||||
|
machine.numberOfStackers >= 3 && step >= machine.numberOfCassettes + 5
|
||||||
|
? Yup.number().required()
|
||||||
|
: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.nullable(),
|
||||||
|
stacker3r:
|
||||||
|
machine.numberOfStackers >= 3 && step >= machine.numberOfCassettes + 6
|
||||||
? Yup.number().required()
|
? Yup.number().required()
|
||||||
: Yup.number()
|
: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,13 @@ import { CURRENCY_MAX } from 'src/utils/constants'
|
||||||
import { transformNumber } from 'src/utils/number'
|
import { transformNumber } from 'src/utils/number'
|
||||||
|
|
||||||
const widthsByNumberOfCassettes = {
|
const widthsByNumberOfCassettes = {
|
||||||
2: { machine: 300, cassette: 225, zeroConf: 200 },
|
2: { machine: 320, cassette: 315 },
|
||||||
3: { machine: 210, cassette: 180, zeroConf: 200 },
|
3: { machine: 305, cassette: 215 },
|
||||||
4: { machine: 200, cassette: 150, zeroConf: 150 }
|
4: { machine: 195, cassette: 190 },
|
||||||
|
5: { machine: 175, cassette: 155 },
|
||||||
|
6: { machine: 170, cassette: 130 },
|
||||||
|
7: { machine: 140, cassette: 125 },
|
||||||
|
8: { machine: 120, cassette: 125 }
|
||||||
}
|
}
|
||||||
|
|
||||||
const DenominationsSchema = Yup.object().shape({
|
const DenominationsSchema = Yup.object().shape({
|
||||||
|
|
@ -45,6 +49,14 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
...R.map(it => it.numberOfCassettes, machines),
|
...R.map(it => it.numberOfCassettes, machines),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
const maxNumberOfStackers = Math.max(
|
||||||
|
...R.map(it => it.numberOfStackers, machines),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const maxNumberOfCashUnits = Math.max(
|
||||||
|
...R.map(it => it.numberOfCassettes + it.numberOfStackers * 2, machines),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
const options = getBillOptions(locale, denominations)
|
const options = getBillOptions(locale, denominations)
|
||||||
const cassetteProps =
|
const cassetteProps =
|
||||||
|
|
@ -61,7 +73,7 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
{
|
{
|
||||||
name: 'id',
|
name: 'id',
|
||||||
header: 'Machine',
|
header: 'Machine',
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine,
|
width: widthsByNumberOfCassettes[maxNumberOfCashUnits]?.machine,
|
||||||
view: it => machines.find(({ deviceId }) => deviceId === it).name,
|
view: it => machines.find(({ deviceId }) => deviceId === it).name,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
editable: false
|
editable: false
|
||||||
|
|
@ -77,7 +89,7 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
stripe: true,
|
stripe: true,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassette,
|
width: widthsByNumberOfCassettes[maxNumberOfCashUnits]?.cassette,
|
||||||
suffix: fiatCurrency,
|
suffix: fiatCurrency,
|
||||||
bold: bold,
|
bold: bold,
|
||||||
view: it => it,
|
view: it => it,
|
||||||
|
|
@ -94,6 +106,52 @@ const getElements = (machines, locale = {}, classes) => {
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
R.until(
|
||||||
|
R.gt(R.__, maxNumberOfStackers),
|
||||||
|
it => {
|
||||||
|
elements.push(
|
||||||
|
{
|
||||||
|
name: `stacker${it}f`,
|
||||||
|
header: `Stacker ${it}F`,
|
||||||
|
size: 'sm',
|
||||||
|
stripe: true,
|
||||||
|
textAlign: 'right',
|
||||||
|
width: widthsByNumberOfCassettes[maxNumberOfCashUnits]?.cassette,
|
||||||
|
suffix: fiatCurrency,
|
||||||
|
bold: bold,
|
||||||
|
view: it => it,
|
||||||
|
input: options?.length > 0 ? Autocomplete : NumberInput,
|
||||||
|
inputProps: cassetteProps,
|
||||||
|
doubleHeader: 'Denominations',
|
||||||
|
isHidden: machine =>
|
||||||
|
it >
|
||||||
|
machines.find(({ deviceId }) => deviceId === machine.id)
|
||||||
|
.numberOfStackers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `stacker${it}r`,
|
||||||
|
header: `Stacker ${it}R`,
|
||||||
|
size: 'sm',
|
||||||
|
stripe: true,
|
||||||
|
textAlign: 'right',
|
||||||
|
width: widthsByNumberOfCassettes[maxNumberOfCashUnits]?.cassette,
|
||||||
|
suffix: fiatCurrency,
|
||||||
|
bold: bold,
|
||||||
|
view: it => it,
|
||||||
|
input: options?.length > 0 ? Autocomplete : NumberInput,
|
||||||
|
inputProps: cassetteProps,
|
||||||
|
doubleHeader: 'Denominations',
|
||||||
|
isHidden: machine =>
|
||||||
|
it >
|
||||||
|
machines.find(({ deviceId }) => deviceId === machine.id)
|
||||||
|
.numberOfStackers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return R.add(1, it)
|
||||||
|
},
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
return elements
|
return elements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@ const CashCassettesFooter = ({
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const cashout = config && fromNamespace('cashOut')(config)
|
const cashout = config && fromNamespace('cashOut')(config)
|
||||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||||
const reducerFn = (
|
const cashoutReducerFn = (
|
||||||
acc,
|
acc,
|
||||||
{ cassette1, cassette2, cassette3, cassette4, id }
|
{ cashUnits: { cassette1, cassette2, cassette3, cassette4 }, id }
|
||||||
) => {
|
) => {
|
||||||
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
|
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
|
||||||
const cassette2Denomination = getCashoutSettings(id).cassette2 ?? 0
|
const cassette2Denomination = getCashoutSettings(id).cassette2 ?? 0
|
||||||
|
|
@ -38,11 +38,49 @@ const CashCassettesFooter = ({
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines))
|
const recyclerReducerFn = (
|
||||||
|
acc,
|
||||||
|
{
|
||||||
|
cashUnits: {
|
||||||
|
stacker1f,
|
||||||
|
stacker1r,
|
||||||
|
stacker2f,
|
||||||
|
stacker2r,
|
||||||
|
stacker3f,
|
||||||
|
stacker3r
|
||||||
|
},
|
||||||
|
id
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const stacker1fDenomination = getCashoutSettings(id).stacker1f ?? 0
|
||||||
|
const stacker1rDenomination = getCashoutSettings(id).stacker1r ?? 0
|
||||||
|
const stacker2fDenomination = getCashoutSettings(id).stacker2f ?? 0
|
||||||
|
const stacker2rDenomination = getCashoutSettings(id).stacker2r ?? 0
|
||||||
|
const stacker3fDenomination = getCashoutSettings(id).stacker3f ?? 0
|
||||||
|
const stacker3rDenomination = getCashoutSettings(id).stacker3r ?? 0
|
||||||
|
return [
|
||||||
|
(acc[0] += stacker1f * stacker1fDenomination),
|
||||||
|
(acc[1] += stacker1r * stacker1rDenomination),
|
||||||
|
(acc[0] += stacker2f * stacker2fDenomination),
|
||||||
|
(acc[1] += stacker2r * stacker2rDenomination),
|
||||||
|
(acc[0] += stacker3f * stacker3fDenomination),
|
||||||
|
(acc[1] += stacker3r * stacker3rDenomination)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalInRecyclers = R.sum(
|
||||||
|
R.reduce(recyclerReducerFn, [0, 0, 0, 0, 0, 0], machines)
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalInCassettes = R.sum(
|
||||||
|
R.reduce(cashoutReducerFn, [0, 0, 0, 0], machines)
|
||||||
|
)
|
||||||
|
|
||||||
const totalInCashBox = R.sum(R.map(it => it.fiat)(bills))
|
const totalInCashBox = R.sum(R.map(it => it.fiat)(bills))
|
||||||
|
|
||||||
const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0)
|
const total = new BigNumber(
|
||||||
|
totalInCassettes + totalInCashBox + totalInRecyclers
|
||||||
|
).toFormat(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.footerContainer}>
|
<div className={classes.footerContainer}>
|
||||||
|
|
@ -62,6 +100,13 @@ const CashCassettesFooter = ({
|
||||||
{numberToFiatAmount(totalInCassettes)} {currencyCode}
|
{numberToFiatAmount(totalInCassettes)} {currencyCode}
|
||||||
</Info1>
|
</Info1>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={classes.flex}>
|
||||||
|
<TxOutIcon className={classes.icon} />
|
||||||
|
<Info2 className={classes.iconLabel}>Recycle:</Info2>
|
||||||
|
<Info1 className={classes.valueDisplay}>
|
||||||
|
{numberToFiatAmount(totalInRecyclers)} {currencyCode}
|
||||||
|
</Info1>
|
||||||
|
</div>
|
||||||
<div className={classes.flex}>
|
<div className={classes.flex}>
|
||||||
<Info2 className={classes.iconLabel}>Total:</Info2>
|
<Info2 className={classes.iconLabel}>Total:</Info2>
|
||||||
<Info1 className={classes.valueDisplay}>
|
<Info1 className={classes.valueDisplay}>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as Yup from 'yup'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { MAX_NUMBER_OF_CASSETTES } from 'src/utils/constants'
|
import { MAX_NUMBER_OF_CASSETTES } from 'src/utils/constants'
|
||||||
|
import { cashUnitCapacity, modelPrettifier } from 'src/utils/machine'
|
||||||
import { defaultToZero } from 'src/utils/number'
|
import { defaultToZero } from 'src/utils/number'
|
||||||
|
|
||||||
import WizardSplash from './WizardSplash'
|
import WizardSplash from './WizardSplash'
|
||||||
|
|
@ -11,7 +12,6 @@ import WizardStep from './WizardStep'
|
||||||
|
|
||||||
const MODAL_WIDTH = 554
|
const MODAL_WIDTH = 554
|
||||||
const MODAL_HEIGHT = 535
|
const MODAL_HEIGHT = 535
|
||||||
const CASHBOX_DEFAULT_CAPACITY = 500
|
|
||||||
|
|
||||||
const CASSETTE_FIELDS = R.map(
|
const CASSETTE_FIELDS = R.map(
|
||||||
it => `cassette${it}`,
|
it => `cassette${it}`,
|
||||||
|
|
@ -37,8 +37,9 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
R.isEmpty(cashoutSettings) || !cashoutSettings?.active
|
R.isEmpty(cashoutSettings) || !cashoutSettings?.active
|
||||||
|
|
||||||
const numberOfCassettes = isCashOutDisabled ? 0 : machine.numberOfCassettes
|
const numberOfCassettes = isCashOutDisabled ? 0 : machine.numberOfCassettes
|
||||||
|
const numberOfStackers = machine.numberOfStackers
|
||||||
|
|
||||||
const LAST_STEP = numberOfCassettes + 1
|
const LAST_STEP = numberOfCassettes + numberOfStackers * 2 + 1
|
||||||
|
|
||||||
const title = `Update counts`
|
const title = `Update counts`
|
||||||
const isLastStep = step === LAST_STEP
|
const isLastStep = step === LAST_STEP
|
||||||
|
|
@ -104,11 +105,57 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
.integer()
|
.integer()
|
||||||
.required()
|
.required()
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(CASHBOX_DEFAULT_CAPACITY)
|
.max(
|
||||||
|
cashUnitCapacity[machine.model].cassette,
|
||||||
|
`${modelPrettifier[machine.model]} maximum cassette capacity is ${
|
||||||
|
cashUnitCapacity[machine.model].cassette
|
||||||
|
} bills`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const makeStackerSteps = R.pipe(
|
||||||
|
R.add(1),
|
||||||
|
R.range(1),
|
||||||
|
R.chain(i => [
|
||||||
|
{
|
||||||
|
type: `stacker ${i}f`,
|
||||||
|
schema: Yup.object().shape({
|
||||||
|
[`stacker${i}f`]: Yup.number()
|
||||||
|
.label('Bill count')
|
||||||
|
.positive()
|
||||||
|
.integer()
|
||||||
|
.required()
|
||||||
|
.min(0)
|
||||||
|
.max(
|
||||||
|
cashUnitCapacity[machine.model].stacker,
|
||||||
|
`${modelPrettifier[machine.model]} maximum stacker capacity is ${
|
||||||
|
cashUnitCapacity[machine.model].stacker
|
||||||
|
} bills`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: `stacker ${i}r`,
|
||||||
|
schema: Yup.object().shape({
|
||||||
|
[`stacker${i}r`]: Yup.number()
|
||||||
|
.label('Bill count')
|
||||||
|
.positive()
|
||||||
|
.integer()
|
||||||
|
.required()
|
||||||
|
.min(0)
|
||||||
|
.max(
|
||||||
|
cashUnitCapacity[machine.model].stacker,
|
||||||
|
`${modelPrettifier[machine.model]} maximum stacker capacity is ${
|
||||||
|
cashUnitCapacity[machine.model].stacker
|
||||||
|
} bills`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
const makeInitialValues = () =>
|
const makeInitialValues = () =>
|
||||||
!R.isEmpty(cashoutSettings)
|
!R.isEmpty(cashoutSettings)
|
||||||
? R.reduce(
|
? R.reduce(
|
||||||
|
|
@ -121,16 +168,19 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
)
|
)
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const steps = R.prepend(
|
const steps = R.pipe(
|
||||||
{
|
R.concat(makeStackerSteps(numberOfStackers)),
|
||||||
type: 'cashbox',
|
R.concat(makeCassetteSteps(numberOfCassettes)),
|
||||||
schema: Yup.object().shape({
|
R.concat([
|
||||||
wasCashboxEmptied: Yup.string().required('Select one option.')
|
{
|
||||||
}),
|
type: 'cashbox',
|
||||||
cashoutRequired: false
|
schema: Yup.object().shape({
|
||||||
},
|
wasCashboxEmptied: Yup.string().required('Select one option.')
|
||||||
makeCassetteSteps(numberOfCassettes)
|
}),
|
||||||
)
|
cashoutRequired: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)([])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -148,7 +198,6 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
name={machine?.name}
|
name={machine?.name}
|
||||||
machine={machine}
|
machine={machine}
|
||||||
cashoutSettings={cashoutSettings}
|
cashoutSettings={cashoutSettings}
|
||||||
cassetteCapacity={CASHBOX_DEFAULT_CAPACITY}
|
|
||||||
error={error}
|
error={error}
|
||||||
lastStep={isLastStep}
|
lastStep={isLastStep}
|
||||||
steps={steps}
|
steps={steps}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ import tejo4CassetteThree from 'src/styling/icons/cassettes/tejo/4-cassettes/4-c
|
||||||
import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg'
|
import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg'
|
||||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
import { comet, errorColor } from 'src/styling/variables'
|
import { comet, errorColor } from 'src/styling/variables'
|
||||||
|
import { cashUnitCapacity } from 'src/utils/machine'
|
||||||
import { numberToFiatAmount } from 'src/utils/number'
|
import { numberToFiatAmount } from 'src/utils/number'
|
||||||
|
import { startCase } from 'src/utils/string'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
content: {
|
content: {
|
||||||
|
|
@ -104,19 +106,41 @@ const styles = {
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const cassetesArtworks = (numberOfCassettes, step) =>
|
const CASHBOX_STEP = 1
|
||||||
[
|
|
||||||
|
const cassetesArtworks = (step, numberOfCassettes, numberOfStackers) => {
|
||||||
|
const cassetteStepsStart = CASHBOX_STEP + 1
|
||||||
|
return [
|
||||||
|
[cassetteOne],
|
||||||
[cassetteOne, cassetteTwo],
|
[cassetteOne, cassetteTwo],
|
||||||
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
|
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
|
||||||
[tejo4CassetteOne, tejo4CassetteTwo, tejo4CassetteThree, tejo4CassetteFour]
|
[tejo4CassetteOne, tejo4CassetteTwo, tejo4CassetteThree, tejo4CassetteFour]
|
||||||
][numberOfCassettes - 2][step - 2]
|
][numberOfCassettes - cassetteStepsStart + 1][step - cassetteStepsStart]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCashUnitFieldName = (step, numberOfCassettes, numberOfStackers) => {
|
||||||
|
if (step === CASHBOX_STEP) return { name: 'cashbox', category: 'cashbox' }
|
||||||
|
const cassetteStepsStart = CASHBOX_STEP + 1
|
||||||
|
if (step < cassetteStepsStart + numberOfCassettes)
|
||||||
|
return {
|
||||||
|
name: `cassette${step - cassetteStepsStart + 1}`,
|
||||||
|
category: 'cassette'
|
||||||
|
}
|
||||||
|
const stackerStepsStart = CASHBOX_STEP + numberOfCassettes + 1
|
||||||
|
if (step < stackerStepsStart + numberOfStackers * 2)
|
||||||
|
return {
|
||||||
|
name: `stacker${Math.ceil((step - stackerStepsStart + 1) / 2)}${
|
||||||
|
(step - stackerStepsStart) % 2 === 0 ? 'f' : 'r'
|
||||||
|
}`,
|
||||||
|
category: 'stacker'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WizardStep = ({
|
const WizardStep = ({
|
||||||
step,
|
step,
|
||||||
name,
|
name,
|
||||||
machine,
|
machine,
|
||||||
cashoutSettings,
|
cashoutSettings,
|
||||||
cassetteCapacity,
|
|
||||||
error,
|
error,
|
||||||
lastStep,
|
lastStep,
|
||||||
steps,
|
steps,
|
||||||
|
|
@ -133,16 +157,20 @@ const WizardStep = ({
|
||||||
{ display: 'No', code: 'NO' }
|
{ display: 'No', code: 'NO' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const cassetteField = `cassette${step - 1}`
|
|
||||||
const numberOfCassettes = machine.numberOfCassettes
|
const numberOfCassettes = machine.numberOfCassettes
|
||||||
const originalCassetteCount = machine?.[cassetteField]
|
const numberOfStackers = machine.numberOfStackers
|
||||||
const cassetteDenomination = cashoutSettings?.[cassetteField]
|
const {
|
||||||
|
name: cashUnitField,
|
||||||
|
category: cashUnitCategory
|
||||||
|
} = getCashUnitFieldName(step, numberOfCassettes, numberOfStackers)
|
||||||
|
const originalCashUnitCount = machine?.cashUnits?.[cashUnitField]
|
||||||
|
const cashUnitDenomination = cashoutSettings?.[cashUnitField]
|
||||||
|
|
||||||
const cassetteCount = values => values[cassetteField] || originalCassetteCount
|
const cassetteCount = values => values[cashUnitField] || originalCashUnitCount
|
||||||
const cassetteTotal = values => cassetteCount(values) * cassetteDenomination
|
const cassetteTotal = values => cassetteCount(values) * cashUnitDenomination
|
||||||
const getPercentage = R.pipe(
|
const getPercentage = R.pipe(
|
||||||
cassetteCount,
|
cassetteCount,
|
||||||
count => 100 * (count / cassetteCapacity),
|
count => 100 * (count / cashUnitCapacity[machine.model][cashUnitCategory]),
|
||||||
R.clamp(0, 100)
|
R.clamp(0, 100)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -161,7 +189,7 @@ const WizardStep = ({
|
||||||
initialValues={{ wasCashboxEmptied: '' }}
|
initialValues={{ wasCashboxEmptied: '' }}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
validationSchema={steps[0].schema}>
|
validationSchema={steps[0].schema}>
|
||||||
{({ values, errors }) => (
|
{({ errors }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div
|
<div
|
||||||
className={classnames(classes.horizontalAlign, classes.form)}>
|
className={classnames(classes.horizontalAlign, classes.form)}>
|
||||||
|
|
@ -236,7 +264,11 @@ const WizardStep = ({
|
||||||
<img
|
<img
|
||||||
className={classes.stepImage}
|
className={classes.stepImage}
|
||||||
alt="cassette"
|
alt="cassette"
|
||||||
src={cassetesArtworks(numberOfCassettes, step)}></img>
|
src={cassetesArtworks(
|
||||||
|
step,
|
||||||
|
numberOfCassettes,
|
||||||
|
numberOfStackers
|
||||||
|
)}></img>
|
||||||
<div className={classes.formWrapper}>
|
<div className={classes.formWrapper}>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
|
|
@ -257,7 +289,13 @@ const WizardStep = ({
|
||||||
<H4
|
<H4
|
||||||
className={classes.cassetteFormTitleContent}
|
className={classes.cassetteFormTitleContent}
|
||||||
noMargin>
|
noMargin>
|
||||||
Cash cassette {step - 1} (dispenser)
|
{startCase(cashUnitField)} (
|
||||||
|
{R.includes('cassette', cashUnitField)
|
||||||
|
? `dispenser`
|
||||||
|
: R.includes('stacker', cashUnitField)
|
||||||
|
? `recycler`
|
||||||
|
: ``}
|
||||||
|
)
|
||||||
</H4>
|
</H4>
|
||||||
</div>
|
</div>
|
||||||
<Cashbox
|
<Cashbox
|
||||||
|
|
@ -276,13 +314,13 @@ const WizardStep = ({
|
||||||
component={NumberInput}
|
component={NumberInput}
|
||||||
decimalPlaces={0}
|
decimalPlaces={0}
|
||||||
width={50}
|
width={50}
|
||||||
placeholder={originalCassetteCount.toString()}
|
placeholder={originalCashUnitCount.toString()}
|
||||||
name={cassetteField}
|
name={cashUnitField}
|
||||||
className={classes.cashboxBills}
|
className={classes.cashboxBills}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<P>
|
<P>
|
||||||
{cassetteDenomination} {fiatCurrency} bills loaded
|
{cashUnitDenomination} {fiatCurrency} bills loaded
|
||||||
</P>
|
</P>
|
||||||
</div>
|
</div>
|
||||||
<P noMargin className={classes.fiatTotal}>
|
<P noMargin className={classes.fiatTotal}>
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,6 @@ const getElements = (
|
||||||
header: `Stacker ${it}F`,
|
header: `Stacker ${it}F`,
|
||||||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||||
stripe: true,
|
stripe: true,
|
||||||
doubleHeader: 'Cash recycling',
|
|
||||||
view: (_, { id, cashUnits }) => (
|
view: (_, { id, cashUnits }) => (
|
||||||
<CashOut
|
<CashOut
|
||||||
className={classes.cashbox}
|
className={classes.cashbox}
|
||||||
|
|
@ -187,7 +186,6 @@ const getElements = (
|
||||||
header: `Stacker ${it}R`,
|
header: `Stacker ${it}R`,
|
||||||
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
width: widthsByCashUnits[getMaxNumberOfCashUnits(machines)]?.cassette,
|
||||||
stripe: true,
|
stripe: true,
|
||||||
doubleHeader: 'Cash recycling',
|
|
||||||
view: (_, { id, cashUnits }) => (
|
view: (_, { id, cashUnits }) => (
|
||||||
<CashOut
|
<CashOut
|
||||||
className={classes.cashbox}
|
className={classes.cashbox}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const CASH_IN_KEY = 'fiatBalanceAlertsCashIn'
|
const CASH_IN_KEY = 'fiatBalanceAlertsCashIn'
|
||||||
const CASH_OUT_KEY = 'fiatBalanceAlertsCashOut'
|
const CASH_OUT_KEY = 'fiatBalanceAlertsCashOut'
|
||||||
|
const RECYCLER_STACKER_KEY = 'fiatBalanceAlertsRecyclerStacker'
|
||||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||||
|
const DEFAULT_NUMBER_OF_STACKERS = 0
|
||||||
const notesMin = 0
|
const notesMin = 0
|
||||||
const notesMax = 9999999
|
const notesMax = 9999999
|
||||||
|
|
||||||
|
|
@ -39,6 +41,11 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
DEFAULT_NUMBER_OF_CASSETTES
|
DEFAULT_NUMBER_OF_CASSETTES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxNumberOfStackers = Math.max(
|
||||||
|
...R.map(it => it.numberOfStackers, machines),
|
||||||
|
DEFAULT_NUMBER_OF_STACKERS
|
||||||
|
)
|
||||||
|
|
||||||
const schema = Yup.object().shape({
|
const schema = Yup.object().shape({
|
||||||
cashInAlertThreshold: Yup.number()
|
cashInAlertThreshold: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
|
|
@ -160,6 +167,76 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
<Form className={classes.form}>
|
||||||
|
<PromptWhenDirty />
|
||||||
|
<Header
|
||||||
|
title="Cash recycling (stackers)"
|
||||||
|
editing={isEditing(RECYCLER_STACKER_KEY)}
|
||||||
|
disabled={isDisabled(RECYCLER_STACKER_KEY)}
|
||||||
|
setEditing={it => setEditing(RECYCLER_STACKER_KEY, it)}
|
||||||
|
/>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
{R.chain(
|
||||||
|
it => [
|
||||||
|
<>
|
||||||
|
<div className={classes.row}>
|
||||||
|
<Cashbox
|
||||||
|
labelClassName={classes.cashboxLabel}
|
||||||
|
emptyPartClassName={classes.cashboxEmptyPart}
|
||||||
|
percent={
|
||||||
|
values[`fillingPercentageStacker${it + 1}f`] ??
|
||||||
|
data[`stacker${it + 1}f`]
|
||||||
|
}
|
||||||
|
applyColorVariant
|
||||||
|
applyFiatBalanceAlertsStyling
|
||||||
|
omitInnerPercentage
|
||||||
|
cashOut
|
||||||
|
/>
|
||||||
|
<div className={classes.col2}>
|
||||||
|
<TL2 className={classes.title}>Stacker {it + 1}F</TL2>
|
||||||
|
<EditableNumber
|
||||||
|
label="Alert me under"
|
||||||
|
name={`fillingPercentageStacker${it + 1}f`}
|
||||||
|
editing={isEditing(RECYCLER_STACKER_KEY)}
|
||||||
|
displayValue={x => (x === '' ? '-' : x)}
|
||||||
|
decoration="%"
|
||||||
|
width={fieldWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
<>
|
||||||
|
<div className={classes.row}>
|
||||||
|
<Cashbox
|
||||||
|
labelClassName={classes.cashboxLabel}
|
||||||
|
emptyPartClassName={classes.cashboxEmptyPart}
|
||||||
|
percent={
|
||||||
|
values[`fillingPercentageStacker${it + 1}r`] ??
|
||||||
|
data[`stacker${it + 1}r`]
|
||||||
|
}
|
||||||
|
applyColorVariant
|
||||||
|
applyFiatBalanceAlertsStyling
|
||||||
|
omitInnerPercentage
|
||||||
|
cashOut
|
||||||
|
/>
|
||||||
|
<div className={classes.col2}>
|
||||||
|
<TL2 className={classes.title}>Stacker {it + 1}R</TL2>
|
||||||
|
<EditableNumber
|
||||||
|
label="Alert me under"
|
||||||
|
name={`fillingPercentageStacker${it + 1}r`}
|
||||||
|
editing={isEditing(RECYCLER_STACKER_KEY)}
|
||||||
|
displayValue={x => (x === '' ? '-' : x)}
|
||||||
|
decoration="%"
|
||||||
|
width={fieldWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
],
|
||||||
|
R.times(R.identity, maxNumberOfStackers)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,26 @@ const modelPrettifier = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cashUnitCapacity = {
|
const cashUnitCapacity = {
|
||||||
|
grandola: {
|
||||||
|
cashbox: 2000,
|
||||||
|
recycler: 2800
|
||||||
|
},
|
||||||
|
aveiro: {
|
||||||
|
cashbox: 1500,
|
||||||
|
stacker: 60,
|
||||||
|
cassette: 500
|
||||||
|
},
|
||||||
tejo: {
|
tejo: {
|
||||||
|
// TODO: add support for the different cashbox configuration in Tejo
|
||||||
cashbox: 1000,
|
cashbox: 1000,
|
||||||
cassette: 500
|
cassette: 500
|
||||||
},
|
},
|
||||||
aveiro: {
|
gaia: {
|
||||||
cashbox: 500,
|
cashbox: 600
|
||||||
cassette: 200,
|
},
|
||||||
stacker: 60
|
sintra: {
|
||||||
|
cashbox: 1000,
|
||||||
|
cassette: 500
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue