import { makeStyles, Box } from '@material-ui/core' import classnames from 'classnames' import { Field, useFormikContext } from 'formik' import * as R from 'ramda' import React, { memo } from 'react' import * as Yup from 'yup' import { NumberInput, RadioGroup, Dropdown } from 'src/components/inputs/formik' import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography' import { errorColor } from 'src/styling/variables' import { transformNumber } from 'src/utils/number' // import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' // import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' const useStyles = makeStyles({ radioLabel: { height: 40, padding: [[0, 10]] }, radio: { padding: 4, margin: 4 }, radioGroup: { flexDirection: 'row' }, error: { color: errorColor }, specialLabel: { height: 40, padding: 0 }, specialGrid: { display: 'grid', gridTemplateColumns: [[182, 162, 181]] }, directionIcon: { marginRight: 2 }, directionName: { marginLeft: 6 }, thresholdWrapper: { display: 'flex', flexDirection: 'column' }, thresholdTitle: { marginTop: 50 }, thresholdContentWrapper: { display: 'flex', flexDirection: 'row' }, thresholdField: { marginRight: 6, width: 75 }, description: { marginTop: 7 }, space: { marginLeft: 6, marginRight: 6 }, lastSpace: { marginLeft: 6 }, suspensionDays: { width: 34 }, input: { marginTop: -2 }, limitedInput: { width: 50 }, daysInput: { width: 60 }, dropdownField: { marginTop: 16, minWidth: 155 } }) // const direction = Yup.string().required() const triggerType = Yup.string().required() const threshold = Yup.object().shape({ threshold: Yup.number() .nullable() .transform(transformNumber) .label('Invalid threshold'), thresholdDays: Yup.number() .transform(transformNumber) .nullable() .label('Invalid threshold days') }) const requirement = Yup.object().shape({ requirement: Yup.string().required(), suspensionDays: Yup.number() .transform(transformNumber) .nullable() }) const Schema = Yup.object() .shape({ triggerType, requirement, threshold // direction }) .test(({ threshold, triggerType }, context) => { const errorMessages = { txAmount: threshold => 'Amount must be greater than or equal to 0', txVolume: threshold => { const thresholdMessage = 'Volume must be greater than or equal to 0' const thresholdDaysMessage = 'Days must be greater than 0' const message = [] if (threshold.threshold < 0) message.push(thresholdMessage) if (threshold.thresholdDays <= 0) message.push(thresholdDaysMessage) return message.join(', ') }, txVelocity: threshold => { const thresholdMessage = 'Transactions must be greater than 0' const thresholdDaysMessage = 'Days must be greater than 0' const message = [] if (threshold.threshold <= 0) message.push(thresholdMessage) if (threshold.thresholdDays <= 0) message.push(thresholdDaysMessage) return message.join(', ') }, consecutiveDays: threshold => 'Days must be greater than 0' } const thresholdValidator = { txAmount: threshold => threshold.threshold >= 0, txVolume: threshold => threshold.threshold >= 0 && threshold.thresholdDays > 0, txVelocity: threshold => threshold.threshold > 0 && threshold.thresholdDays > 0, consecutiveDays: threshold => threshold.thresholdDays > 0 } if (triggerType && thresholdValidator[triggerType](threshold)) return return context.createError({ path: 'threshold', message: errorMessages[triggerType](threshold) }) }) .test(({ requirement }, context) => { const requirementValidator = requirement => requirement.requirement === 'suspend' ? requirement.suspensionDays > 0 : true if (requirement && requirementValidator(requirement)) return return context.createError({ path: 'requirement', message: 'Suspension days must be greater than 0' }) }) // Direction V2 only // const directionSchema = Yup.object().shape({ direction }) // const directionOptions = [ // { // display: 'Both', // code: 'both' // }, // { // display: 'Only cash-in', // code: 'cashIn' // }, // { // display: 'Only cash-out', // code: 'cashOut' // } // ] // const directionOptions2 = [ // { // display: ( // <> // in // // ), // code: 'cashIn' // }, // { // display: ( // <> // out // // ), // code: 'cashOut' // }, // { // display: ( // <> // // // // // // // // // // ), // code: 'both' // } // ] // const Direction = () => { // const classes = useStyles() // const { errors } = useFormikContext() // const titleClass = { // [classes.error]: errors.direction // } // return ( // <> // //

