Merge pull request #1127 from josepfo/fix/twilio-api-ui-fields

fix: format twilio api response
This commit is contained in:
Rafael Taranto 2022-02-24 20:51:25 +00:00 committed by GitHub
commit df73dd847f
6 changed files with 203 additions and 114 deletions

View file

@ -125,7 +125,7 @@ async function updateCustomer (id, data, userToken) {
} }
if (data.subscriberInfo) { if (data.subscriberInfo) {
Promise.all([getCustomerById(id), settingsLoader.loadLatest()]) await Promise.all([getCustomerById(id), settingsLoader.loadLatest()])
.then(([customer, config]) => sms.getLookup(config, customer.phone)) .then(([customer, config]) => sms.getLookup(config, customer.phone))
.then(res => updateSubscriberData(id, res, userToken)) .then(res => updateSubscriberData(id, res, userToken))
.catch(logger.error) .catch(logger.error)
@ -575,6 +575,7 @@ function getCustomerById (id) {
.then(assignCustomerData) .then(assignCustomerData)
.then(getCustomInfoRequestsData) .then(getCustomInfoRequestsData)
.then(camelizeDeep) .then(camelizeDeep)
.then(formatSubscriberInfo)
} }
function assignCustomerData (customer) { function assignCustomerData (customer) {
@ -582,6 +583,26 @@ function assignCustomerData (customer) {
.then(customerEditedData => selectLatestData(customer, customerEditedData)) .then(customerEditedData => selectLatestData(customer, customerEditedData))
} }
function formatSubscriberInfo(customer) {
const subscriberInfo = customer.subscriberInfo
if(!subscriberInfo) return customer
const result = subscriberInfo.result
if(subscriberInfo.status !== 'successful' || _.isEmpty(result)) return customer
const name = _.get('belongs_to.name')(result)
const street = _.get('current_addresses[0].street_line_1')(result)
const city = _.get('current_addresses[0].city')(result)
const stateCode = _.get('current_addresses[0].state_code')(result)
const postalCode = _.get('current_addresses[0].postal_code')(result)
customer.subscriberInfo = {
name,
address: `${street ?? ''} ${city ?? ''}${street || city ? ',' : ''} ${stateCode ?? ''} ${postalCode ?? ''}`
}
return customer
}
/** /**
* Query the specific customer manually edited data * Query the specific customer manually edited data
* *

View file

@ -1,4 +1,3 @@
import { DialogActions, DialogContent, Dialog } from '@material-ui/core'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { parse, format } from 'date-fns/fp' import { parse, format } from 'date-fns/fp'
@ -7,9 +6,9 @@ import { useState, React } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import ImagePopper from 'src/components/ImagePopper' import ImagePopper from 'src/components/ImagePopper'
import { FeatureButton, Button, IconButton } from 'src/components/buttons' import { FeatureButton } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/formik' import { TextInput } from 'src/components/inputs/formik'
import { H3, Info3, H2 } from 'src/components/typography' import { H3, Info3 } from 'src/components/typography'
import { import {
OVERRIDE_AUTHORIZED, OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED OVERRIDE_REJECTED
@ -17,7 +16,6 @@ import {
import { ReactComponent as CardIcon } from 'src/styling/icons/ID/card/comet.svg' import { ReactComponent as CardIcon } from 'src/styling/icons/ID/card/comet.svg'
import { ReactComponent as PhoneIcon } from 'src/styling/icons/ID/phone/comet.svg' import { ReactComponent as PhoneIcon } from 'src/styling/icons/ID/phone/comet.svg'
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg' import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/comet.svg' import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/comet.svg'
import { ReactComponent as CustomerListViewReversedIcon } from 'src/styling/icons/circle buttons/customer-list-view/white.svg' import { ReactComponent as CustomerListViewReversedIcon } from 'src/styling/icons/circle buttons/customer-list-view/white.svg'
import { ReactComponent as CustomerListViewIcon } from 'src/styling/icons/circle buttons/customer-list-view/zodiac.svg' import { ReactComponent as CustomerListViewIcon } from 'src/styling/icons/circle buttons/customer-list-view/zodiac.svg'
@ -74,11 +72,11 @@ const CustomerData = ({
updateCustomRequest, updateCustomRequest,
authorizeCustomRequest, authorizeCustomRequest,
updateCustomEntry, updateCustomEntry,
retrieveAdditionalData retrieveAdditionalDataDialog,
setRetrieve
}) => { }) => {
const classes = useStyles() const classes = useStyles()
const [listView, setListView] = useState(false) const [listView, setListView] = useState(false)
const [retrieve, setRetrieve] = useState(false)
const idData = R.path(['idCardData'])(customer) const idData = R.path(['idCardData'])(customer)
const rawExpirationDate = R.path(['expirationDate'])(idData) const rawExpirationDate = R.path(['expirationDate'])(idData)
@ -103,7 +101,7 @@ const CustomerData = ({
) )
const phone = R.path(['phone'])(customer) const phone = R.path(['phone'])(customer)
const smsData = R.path(['subscriberInfo', 'result'])(customer) const smsData = R.path(['subscriberInfo'])(customer)
const isEven = elem => elem % 2 === 0 const isEven = elem => elem % 2 === 0
@ -179,7 +177,8 @@ const CustomerData = ({
}), }),
validationSchema: customerDataSchemas.idCardData, validationSchema: customerDataSchemas.idCardData,
initialValues: initialValues.idCardData, initialValues: initialValues.idCardData,
isAvailable: !R.isNil(idData) isAvailable: !R.isNil(idData),
editable: true
}, },
{ {
fields: smsDataElements, fields: smsDataElements,
@ -199,7 +198,8 @@ const CustomerData = ({
retrieveAdditionalData: () => setRetrieve(true), retrieveAdditionalData: () => setRetrieve(true),
initialValues: initialValues.smsData, initialValues: initialValues.smsData,
isAvailable: !R.isNil(phone), isAvailable: !R.isNil(phone),
hasAdditionalData: !R.isNil(smsData) && !R.isEmpty(smsData) hasAdditionalData: !R.isNil(smsData) && !R.isEmpty(smsData),
editable: false
}, },
{ {
title: 'Name', title: 'Name',
@ -207,7 +207,8 @@ const CustomerData = ({
authorize: () => {}, authorize: () => {},
reject: () => {}, reject: () => {},
save: () => {}, save: () => {},
isAvailable: false isAvailable: false,
editable: true
}, },
{ {
title: 'Sanctions check', title: 'Sanctions check',
@ -217,7 +218,8 @@ const CustomerData = ({
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }), updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }), reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
children: <Info3>{sanctionsDisplay}</Info3>, children: <Info3>{sanctionsDisplay}</Info3>,
isAvailable: !R.isNil(sanctions) isAvailable: !R.isNil(sanctions),
editable: true
}, },
{ {
fields: customerDataElements.frontCamera, fields: customerDataElements.frontCamera,
@ -244,7 +246,8 @@ const CustomerData = ({
hasImage: true, hasImage: true,
validationSchema: customerDataSchemas.frontCamera, validationSchema: customerDataSchemas.frontCamera,
initialValues: initialValues.frontCamera, initialValues: initialValues.frontCamera,
isAvailable: !R.isNil(customer.frontCameraPath) isAvailable: !R.isNil(customer.frontCameraPath),
editable: true
}, },
{ {
fields: customerDataElements.idCardPhoto, fields: customerDataElements.idCardPhoto,
@ -269,7 +272,8 @@ const CustomerData = ({
hasImage: true, hasImage: true,
validationSchema: customerDataSchemas.idCardPhoto, validationSchema: customerDataSchemas.idCardPhoto,
initialValues: initialValues.idCardPhoto, initialValues: initialValues.idCardPhoto,
isAvailable: !R.isNil(customer.idCardPhotoPath) isAvailable: !R.isNil(customer.idCardPhotoPath),
editable: true
}, },
{ {
fields: customerDataElements.usSsn, fields: customerDataElements.usSsn,
@ -282,7 +286,8 @@ const CustomerData = ({
deleteEditedData: () => deleteEditedData({ usSsn: null }), deleteEditedData: () => deleteEditedData({ usSsn: null }),
validationSchema: customerDataSchemas.usSsn, validationSchema: customerDataSchemas.usSsn,
initialValues: initialValues.usSsn, initialValues: initialValues.usSsn,
isAvailable: !R.isNil(customer.usSsn) isAvailable: !R.isNil(customer.usSsn),
editable: true
} }
] ]
@ -373,7 +378,7 @@ const CustomerData = ({
name: it, name: it,
label: onlyFirstToUpper(it), label: onlyFirstToUpper(it),
component: TextInput, component: TextInput,
editable: true editable: false
}) })
}, R.keys(smsData) ?? []) }, R.keys(smsData) ?? [])
@ -392,7 +397,8 @@ const CustomerData = ({
validationSchema, validationSchema,
initialValues, initialValues,
hasImage, hasImage,
hasAdditionalData hasAdditionalData,
editable
}, },
idx idx
) => { ) => {
@ -412,7 +418,8 @@ const CustomerData = ({
initialValues={initialValues} initialValues={initialValues}
save={save} save={save}
deleteEditedData={deleteEditedData} deleteEditedData={deleteEditedData}
retrieveAdditionalData={retrieveAdditionalData}></EditableCard> retrieveAdditionalData={retrieveAdditionalData}
editable={editable}></EditableCard>
) )
} }
@ -491,67 +498,9 @@ const CustomerData = ({
</div> </div>
)} )}
</div> </div>
<RetrieveDataDialog {retrieveAdditionalDataDialog}
setRetrieve={setRetrieve}
retrieveAdditionalData={retrieveAdditionalData}
open={retrieve}></RetrieveDataDialog>
</div> </div>
) )
} }
const RetrieveDataDialog = ({
setRetrieve,
retrieveAdditionalData,
open,
props
}) => {
const classes = useStyles()
return (
<Dialog
open={open}
aria-labelledby="form-dialog-title"
PaperProps={{
style: {
borderRadius: 8,
minWidth: 656,
bottom: 125,
right: 7
}
}}
{...props}>
<div className={classes.closeButton}>
<IconButton
size={16}
aria-label="close"
onClick={() => setRetrieve(false)}>
<CloseIcon />
</IconButton>
</div>
<H2 className={classes.dialogTitle}>{'Retrieve API data from Twilio'}</H2>
<DialogContent className={classes.dialogContent}>
<Info3>{`With this action you'll be using Twilio's API to retrieve additional
data from this user. This includes name and address, if available.\n`}</Info3>
<Info3>{` There is a small cost from Twilio for each retrieval. Would you like
to proceed?`}</Info3>
</DialogContent>
<DialogActions className={classes.dialogActions}>
<Button
backgroundColor="grey"
className={classes.cancelButton}
onClick={() => setRetrieve(false)}>
Cancel
</Button>
<Button
onClick={() => {
retrieveAdditionalData()
setRetrieve(false)
}}>
Confirm
</Button>
</DialogActions>
</Dialog>
)
}
export default CustomerData export default CustomerData

View file

@ -1,4 +1,4 @@
import { offColor, spacer } from 'src/styling/variables' import { offColor } from 'src/styling/variables'
export default { export default {
header: { header: {
@ -45,26 +45,5 @@ export default {
left: '100%', left: '100%',
marginLeft: 15 marginLeft: 15
} }
},
closeButton: {
display: 'flex',
padding: [[spacer * 2, spacer * 2, 0, spacer * 2]],
paddingRight: spacer * 1.5,
justifyContent: 'end'
},
dialogTitle: {
margin: [[0, spacer * 2, spacer, spacer * 4 + spacer]]
},
dialogContent: {
width: 615,
marginLeft: 16
},
dialogActions: {
padding: spacer * 4,
paddingTop: spacer * 2
},
cancelButton: {
marginRight: 8,
padding: 0
} }
} }

View file

@ -1,18 +1,27 @@
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Breadcrumbs, Box } from '@material-ui/core' import {
makeStyles,
Breadcrumbs,
Box,
DialogActions,
DialogContent,
Dialog
} from '@material-ui/core'
import NavigateNextIcon from '@material-ui/icons/NavigateNext' import NavigateNextIcon from '@material-ui/icons/NavigateNext'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React, { memo, useState } from 'react' import React, { memo, useState } from 'react'
import { useHistory, useParams } from 'react-router-dom' import { useHistory, useParams } from 'react-router-dom'
import { ActionButton } from 'src/components/buttons' import ErrorMessage from 'src/components/ErrorMessage'
import { Button, IconButton, ActionButton } from 'src/components/buttons'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import { Label1, Label2 } from 'src/components/typography' import { Label1, Label2, H2, Info3 } from 'src/components/typography'
import { import {
OVERRIDE_AUTHORIZED, OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED OVERRIDE_REJECTED
} from 'src/pages/Customers/components/propertyCard' } from 'src/pages/Customers/components/propertyCard'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.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 AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/block/white.svg' import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/block/white.svg'
@ -280,9 +289,10 @@ const GET_ACTIVE_CUSTOM_REQUESTS = gql`
const CustomerProfile = memo(() => { const CustomerProfile = memo(() => {
const history = useHistory() const history = useHistory()
const [retrieve, setRetrieve] = useState(false)
const [showCompliance, setShowCompliance] = useState(false) const [showCompliance, setShowCompliance] = useState(false)
const [wizard, setWizard] = useState(false) const [wizard, setWizard] = useState(false)
const [error] = useState(null) const [error, setError] = useState(null)
const [clickedItem, setClickedItem] = useState('overview') const [clickedItem, setClickedItem] = useState('overview')
const { id: customerId } = useParams() const { id: customerId } = useParams()
@ -323,7 +333,11 @@ const CustomerProfile = memo(() => {
}) })
const [setCustomer] = useMutation(SET_CUSTOMER, { const [setCustomer] = useMutation(SET_CUSTOMER, {
onCompleted: () => getCustomer() onCompleted: () => {
getCustomer()
setRetrieve(false)
},
onError: error => setError(error)
}) })
const [authorizeCustomRequest] = useMutation(SET_AUTHORIZED_REQUEST, { const [authorizeCustomRequest] = useMutation(SET_AUTHORIZED_REQUEST, {
@ -640,7 +654,20 @@ const CustomerProfile = memo(() => {
updateCustomRequest={setCustomerCustomInfoRequest} updateCustomRequest={setCustomerCustomInfoRequest}
authorizeCustomRequest={authorizeCustomRequest} authorizeCustomRequest={authorizeCustomRequest}
updateCustomEntry={updateCustomEntry} updateCustomEntry={updateCustomEntry}
retrieveAdditionalData={retrieveAdditionalData}></CustomerData> setRetrieve={setRetrieve}
retrieveAdditionalDataDialog={
<RetrieveDataDialog
onDismissed={() => {
setError(null)
setRetrieve(false)
}}
onConfirmed={() => {
setError(null)
retrieveAdditionalData()
}}
error={error}
open={retrieve}></RetrieveDataDialog>
}></CustomerData>
</div> </div>
)} )}
{isNotes && ( {isNotes && (
@ -677,4 +704,64 @@ const CustomerProfile = memo(() => {
) )
}) })
const RetrieveDataDialog = ({
onConfirmed,
onDismissed,
open,
error,
props
}) => {
const classes = useStyles()
return (
<Dialog
open={open}
aria-labelledby="form-dialog-title"
PaperProps={{
style: {
borderRadius: 8,
minWidth: 656,
bottom: 125,
right: 7
}
}}
{...props}>
<div className={classes.closeButton}>
<IconButton
size={16}
aria-label="close"
onClick={() => onDismissed(false)}>
<CloseIcon />
</IconButton>
</div>
<H2 className={classes.dialogTitle}>{'Retrieve API data from Twilio'}</H2>
<DialogContent className={classes.dialogContent}>
<Info3>{`With this action you'll be using Twilio's API to retrieve additional
data from this user. This includes name and address, if available.\n`}</Info3>
<Info3>{` There is a small cost from Twilio for each retrieval. Would you like
to proceed?`}</Info3>
</DialogContent>
{error && (
<ErrorMessage className={classes.errorMessage}>
Failed to save
</ErrorMessage>
)}
<DialogActions className={classes.dialogActions}>
<Button
backgroundColor="grey"
className={classes.cancelButton}
onClick={() => onDismissed(false)}>
Cancel
</Button>
<Button
onClick={() => {
onConfirmed()
}}>
Confirm
</Button>
</DialogActions>
</Dialog>
)
}
export default CustomerProfile export default CustomerProfile

View file

@ -1,4 +1,4 @@
import { comet, subheaderColor } from 'src/styling/variables' import { comet, subheaderColor, spacer } from 'src/styling/variables'
export default { export default {
labelLink: { labelLink: {
@ -53,5 +53,29 @@ export default {
backgroundColor: subheaderColor, backgroundColor: subheaderColor,
borderRadius: 8, borderRadius: 8,
padding: [[0, 5]] padding: [[0, 5]]
},
closeButton: {
display: 'flex',
padding: [[spacer * 2, spacer * 2, 0, spacer * 2]],
paddingRight: spacer * 1.5,
justifyContent: 'end'
},
dialogTitle: {
margin: [[0, spacer * 2, spacer, spacer * 4 + spacer]]
},
dialogContent: {
width: 615,
marginLeft: 16
},
dialogActions: {
padding: spacer * 4,
paddingTop: spacer * 2
},
cancelButton: {
marginRight: 8,
padding: 0
},
errorMessage: {
marginLeft: 38
} }
} }

View file

@ -145,7 +145,8 @@ const EditableCard = ({
initialValues, initialValues,
deleteEditedData, deleteEditedData,
retrieveAdditionalData, retrieveAdditionalData,
hasAdditionalData = true hasAdditionalData = true,
editable
}) => { }) => {
const classes = useStyles() const classes = useStyles()
@ -274,6 +275,7 @@ const EditableCard = ({
</ActionButton> </ActionButton>
)} )}
</div> </div>
{editable && (
<ActionButton <ActionButton
color="primary" color="primary"
Icon={EditIcon} Icon={EditIcon}
@ -281,6 +283,33 @@ const EditableCard = ({
onClick={() => setEditing(true)}> onClick={() => setEditing(true)}>
Edit Edit
</ActionButton> </ActionButton>
)}
{!editable &&
authorize &&
authorized.label !== 'Accepted' && (
<div className={classes.button}>
<ActionButton
color="spring"
type="button"
Icon={AuthorizeIcon}
InverseIcon={AuthorizeIcon}
onClick={() => authorize()}>
Authorize
</ActionButton>
</div>
)}
{!editable &&
authorize &&
authorized.label !== 'Rejected' && (
<ActionButton
color="tomato"
type="button"
Icon={BlockIcon}
InverseIcon={BlockIcon}
onClick={() => reject()}>
Reject
</ActionButton>
)}
</div> </div>
)} )}
{editing && ( {editing && (