feat: add cashin bill counter

fix: fiat amount formatting
This commit is contained in:
Sérgio Salgado 2021-12-20 01:16:43 +00:00
parent 219dca7f90
commit 98a2797494
17 changed files with 239 additions and 218 deletions

View file

@ -68,4 +68,10 @@ function getBillsByBatchId (id) {
return db.any(sql, [id])
}
module.exports = { createCashboxBatch, updateMachineWithBatch, getBatches, getBillsByBatchId, editBatchById }
module.exports = {
createCashboxBatch,
updateMachineWithBatch,
getBatches,
getBillsByBatchId,
editBatchById
}

View file

@ -2,7 +2,9 @@ const bills = require('../../services/bills')
const resolvers = {
Query: {
bills: () => bills.getBills()
bills: () => bills.getBills(),
looseBills: () => bills.getLooseBills(),
looseBillsByMachine: (...[, { deviceId }]) => bills.getLooseBillsByMachine(deviceId)
}
}

View file

@ -2,14 +2,17 @@ const { gql } = require('apollo-server-express')
const typeDef = gql`
type Bill {
id: ID
fiat: Int
deviceId: ID
created: Date
cashbox: Int
cashboxBatchId: ID
}
type Query {
bills: [Bill] @auth
looseBills: [Bill] @auth
looseBillsByMachine(deviceId: ID): [Bill] @auth
}
`

View file