// In which type of transactions will it trigger? //

//
// // // ) // } // const txDirection = { // schema: directionSchema, // options: directionOptions, // Component: Direction, // initialValues: { direction: '' } // } // TYPE const typeSchema = Yup.object() .shape({ triggerType: Yup.string('The trigger type must be a string').required( 'The trigger type is required' ), threshold: Yup.object({ threshold: Yup.number() .transform(transformNumber) .nullable(), thresholdDays: Yup.number() .transform(transformNumber) .nullable() }) }) .test(({ threshold, triggerType }, context) => { const errorMessages = { txAmount: threshold => 'Amount must be greater than or equal to 0', txVolume: threshold => { const thresholdMessage = 'Volume must be greater than or equal to 0' const thresholdDaysMessage = 'Days must be greater than 0' const message = [] if (!threshold.threshold || threshold.threshold < 0) message.push(thresholdMessage) if (!threshold.thresholdDays || threshold.thresholdDays <= 0) message.push(thresholdDaysMessage) return message.join(', ') }, txVelocity: threshold => { const thresholdMessage = 'Transactions must be greater than 0' const thresholdDaysMessage = 'Days must be greater than 0' const message = [] if (!threshold.threshold || threshold.threshold <= 0) message.push(thresholdMessage) if (!threshold.thresholdDays || threshold.thresholdDays <= 0) message.push(thresholdDaysMessage) return message.join(', ') }, consecutiveDays: threshold => 'Days must be greater than 0' } const thresholdValidator = { txAmount: threshold => threshold.threshold >= 0, txVolume: threshold => threshold.threshold >= 0 && threshold.thresholdDays > 0, txVelocity: threshold => threshold.threshold > 0 && threshold.thresholdDays > 0, consecutiveDays: threshold => threshold.thresholdDays > 0 } if (!triggerType) return if (triggerType && thresholdValidator[triggerType](threshold)) return return context.createError({ path: 'threshold', message: errorMessages[triggerType](threshold) }) }) const typeOptions = [ { display: 'Transaction amount', code: 'txAmount' }, { display: 'Transaction volume', code: 'txVolume' }, { display: 'Transaction velocity', code: 'txVelocity' }, { display: 'Consecutive days', code: 'consecutiveDays' } ] const Type = ({ ...props }) => { const classes = useStyles() const { errors, touched, values, setTouched, handleChange } = useFormikContext() const typeClass = { [classes.error]: errors.triggerType && touched.triggerType } const containsType = R.contains(values?.triggerType) const isThresholdCurrencyEnabled = containsType(['txAmount', 'txVolume']) const isTransactionAmountEnabled = containsType(['txVelocity']) const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity']) const isConsecutiveDaysEnabled = containsType(['consecutiveDays']) const hasAmountError = !!errors.threshold && !!touched.threshold?.threshold && !isConsecutiveDaysEnabled && (!values.threshold?.threshold || values.threshold?.threshold < 0) const hasDaysError = !!errors.threshold && !!touched.threshold?.thresholdDays && !containsType(['txAmount']) && (!values.threshold?.thresholdDays || values.threshold?.thresholdDays < 0) const triggerTypeError = !!(hasDaysError || hasAmountError) const thresholdClass = { [classes.error]: triggerTypeError } const isRadioGroupActive = () => { return ( isThresholdCurrencyEnabled || isTransactionAmountEnabled || isThresholdDaysEnabled || isConsecutiveDaysEnabled ) } return ( <>

