feat: create a new batching function which pairs with machine value updates

refactor: abstract amount of cassettes from the cassette wizard

fix: dashboard cassettes
This commit is contained in:
Sérgio Salgado 2021-11-29 23:24:04 +00:00
parent f14674c4f3
commit ec90776d2a
9 changed files with 180 additions and 105 deletions

View file

@ -1,3 +1,4 @@
const constants = require('./constants')
const db = require('./db') const db = require('./db')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const uuid = require('uuid') const uuid = require('uuid')
@ -18,6 +19,34 @@ function createCashboxBatch (deviceId, cashboxCount) {
}) })
} }
function updateMachineWithBatch (machineContext, oldCashboxCount) {
const isValidContext = _.has(['deviceId', 'cashbox', 'cassettes'], machineContext)
const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(machineContext.cassettes))
if (!isValidContext && !isCassetteAmountWithinRange)
throw new Error('Insufficient info to create a new cashbox batch')
if (_.isEqual(0, oldCashboxCount)) throw new Error('Cashbox is empty. Cashbox batch could not be created.')
return db.tx(t => {
const deviceId = machineContext.deviceId
const batchId = uuid.v4()
const q1 = t.none(`INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-in-empty')`, [batchId, deviceId])
const q2 = t.none(`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
cash_in_txs.device_id = $2 AND
bills.cashbox_batch_id IS NULL`, [batchId, deviceId])
const q3 = t.none(`UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 WHERE device_id=$6`, [
machineContext.cashbox,
machineContext.cassettes[0],
machineContext.cassettes[1],
machineContext.cassettes[2],
machineContext.cassettes[3],
machineContext.deviceId
])
return t.batch([q1, q2, q3])
})
}
function getBatches () { function getBatches () {
const sql = ` const sql = `
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
@ -39,4 +68,4 @@ function getBillsByBatchId (id) {
return db.any(sql, [id]) return db.any(sql, [id])
} }
module.exports = { createCashboxBatch, getBatches, getBillsByBatchId, editBatchById } module.exports = { createCashboxBatch, updateMachineWithBatch, getBatches, getBillsByBatchId, editBatchById }

View file

@ -7,6 +7,8 @@ const anonymousCustomer = {
const CASSETTE_MAX_CAPACITY = 500 const CASSETTE_MAX_CAPACITY = 500
const CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES = 2
const CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES = 4
const AUTHENTICATOR_ISSUER_ENTITY = 'Lamassu' const AUTHENTICATOR_ISSUER_ENTITY = 'Lamassu'
const AUTH_TOKEN_EXPIRATION_TIME = '30 minutes' const AUTH_TOKEN_EXPIRATION_TIME = '30 minutes'
const REGISTRATION_TOKEN_EXPIRATION_TIME = '30 minutes' const REGISTRATION_TOKEN_EXPIRATION_TIME = '30 minutes'
@ -30,5 +32,7 @@ module.exports = {
USER_SESSIONS_TABLE_NAME, USER_SESSIONS_TABLE_NAME,
USER_SESSIONS_CLEAR_INTERVAL, USER_SESSIONS_CLEAR_INTERVAL,
CASH_OUT_DISPENSE_READY, CASH_OUT_DISPENSE_READY,
CONFIRMATION_CODE CONFIRMATION_CODE,
CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES,
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES
} }

View file

@ -3,6 +3,7 @@ const pgp = require('pg-promise')()
const axios = require('axios') const axios = require('axios')
const uuid = require('uuid') const uuid = require('uuid')
const batching = require('./cashbox-batches')
const db = require('./db') const db = require('./db')
const pairing = require('./pairing') const pairing = require('./pairing')
const { checkPings, checkStuckScreen } = require('./notifier') const { checkPings, checkStuckScreen } = require('./notifier')
@ -140,8 +141,15 @@ function emptyCashInBills (rec) {
} }
function setCassetteBills (rec) { function setCassetteBills (rec) {
const sql = 'update devices set cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 where device_id=$6' return db.oneOrNone(`SELECT cashbox FROM devices WHERE device_id=$1 LIMIT 1`, [rec.deviceId])
return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId]) .then(oldCashboxValue => {
if (_.isNil(oldCashboxValue) || rec.cashbox === oldCashboxValue.cashbox) {
const sql = 'UPDATE devices SET cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 WHERE device_id=$6'
return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId])
}
return batching.updateMachineWithBatch({ ...rec, oldCashboxValue })
})
} }
function unpair (rec) { function unpair (rec) {

View file

@ -18,7 +18,8 @@ const resolvers = {
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId) machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId)
}, },
Mutation: { Mutation: {
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context) machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context]) =>
machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context)
} }
} }

