diff --git a/lib/cash-in/cash-in-atomic.js b/lib/cash-in/cash-in-atomic.js index e3441a76..56a810ad 100644 --- a/lib/cash-in/cash-in-atomic.js +++ b/lib/cash-in/cash-in-atomic.js @@ -41,7 +41,7 @@ function insertNewBills (t, billRows, machineTx) { if (_.isEmpty(bills)) return Promise.resolve([]) 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 deviceID = machineTx.deviceId const sql2 = `update devices set cashbox = cashbox + $2 diff --git a/lib/constants.js b/lib/constants.js index 280c300a..2853ff17 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -13,7 +13,29 @@ const anonymousCustomer = { 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_MAXIMUM_AMOUNT_OF_CASSETTES = 4 @@ -39,7 +61,7 @@ const BALANCE_FETCH_SPEED_MULTIPLIER = { module.exports = { anonymousCustomer, - CASSETTE_MAX_CAPACITY, + CASH_UNIT_CAPACITY, AUTHENTICATOR_ISSUER_ENTITY, AUTH_TOKEN_EXPIRATION_TIME, REGISTRATION_TOKEN_EXPIRATION_TIME, diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index 82d27164..d9e20e4b 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -167,13 +167,25 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => { )(cassettes) : 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) return _.flow( - _.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'coins', 'rates']), + _.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'stackers', 'coins', 'rates']), _.update('cassettes', massageCassettes), + _.update('stackers', massageStackers), + /* [{ 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 */ /* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */ - ({ areThereAvailablePromoCodes, balances, cassettes, coins, rates }) => ({ + ({ areThereAvailablePromoCodes, balances, cassettes, stackers, coins, rates }) => ({ areThereAvailablePromoCodes, cassettes, + stackers, coins: _.flow( _.reduce( (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) => plugins(settings, deviceId) .pollQueries() - .then(pq => ({ - static: staticConfig({ - currentConfigVersion, - deviceId, - deviceName, - pq, - settings, - }), - dynamic: dynamicConfig({ - deviceId, - operatorId, - pid, - pq, - settings, - }), - })) + .then(pq => { + console.log(pq) + return { + static: staticConfig({ + currentConfigVersion, + deviceId, + deviceName, + pq, + settings, + }), + dynamic: dynamicConfig({ + deviceId, + operatorId, + pid, + pq, + settings, + }), + } + }) const massageTerms = terms => (terms.active && terms.text) ? ({ diff --git a/lib/graphql/types.js b/lib/graphql/types.js index d09d0755..26bac048 100644 --- a/lib/graphql/types.js +++ b/lib/graphql/types.js @@ -169,14 +169,25 @@ type PhysicalCassette { count: Int! } +type PhysicalStacker { + denomination: Int! + count: Int! +} + type Cassettes { physical: [PhysicalCassette!]! virtual: [Int!]! } +type Stackers { + physical: [PhysicalStacker!]! + virtual: [Int!]! +} + type DynamicConfig { areThereAvailablePromoCodes: Boolean! cassettes: Cassettes + stackers: Stackers coins: [DynamicCoinValues!]! reboot: Boolean! shutdown: Boolean! diff --git a/lib/notifier/codes.js b/lib/notifier/codes.js index 105437e8..cb7da476 100644 --- a/lib/notifier/codes.js +++ b/lib/notifier/codes.js @@ -15,6 +15,8 @@ const CODES_DISPLAY = { HIGH_CRYPTO_BALANCE: 'High Crypto Balance', CASH_BOX_FULL: 'Cash box full', LOW_CASH_OUT: 'Low Cash-out', + LOW_RECYCLER_STACKER: 'Low Recycler Stacker', + HIGH_RECYCLER_STACKER: 'High Recycler Stacker', CASHBOX_REMOVED: 'Cashbox removed' } diff --git a/lib/plugins.js b/lib/plugins.js index adeaaca8..384a5df3 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -27,7 +27,7 @@ const loyalty = require('./loyalty') const transactionBatching = require('./tx-batching') 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') @@ -147,27 +147,63 @@ function plugins (settings, deviceId) { 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) { const cashOutConfig = configManager.getCashOut(deviceId, settings.config) if (!cashOutConfig.active) return Promise.resolve() return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)]) - .then(([rec, _redeemableTxs]) => { + .then(([_cassettes, _redeemableTxs]) => { const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs) const denominations = [] _.forEach(it => { denominations.push(cashOutConfig[`cassette${it + 1}`]) - }, _.times(_.identity(), rec.numberOfCassettes)) + }, _.times(_.identity(), _cassettes.numberOfCassettes)) const virtualCassettes = [Math.max(...denominations) * 2] const counts = argv.cassettes ? 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!') } @@ -177,7 +213,7 @@ function plugins (settings, deviceId) { denomination: parseInt(denominations[it], 10), count: parseInt(counts[it], 10) }) - }, _.times(_.identity(), rec.numberOfCassettes)) + }, _.times(_.identity(), _cassettes.numberOfCassettes)) try { 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 () { const sql = `select id from user_config where type=$1 @@ -240,6 +321,7 @@ function plugins (settings, deviceId) { return Promise.all([ buildAvailableCassettes(), + buildAvailableStackers(), fetchCurrentConfigVersion(), millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)), loyalty.getNumberOfAvailablePromoCodes(), @@ -250,6 +332,7 @@ function plugins (settings, deviceId) { ]) .then(([ cassettes, + stackers, configVersion, timezone, numberOfAvailablePromoCodes, @@ -273,6 +356,7 @@ function plugins (settings, deviceId) { return { cassettes, + stackers, rates: buildRates(tickers), balances: buildBalances(balances), coins, @@ -652,7 +736,10 @@ function plugins (settings, deviceId) { const denomination3f = cashOutConfig.stacker3f const denomination3r = cashOutConfig.stacker3r 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) @@ -667,7 +754,7 @@ function plugins (settings, deviceId) { } : 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', cassette: 1, @@ -679,7 +766,7 @@ function plugins (settings, deviceId) { } : 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', cassette: 2, @@ -691,7 +778,7 @@ function plugins (settings, deviceId) { } : 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', cassette: 3, @@ -703,7 +790,7 @@ function plugins (settings, deviceId) { } : 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', cassette: 4, @@ -715,9 +802,9 @@ function plugins (settings, deviceId) { } : 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, machineName, deviceId: device.deviceId, @@ -727,9 +814,9 @@ function plugins (settings, deviceId) { } : 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, machineName, deviceId: device.deviceId, @@ -739,21 +826,21 @@ function plugins (settings, deviceId) { } : 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, machineName, deviceId: device.deviceId, - notes: device.cashUnits.stacker1f, - denomination: denomination1f, + notes: device.cashUnits.stacker2f, + denomination: denomination2f, fiatCode } : 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, machineName, deviceId: device.deviceId, @@ -763,9 +850,9 @@ function plugins (settings, deviceId) { } : 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, machineName, deviceId: device.deviceId, @@ -775,9 +862,9 @@ function plugins (settings, deviceId) { } : 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, machineName, deviceId: device.deviceId, diff --git a/lib/postgresql_interface.js b/lib/postgresql_interface.js index 1ae15ae1..17369a1d 100644 --- a/lib/postgresql_interface.js +++ b/lib/postgresql_interface.js @@ -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 // last known state. exports.machineEvent = function machineEvent (rec) { diff --git a/migrations/1619968992683-fiat-balance-notification-to-percent.js b/migrations/1619968992683-fiat-balance-notification-to-percent.js index 713761d7..a3d6b500 100644 --- a/migrations/1619968992683-fiat-balance-notification-to-percent.js +++ b/migrations/1619968992683-fiat-balance-notification-to-percent.js @@ -1,6 +1,6 @@ const _ = require('lodash/fp') const { migrationSaveConfig, loadLatest } = require('../lib/new-settings-loader') -const { CASSETTE_MAX_CAPACITY } = require('../lib/constants') +const CASSETTE_MAX_CAPACITY = 500 exports.up = function (next) { return loadLatest() diff --git a/migrations/1681428616990-aveiro-recycler-settings.js b/migrations/1681428616990-aveiro-recycler-settings.js index cb000e80..86ea6dd5 100644 --- a/migrations/1681428616990-aveiro-recycler-settings.js +++ b/migrations/1681428616990-aveiro-recycler-settings.js @@ -47,7 +47,9 @@ exports.up = function (next) { ADD COLUMN denomination_2f INTEGER, ADD COLUMN denomination_2r 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) diff --git a/new-lamassu-admin/src/pages/Cashout/Wizard.js b/new-lamassu-admin/src/pages/Cashout/Wizard.js index 5243f0b8..bb646758 100644 --- a/new-lamassu-admin/src/pages/Cashout/Wizard.js +++ b/new-lamassu-admin/src/pages/Cashout/Wizard.js @@ -17,7 +17,8 @@ const MODAL_WIDTH = 554 const MODAL_HEIGHT = 520 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({ step: 0, config: { active: true } @@ -83,30 +84,68 @@ const Wizard = ({ machine, locale, onClose, save, error }) => { } } ], - R.range( - machine.numberOfCassettes + 1, - machine.numberOfCassettes + machine.numberOfStackers + 1 - ) + R.range(1, machine.numberOfStackers + 1) ) ) const schema = () => Yup.object().shape({ - cassette1: Yup.number().required(), + cassette1: + machine.numberOfCassettes >= 1 && step >= 1 + ? Yup.number().required() + : Yup.number() + .transform(transformNumber) + .nullable(), cassette2: - machine.numberOfCassettes > 1 && step >= 2 + machine.numberOfCassettes >= 2 && step >= 2 ? Yup.number().required() : Yup.number() .transform(transformNumber) .nullable(), cassette3: - machine.numberOfCassettes > 2 && step >= 3 + machine.numberOfCassettes >= 3 && step >= 3 ? Yup.number().required() : Yup.number() .transform(transformNumber) .nullable(), 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() .transform(transformNumber) diff --git a/new-lamassu-admin/src/pages/Cashout/helper.js b/new-lamassu-admin/src/pages/Cashout/helper.js index c80216d1..0539f165 100644 --- a/new-lamassu-admin/src/pages/Cashout/helper.js +++ b/new-lamassu-admin/src/pages/Cashout/helper.js @@ -9,9 +9,13 @@ import { CURRENCY_MAX } from 'src/utils/constants' import { transformNumber } from 'src/utils/number' const widthsByNumberOfCassettes = { - 2: { machine: 300, cassette: 225, zeroConf: 200 }, - 3: { machine: 210, cassette: 180, zeroConf: 200 }, - 4: { machine: 200, cassette: 150, zeroConf: 150 } + 2: { machine: 320, cassette: 315 }, + 3: { machine: 305, cassette: 215 }, + 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({ @@ -45,6 +49,14 @@ const getElements = (machines, locale = {}, classes) => { ...R.map(it => it.numberOfCassettes, machines), 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 cassetteProps = @@ -61,7 +73,7 @@ const getElements = (machines, locale = {}, classes) => { { name: 'id', header: 'Machine', - width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.machine, + width: widthsByNumberOfCassettes[maxNumberOfCashUnits]?.machine, view: it => machines.find(({ deviceId }) => deviceId === it).name, size: 'sm', editable: false @@ -77,7 +89,7 @@ const getElements = (machines, locale = {}, classes) => { size: 'sm', stripe: true, textAlign: 'right', - width: widthsByNumberOfCassettes[maxNumberOfCassettes]?.cassette, + width: widthsByNumberOfCassettes[maxNumberOfCashUnits]?.cassette, suffix: fiatCurrency, bold: bold, view: it => it, @@ -94,6 +106,52 @@ const getElements = (machines, locale = {}, classes) => { 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 } diff --git a/new-lamassu-admin/src/pages/Maintenance/CashCassettesFooter.js b/new-lamassu-admin/src/pages/Maintenance/CashCassettesFooter.js index 4f619c2a..cc417cc4 100644 --- a/new-lamassu-admin/src/pages/Maintenance/CashCassettesFooter.js +++ b/new-lamassu-admin/src/pages/Maintenance/CashCassettesFooter.js @@ -22,9 +22,9 @@ const CashCassettesFooter = ({ const classes = useStyles() const cashout = config && fromNamespace('cashOut')(config) const getCashoutSettings = id => fromNamespace(id)(cashout) - const reducerFn = ( + const cashoutReducerFn = ( acc, - { cassette1, cassette2, cassette3, cassette4, id } + { cashUnits: { cassette1, cassette2, cassette3, cassette4 }, id } ) => { const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 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 total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0) + const total = new BigNumber( + totalInCassettes + totalInCashBox + totalInRecyclers + ).toFormat(0) return (