Choose trigger type

{ handleChange(e) setTouched({ threshold: false, thresholdDays: false }) }} />
{isRadioGroupActive() && (

Threshold

)}
{isThresholdCurrencyEnabled && ( <> {props.currency} )} {isTransactionAmountEnabled && ( <> transactions )} {isThresholdDaysEnabled && ( <> in days )} {isConsecutiveDaysEnabled && ( <> consecutive days )}
) } const type = currency => ({ schema: typeSchema, options: typeOptions, Component: Type, props: { currency }, initialValues: { triggerType: '', threshold: { threshold: '', thresholdDays: '' } } }) const requirementSchema = Yup.object() .shape({ requirement: Yup.object({ requirement: Yup.string().required(), suspensionDays: Yup.number().when('requirement', { is: value => value === 'suspend', then: Yup.number() .nullable() .transform(transformNumber), otherwise: Yup.number() .nullable() .transform(() => null) }), customInfoRequestId: Yup.string().when('requirement', { is: value => value === 'custom', then: Yup.string(), otherwise: Yup.string() .nullable() .transform(() => '') }) }).required() }) .test(({ requirement }, context) => { const requirementValidator = (requirement, type) => { switch (type) { case 'suspend': return requirement.requirement === type ? requirement.suspensionDays > 0 : true case 'custom': return requirement.requirement === type ? !R.isNil(requirement.customInfoRequestId) : true default: return true } } if (requirement && !requirementValidator(requirement, 'suspend')) return context.createError({ path: 'requirement', message: 'Suspension days must be greater than 0' }) if (requirement && !requirementValidator(requirement, 'custom')) return context.createError({ path: 'requirement', message: 'You must select an item' }) }) const requirementOptions = [ { display: 'SMS verification', code: 'sms' }, { display: 'ID card image', code: 'idCardPhoto' }, { display: 'ID data', code: 'idCardData' }, { display: 'Customer camera', code: 'facephoto' }, { display: 'Sanctions', code: 'sanctions' }, { display: 'US SSN', code: 'usSsn' }, // { display: 'Super user', code: 'superuser' }, { display: 'Suspend', code: 'suspend' }, { display: 'Block', code: 'block' } ] const hasRequirementError = (errors, touched, values) => !!errors.requirement && !!touched.requirement?.suspensionDays && (!values.requirement?.suspensionDays || values.requirement?.suspensionDays < 0) const hasCustomRequirementError = (errors, touched, values) => !!errors.requirement && !!touched.requirement?.customInfoRequestId && (!values.requirement?.customInfoRequestId || !R.isNil(values.requirement?.customInfoRequestId)) const Requirement = ({ customInfoRequests }) => { const classes = useStyles() const { touched, errors, values, handleChange, setTouched } = useFormikContext() const isSuspend = values?.requirement?.requirement === 'suspend' const isCustom = values?.requirement?.requirement === 'custom' const makeCustomReqOptions = () => customInfoRequests.map(it => ({ value: it.id, display: it.customRequest.name })) const enableCustomRequirement = customInfoRequests?.length > 0 const customInfoOption = { display: 'Custom information requirement', code: 'custom' } const options = enableCustomRequirement ? [...requirementOptions, customInfoOption] : [...requirementOptions] const titleClass = { [classes.error]: (!!errors.requirement && !isSuspend && !isCustom) || (isSuspend && hasRequirementError(errors, touched, values)) || (isCustom && hasCustomRequirementError(errors, touched, values)) } return ( <>

Choose a requirement

{ handleChange(e) setTouched({ suspensionDays: false }) }} /> {isSuspend && ( )} {isCustom && (
)} ) } const requirements = customInfoRequests => ({ schema: requirementSchema, options: requirementOptions, Component: Requirement, props: { customInfoRequests }, hasRequirementError: hasRequirementError, hasCustomRequirementError: hasCustomRequirementError, initialValues: { requirement: { requirement: '', suspensionDays: '', customInfoRequestId: '' } } }) const getView = (data, code, compare) => it => { if (!data) return '' return R.compose(R.prop(code), R.find(R.propEq(compare ?? 'code', it)))(data) } // const DirectionDisplay = ({ code }) => { // const classes = useStyles() // const displayName = getView(directionOptions, 'display')(code) // const showCashIn = code === 'cashIn' || code === 'both' // const showCashOut = code === 'cashOut' || code === 'both' // return ( //
// {showCashOut && } // {showCashIn && } // {displayName} //
// ) // } const customReqIdMatches = customReqId => it => { return it.id === customReqId } const RequirementInput = ({ customInfoRequests }) => { const { values } = useFormikContext() const classes = useStyles() const requirement = values?.requirement?.requirement const customRequestId = R.path(['requirement', 'customInfoRequestId'])(values) ?? '' const isSuspend = requirement === 'suspend' const display = customRequestId ? R.path(['customRequest', 'name'])( R.find(customReqIdMatches(customRequestId))(customInfoRequests) ) ?? '' : getView(requirementOptions, 'display')(requirement) return ( {`${display} ${isSuspend ? 'for' : ''}`} {isSuspend && ( )} {isSuspend && 'days'} ) } const RequirementView = ({ requirement, suspensionDays, customInfoRequestId, customInfoRequests }) => { const classes = useStyles() const display = requirement === 'custom' ? R.path(['customRequest', 'name'])( R.find(customReqIdMatches(customInfoRequestId))(customInfoRequests) ) ?? '' : getView(requirementOptions, 'display')(requirement) const isSuspend = requirement === 'suspend' return ( {`${display} ${isSuspend ? 'for' : ''}`} {isSuspend && ( {suspensionDays} )} {isSuspend && 'days'} ) } 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 ? ( ) : ( {threshold} ) const ThresholdDays = isEdit ? ( ) : ( {thresholdDays} ) switch (config?.triggerType) { case 'txAmount': return ( {Threshold} {currency} ) case 'txVolume': return ( {Threshold} {currency} in {ThresholdDays} days ) case 'txVelocity': return ( {Threshold} transactions in {ThresholdDays} days ) case 'consecutiveDays': return ( {ThresholdDays} days ) default: return '' } } const ThresholdInput = memo(({ currency }) => { const { values } = useFormikContext() return }) const ThresholdView = ({ config, currency }) => { return } const getElements = (currency, classes, customInfoRequests) => [ { name: 'triggerType', size: 'sm', width: 230, input: ({ field: { value: name } }) => ( <>{getView(typeOptions, 'display')(name)} ), view: getView(typeOptions, 'display'), inputProps: { options: typeOptions, valueProp: 'code', labelProp: 'display', optionsLimit: null } }, { name: 'requirement', size: 'sm', width: 230, bypassField: true, input: () => , view: it => ( ) }, { name: 'threshold', size: 'sm', width: 284, textAlign: 'right', input: () => , view: (it, config) => } // { // name: 'direction', // size: 'sm', // width: 282, // view: it => , // input: RadioGroup, // inputProps: { // labelClassName: classes.tableRadioLabel, // className: classes.tableRadioGroup, // options: directionOptions2 // } // } ] const triggerOrder = R.map(R.prop('code'))(typeOptions) const sortBy = [ R.comparator( (a, b) => triggerOrder.indexOf(a.triggerType) < triggerOrder.indexOf(b.triggerType) ) ] const fromServer = (triggers, customInfoRequests) => { return R.map( ({ requirement, suspensionDays, threshold, thresholdDays, customInfoRequestId, ...rest }) => ({ requirement: { requirement, suspensionDays, customInfoRequestId }, 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, customInfoRequestId: requirement.customInfoRequestId, ...rest }))(triggers) export { Schema, getElements, // txDirection, type, requirements, sortBy, fromServer, toServer, getView, requirementOptions }