feat: compliance triggers and customers up to spec

This commit is contained in:
Taranto 2020-08-17 17:48:52 +01:00 committed by Josh Harvey
parent b153a23f25
commit 6f5cb385b0
8 changed files with 400 additions and 104 deletions

View file

@ -93,6 +93,7 @@ const ECol = ({
}) => {
const {
name,
bypassField,
input,
editable = true,
size,
@ -122,6 +123,9 @@ const ECol = ({
innerProps.getLabel = view
}
const isEditing = editing && editable
const isField = !bypassField
return (
<Td
className={{
@ -133,11 +137,11 @@ const ECol = ({
size={size}
bold={bold}
textAlign={textAlign}>
{editing && editable ? (
{isEditing && isField && (
<Field name={name} component={input} {...innerProps} />
) : (
values && <>{view(values[name])}</>
)}
{isEditing && !isField && <config.input name={name} />}
{!isEditing && values && <>{view(values[name], values)}</>}
{suffix && (
<SuffixComponent className={classes.suffix}>{suffix}</SuffixComponent>
)}

View file

@ -3,12 +3,12 @@ import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import React, { memo } from 'react'
// import { ActionButton } from 'src/components/buttons'
import { ActionButton } from 'src/components/buttons'
import { H3 } from 'src/components/typography'
// import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg'
// import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
// import { ReactComponent as RejectReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
// import { ReactComponent as RejectIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg'
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
import { ReactComponent as RejectReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
import { ReactComponent as RejectIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
import { propertyCardStyles } from './PropertyCard.styles'
@ -24,48 +24,46 @@ const PropertyCard = memo(
const propertyCardClassNames = {
[classes.propertyCard]: true,
[classes.propertyCardPending]: true
// [classes.propertyCardPending]: state === OVERRIDE_PENDING
// [classes.propertyCardRejected]: state === OVERRIDE_REJECTED,
// [classes.propertyCardAccepted]: state === OVERRIDE_AUTHORIZED
[classes.propertyCardPending]: state === OVERRIDE_PENDING,
[classes.propertyCardRejected]: state === OVERRIDE_REJECTED,
[classes.propertyCardAccepted]: state === OVERRIDE_AUTHORIZED
}
// const label1ClassNames = {
// [classes.label1]: true,
// [classes.label1Pending]: true
// [classes.label1Pending]: state === OVERRIDE_PENDING
// [classes.label1Rejected]: state === OVERRIDE_REJECTED,
// [classes.label1Accepted]: state === OVERRIDE_AUTHORIZED
// }
const label1ClassNames = {
[classes.label1]: true,
[classes.label1Pending]: state === OVERRIDE_PENDING,
[classes.label1Rejected]: state === OVERRIDE_REJECTED,
[classes.label1Accepted]: state === OVERRIDE_AUTHORIZED
}
// const AuthorizeButton = () => (
// <ActionButton
// className={classes.cardActionButton}
// color="secondary"
// Icon={AuthorizeIcon}
// InverseIcon={AuthorizeReversedIcon}
// onClick={() => authorize()}>
// Authorize
// </ActionButton>
// )
const AuthorizeButton = () => (
<ActionButton
className={classes.cardActionButton}
color="secondary"
Icon={AuthorizeIcon}
InverseIcon={AuthorizeReversedIcon}
onClick={() => authorize()}>
Authorize
</ActionButton>
)
// const RejectButton = () => (
// <ActionButton
// className={classes.cardActionButton}
// color="secondary"
// Icon={RejectIcon}
// InverseIcon={RejectReversedIcon}
// onClick={() => reject()}>
// Reject
// </ActionButton>
// )
const RejectButton = () => (
<ActionButton
className={classes.cardActionButton}
color="secondary"
Icon={RejectIcon}
InverseIcon={RejectReversedIcon}
onClick={() => reject()}>
Reject
</ActionButton>
)
// const authorizedAsString =
// state === OVERRIDE_PENDING
// ? 'Pending'
// : state === OVERRIDE_REJECTED
// ? 'Rejected'
// : 'Accepted'
const authorizedAsString =
state === OVERRIDE_PENDING
? 'Pending'
: state === OVERRIDE_REJECTED
? 'Rejected'
: 'Accepted'
return (
<Paper
@ -73,18 +71,17 @@ const PropertyCard = memo(
elevation={0}>
<div className={classes.rowSpaceBetween}>
<H3>{title}</H3>
{/* <div className={classnames(label1ClassNames)}>
<div className={classnames(label1ClassNames)}>
{authorizedAsString}
</div> */}
</div>
</div>
<Paper className={classes.cardProperties} elevation={0}>
{children}
</Paper>
{/* V2 */}
{/* <div className={classes.buttonsWrapper}>
<div className={classes.buttonsWrapper}>
{state !== OVERRIDE_AUTHORIZED && AuthorizeButton()}
{state !== OVERRIDE_REJECTED && RejectButton()}
</div> */}
</div>
</Paper>
)
}

View file

@ -52,7 +52,7 @@ const propertyCardStyles = {
display: 'flex',
borderRadius: 8,
width: '100%',
height: 'calc(100% - 75px)',
height: 'calc(100% - 104px)',
padding: [[20]],
boxSizing: 'border-box',
boxShadow: '0 0 8px 0 rgba(0, 0, 0, 0.04)',

View file

@ -12,7 +12,12 @@ import { cpcStyles } from './Transactions.styles'
const useStyles = makeStyles(cpcStyles)
const CopyToClipboard = ({ className, children, ...props }) => {
const CopyToClipboard = ({
className,
buttonClassname,
children,
...props
}) => {
const [anchorEl, setAnchorEl] = useState(null)
useEffect(() => {
@ -39,7 +44,7 @@ const CopyToClipboard = ({ className, children, ...props }) => {
<div className={classnames(classes.address, className)}>
{children}
</div>
<div className={classes.buttonWrapper}>
<div className={classnames(classes.buttonWrapper, buttonClassname)}>
<ReactCopyToClipboard text={R.replace(/\s/g, '')(children)}>
<button
aria-describedby={id}

View file

@ -12,7 +12,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
import { mainStyles } from './Triggers.styles'
import Wizard from './Wizard'
import { Schema, elements, sortBy } from './helper'
import { Schema, getElements, sortBy, fromServer, toServer } from './helper'
const useStyles = makeStyles(mainStyles)
@ -34,7 +34,7 @@ const Triggers = () => {
const [error, setError] = useState(false)
const { data } = useQuery(GET_INFO)
const triggers = data?.config?.triggers ?? []
const triggers = fromServer(data?.config?.triggers ?? [])
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: () => setWizard(false),
@ -45,12 +45,14 @@ const Triggers = () => {
const add = rawConfig => {
const toSave = R.concat([{ id: v4(), ...rawConfig }])(triggers)
setError(false)
return saveConfig({ variables: { config: { triggers: toSave } } })
return saveConfig({ variables: { config: { triggers: toServer(toSave) } } })
}
const save = config => {
setError(false)
return saveConfig({ variables: { config } })
return saveConfig({
variables: { config: { triggers: toServer(config.triggers) } }
})
}
const currency = R.path(['fiatCurrency'])(
@ -78,7 +80,7 @@ const Triggers = () => {
enableDelete
save={save}
validationSchema={Schema}
elements={elements}
elements={getElements(currency, classes)}
/>
{wizard && (
<Wizard

View file

@ -21,6 +21,13 @@ const mainStyles = {
padding: 4,
margin: 4
},
tableRadioGroup: {
flexDirection: 'row',
justifyContent: 'space-between'
},
tableRadioLabel: {
marginRight: 0
},
closeButton: {
position: 'absolute',
width: 16,

View file

@ -88,19 +88,19 @@ const getTypeText = (config, currency) => {
switch (config.triggerType) {
case 'txAmount':
return `makes a single transaction over ${orUnderline(
config.threshold
config.threshold.threshold
)} ${currency}`
case 'txVolume':
return `makes transactions over ${orUnderline(
config.threshold
)} ${currency} in ${orUnderline(config.days)} days`
config.threshold.threshold
)} ${currency} in ${orUnderline(config.threshold.thresholdDays)} days`
case 'txVelocity':
return `makes ${orUnderline(
config.threshold
)} transactions in ${orUnderline(config.days)} days`
config.threshold.threshold
)} transactions in ${orUnderline(config.threshold.thresholdDays)} days`
case 'consecutiveDays':
return `at least one transaction every day for ${orUnderline(
config.days
config.threshold.thresholdDays
)} days`
default:
return ''
@ -108,7 +108,7 @@ const getTypeText = (config, currency) => {
}
const getRequirementText = config => {
switch (config.requirement) {
switch (config.requirement?.requirement) {
case 'sms':
return 'asked to enter code provided through SMS verification'
case 'idPhoto':
@ -122,7 +122,9 @@ const getRequirementText = config => {
case 'superuser':
return ''
case 'suspend':
return 'suspended'
return `suspended for ${orUnderline(
config.requirement.suspensionDays
)} days`
case 'block':
return 'blocked'
default:
@ -155,7 +157,6 @@ const InfoPanel = ({ step, config = {}, liveValues = {}, currency }) => {
const GetValues = ({ setValues }) => {
const { values } = useFormikContext()
useEffect(() => {
console.log('triggered')
setValues && values && setValues(values)
}, [setValues, values])

View file

@ -2,12 +2,11 @@ import { makeStyles, Box } from '@material-ui/core'
import classnames from 'classnames'
import { Field, useFormikContext } from 'formik'
import * as R from 'ramda'
import React from 'react'
import React, { memo } from 'react'
import * as Yup from 'yup'
import { TextInput, RadioGroup } from 'src/components/inputs/formik'
import Autocomplete from 'src/components/inputs/formik/Autocomplete'
import { H4 } from 'src/components/typography'
import { H4, Label2, Label1, Info2 } 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 { errorColor } from 'src/styling/variables'
@ -40,13 +39,45 @@ const useStyles = makeStyles({
},
directionName: {
marginLeft: 6
},
thresholdWrapper: {
display: 'flex'
},
thresholdField: {
margin: 10,
width: 208
},
space: {
marginLeft: 6,
marginRight: 6
},
lastSpace: {
marginLeft: 6
},
suspensionDays: {
width: 34
},
input: {
marginTop: -2
},
limitedInput: {
width: 50
},
daysInput: {
width: 60
}
})
const cashDirection = Yup.string().required('Required')
const triggerType = Yup.string().required('Required')
const threshold = Yup.number().required('Required')
const requirement = Yup.string().required('Required')
const threshold = Yup.object().shape({
threshold: Yup.number(),
thresholdDays: Yup.number()
})
const requirement = Yup.object().shape({
requirement: Yup.string().required('Required'),
suspensionDays: Yup.number()
})
const Schema = Yup.object().shape({
triggerType,
@ -59,9 +90,52 @@ const Schema = Yup.object().shape({
const directionSchema = Yup.object().shape({ cashDirection })
const directionOptions = [
{ display: 'Both', code: 'both' },
{ display: 'Only cash-in', code: 'cashIn' },
{ display: 'Only cash-out', code: 'cashOut' }
{
display: 'Both',
code: 'both'
},
{
display: 'Only cash-in',
code: 'cashIn'
},
{
display: 'Only cash-out',
code: 'cashOut'
}
]
const directionOptions2 = [
{
display: (
<>
<TxInIcon /> in
</>
),
code: 'cashIn'
},
{
display: (
<>
<TxOutIcon /> out
</>
),
code: 'cashOut'
},
{
display: (
<>
<Box display="flex">
<Box mr={0.25}>
<TxOutIcon />
</Box>
<Box>
<TxInIcon />
</Box>
</Box>
</>
),
code: 'both'
}
]
const Direction = () => {
@ -113,12 +187,25 @@ const typeOptions = [
const Type = () => {
const classes = useStyles()
const { errors, touched } = useFormikContext()
const { errors, touched, values } = useFormikContext()
const typeClass = {
[classes.error]: errors.triggerType && touched.triggerType
}
const containsType = R.contains(values?.triggerType)
const isThresholdEnabled = containsType([
'txAmount',
'txVolume',
'txVelocity'
])
const isThresholdDaysEnabled = containsType([
'txVolume',
'txVelocity',
'consecutiveDays'
])
return (
<>
<Box display="flex" alignItems="center">
@ -133,13 +220,26 @@ const Type = () => {
className={classes.radioGroup}
/>
<Field
component={TextInput}
label="Threshold"
size="lg"
name="threshold"
options={typeOptions}
/>
<div className={classes.thresholdWrapper}>
{isThresholdEnabled && (
<Field
className={classes.thresholdField}
component={TextInput}
label="Threshold"
size="lg"
name="threshold.threshold"
/>
)}
{isThresholdDaysEnabled && (
<Field
className={classes.thresholdField}
component={TextInput}
label="Threshold Days"
size="lg"
name="threshold.thresholdDays"
/>
)}
</div>
</>
)
}
@ -168,12 +268,14 @@ const requirementOptions = [
const Requirement = () => {
const classes = useStyles()
const { errors } = useFormikContext()
const { errors, values } = useFormikContext()
const titleClass = {
[classes.error]: errors.requirement
}
const isSuspend = values?.requirement?.requirement === 'suspend'
return (
<>
<Box display="flex" alignItems="center">
@ -181,12 +283,22 @@ const Requirement = () => {
</Box>
<Field
component={RadioGroup}
name="requirement"
name="requirement.requirement"
options={requirementOptions}
labelClassName={classes.specialLabel}
radioClassName={classes.radio}
className={classnames(classes.radioGroup, classes.specialGrid)}
/>
{isSuspend && (
<Field
className={classes.thresholdField}
component={TextInput}
label="Days"
size="lg"
name="requirement.suspensionDays"
/>
)}
</>
)
}
@ -219,7 +331,149 @@ const DirectionDisplay = ({ code }) => {
)
}
const elements = [
const RequirementInput = () => {
const { values } = useFormikContext()
const classes = useStyles()
const requirement = values?.requirement?.requirement
const isSuspend = requirement === 'suspend'
const display = getView(requirementOptions, 'display')(requirement)
return (
<Box display="flex" alignItems="baseline">
{`${display} ${isSuspend ? 'for' : ''}`}
{isSuspend && (
<Field
bold
className={classes.suspensionDays}
name="requirement.suspensionDays"
component={TextInput}
textAlign="center"
/>
)}
{isSuspend && 'days'}
</Box>
)
}
const RequirementView = ({ requirement, suspensionDays }) => {
const classes = useStyles()
const display = getView(requirementOptions, 'display')(requirement)
const isSuspend = requirement === 'suspend'
return (
<Box display="flex" alignItems="baseline">
{`${display} ${isSuspend ? 'for' : ''}`}
{isSuspend && (
<Info2 className={classes.space} noMargin>
{suspensionDays}
</Info2>
)}
{isSuspend && 'days'}
</Box>
)
}
const DisplayThreshold = ({ config, currency, isEdit }) => {
const classes = useStyles()
const inputClasses = {
[classes.input]: true,
[classes.limitedInput]: config?.triggerType === 'txVelocity',
[classes.daysInput]: config?.triggerType === 'consecutiveDays'
}
const threshold = config?.threshold?.threshold
const thresholdDays = config?.threshold?.thresholdDays
const Threshold = isEdit ? (
<Field
bold
className={classnames(inputClasses)}
name="threshold.threshold"
component={TextInput}
textAlign="right"
/>
) : (
<Info2 noMargin>{threshold}</Info2>
)
const ThresholdDays = isEdit ? (
<Field
bold
className={classnames(inputClasses)}
name="threshold.thresholdDays"
component={TextInput}
textAlign="right"
/>
) : (
<Info2 noMargin>{thresholdDays}</Info2>
)
switch (config?.triggerType) {
case 'txAmount':
return (
<Box display="flex" alignItems="baseline" justifyContent="right">
{Threshold}
<Label2 noMargin className={classes.lastSpace}>
{currency}
</Label2>
</Box>
)
case 'txVolume':
return (
<Box display="flex" alignItems="baseline" justifyContent="right">
{Threshold}
<Label2 noMargin className={classes.lastSpace}>
{currency}
</Label2>
<Label1 noMargin className={classes.space}>
in
</Label1>
{ThresholdDays}
<Label1 noMargin className={classes.lastSpace}>
days
</Label1>
</Box>
)
case 'txVelocity':
return (
<Box display="flex" alignItems="baseline" justifyContent="right">
{Threshold}
<Label1 className={classes.space} noMargin>
transactions in
</Label1>
{ThresholdDays}
<Label1 className={classes.lastSpace} noMargin>
days
</Label1>
</Box>
)
case 'consecutiveDays':
return (
<Box display="flex" alignItems="baseline" justifyContent="right">
{ThresholdDays}
<Label1 className={classes.lastSpace} noMargin>
days
</Label1>
</Box>
)
default:
return ''
}
}
const ThresholdInput = memo(({ currency }) => {
const { values } = useFormikContext()
return <DisplayThreshold isEdit={true} config={values} currency={currency} />
})
const ThresholdView = ({ config, currency }) => {
return <DisplayThreshold config={config} currency={currency} />
}
const getElements = (currency, classes) => [
{
name: 'triggerType',
size: 'sm',
@ -239,35 +493,28 @@ const elements = [
name: 'requirement',
size: 'sm',
width: 230,
input: ({ field: { value: name } }) => (
<>{getView(requirementOptions, 'display')(name)}</>
),
view: getView(requirementOptions, 'display'),
inputProps: {
options: requirementOptions,
valueProp: 'code',
getLabel: R.path(['display']),
limit: null
}
bypassField: true,
input: RequirementInput,
view: it => <RequirementView {...it} />
},
{
name: 'threshold',
size: 'sm',
width: 260,
width: 284,
textAlign: 'right',
input: TextInput
input: () => <ThresholdInput currency={currency} />,
view: (it, config) => <ThresholdView config={config} currency={currency} />
},
{
name: 'cashDirection',
size: 'sm',
width: 282,
view: it => <DirectionDisplay code={it} />,
input: Autocomplete,
input: RadioGroup,
inputProps: {
options: directionOptions,
valueProp: 'code',
getLabel: R.path(['display']),
limit: null
labelClassName: classes.tableRadioLabel,
className: classes.tableRadioGroup,
options: directionOptions2
}
}
]
@ -280,4 +527,37 @@ const sortBy = [
)
]
export { Schema, elements, direction, type, requirements, sortBy }
const fromServer = triggers =>
R.map(
({ requirement, suspensionDays, threshold, thresholdDays, ...rest }) => ({
requirement: {
requirement,
suspensionDays
},
threshold: {
threshold,
thresholdDays
},
...rest
})
)(triggers)
const toServer = triggers =>
R.map(({ requirement, threshold, ...rest }) => ({
requirement: requirement.requirement,
suspensionDays: requirement.suspensionDays,
threshold: threshold.threshold,
thresholdDays: threshold.thresholdDays,
...rest
}))(triggers)
export {
Schema,
getElements,
direction,
type,
requirements,
sortBy,
fromServer,
toServer
}