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
}