View file

@ -2,12 +2,15 @@ import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React, { useState } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import { IconButton } from 'src/components/buttons'
import { Table as EditableTable } from 'src/components/editableTable' import { Table as EditableTable } from 'src/components/editableTable'
import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox' import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik' import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
import Wizard from 'src/pages/Maintenance/Wizard/Wizard'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import styles from './Cassettes.styles' import styles from './Cassettes.styles'
@ -82,6 +85,8 @@ const SET_CASSETTE_BILLS = gql`
const CashCassettes = ({ machine, config, refetchData }) => { const CashCassettes = ({ machine, config, refetchData }) => {
const classes = useStyles() const classes = useStyles()
const [wizard, setWizard] = useState(false)
const cashout = config && fromNamespace('cashOut')(config) const cashout = config && fromNamespace('cashOut')(config)
const locale = config && fromNamespace('locale')(config) const locale = config && fromNamespace('locale')(config)
const fillingPercentageSettings = const fillingPercentageSettings =
@ -147,39 +152,62 @@ const CashCassettes = ({ machine, config, refetchData }) => {
1 1
) )
elements.push({
name: 'edit',
header: 'Edit',
width: 87,
view: () => {
return (
<IconButton
onClick={() => {
setWizard(true)
}}>
<EditIcon />
</IconButton>
)
}
})
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, { const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
refetchQueries: () => refetchData() refetchQueries: () => refetchData()
}) })
const onSave = ( const onSave = (_, cashbox, cassettes) =>
...[, { deviceId, cashbox, cassette1, cassette2, cassette3, cassette4 }] setCassetteBills({
) => {
return setCassetteBills({
variables: { variables: {
action: 'setCassetteBills', action: 'setCassetteBills',
deviceId: deviceId, deviceId: machine.deviceId,
cashbox, cashbox,
cassette1, ...cassettes
cassette2,
cassette3,
cassette4
} }
}) })
}
return machine.name ? ( return machine.name ? (
<EditableTable <>
error={error?.message} <EditableTable
enableEdit error={error?.message}
editWidth={widthsByNumberOfCassettes[numberOfCassettes].editWidth} editWidth={widthsByNumberOfCassettes[numberOfCassettes].editWidth}
stripeWhen={isCashOutDisabled} stripeWhen={isCashOutDisabled}
disableRowEdit={isCashOutDisabled} disableRowEdit={isCashOutDisabled}
name="cashboxes" name="cashboxes"
elements={elements} elements={elements}
data={[machine] || []} data={[machine]}
save={onSave} save={onSave}
validationSchema={ValidationSchema} validationSchema={ValidationSchema}
/> />
{wizard && (
<Wizard
machine={machine}
cashoutSettings={getCashoutSettings(machine.deviceId)}
onClose={() => {
setWizard(false)
}}
error={error?.message}
save={onSave}
locale={locale}
/>
)}
</>
) : null ) : null
} }

View file

@ -54,7 +54,7 @@ const getMachineID = path => path.slice(path.lastIndexOf('/') + 1)
const Machines = () => { const Machines = () => {
const location = useLocation() const location = useLocation()
const { data, refetch } = useQuery(GET_INFO, { const { data, loading, refetch } = useQuery(GET_INFO, {
variables: { variables: {
deviceId: getMachineID(location.pathname) deviceId: getMachineID(location.pathname)
} }
@ -70,50 +70,52 @@ const Machines = () => {
const machineID = R.path(['deviceId'])(machine) ?? null const machineID = R.path(['deviceId'])(machine) ?? null
return ( return (
<Grid container className={classes.grid}> !loading && (
<Grid item xs={3}> <Grid container className={classes.grid}>
<Grid item xs={12}> <Grid item xs={3}>
<div className={classes.breadcrumbsContainer}> <Grid item xs={12}>
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}> <div className={classes.breadcrumbsContainer}>
<Link to="/dashboard" className={classes.breadcrumbLink}> <Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
<Label3 noMargin className={classes.subtitle}> <Link to="/dashboard" className={classes.breadcrumbLink}>
Dashboard <Label3 noMargin className={classes.subtitle}>
</Label3> Dashboard
</Link> </Label3>
<TL2 noMargin className={classes.subtitle}> </Link>
{machineName} <TL2 noMargin className={classes.subtitle}>
</TL2> {machineName}
</Breadcrumbs> </TL2>
<Overview data={machine} onActionSuccess={refetch} /> </Breadcrumbs>
<Overview data={machine} onActionSuccess={refetch} />
</div>
</Grid>
</Grid>
<Grid item xs={9}>
<div className={classes.content}>
<div
className={classnames(classes.detailItem, classes.detailsMargin)}>
<TL1 className={classes.subtitle}>{'Details'}</TL1>
<Details data={machine} timezone={timezone} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
<Cassettes
refetchData={refetch}
machine={machine}
config={config ?? false}
/>
</div>
<div className={classes.transactionsItem}>
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
<Transactions id={machineID} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
<Commissions name={'commissions'} id={machineID} />
</div>
</div> </div>
</Grid> </Grid>
</Grid> </Grid>
<Grid item xs={9}> )
<div className={classes.content}>
<div
className={classnames(classes.detailItem, classes.detailsMargin)}>
<TL1 className={classes.subtitle}>{'Details'}</TL1>
<Details data={machine} timezone={timezone} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
<Cassettes
refetchData={refetch}
machine={machine}
config={config ?? false}
/>
</div>
<div className={classes.transactionsItem}>
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
<Transactions id={machineID} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
<Commissions name={'commissions'} id={machineID} />
</div>
</div>
</Grid>
</Grid>
) )
} }

View file

@ -62,14 +62,6 @@ const ValidationSchema = Yup.object().shape({
.max(500) .max(500)
}) })
const CREATE_BATCH = gql`
mutation createBatch($deviceId: ID, $cashboxCount: Int) {
createBatch(deviceId: $deviceId, cashboxCount: $cashboxCount) {
id
}
}
`
const GET_MACHINES_AND_CONFIG = gql` const GET_MACHINES_AND_CONFIG = gql`
query getData { query getData {
machines { machines {
@ -146,7 +138,6 @@ const CashCassettes = () => {
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, { const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
}) })
const [createBatch] = useMutation(CREATE_BATCH)
const [saveConfig] = useMutation(SAVE_CONFIG, { const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: () => setEditingSchema(false), onCompleted: () => setEditingSchema(false),
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
@ -163,32 +154,17 @@ const CashCassettes = () => {
...R.map(it => it.numberOfCassettes, machines), ...R.map(it => it.numberOfCassettes, machines),
0 0
) )
const cashboxCounts = R.reduce(
(ret, m) => R.assoc(m.id, m.cashbox, ret),
{},
machines
)
const onSave = (id, cashbox, cassette1, cassette2, cassette3, cassette4) => { const getCashoutSettings = id => fromNamespace(id)(cashout)
const oldCashboxCount = cashboxCounts[id] const isCashOutDisabled = ({ id }) => !getCashoutSettings(id).active
if (cashbox < oldCashboxCount) {
createBatch({
variables: {
deviceId: id,
cashboxCount: oldCashboxCount
}
})
}
const onSave = (id, cashbox, cassettes) => {
return setCassetteBills({ return setCassetteBills({
variables: { variables: {
action: 'setCassetteBills', action: 'setCassetteBills',
deviceId: id, deviceId: id,
cashbox, cashbox,
cassette1, ...cassettes
cassette2,
cassette3,
cassette4
} }
}) })
} }
@ -208,9 +184,6 @@ const CashCassettes = () => {
} }
} }
const getCashoutSettings = id => fromNamespace(id)(cashout)
const isCashOutDisabled = ({ id }) => !getCashoutSettings(id).active
const radioButtonOptions = [ const radioButtonOptions = [
{ display: 'Automatic', code: AUTOMATIC }, { display: 'Automatic', code: AUTOMATIC },
{ display: 'Manual', code: MANUAL } { display: 'Manual', code: MANUAL }
@ -299,7 +272,7 @@ const CashCassettes = () => {
<TitleSection <TitleSection
title="Cash Cassettes" title="Cash Cassettes"
button={{ button={{
text: 'Cashbox history', text: 'Cashbox history a',
icon: HistoryIcon, icon: HistoryIcon,
inverseIcon: ReverseHistoryIcon, inverseIcon: ReverseHistoryIcon,
toggle: setShowHistory toggle: setShowHistory

View file

@ -3,6 +3,7 @@ import React, { useState } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import Modal from 'src/components/Modal' import Modal from 'src/components/Modal'
import { defaultToZero } from 'src/utils/number'
import WizardSplash from './WizardSplash' import WizardSplash from './WizardSplash'
import WizardStep from './WizardStep' import WizardStep from './WizardStep'
@ -11,6 +12,8 @@ const MODAL_WIDTH = 554
const MODAL_HEIGHT = 520 const MODAL_HEIGHT = 520
const CASHBOX_DEFAULT_CAPACITY = 500 const CASHBOX_DEFAULT_CAPACITY = 500
const CASSETTE_FIELDS = ['cassette1', 'cassette2', 'cassette3', 'cassette4']
const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => { const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
const [{ step, config }, setState] = useState({ const [{ step, config }, setState] = useState({
step: 0, step: 0,
@ -27,6 +30,17 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
const title = `Update counts` const title = `Update counts`
const isLastStep = step === LAST_STEP const isLastStep = step === LAST_STEP
const buildCassetteObj = cassetteInput => {
return R.reduce(
(acc, value) => {
acc[value] = defaultToZero(cassetteInput[value])
return acc
},
{},
CASSETTE_FIELDS
)
}
const onContinue = it => { const onContinue = it => {
const newConfig = R.merge(config, it) const newConfig = R.merge(config, it)
@ -37,9 +51,9 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
].includes('YES') ].includes('YES')
const cashbox = wasCashboxEmptied ? 0 : machine?.cashbox const cashbox = wasCashboxEmptied ? 0 : machine?.cashbox
const cassettes = buildCassetteObj(it)
const { cassette1, cassette2, cassette3, cassette4 } = R.map(parseInt, it) save(machine.id, cashbox, cassettes)
save(machine.id, cashbox, cassette1, cassette2, cassette3, cassette4)
return onClose() return onClose()
} }
@ -66,6 +80,18 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
})) }))
) )
const makeInitialValues = () =>
!R.isEmpty(cashoutSettings)
? R.reduce(
(acc, value) => {
acc[`cassette${value}`] = ''
return acc
},
{},
R.range(1, numberOfCassettes + 1)
)
: {}
const steps = R.prepend( const steps = R.prepend(
{ {
type: 'cashbox', type: 'cashbox',
@ -99,6 +125,7 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
steps={steps} steps={steps}
fiatCurrency={locale.fiatCurrency} fiatCurrency={locale.fiatCurrency}
onContinue={onContinue} onContinue={onContinue}
initialValues={makeInitialValues()}
/> />
)} )}
</Modal> </Modal>

View file

@ -4,4 +4,7 @@ const isValidNumber = R.both(R.is(Number), R.complement(R.equals(NaN)))
const transformNumber = value => (isValidNumber(value) ? value : null) const transformNumber = value => (isValidNumber(value) ? value : null)
export { transformNumber } const defaultToZero = value =>
isValidNumber(parseInt(value)) ? parseInt(value) : 0
export { defaultToZero, transformNumber }