@ -1,25 +1,37 @@
const _ = require('lodash/fp')
const db = require('../../db')
// Get all bills with device id
const getBills = () => {
return Promise.reject(new Error('This functionality hasn\'t been implemented yet'))
/* return db.any(`
SELECT d.device_id, b.fiat, b.created, d.cashbox
FROM cash_in_txs
INNER JOIN bills AS b ON b.cash_in_txs_id = cash_in_txs.id
INNER JOIN devices as d ON d.device_id = cash_in_txs.device_id
ORDER BY device_id, created DESC`
)
.then(res => {
return res.map(item => ({
fiat: item.fiat,
deviceId: item.device_id,
cashbox: item.cashbox,
created: item.created
}))
}) */
const sql = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (
SELECT id, device_id FROM cash_in_txs
) AS cit ON cit.id = b.cash_in_txs_id`
return db.any(sql)
.then(res => _.map(_.mapKeys(_.camelCase), res))
}
function getLooseBills () {
const sql = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (
SELECT id, device_id FROM cash_in_txs
) AS cit ON cit.id = b.cash_in_txs_id WHERE b.cashbox_batch_id IS NULL`
return db.any(sql)
.then(res => _.map(_.mapKeys(_.camelCase), res))
}
function getLooseBillsByMachine (machineId) {
const sql = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (
SELECT id, device_id FROM cash_in_txs WHERE device_id = $1
) AS cit ON cit.id = b.cash_in_txs_id WHERE b.cashbox_batch_id IS NULL`
return db.any(sql, [machineId])
.then(res => _.map(_.mapKeys(_.camelCase), res))
}
module.exports = {
getBills
getBills,
getLooseBills,
getLooseBillsByMachine
}

View file

@ -4,6 +4,7 @@ import React from 'react'
import Chip from 'src/components/Chip'
import { Info2, Label1, Label2 } from 'src/components/typography'
import { numberToFiatAmount } from 'src/utils/number'
import { cashboxStyles, gridStyles } from './Cashbox.styles'
@ -64,11 +65,9 @@ const CashIn = ({ currency, notes, total }) => {
<Info2 className={classes.noMarginText}>{notes} notes</Info2>
</div>
<div className={classes.innerRow}>
{/* Feature on hold until this can be calculated
<Label1 className={classes.noMarginText}>
{total} {currency.code}
</Label1>
*/}
</div>
</div>
</div>
@ -112,7 +111,7 @@ const CashOut = ({
</div>
<div className={classes.innerRow}>
<Label1 className={classes.noMarginText}>
{notes * denomination} {currency.code}
{numberToFiatAmount(notes * denomination)} {currency.code}
</Label1>
</div>
</div>

View file

@ -15,6 +15,7 @@ import { ReactComponent as DashLogo } from 'src/styling/logos/icon-dash-colour.s
import { ReactComponent as EthereumLogo } from 'src/styling/logos/icon-ethereum-colour.svg'
import { ReactComponent as LitecoinLogo } from 'src/styling/logos/icon-litecoin-colour.svg'
import { ReactComponent as ZCashLogo } from 'src/styling/logos/icon-zcash-colour.svg'
import { numberToFiatAmount } from 'src/utils/number'
import styles from './ATMWallet.styles'
@ -51,9 +52,6 @@ const GET_OPERATOR_BY_USERNAME = gql`
}
`
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const CHIPS_PER_ROW = 6
const Assets = ({ balance, wallets, currency }) => {
@ -69,7 +67,7 @@ const Assets = ({ balance, wallets, currency }) => {
<P className={classes.fieldHeader}>Available balance</P>
<div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance)}
{numberToFiatAmount(balance)}
</Info2>
<Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)}
@ -81,7 +79,7 @@ const Assets = ({ balance, wallets, currency }) => {
<P className={classes.fieldHeader}>Total balance in wallets</P>
<div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}>
{formatCurrency(walletFiatSum())}
{numberToFiatAmount(walletFiatSum())}
</Info2>
<Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)}
@ -93,7 +91,7 @@ const Assets = ({ balance, wallets, currency }) => {
<P className={classes.fieldHeader}>Total assets</P>
<div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance)}
{numberToFiatAmount(balance)}
</Info2>
<Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)}
@ -144,17 +142,11 @@ const WalletInfoChip = ({ wallet, currency }) => {
<div className={classes.walletValueWrapper}>
<Label2 className={classes.fieldHeader}>{wallet.name} value</Label2>
<Label2 className={classes.walletValue}>
{wallet.amount.toFixed(1).toLocaleString('en-US', {
maximumFractionDigits: 2
})}{' '}
{wallet.cryptoCode}
{numberToFiatAmount(wallet.amount.toFixed(1))} {wallet.cryptoCode}
</Label2>
<Label2 className={classes.fieldHeader}>Hedged value</Label2>
<Label2 className={classes.walletValue}>
{wallet.fiatValue.toLocaleString('en-US', {
maximumFractionDigits: 2
})}{' '}
{currency}
{numberToFiatAmount(wallet.fiatValue)} {currency}
</Label2>
</div>
</Paper>

View file

@ -9,13 +9,11 @@ import { Tooltip } from 'src/components/Tooltip'
import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable'
import { H4, Info2, P } from 'src/components/typography'
import { numberToFiatAmount } from 'src/utils/number'
import { formatDate } from 'src/utils/timezones'
import styles from './Accounting.styles'
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const useStyles = makeStyles(styles)
const GET_OPERATOR_BY_USERNAME = gql`
@ -64,7 +62,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
<P className={classes.fieldHeader}>Pazuz fiat balance</P>
<div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance)}
{numberToFiatAmount(balance)}
</Info2>
<Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)}
@ -76,7 +74,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
<P className={classes.fieldHeader}>Hedging reserve</P>
<div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}>
{formatCurrency(hedgingReserve)}
{numberToFiatAmount(hedgingReserve)}
</Info2>
<Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)}
@ -88,7 +86,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
<P className={classes.fieldHeader}>Available balance</P>
<div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance - hedgingReserve)}
{numberToFiatAmount(balance - hedgingReserve)}
</Info2>
<Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)}
@ -143,7 +141,7 @@ const Accounting = () => {
size: 'sm',
textAlign: 'right',
view: it =>
`${formatCurrency(it.fiatAmount)} ${R.toUpper(it.fiatCurrency)}`
`${numberToFiatAmount(it.fiatAmount)} ${R.toUpper(it.fiatCurrency)}`
},
{
header: 'Balance after operation',
@ -151,7 +149,9 @@ const Accounting = () => {
size: 'sm',
textAlign: 'right',
view: it =>
`${formatCurrency(it.fiatBalanceAfter)} ${R.toUpper(it.fiatCurrency)}`
`${numberToFiatAmount(it.fiatBalanceAfter)} ${R.toUpper(
it.fiatCurrency
)}`
},
{
header: 'Date',

View file

@ -13,6 +13,7 @@ import { ReactComponent as DownIcon } from 'src/styling/icons/dashboard/down.svg
import { ReactComponent as EqualIcon } from 'src/styling/icons/dashboard/equal.svg'
import { ReactComponent as UpIcon } from 'src/styling/icons/dashboard/up.svg'
import { fromNamespace } from 'src/utils/config'
import { numberToFiatAmount } from 'src/utils/number'
import { DAY, WEEK, MONTH } from 'src/utils/time'
import styles from './Analytics.styles'
@ -97,9 +98,7 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
<div className={classes.overviewEntry}>
<P noMargin>{label}</P>
<Info2 noMargin className={classes.overviewFieldWrapper}>
<span>
{value.toLocaleString('en-US', { maximumFractionDigits: 2 })}
</span>
<span>{numberToFiatAmount(value)}</span>
{!!currency && ` ${currency}`}
</Info2>
<span className={classes.overviewGrowth}>
@ -107,7 +106,7 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
{R.lt(growthRate, 0) && <DownIcon height={10} />}
{R.equals(growthRate, 0) && <EqualIcon height={10} />}
<P noMargin className={classnames(growthClasses)}>
{growthRate.toLocaleString('en-US', { maximumFractionDigits: 2 })}%
{numberToFiatAmount(growthRate)}%
</P>
</span>
</div>

View file

@ -6,6 +6,7 @@ import React, { memo } from 'react'
import { Info2, Label3, P } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { numberToFiatAmount } from 'src/utils/number'
import { singularOrPlural } from 'src/utils/string'
import { formatDate, formatDateNonUtc } from 'src/utils/timezones'
@ -13,9 +14,6 @@ import styles from './GraphTooltip.styles'
const useStyles = makeStyles(styles)
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const GraphTooltip = ({
coords,
data,
@ -67,7 +65,7 @@ const GraphTooltip = ({
{singularOrPlural(R.length(data), 'transaction', 'transactions')}
</P>
<P noMargin className={classes.dotOtTransactionVolume}>
{formatCurrency(transactions.volume)} {currency} in volume
{numberToFiatAmount(transactions.volume)} {currency} in volume
</P>
<div className={classes.dotOtTransactionClasses}>
<Label3 noMargin>

View file

@ -14,8 +14,10 @@ import React, { useContext } from 'react'
import AppContext from 'src/AppContext'
import TitleSection from 'src/components/layout/TitleSection'
import { H4, Label2, P, Info2 } from 'src/components/typography'
import { numberToFiatAmount } from 'src/utils/number'
import styles from './Assets.styles'
const useStyles = makeStyles(styles)
const GET_OPERATOR_BY_USERNAME = gql`
@ -105,7 +107,7 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
</Cell>
<Cell align="right">
<P>{`${selectAmountPrefix(asset)}
${formatCurrency(Math.abs(asset.amount))} ${
${numberToFiatAmount(Math.abs(asset.amount))} ${
asset.currency
}`}</P>
</Cell>
@ -117,7 +119,9 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
<Info2>{`Total ${R.toLower(title)}`}</Info2>
</Cell>
<Cell align="right">
<Info2>{`${formatCurrency(totalAmount)} ${currency}`}</Info2>
<Info2>{`${numberToFiatAmount(
totalAmount
)} ${currency}`}</Info2>
</Cell>
</TableRow>
</TableBody>
@ -128,9 +132,6 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
)
}
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const Assets = () => {
const classes = useStyles()
const { userData } = useContext(AppContext)

View file

@ -82,7 +82,7 @@ const SET_CASSETTE_BILLS = gql`
}
`
const CashCassettes = ({ machine, config, refetchData }) => {
const CashCassettes = ({ machine, config, refetchData, bills }) => {
const classes = useStyles()
const [wizard, setWizard] = useState(false)
@ -105,7 +105,11 @@ const CashCassettes = ({ machine, config, refetchData }) => {
width: widthsByNumberOfCassettes[numberOfCassettes].cashbox,
stripe: false,
view: value => (
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
<CashIn
currency={{ code: fiatCurrency }}
notes={value}
total={R.sum(R.map(it => it.fiat)(bills))}
/>
),
input: NumberInput,
inputProps: {

View file

@ -46,6 +46,12 @@ const GET_INFO = gql`
note
}
}
looseBillsByMachine(deviceId: $deviceId) {
id
fiat
deviceId
created
}
config
}
`
@ -59,12 +65,14 @@ const Machines = () => {
deviceId: getMachineID(location.pathname)
}
})
const classes = useStyles()
const timezone = R.path(['config', 'locale_timezone'], data) ?? {}
const machine = R.path(['machine'])(data) ?? {}
const config = R.path(['config'])(data) ?? {}
const bills = R.path(['looseBillsByMachine'])(data) ?? []
const machineName = R.path(['name'])(machine) ?? null
const machineID = R.path(['deviceId'])(machine) ?? null
@ -102,6 +110,7 @@ const Machines = () => {
refetchData={refetch}
machine={machine}
config={config ?? false}
bills={bills}
/>
</div>
<div className={classes.transactionsItem}>

View file

@ -75,24 +75,21 @@ const GET_MACHINES_AND_CONFIG = gql`
numberOfCassettes
}
config
looseBills {
id
fiat
created
deviceId
}
}
`
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) {
saveConfig(config: $config)
}
`
/*
// for cash in total calculation
bills {
fiat
deviceId
created
cashbox
}
*/
const SET_CASSETTE_BILLS = gql`
mutation MachineAction(
$deviceId: ID!
@ -128,7 +125,7 @@ const CashCassettes = () => {
const [editingSchema, setEditingSchema] = useState(null)
const [selectedRadio, setSelectedRadio] = useState(null)
const { data } = useQuery(GET_MACHINES_AND_CONFIG)
const { data, loading: dataLoading } = useQuery(GET_MACHINES_AND_CONFIG)
const [wizard, setWizard] = useState(false)
const [machineId, setMachineId] = useState('')
@ -143,9 +140,11 @@ const CashCassettes = () => {
refetchQueries: () => ['getData']
})
const bills = R.groupBy(bill => bill.deviceId)(R.path(['bills'])(data) ?? [])
const bills = R.groupBy(bill => bill.deviceId)(
R.path(['looseBills'])(data) ?? []
)
const deviceIds = R.uniq(
R.map(R.prop('deviceId'))(R.path(['bills'])(data) ?? [])
R.map(R.prop('deviceId'))(R.path(['looseBills'])(data) ?? [])
)
const cashout = data?.config && fromNamespace('cashOut')(data.config)
const locale = data?.config && fromNamespace('locale')(data.config)
@ -206,8 +205,12 @@ const CashCassettes = () => {
name: 'cashbox',
header: 'Cash box',
width: maxNumberOfCassettes > 2 ? 140 : 280,
view: value => (
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
view: (value, { id }) => (
<CashIn
currency={{ code: fiatCurrency }}
notes={value}
total={R.sum(R.map(it => it.fiat, bills[id]))}
/>
),
input: NumberInput,
inputProps: {
@ -222,7 +225,7 @@ const CashCassettes = () => {
elements.push({
name: `cassette${it}`,
header: `Cassette ${it}`,
width: (maxNumberOfCassettes > 2 ? 700 : 560) / maxNumberOfCassettes,
width: (maxNumberOfCassettes > 2 ? 560 : 650) / maxNumberOfCassettes,
stripe: true,
doubleHeader: 'Cash-out',
view: (value, { id }) => (
@ -268,6 +271,7 @@ const CashCassettes = () => {
})
return (
!dataLoading && (
<>
<TitleSection
title="Cash Boxes & Cassettes"
@ -285,7 +289,7 @@ const CashCassettes = () => {
<Box
display="flex"
alignItems="center"
justifyContent="flex-end"
justifyContent="end"
mr="-4px">
{cashboxReset && (
<P className={classes.selection}>
@ -332,7 +336,7 @@ const CashCassettes = () => {
/>
{wizard && (
<Wizard
machine={R.find(R.propEq('id', machineId))(machines)}
machine={R.find(R.propEq('id', machineId), machines)}
cashoutSettings={getCashoutSettings(machineId)}
onClose={() => {
setWizard(false)
@ -349,8 +353,8 @@ const CashCassettes = () => {
handleClose={() => setEditingSchema(null)}
open={true}>
<P className={classes.descriptions}>
We can automatically assume you emptied a bill validator's cash box
when the machine detects that it has been removed.
We can automatically assume you emptied a bill validator's cash
box when the machine detects that it has been removed.
</P>
<RadioGroup
name="set-automatic-reset"
@ -360,8 +364,9 @@ const CashCassettes = () => {
className={classes.radioButtons}
/>
<P className={classes.descriptions}>
Assume the cash box is emptied whenever it's removed, creating a new
batch on the history screen and setting its current balance to zero.
Assume the cash box is emptied whenever it's removed, creating a
new batch on the history screen and setting its current balance to
zero.
</P>
<RadioGroup
name="set-manual-reset"
@ -384,6 +389,7 @@ const CashCassettes = () => {
)}
</>
)
)
}
export default CashCassettes

View file

@ -1,20 +1,17 @@
import { makeStyles } from '@material-ui/core'
// import BigNumber from 'bignumber.js'
import BigNumber from 'bignumber.js'
import * as R from 'ramda'
import React from 'react'
import { Info1, Info2, Info3 } from 'src/components/typography/index'
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { fromNamespace } from 'src/utils/config'
import { numberToFiatAmount } from 'src/utils/number.js'
import styles from './CashCassettesFooter.styles.js'
const useStyles = makeStyles(styles)
/* const sortDate = function(a, b) {
return new Date(b.created).getTime() - new Date(a.created).getTime()
} */
const CashCassettesFooter = ({
machines,
config,
@ -43,44 +40,34 @@ const CashCassettesFooter = ({
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines))
/* const totalInCashBox = R.sum(
R.flatten(
R.map(id => {
const sliceIdx = R.path([id, 0, 'cashbox'])(bills) ?? 0
return R.map(
R.prop('fiat'),
R.slice(0, sliceIdx, R.sort(sortDate, bills[id] ?? []))
)
}, deviceIds)
)
) */
const totalInCashBox = R.sum(R.map(it => it.fiat)(bills))
// const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0)
const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0)
return (
<div className={classes.footerContainer}>
<div className={classes.footerContent}>
<Info3 className={classes.footerLabel}>Cash value in System</Info3>
{/* <div className={classes.flex}>
<div className={classes.flex}>
<TxInIcon className={classes.icon} />
<Info2 className={classes.iconLabel}>Cash-in:</Info2>
<Info1 className={classes.valueDisplay}>
{totalInCashBox} {currencyCode}
{numberToFiatAmount(totalInCashBox)} {currencyCode}
</Info1>
</div> */}
</div>
<div className={classes.flex}>
<TxOutIcon className={classes.icon} />
<Info2 className={classes.iconLabel}>Cash-out:</Info2>
<Info1 className={classes.valueDisplay}>
{totalInCassettes} {currencyCode}
{numberToFiatAmount(totalInCassettes)} {currencyCode}
</Info1>
</div>
{/* <div className={classes.flex}>
<div className={classes.flex}>
<Info2 className={classes.iconLabel}>Total:</Info2>
<Info1 className={classes.valueDisplay}>
{total} {currencyCode}
{numberToFiatAmount(total)} {currencyCode}
</Info1>
</div> */}
</div>
</div>
</div>
)

View file

@ -24,9 +24,7 @@ export default {
boxShadow: [[0, -1, 10, 0, 'rgba(50, 50, 50, 0.1)']]
},
flex: {
display: 'flex',
// temp marginLeft until cashIn square is enabled
marginLeft: -640
display: 'flex'
},
icon: {
alignSelf: 'center',

View file

@ -22,6 +22,7 @@ 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 { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { comet, errorColor } from 'src/styling/variables'
import { numberToFiatAmount } from 'src/utils/number'
const styles = {
content: {
@ -279,7 +280,8 @@ const WizardStep = ({
</P>
</div>
<P noMargin className={classes.fiatTotal}>
= {cassetteTotal(values)} {fiatCurrency}
= {numberToFiatAmount(cassetteTotal(values))}{' '}
{fiatCurrency}
</P>
</div>
</div>

View file

@ -7,4 +7,7 @@ const transformNumber = value => (isValidNumber(value) ? value : null)
const defaultToZero = value =>
isValidNumber(parseInt(value)) ? parseInt(value) : 0
export { defaultToZero, transformNumber }
const numberToFiatAmount = value =>
value.toLocaleString('en-US', { maximumFractionDigits: 2 })
export { defaultToZero, transformNumber, numberToFiatAmount }