lamassu-server/new-lamassu-admin/src/pages/Customers/helper.jsx
2025-05-12 09:45:42 +01:00

547 lines
14 KiB
JavaScript

import React from 'react'
import { parse, isValid, format } from 'date-fns/fp'
import { Field, useFormikContext } from 'formik'
import { parsePhoneNumberFromString } from 'libphonenumber-js'
import * as R from 'ramda'
import { H4 } from 'src/components/typography'
import { validate as uuidValidate } from 'uuid'
import * as Yup from 'yup'
import {
RadioGroup,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { MANUAL } from 'src/utils/constants'
import { Upload } from './components'
const CUSTOMER_BLOCKED = 'blocked'
const CUSTOM = 'custom'
const REQUIREMENT = 'requirement'
const ID_CARD_DATA = 'idCardData'
const getAuthorizedStatus = (it, triggers, customRequests) => {
const fields = R.concat(
['frontCamera', 'idCardData', 'idCardPhoto', 'email', 'usSsn', 'sanctions'],
R.map(ite => ite.id, customRequests)
)
const fieldsWithPathSuffix = ['frontCamera', 'idCardPhoto']
const isManualField = fieldName => {
const triggerName = R.equals(fieldName, 'frontCamera')
? 'facephoto'
: fieldName
const manualOverrides = R.filter(
ite => R.equals(R.toLower(ite.automation), MANUAL),
triggers?.overrides ?? []
)
return (
!!R.find(
ite => R.equals(ite.requirement, triggerName),
manualOverrides
) || R.equals(R.toLower(triggers.automation ?? ''), MANUAL)
)
}
const pendingFieldStatus = R.map(ite => {
if (isManualField(ite)) {
if (uuidValidate(ite)) {
const request = R.find(
iter => iter.infoRequestId === ite,
it.customInfoRequests
)
return !R.isNil(request) && R.equals(request.override, 'automatic')
}
const regularFieldValue = R.includes(ite, fieldsWithPathSuffix)
? it[`${ite}Path`]
: it[`${ite}`]
if (R.isNil(regularFieldValue)) return false
return R.equals(it[`${ite}Override`], 'automatic')
}
return false
}, fields)
const rejectedFieldStatus = R.map(ite => {
if (isManualField(ite)) {
if (uuidValidate(ite)) {
const request = R.find(
iter => iter.infoRequestId === ite,
it.customInfoRequests
)
return !R.isNil(request) && R.equals(request.override, 'blocked')
}
const regularFieldValue = R.includes(ite, fieldsWithPathSuffix)
? it[`${ite}Path`]
: it[`${ite}`]
if (R.isNil(regularFieldValue)) return false
return R.equals(it[`${ite}Override`], 'blocked')
}
return false
}, fields)
if (it.authorizedOverride === CUSTOMER_BLOCKED)
return { label: 'Blocked', type: 'error' }
if (it.isSuspended)
return it.daysSuspended > 0
? { label: `${it.daysSuspended} day suspension`, type: 'warning' }
: { label: `< 1 day suspension`, type: 'warning' }
if (R.any(ite => ite === true, rejectedFieldStatus))
return { label: 'Rejected', type: 'error' }
if (R.any(ite => ite === true, pendingFieldStatus))
return { label: 'Pending', type: 'warning' }
return { label: 'Authorized', type: 'success' }
}
const getFormattedPhone = (phone, country) => {
const phoneNumber =
phone && country ? parsePhoneNumberFromString(phone, country) : null
return phoneNumber ? phoneNumber.formatInternational() : phone
}
const getName = it => {
const idData = R.path(['idCardData'])(it)
return `${R.path(['firstName'])(idData) ?? ''} ${
R.path(['lastName'])(idData) ?? ''
}`.trim()
}
// Manual Entry Wizard
const entryOptions = [
{ display: 'Custom entry', code: 'custom' },
{ display: 'Populate existing requirement', code: 'requirement' }
]
const dataOptions = [
{ display: 'Text', code: 'text' }
// TODO: Requires backend modifications to support File and Image
// { display: 'File', code: 'file' },
// { display: 'Image', code: 'image' }
]
const requirementOptions = [
{ display: 'ID card image', code: 'idCardPhoto' },
{ display: 'ID data', code: 'idCardData' },
{ display: 'US SSN', code: 'usSsn' },
{ display: 'Email', code: 'email' },
{ display: 'Customer camera', code: 'frontCamera' }
]
const customTextOptions = [
{ label: 'Data entry title', name: 'title' },
{ label: 'Data entry', name: 'data' }
]
const customUploadOptions = [{ label: 'Data entry title', name: 'title' }]
const entryTypeSchema = Yup.lazy(values => {
if (values.entryType === 'custom') {
return Yup.object().shape({
entryType: Yup.string().required(),
dataType: Yup.string().required()
})
} else if (values.entryType === 'requirement') {
return Yup.object().shape({
entryType: Yup.string().required(),
requirement: Yup.string().required()
})
}
})
const customFileSchema = Yup.object().shape({
title: Yup.string().required(),
file: Yup.mixed().required()
})
const customImageSchema = Yup.object().shape({
title: Yup.string().required(),
image: Yup.mixed().required()
})
const customTextSchema = Yup.object().shape({
title: Yup.string().required(),
data: Yup.string().required()
})
const updateRequirementOptions = it => [
{
display: 'Custom information requirement',
code: 'custom'
},
...it
]
const EntryType = ({ customInfoRequirementOptions }) => {
const { values } = useFormikContext()
const displayCustomOptions = values.entryType === CUSTOM
const displayRequirementOptions = values.entryType === REQUIREMENT
const Entry = ({ title, name, options, className }) => (
<div>
<div className="flex items-center">
<H4>{title}</H4>
</div>
<Field
component={RadioGroup}
name={name}
options={options}
radioClassName="p-1 m-1"
labelClassName={className}
className="grid grid-cols-[182px_162px_141px]"
/>
</div>
)
return (
<>
<Entry
title="Type of entry"
name="entryType"
options={entryOptions}
className="w-62"
/>
{displayCustomOptions && (
<Entry title="Type of data" name="dataType" options={dataOptions} />
)}
{displayRequirementOptions && (
<Entry
title="Requirements"
name="requirement"
options={requirementOptions}
/>
)}
</>
)
}
const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => {
const typeOfEntrySelected = selectedValues?.entryType
const dataTypeSelected = selectedValues?.dataType
const requirementSelected = selectedValues?.requirement
const displayRequirements = typeOfEntrySelected === 'requirement'
const isCustomInfoRequirement = requirementSelected === CUSTOM
const updatedRequirementOptions = !R.isEmpty(customInfoRequirementOptions)
? updateRequirementOptions(requirementOptions)
: requirementOptions
const requirementName = displayRequirements
? R.find(R.propEq('code', requirementSelected))(updatedRequirementOptions)
.display
: ''
const title = displayRequirements
? `Requirement ${requirementName}`
: `Custom ${dataTypeSelected} entry`
const elements = displayRequirements
? requirementElements[requirementSelected]
: customElements[dataTypeSelected]
const upload = displayRequirements
? requirementSelected === 'idCardPhoto' ||
requirementSelected === 'frontCamera'
: dataTypeSelected === 'file' || dataTypeSelected === 'image'
return (
<>
<div className="flex items-center">
<H4>{title}</H4>
</div>
{isCustomInfoRequirement && (
<Autocomplete
fullWidth
label={`Available requests`}
className="w-37"
isOptionEqualToValue={R.eqProps('code')}
labelProp={'display'}
options={customInfoRequirementOptions}
onChange={(evt, it) => {}}
/>
)}
<div className="mb-6">
{!upload &&
!isCustomInfoRequirement &&
elements.options.map(({ label, name }, idx) => (
<Field
key={idx}
name={name}
label={label}
component={TextInput}
width={390}
/>
))}
</div>
{upload && (
<Upload
type={
displayRequirements ? requirementSelected : dataTypeSelected
}></Upload>
)}
</>
)
}
const customElements = {
text: {
schema: customTextSchema,
options: customTextOptions,
Component: ManualDataEntry,
initialValues: { data: '', title: '' },
saveType: 'customEntry'
},
file: {
schema: customFileSchema,
options: customUploadOptions,
Component: ManualDataEntry,
initialValues: { file: null, title: '' },
saveType: 'customEntryUpload'
},
image: {
schema: customImageSchema,
options: customUploadOptions,
Component: ManualDataEntry,
initialValues: { image: null, title: '' },
saveType: 'customEntryUpload'
}
}
const entryType = {
schema: entryTypeSchema,
options: entryOptions,
Component: EntryType,
initialValues: { entryType: '' }
}
// Customer data
const customerDataElements = {
idCardData: [
{
name: 'firstName',
label: 'First name',
component: TextInput,
editable: true
},
{
name: 'documentNumber',
label: 'ID number',
component: TextInput,
editable: true
},
{
name: 'dateOfBirth',
label: 'Birthdate',
component: TextInput,
editable: true
},
{
name: 'gender',
label: 'Gender',
component: TextInput,
editable: true
},
{
name: 'lastName',
label: 'Last name',
component: TextInput,
editable: true
},
{
name: 'expirationDate',
label: 'Expiration date',
component: TextInput,
editable: true
},
{
name: 'country',
label: 'Country',
component: TextInput,
editable: true
}
],
usSsn: [
{
name: 'usSsn',
label: 'US SSN',
component: TextInput,
size: 190,
editable: true
}
],
email: [
{
name: 'email',
label: 'Email',
component: TextInput,
size: 190,
editable: false
}
],
idCardPhoto: [{ name: 'idCardPhoto' }],
frontCamera: [{ name: 'frontCamera' }]
}
const customerDataSchemas = {
idCardData: Yup.object().shape({
firstName: Yup.string().required(),
lastName: Yup.string().required(),
documentNumber: Yup.string().required(),
dateOfBirth: Yup.string()
.test({
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val)),
message: 'Date must be in format YYYY-MM-DD'
})
.required(),
gender: Yup.string().required(),
country: Yup.string().required(),
expirationDate: Yup.string()
.test({
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val)),
message: 'Date must be in format YYYY-MM-DD'
})
.required()
}),
usSsn: Yup.object().shape({
usSsn: Yup.string().required()
}),
idCardPhoto: Yup.object().shape({
idCardPhoto: Yup.mixed().required()
}),
frontCamera: Yup.object().shape({
frontCamera: Yup.mixed().required()
}),
email: Yup.object().shape({
email: Yup.string().required()
})
}
const requirementElements = {
idCardData: {
schema: customerDataSchemas.idCardData,
options: customerDataElements.idCardData,
Component: ManualDataEntry,
initialValues: {
firstName: '',
lastName: '',
documentNumber: '',
dateOfBirth: '',
gender: '',
country: '',
expirationDate: ''
},
saveType: 'customerData'
},
usSsn: {
schema: customerDataSchemas.usSsn,
options: customerDataElements.usSsn,
Component: ManualDataEntry,
initialValues: { usSsn: '' },
saveType: 'customerData'
},
email: {
schema: customerDataSchemas.email,
options: customerDataElements.email,
Component: ManualDataEntry,
initialValues: { email: '' },
saveType: 'customerData'
},
idCardPhoto: {
schema: customerDataSchemas.idCardPhoto,
options: customerDataElements.idCardPhoto,
Component: ManualDataEntry,
initialValues: { idCardPhoto: null },
saveType: 'customerDataUpload'
},
frontCamera: {
schema: customerDataSchemas.frontCamera,
options: customerDataElements.frontCamera,
Component: ManualDataEntry,
initialValues: { frontCamera: null },
saveType: 'customerDataUpload'
},
custom: {
// schema: customerDataSchemas.customInfoRequirement,
Component: ManualDataEntry,
initialValues: { customInfoRequirement: null },
saveType: 'customInfoRequirement'
}
}
const tryFormatDate = rawDate => {
try {
return (
(rawDate &&
format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDate))) ??
''
)
} catch (err) {
return ''
}
}
const formatDates = values => {
R.map(
elem =>
(values[elem] = format('yyyyMMdd')(
parse(new Date(), 'yyyy-MM-dd', values[elem])
))
)(['dateOfBirth', 'expirationDate'])
return values
}
const mapKeys = pair => {
const [key, value] = pair
if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') {
return ['path', value]
}
if (key === 'txCustomerPhotoAt' || key === 'frontCameraAt') {
return ['date', value]
}
return pair
}
const addPhotoDir = R.map(it => {
const hasFrontCameraData = R.has('id')(it)
return hasFrontCameraData
? { ...it, photoDir: 'operator-data/customersphotos' }
: { ...it, photoDir: 'front-camera-photo' }
})
const standardizeKeys = R.map(R.compose(R.fromPairs, R.map(mapKeys), R.toPairs))
const filterByPhotoAvailable = R.filter(
tx => !R.isNil(tx.date) && !R.isNil(tx.path)
)
const formatPhotosData = R.compose(
filterByPhotoAvailable,
addPhotoDir,
standardizeKeys
)
export {
getAuthorizedStatus,
getFormattedPhone,
getName,
entryType,
customElements,
requirementElements,
formatPhotosData,
customerDataElements,
customerDataSchemas,
formatDates,
tryFormatDate,
REQUIREMENT,
CUSTOM,
ID_CARD_DATA
}