Skip to content

Commit

Permalink
(Review third) Update FI profile - mail api integration (#283)
Browse files Browse the repository at this point in the history
Part 3 of #222 

<img width="1226" alt="Screenshot 2024-02-29 at 4 05 55 PM"
src="https://github.com/cfpb/sbl-frontend/assets/2592907/9dd7c191-a102-44ff-9709-3758d9e72f5b">


## Changes

- Formats form data for email submission
- Triggers Mail API call upon submission
- Replace "Simulated" submission with actual Mail API call

## Planned improvements
- [ ] Data submission (formatFinancialProfileObject): Do further
comparison against `defaultValues` to determine which data was changed,
so that we only send SBL Help the info they actually need to update.
This may also be resolved if we can address the [`forwardRef`
errors](cfpb/design-system-react#316) we've
been seeing, which may fix the issue of changes to these fields not
being registered in `react-hook-form`

## How to test this PR

1. In `console` ensure routing is enabled: `setIsRoutingEnabled(true)`
1. Login as a user with an associated institution
2. Click on the Institution to `View institution profile`
3. Click on `Update institution profile`
4. Change some Institution data
5. Hit submit
6. Hopefully an email gets generated 🤞🏾 
7. In `console`, you should also see the data that will be sent. 
<img width="549" alt="Screenshot 2024-02-28 at 5 41 29 PM"
src="https://github.com/cfpb/sbl-frontend/assets/2592907/f0515aae-b02b-473c-88b3-4a7a9a881536">

## Notes
- Submission data format (decoded)
``` decoded
{
     "tax_id": "tax_id",
     "rssd_id": "rss_id",
     "parent_legal_name": "parent-name",
     "parent_lei": "parent_lei",
     "parent_rssd_id": "parent-rssd",
     "top_holder_legal_name": "top-name",
     "top_holder_lei": "top-lei",
     "top_holder_rssd_id": "top-rssd",
     "sbl_institution_types": "bankSavings,minorityDepository,other",
     "sbl_institution_types_other": "Test SBL Type",
     "additional_details": "details"
}
```
- Submission data format (encoded)
```
tax_id=tax_id&rssd_id=rss_id&parent_legal_name=parent-name&parent_lei=parent_lei&parent_rssd_id=parent-rssd&top_holder_legal_name=top-name&top_holder_lei=top-lei&top_holder_rssd_id=top-rssd&sbl_institution_types=bankSavings%2CminorityDepository%2Cother&sbl_institution_types_other=Test+SBL+Type&additional_details=details
```

---------

Co-authored-by: shindigira <shindigira@gmail.com>
  • Loading branch information
meissadia and shindigira committed Mar 20, 2024
1 parent e083993 commit d5f3f60
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 170 deletions.
62 changes: 41 additions & 21 deletions src/api/requests/submitUpdateFinancialProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,57 @@ import { request } from 'api/axiosService';
import type { CaseType } from 'api/common';
import { caseTypes } from 'api/common';
import type { SblAuthProperties } from 'api/useSblAuth';
import type { UFPSchema } from 'pages/Filing/UpdateFinancialProfile/types';

// Used to remove 'checkboxes' property
function omit(key: string, object: UFPSchema): Record<string, string> {
// @ts-expect-error intentional key omission
const { [key]: omitted, ...rest } = object;
return rest;
}

// Pulls 'checkboxes' property out to keep the object flat, and then reinserts every checkbox property at first depth
const formatFinancialProfileObject = (
object: UFPSchema,
): Record<string, string> => {
const solution = omit('checkboxes', object);
for (const key of Object.keys(object.checkboxes)) {
solution[key] = String(object.checkboxes[key]);
import { checkboxOptions } from 'pages/Filing/UpdateFinancialProfile/types';
import type { InstitutionDetailsApiType } from 'types/formTypes';
import { One } from 'utils/constants';

export const collectChangedData = (
formData: InstitutionDetailsApiType,
changedFields: Record<string, boolean | undefined>,
): InstitutionDetailsApiType => {
const result: InstitutionDetailsApiType = {};

// Include only fields which have been identified as "changed"
for (const key of Object.keys(changedFields)) {
result[key] = formData[key] as string;
}

// Institution types are not registered as "changed" by react-hook-form (because they're in an array?), so we have to manually process them.
if (
formData.sbl_institution_types &&
typeof formData.sbl_institution_types === 'object'
) {
const sblInstitutionTypes = [];
for (const key of formData.sbl_institution_types.keys()) {
if (formData.sbl_institution_types[key]) {
const indexToTypeArray = Number(key) - One;
sblInstitutionTypes.push(checkboxOptions[indexToTypeArray].label);
}
}

result.sbl_institution_types = sblInstitutionTypes.join(', ');

// TODO: Okay to merge 'Other' into this listing?
if (sblInstitutionTypes.includes('Other'))
result.sbl_institution_types += ` (${formData.sbl_institution_types_other})`;
}
return solution;

// TODO: additional_details is not registering as "changed" (due to ref forwarding issue?), need to manually process them.
if ((formData.additional_details ?? '').length > 0)
result.additional_details = formData.additional_details;

return result;
};

const submitUpdateFinancialProfile = async (
auth: SblAuthProperties,
financialProfileObject: UFPSchema,
financialProfileObject: Record<string, string>,
): Promise<null> => {
return request<null>({
url: `/send`,
method: 'post',
// ex: 'userName=test%40gmail.com&password=Password%21&grant_type=password'
body: new URLSearchParams(
formatFinancialProfileObject(financialProfileObject),
),
body: new URLSearchParams(financialProfileObject),
headers: {
Authorization: `Bearer ${auth.user?.access_token}`,
'Content-Type': 'application/x-www-form-urlencoded',
Expand Down
89 changes: 42 additions & 47 deletions src/pages/Filing/UpdateFinancialProfile/UfpForm.tsx
Original file line number Diff line number Diff line change
@@ -1,107 +1,100 @@
import { zodResolver } from '@hookform/resolvers/zod';
import submitUpdateFinancialProfile, {
collectChangedData,
} from 'api/requests/submitUpdateFinancialProfile';
import useSblAuth from 'api/useSblAuth';
import CrumbTrail from 'components/CrumbTrail';
import FormButtonGroup from 'components/FormButtonGroup';
import FormErrorHeader from 'components/FormErrorHeader';
import FormHeaderWrapper from 'components/FormHeaderWrapper';
import FormWrapper from 'components/FormWrapper';
import { Button, Link, TextIntroduction } from 'design-system-react';
import type { JSXElement } from 'design-system-react/dist/types/jsxElement';
import type { UFPSchema } from 'pages/Filing/UpdateFinancialProfile/types';
import { ufpSchema } from 'pages/Filing/UpdateFinancialProfile/types';
import { scenarios } from 'pages/Summary/Summary.data';
import { useState } from 'react';
import { useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { Navigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import type { InstitutionDetailsApiType } from 'types/formTypes';
import { institutionDetailsApiTypeSchema } from 'types/formTypes';
import { Five } from 'utils/constants';
import getIsRoutingEnabled from 'utils/getIsRoutingEnabled';
import AdditionalDetails from './AdditionalDetails';
import FinancialInstitutionDetailsForm from './FinancialInstitutionDetailsForm';
import UpdateAffiliateInformation from './UpdateAffiliateInformation';
import UpdateIdentifyingInformation from './UpdateIdentifyingInformation';
import buildUfpDefaults from './buildUfpDefaults';
import buildProfileFormDefaults from './buildProfileFormDefaults';

export default function UFPForm({
data,
}: {
data: InstitutionDetailsApiType;
}): JSXElement {
const [submitted, setSubmitted] = useState(false);
const { lei } = useParams();

const defaultValues = buildUfpDefaults(data);
const auth = useSblAuth();
const isRoutingEnabled = getIsRoutingEnabled();
const navigate = useNavigate();

const defaultValues = useMemo(() => buildProfileFormDefaults(data), [data]);

const {
register,
// trigger,
control,
setValue,
trigger,
getValues,
register,
reset,
setValue,
formState: { errors: formErrors, dirtyFields },
} = useForm<UFPSchema>({
resolver: zodResolver(ufpSchema),
} = useForm<InstitutionDetailsApiType>({
resolver: zodResolver(institutionDetailsApiTypeSchema),
defaultValues,
});

// TODO: Render this based on the actual API call result
// TODO: No need to track "submitted" state once we implement validations
// https://github.com/cfpb/sbl-frontend/pull/276/files#r1509023108
if (isRoutingEnabled && submitted) {
return (
<Navigate
to='/summary'
state={{ scenario: scenarios.SuccessInstitutionProfileUpdate }}
/>
);
}

// Used for error scrolling
const formErrorHeaderId = 'UFPFormErrorHeader';

// NOTE: This function is used for submitting the multipart/formData
const onSubmitButtonAction = async (): Promise<void> => {
const passesValidation = await trigger();
// const passesValidation = await trigger();
// TODO: Will be used for debugging after clicking 'Submit'
// eslint-disable-next-line no-console
console.log('passes validation?', passesValidation);
// console.log('passes validation?', passesValidation);
// if (passesValidation) {
const preFormattedData = getValues();
// TODO: Will be used for debugging after clicking 'Submit'
// eslint-disable-next-line no-console
console.log(
'data to be submitted (before format):',
JSON.stringify(preFormattedData, null, Five),
);

// TODO: Send data in human readable format
// POST formData
// const response = await submitUpdateFinancialProfile(
// auth,
// preFormattedData,
// )
try {
const formData = getValues();
const postableData = collectChangedData(formData, dirtyFields);
postableData.Note =
'This data reflects the institution data that has been changed';
// eslint-disable-next-line no-console
console.log(
'data being submitted:',
JSON.stringify(postableData, null, Five),
);
await submitUpdateFinancialProfile(auth, postableData);
if (isRoutingEnabled)
navigate('/summary', {
state: { scenario: scenarios.SuccessInstitutionProfileUpdate },
});
} catch (error) {
// eslint-disable-next-line no-console
console.log('Error submitting UFP', error);
}
// }
setSubmitted(true);
};

// Reset form data to the defaultValues
const onClearform = (): void => reset();

// TODO: Will be used for debugging errors after clicking 'Submit'
// eslint-disable-next-line no-console
console.log('formErrors:', formErrors);

// TODO: Use dirtyFields to determine which data to send to SBL Help
// TODO: Nested fields (sbl_institution_types) do not register as dirty when content changes, will need to always check those values
console.log('dirtyFields:', dirtyFields);
// console.log('formErrors:', formErrors);

return (
<FormWrapper>
<div id='update-financial-profile'>
<FormHeaderWrapper>
<CrumbTrail>
<Link href='/landing' key='home'>
<Link isRouterLink href='/landing' key='home'>
Platform home
</Link>
{lei ? (
Expand Down Expand Up @@ -144,6 +137,8 @@ export default function UFPForm({
aria-label='Submit User Profile'
size='default'
type='submit'
// TODO: Allow form submission without changed data?
// disabled={!(hasChanges || Object.keys(dirtyFields).length > 0)}
/>
<Button
label='Clear form'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ import { FormSectionWrapper } from '../../../components/FormSectionWrapper';
import InputEntry from '../../../components/InputEntry';
import { DisplayField } from '../ViewInstitutionProfile/DisplayField';
import type { CheckboxOption } from './types';
import { checkboxOptions, sblInstitutionTypeMap } from './types';
import { checkboxOptions } from './types';

const elements = {
taxID: 'tax_id',
rssdID: 'rssd_id',
};

const SLB_INSTITUTION_TYPE_OTHER = '13';

function FieldFederalPrudentialRegulator({
data,
register,
Expand Down Expand Up @@ -74,7 +76,7 @@ function UpdateIdentifyingInformation({
formErrors: string[];
}): JSXElement {
const typeOtherData = data.sbl_institution_types.find(item => {
return item.sbl_type.id === sblInstitutionTypeMap.other;
return item.sbl_type.id === SLB_INSTITUTION_TYPE_OTHER;
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { InstitutionDetailsApiType } from 'types/formTypes';
import { buildEmailDomainString } from 'utils/formatting';

// Map the Institutions API data to an easily trackable format for react-hook-form
const buildProfileFormDefaults = (
data: InstitutionDetailsApiType,
): InstitutionDetailsApiType => {
const formDefaults: InstitutionDetailsApiType = structuredClone(data);
formDefaults.domains = buildEmailDomainString(data.domains);
formDefaults.additional_details = ''; // Only part of outgoing data

// Building an easier format to track checkboxes via react-hook-form
formDefaults.sbl_institution_types = [];
if (data.sbl_institution_types) {
for (const currentType of data.sbl_institution_types) {
if (typeof currentType === 'object') {
const { details, sbl_type: sblType } = currentType;
const { id: currentId } = sblType;

formDefaults.sbl_institution_types[Number(currentId)] = true;

// Other's details
if (currentId === '13')
formDefaults.sbl_institution_types_other = details;
}
}
}

return formDefaults;
};

export default buildProfileFormDefaults;

0 comments on commit d5f3f60

Please sign in to comment.