import * as _ from 'lodash/fp' import * as R from 'ramda' import React from 'react' import { v4 } from 'uuid' import * as Yup from 'yup' import { Autocomplete, NumberInput } from 'src/components/inputs/formik' import { bold } from 'src/styling/helpers' 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 { primaryColor, secondaryColorDark } from 'src/styling/variables' import denominations from 'src/utils/bill-denominations' import { getBillOptions } from 'src/utils/bill-options' import { CURRENCY_MAX } from 'src/utils/constants' const ALL_MACHINES = { name: 'All Machines', deviceId: 'ALL_MACHINES' } const ALL_COINS = { display: 'All Coins', code: 'ALL_COINS' } const cashInAndOutHeaderStyle = { marginLeft: 6, whiteSpace: 'nowrap' } const cashInHeader = (
Cash-in
) const cashOutHeader = (
Cash-out
) const getView = (data, code, compare) => it => { if (!data) return '' // The following boolean should come undefined if it is rendering an unpaired machine const attribute = R.find(R.propEq(compare ?? 'code', it))(data) return attribute ? R.prop(code, attribute) : 'Unpaired machine' } const displayCodeArray = data => it => { if (!it) return it return R.compose(R.join(', '), R.map(getView(data, 'display')))(it) } const onCryptoChange = (prev, curr, setValue) => { const hasAllCoins = R.includes(ALL_COINS.code)(curr) const hadAllCoins = R.includes(ALL_COINS.code)(prev) if (hasAllCoins && hadAllCoins && R.length(curr) > 1) { return setValue(R.reject(R.equals(ALL_COINS.code))(curr)) } if (hasAllCoins && !hadAllCoins) { return setValue([ALL_COINS.code]) } setValue(curr) } const getOverridesFields = (getData, currency, auxElements) => { const machineData = [ALL_MACHINES].concat(getData(['machines'])) const rawCryptos = getData(['cryptoCurrencies']) const cryptoData = [ALL_COINS].concat( R.map(it => ({ display: it.code, code: it.code }))(rawCryptos ?? []) ) return [ { name: 'machine', width: 196, size: 'sm', view: getView(machineData, 'name', 'deviceId'), input: Autocomplete, inputProps: { options: machineData, valueProp: 'deviceId', labelProp: 'name' } }, { name: 'cryptoCurrencies', width: 280, size: 'sm', view: displayCodeArray(cryptoData), input: Autocomplete, inputProps: { options: cryptoData, valueProp: 'code', labelProp: 'display', multiple: true, onChange: onCryptoChange, shouldStayOpen: true } }, { header: cashInHeader, name: 'cashIn', display: 'Cash-in', width: 130, input: NumberInput, textAlign: 'right', suffix: '%', bold: bold, inputProps: { decimalPlaces: 3 } }, { header: cashOutHeader, name: 'cashOut', display: 'Cash-out', width: 130, input: NumberInput, textAlign: 'right', suffix: '%', bold: bold, inputProps: { decimalPlaces: 3 } }, { name: 'fixedFee', display: 'Fixed fee', width: 144, input: NumberInput, doubleHeader: 'Cash-in only', textAlign: 'right', suffix: currency, bold: bold, inputProps: { decimalPlaces: 2 } }, { name: 'minimumTx', display: 'Minimun Tx', width: 169, doubleHeader: 'Cash-in only', textAlign: 'center', editingAlign: 'right', input: NumberInput, suffix: currency, bold: bold, inputProps: { decimalPlaces: 2 } } ] } const mainFields = currency => [ { header: cashInHeader, name: 'cashIn', display: 'Cash-in', width: 169, size: 'lg', editingAlign: 'right', input: NumberInput, suffix: '%', bold: bold, inputProps: { decimalPlaces: 3 } }, { header: cashOutHeader, name: 'cashOut', display: 'Cash-out', width: 169, size: 'lg', editingAlign: 'right', input: NumberInput, suffix: '%', bold: bold, inputProps: { decimalPlaces: 3 } }, { name: 'fixedFee', display: 'Fixed fee', width: 169, size: 'lg', doubleHeader: 'Cash-in only', textAlign: 'center', editingAlign: 'right', input: NumberInput, suffix: currency, bold: bold, inputProps: { decimalPlaces: 2 } }, { name: 'minimumTx', display: 'Minimun Tx', width: 169, size: 'lg', doubleHeader: 'Cash-in only', textAlign: 'center', editingAlign: 'right', input: NumberInput, suffix: currency, bold: bold, inputProps: { decimalPlaces: 2 } } ] const overrides = (auxData, currency, auxElements) => { const getData = R.path(R.__, auxData) return getOverridesFields(getData, currency, auxElements) } const percentMax = 100 const getSchema = locale => { const bills = getBillOptions(locale, denominations).map(it => parseInt(it.code) ) const highestBill = R.isEmpty(bills) ? CURRENCY_MAX : Math.max(...bills) return Yup.object().shape({ cashIn: Yup.number() .label('Cash-in') .min(0) .max(percentMax) .required(), cashOut: Yup.number() .label('Cash-out') .min(0) .max(percentMax) .required(), fixedFee: Yup.number() .label('Fixed Fee') .min(0) .max(highestBill) .required(), minimumTx: Yup.number() .label('Minimum Tx') .min(0) .max(highestBill) .required() }) } const getAlreadyUsed = (id, machine, values) => { const getCrypto = R.prop('cryptoCurrencies') const getMachineId = R.prop('machine') const filteredOverrides = R.filter(R.propEq('machine', machine))(values) const originalValue = R.find(R.propEq('id', id))(values) const originalCryptos = getCrypto(originalValue) const originalMachineId = getMachineId(originalValue) const alreadyUsed = R.compose( R.uniq, R.flatten, R.map(getCrypto) )(filteredOverrides) if (machine !== originalMachineId) return alreadyUsed ?? [] return R.difference(alreadyUsed, originalCryptos) } const getOverridesSchema = (values, rawData, locale) => { const getData = R.path(R.__, rawData) const machineData = [ALL_MACHINES].concat(getData(['machines'])) const rawCryptos = getData(['cryptoCurrencies']) const cryptoData = [ALL_COINS].concat( R.map(it => ({ display: it.code, code: it.code }))(rawCryptos ?? []) ) const bills = getBillOptions(locale, denominations).map(it => parseInt(it.code) ) const highestBill = R.isEmpty(bills) ? CURRENCY_MAX : Math.max(...bills) return Yup.object().shape({ machine: Yup.string() .nullable() .label('Machine') .required(), cryptoCurrencies: Yup.array() .test({ test() { const { id, machine, cryptoCurrencies } = this.parent const alreadyUsed = getAlreadyUsed(id, machine, values) const isAllMachines = machine === ALL_MACHINES.deviceId const isAllCoins = R.includes(ALL_COINS.code, cryptoCurrencies) if (isAllMachines && isAllCoins) { return this.createError({ message: `All machines and all coins should be configured in the default setup table` }) } const repeated = R.intersection(alreadyUsed, cryptoCurrencies) if (!R.isEmpty(repeated)) { const codes = displayCodeArray(cryptoData)(repeated) const machineView = getView( machineData, 'name', 'deviceId' )(machine) const message = `${codes} already overriden for machine: ${machineView}` return this.createError({ message }) } return true } }) .label('Crypto Currencies') .required() .min(1), cashIn: Yup.number() .label('Cash-in') .min(0) .max(percentMax) .required(), cashOut: Yup.number() .label('Cash-out') .min(0) .max(percentMax) .required(), fixedFee: Yup.number() .label('Fixed Fee') .min(0) .max(highestBill) .required(), minimumTx: Yup.number() .label('Minimum Tx') .min(0) .max(highestBill) .required() }) } const defaults = { cashIn: '', cashOut: '', fixedFee: '', minimumTx: '' } const overridesDefaults = { machine: null, cryptoCurrencies: [], cashIn: '', cashOut: '', fixedFee: '', minimumTx: '' } const getOrder = ({ machine, cryptoCurrencies }) => { const isAllMachines = machine === ALL_MACHINES.deviceId const isAllCoins = R.contains(ALL_COINS.code, cryptoCurrencies) if (isAllMachines && isAllCoins) return 0 if (isAllMachines) return 1 if (isAllCoins) return 2 return 3 } const createCommissions = (cryptoCode, deviceId, isDefault, config) => { return { minimumTx: config.minimumTx, fixedFee: config.fixedFee, cashOut: config.cashOut, cashIn: config.cashIn, machine: deviceId, cryptoCurrencies: [cryptoCode], default: isDefault, id: v4() } } const getCommissions = (cryptoCode, deviceId, config) => { const overrides = R.prop('overrides', config) ?? [] if (!overrides && R.isEmpty(overrides)) { return createCommissions(cryptoCode, deviceId, true, config) } const specificOverride = R.find( it => it.machine === deviceId && _.includes(cryptoCode)(it.cryptoCurrencies) )(overrides) if (specificOverride !== undefined) return createCommissions(cryptoCode, deviceId, false, specificOverride) const machineOverride = R.find( it => it.machine === deviceId && _.includes('ALL_COINS')(it.cryptoCurrencies) )(overrides) if (machineOverride !== undefined) return createCommissions(cryptoCode, deviceId, false, machineOverride) const coinOverride = R.find( it => it.machine === 'ALL_MACHINES' && _.includes(cryptoCode)(it.cryptoCurrencies) )(overrides) if (coinOverride !== undefined) return createCommissions(cryptoCode, deviceId, false, coinOverride) return createCommissions(cryptoCode, deviceId, true, config) } const getListCommissionsSchema = locale => { const bills = getBillOptions(locale, denominations).map(it => parseInt(it.code) ) const highestBill = R.isEmpty(bills) ? CURRENCY_MAX : Math.max(...bills) return Yup.object().shape({ machine: Yup.string() .label('Machine') .required(), cryptoCurrencies: Yup.array() .label('Crypto Currency') .required() .min(1), cashIn: Yup.number() .label('Cash-in') .min(0) .max(percentMax) .required(), cashOut: Yup.number() .label('Cash-out') .min(0) .max(percentMax) .required(), fixedFee: Yup.number() .label('Fixed Fee') .min(0) .max(highestBill) .required(), minimumTx: Yup.number() .label('Minimum Tx') .min(0) .max(highestBill) .required() }) } const getTextStyle = (obj, isEditing) => { return { color: obj.default ? primaryColor : secondaryColorDark } } const commissionsList = (auxData, currency, auxElements) => { const getData = R.path(R.__, auxData) return getListCommissionsFields(getData, currency, defaults) } const getListCommissionsFields = (getData, currency, defaults) => { const machineData = [ALL_MACHINES].concat(getData(['machines'])) return [ { name: 'machine', width: 196, size: 'sm', view: getView(machineData, 'name', 'deviceId'), editable: false }, { name: 'cryptoCurrencies', display: 'Crypto Currency', width: 255, view: R.prop(0), size: 'sm', editable: false }, { header: cashInHeader, name: 'cashIn', display: 'Cash-in', width: 130, input: NumberInput, textAlign: 'right', suffix: '%', textStyle: obj => getTextStyle(obj), inputProps: { decimalPlaces: 3 } }, { header: cashOutHeader, name: 'cashOut', display: 'Cash-out', width: 140, input: NumberInput, textAlign: 'right', greenText: true, suffix: '%', textStyle: obj => getTextStyle(obj), inputProps: { decimalPlaces: 3 } }, { name: 'fixedFee', display: 'Fixed fee', width: 144, input: NumberInput, doubleHeader: 'Cash-in only', textAlign: 'right', suffix: currency, textStyle: obj => getTextStyle(obj), inputProps: { decimalPlaces: 2 } }, { name: 'minimumTx', display: 'Minimun Tx', width: 144, input: NumberInput, doubleHeader: 'Cash-in only', textAlign: 'right', suffix: currency, textStyle: obj => getTextStyle(obj), inputProps: { decimalPlaces: 2 } } ] } export { mainFields, overrides, getSchema, getOverridesSchema, defaults, overridesDefaults, getOrder, getCommissions, getListCommissionsSchema, commissionsList }