import classnames from 'classnames' import { Field, useFormikContext } from 'formik' import * as R from 'ramda' import React, { memo } from 'react' import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography' import * as Yup from 'yup' import { NumberInput, RadioGroup, Dropdown } from 'src/components/inputs/formik' import { transformNumber } from 'src/utils/number' import { onlyFirstToUpper } from 'src/utils/string' 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' }) }) // 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 { errors, touched, values, setTouched, handleChange } = useFormikContext() const typeClass = { 'text-tomato': 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 = { 'text-tomato': 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: schema => schema.nullable().transform(transformNumber), otherwise: schema => schema.nullable().transform(() => null) }), customInfoRequestId: Yup.string().when('requirement', { is: value => value !== 'custom', then: schema => schema.nullable().transform(() => '') }), externalService: Yup.string().when('requirement', { is: value => value !== 'external', then: schema => schema.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 case 'external': return requirement.requirement === type ? !R.isNil(requirement.externalService) : 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' }) if (requirement && !requirementValidator(requirement, 'external')) return context.createError({ path: 'requirement', message: 'You must select an item' }) }) const requirementOptions = [ { display: 'SMS verification', code: 'sms' }, { display: 'Email verification', code: 'email' }, { 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' }, { display: 'External verification', code: 'external' } ] 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 hasExternalRequirementError = (errors, touched, values) => !!errors.requirement && !!touched.requirement?.externalService && !values.requirement?.externalService const Requirement = ({ config = {}, triggers, emailAuth, complianceServices, customInfoRequests = [] }) => { const { touched, errors, values, handleChange, setTouched } = useFormikContext() const isSuspend = values?.requirement?.requirement === 'suspend' const isCustom = values?.requirement?.requirement === 'custom' const isExternal = values?.requirement?.requirement === 'external' const customRequirementsInUse = R.reduce( (acc, value) => { if (value.requirement.requirement === 'custom') acc.push({ triggerType: value.triggerType, id: value.requirement.customInfoRequestId }) return acc }, [], triggers ) const availableCustomRequirements = R.filter( it => !R.includes( { triggerType: config.triggerType, id: it.id }, customRequirementsInUse ), customInfoRequests ) const makeCustomReqOptions = () => availableCustomRequirements.map(it => ({ value: it.id, display: it.customRequest.name })) const enableCustomRequirement = !R.isEmpty(availableCustomRequirements) const customInfoOption = { display: 'Custom information requirement', code: 'custom' } const itemToRemove = emailAuth ? 'sms' : 'email' const reqOptions = requirementOptions.filter(it => it.code !== itemToRemove) const options = R.clone(reqOptions) enableCustomRequirement && options.push(customInfoOption) const titleClass = { 'text-tomato': (!!errors.requirement && !isSuspend && !isCustom && !isExternal) || (isSuspend && hasRequirementError(errors, touched, values)) || (isCustom && hasCustomRequirementError(errors, touched, values)) || (isExternal && hasExternalRequirementError(errors, touched, values)) } return ( <>

Choose a requirement

{ handleChange(e) setTouched({ suspensionDays: false }) }} /> {isSuspend && ( )} {isCustom && (
)} {isExternal && (
({ value: it.code, display: it.display }))} />
)} ) } const requirements = ( config, triggers, customInfoRequests, complianceServices, emailAuth ) => ({ schema: requirementSchema, options: requirementOptions, Component: Requirement, props: { config, triggers, customInfoRequests, emailAuth, complianceServices }, hasRequirementError: hasRequirementError, hasCustomRequirementError: hasCustomRequirementError, hasExternalRequirementError: hasExternalRequirementError, initialValues: { requirement: { requirement: '', suspensionDays: '', customInfoRequestId: '', externalService: '' } } }) const getView = (data, code, compare) => it => { if (!data) return '' return R.compose(R.prop(code), R.find(R.propEq(compare ?? 'code', it)))(data) } const customReqIdMatches = customReqId => it => { return it.id === customReqId } const RequirementInput = ({ customInfoRequests = [] }) => { const { values } = useFormikContext() 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, externalService, customInfoRequests = [] }) => { const display = requirement === 'custom' ? (R.path(['customRequest', 'name'])( R.find(customReqIdMatches(customInfoRequestId))(customInfoRequests) ) ?? '') : requirement === 'external' ? `External verification (${onlyFirstToUpper(externalService)})` : getView(requirementOptions, 'display')(requirement) const isSuspend = requirement === 'suspend' return (
{`${display} ${isSuspend ? 'for' : ''}`} {isSuspend && ( {suspensionDays} )} {isSuspend && 'days'}
) } const DisplayThreshold = ({ config, currency, isEdit }) => { const inputClasses = { '-mt-1': true, 'w-13': config?.triggerType === 'txVelocity', 'w-15': 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, 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: 260, bypassField: true, input: () => , view: it => ( ) }, { name: 'threshold', size: 'sm', width: 254, textAlign: 'right', input: () => , view: (it, config) => } ] 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 => { return R.map( ({ requirement, suspensionDays, threshold, thresholdDays, customInfoRequestId, externalService, ...rest }) => ({ requirement: { requirement, suspensionDays, customInfoRequestId, externalService }, 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, externalService: requirement.externalService, ...rest }))(triggers) export { Schema, getElements, // txDirection, type, requirements, sortBy, fromServer, toServer, getView, requirementOptions }