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([])
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) ? ({
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
|
|||
137
lib/plugins.js
137
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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue