src/applications/pre-need/utils/helpers.js
import React from 'react';
import { merge } from 'lodash';
import PropTypes from 'prop-types';
import get from 'platform/utilities/data/get';
import omit from 'platform/utilities/data/omit';
import * as Sentry from '@sentry/browser';
import dateRangeUI from 'platform/forms-system/src/js/definitions/dateRange';
import fullNameUI from 'platform/forms/definitions/fullName';
import ssnUI from 'platform/forms-system/src/js/definitions/ssn';
import TextWidget from 'platform/forms-system/src/js/widgets/TextWidget';
import { $$ } from 'platform/forms-system/src/js/utilities/ui';
import { focusElement } from 'platform/utilities/ui';
import {
stringifyFormReplacer,
filterViewFields,
} from 'platform/forms-system/src/js/helpers';
import environment from 'platform/utilities/environment';
import { fetchAndUpdateSessionExpiration as fetch } from 'platform/utilities/api';
import * as autosuggest from 'platform/forms-system/src/js/definitions/autosuggest';
import ApplicantDescription from 'platform/forms/components/ApplicantDescription';
import { serviceLabels } from './labels';
import RaceEthnicityReviewField from '../components/RaceEthnicityReviewField';
import ServicePeriodView from '../components/ServicePeriodView';
export const nonRequiredFullNameUI = omit('required', fullNameUI);
export const createPayload = (file, formId, password) => {
const payload = new FormData();
payload.set('form_id', formId);
payload.append('file', file);
if (password) {
payload.append('password', password);
}
return payload;
};
export function parseResponse({ data }) {
const { name } = data.attributes;
const focusFileCard = () => {
const target = $$('.schemaform-file-list li').find(entry =>
entry.textContent?.trim().includes(name),
);
if (target) {
focusElement(target);
}
};
setTimeout(() => {
focusFileCard();
}, 100);
return {
name,
confirmationCode: data.attributes.confirmationCode,
};
}
export const applicantDetailsSubHeader = (
<div className="applicantDetailsSubHeader">
<h3 className="vads-u-font-size--h5">Applicant details</h3>
</div>
);
export function ApplicantDescriptionWrapper({ formContext }) {
return (
<div className="ApplicantDescriptionWrapper">
<ApplicantDescription formContext={formContext} />
</div>
);
}
export const applicantDemographicsSubHeader = (
<div className="applicantDemographicsSubHeader">
<h3 className="vads-u-font-size--h5">Applicant demographics</h3>
</div>
);
export const applicantDemographicsDescription = (
<div className="applicantDemographicsDescription">
<p>
We require some basic details as part of your application. Please know we
need to gather the data for statistical purposes.
</p>
</div>
);
export const sponsorDeceasedDescription = (
<div className="sponsorDeceasedDescriptionNotProd">
<p>
We’ll now ask you questions about the sponsor’s passing. We understand
that the questions may be difficult to answer, but your answers will help
us determine eligibility for your application.
</p>
</div>
);
export const sponsorDetailsSubHeader = (
<div className="sponsorDetailsSubHeader">
<h3 className="vads-u-font-size--h5">Sponsor details</h3>
</div>
);
export const sponsorDemographicsSubHeader = (
<div className="sponsorDemographicsSubHeader">
<h3 className="vads-u-font-size--h5">Sponsor demographics</h3>
</div>
);
export const sponsorDemographicsDescription = environment.isProduction() ? (
<div className="sponsorDemographicsDescription">
<p>
We require some basic details about the applicant’s sponsor as part of the
application. Please know we need to gather the data for statistical
purposes.
</p>
</div>
) : (
<div className="sponsorDemographicsDescriptionNotProd">
<p>
We require some basic details about the applicant’s sponsor as part of the
application. Please know we need to gather the data for statistical
purposes.
</p>
</div>
);
export const sponsorDeceasedSubheader = (
<div className="sponsorDeceasedSubheader">
<p>Has the sponsor died?</p>
</div>
);
export const sponsorDateOfDeathSubheader = (
<div className="sponsorDateOfDeathSubheader">
<p>When did the sponsor pass away?</p>
</div>
);
export const sponsorMilitaryDetailsSubHeader = (
<div className="sponsorMilitaryDetailsSubHeader">
<h3 className="vads-u-font-size--h5">Sponsor’s military details</h3>
</div>
);
export const militaryDetailsSubHeader = (
<div className="militaryDetailsSubHeader">
<h3 className="vads-u-font-size--h5">Military details</h3>
</div>
);
export const contactInfoDescription = (
<va-additional-info trigger="Why do we need your contact information?">
<p>
We may contact you by phone if we need more information about your
application.
</p>
<p>
You can also provide your email address to receive updates about new
openings in VA national cemeteries or other burial benefits.
</p>
</va-additional-info>
);
export const applicantInformationDescription = (
<va-additional-info trigger="What if the applicant is not a service member or Veteran?">
<ul>
<>
<li>
A <strong>spouse</strong> is a person who is or was legally married to
a service member or Veteran. A <strong>surviving spouse</strong> is
someone who was legally married to the service member or Veteran at
the time of their death and includes a surviving spouse who remarried.
</li>
<li>
An <strong>unmarried adult child</strong> is an individual who became
physically or mentally disabled permanently and incapable of
self-support before the age of 21, or before 23 years of age if
pursuing a full-time course of instruction at an approved educational
institution.
</li>
<li>
For <strong>other</strong> applicants such as the parent of a service
member, we’ll ask questions about the service member (the sponsor) to
determine eligibility for burial in a VA national cemetery.
</li>
</>
</ul>
</va-additional-info>
);
export const applicantDetailsDescription = (
<va-additional-info trigger="Are you filling out this application on behalf of someone else?">
<p>
If you’re filling out the form on behalf of someone else, you’ll need to
provide their details below. As the preparer, we’ll ask for your own
details later.
</p>
</va-additional-info>
);
export const sponsorDetailsDescription = (
<va-additional-info trigger="What is a sponsor?">
<ul>
<>
<li>
You’re considered the sponsor if you’re the service member or Veteran
sponsoring the applicant’s benefits. We’ll ask you to provide your
details.
</li>
<li>
If you’re not the sponsor, you’ll still need to provide the details
for the service member or Veteran who is sponsoring the applicant’s
benefits.
</li>
</>
</ul>
</va-additional-info>
);
// do not render with a prod flag
export const applicantContactInfoDescriptionVet = (
<va-additional-info trigger="Why do we need your contact details?">
<p>
We may reach out by phone if we need more information about your
application.
</p>
<p>
Your email address will be used to send a confirmation message once you’ve
submitted your application.
</p>
</va-additional-info>
);
// do not render with a prod flag
export const applicantContactInfoDescriptionNonVet = (
<va-additional-info trigger="Why do we need the applicant’s contact details?">
<p>
We may reach out by phone if we need more information about the
application.
</p>
<p>
Their email address will be used to send a confirmation message once the
application is submitted.
</p>
</va-additional-info>
);
export const PreparerPhoneNumberDescription = (
<va-additional-info trigger="Why do we need your phone number?">
<p>
We may contact you by phone if we need more information about the
application.
</p>
</va-additional-info>
);
export const veteranRelationshipDescription = (
<va-alert
status="info"
background-only
role="status"
id="veteran-relationship"
>
You’re applying as the <strong>service member or Veteran</strong>. We’ll ask
you questions about your military status and history to determine if you
qualify for burial in a VA national cemetery.
</va-alert>
);
export const authorizedAgentDescription = (
// TODO va-additional-info component to be replaced with a more optimal solution
<va-additional-info
trigger={"If you're applying for someone else, who can you sign for?"}
>
<p>A preparer can sign for an applicant who’s:</p>
<ul>
<>
<li>
Mentally incompetent <strong>or</strong>
</li>
<li>Physically unable to sign the application</li>
</>
</ul>
</va-additional-info>
);
export const spouseRelationshipDescription = (
<va-alert
status="info"
background-only
role="status"
id="spouse-relationship"
>
You’re applying as the{' '}
<strong>legally married spouse or surviving spouse</strong> of the service
member or Veteran who’s your sponsor. We’ll ask you questions about your
sponsor’s military status and history to determine if you qualify for burial
in a VA national cemetery.
</va-alert>
);
export const childRelationshipDescription = (
<va-alert status="info" background-only role="status" id="child-relationship">
You’re applying as the <strong>unmarried adult child</strong> of the service
member or Veteran who’s your sponsor. We’ll ask you questions about your
sponsor’s military status and history to determine if you qualify for burial
in a VA national cemetery. You’ll also need to provide supporting documents
with information about your disability.
</va-alert>
);
export const otherRelationshipDescription = (
<va-alert status="info" background-only role="status" id="other-relationship">
You’re applying on <strong>behalf</strong> of the service member or Veteran
who’s your sponsor. We’ll ask you questions about your sponsor’s military
status and history to determine if they qualify for burial in a VA national
cemetery.
</va-alert>
);
export const sponsorMilitaryStatusDescription = (
<va-alert status="info" background-only>
If you’re not sure what your sponsor’s status is—or if it isn’t listed
here—don’t worry. You can upload supporting documents showing your sponsor’s
service history later in this application.
</va-alert>
);
export const desiredCemeteryNoteDescriptionVeteran = (
<va-alert status="info" background-only id="burial-cemetary-note">
<strong>Please note:</strong> This doesn’t guarantee you’ll be buried in
your preferred cemetery, but we’ll try to fulfill your wishes. If space is
unavailable, we’ll work with your family to assign a gravesite in a cemetery
with available space at the time of need.
</va-alert>
);
export const desiredCemeteryNoteDescriptionNonVeteran = (
<va-alert status="info" background-only id="burial-cemetary-note">
<strong>Please note:</strong> This doesn’t guarantee the applicant will be
buried in their preferred cemetery, but we’ll try to fulfill their wishes.
If space is unavailable, we’ll work with their family to assign a gravesite
in a cemetery with available space at the time of need.
</va-alert>
);
export function preparerAddressHasState(item) {
const country = get(
'application.applicant.view:applicantInfo.mailingAddress.country',
item,
);
const countriesWithStates = ['USA', 'CAN'];
return countriesWithStates.includes(country);
}
export function applicantsMailingAddressHasState(item) {
const country = get('application.claimant.address.country', item);
const countriesWithStates = ['USA', 'CAN'];
return countriesWithStates.includes(country);
}
export function sponsorMailingAddressHasState(item) {
const country = get('application.veteran.address.country', item);
const countriesWithStates = ['USA', 'CAN'];
return countriesWithStates.includes(country);
}
export function isVeteran(item) {
return get('application.claimant.relationshipToVet', item) === '1';
}
export function isSponsorDeceased(item) {
return get('application.veteran.isDeceased', item) === 'yes';
}
export function isSpouse(item) {
return get('application.claimant.relationshipToVet', item) === '2';
}
export function isUnmarriedChild(item) {
return get('application.claimant.relationshipToVet', item) === '3';
}
export function isVeteranAndHasServiceName(item) {
return (
isVeteran(item) &&
get('application.veteran.view:hasServiceName', item) === true
);
}
export function isNotVeteranAndHasServiceName(item) {
return (
!isVeteran(item) &&
get('application.veteran.view:hasServiceName', item) === true
);
}
export function buriedWSponsorsEligibility(item) {
return get('application.hasCurrentlyBuried', item) === '1';
}
export function isAuthorizedAgent(item) {
return (
get('application.applicant.applicantRelationshipToClaimant', item) ===
'Authorized Agent/Rep'
);
}
export function requiresSponsorInfo(item) {
const sponsor = item['view:sponsor'];
return sponsor === undefined || sponsor === 'Other';
}
export function formatName(name) {
const { first, middle, last, suffix } = name;
return (
(first || last) &&
`${first} ${middle ? `${middle} ` : ''}${last}${
suffix ? `, ${suffix}` : ''
}`
);
}
export function claimantHeader({ formData }) {
const name = formatName(formData.claimant.name);
return <h4 className="highlight">{name}</h4>;
}
export function transform(formConfig, form) {
// Copy over sponsor data if the claimant is the veteran.
const populateSponsorData = application =>
isVeteran({ application })
? merge({}, application, {
veteran: {
address: application.claimant.address,
currentName: application.claimant.name,
dateOfBirth: application.claimant.dateOfBirth,
ssn: application.claimant.ssn,
isDeceased: 'no',
serviceName:
// eslint-disable-next-line no-nested-ternary
application.veteran.serviceName === undefined
? application.claimant.name
: application.veteran.serviceName.first === undefined
? application.claimant.name
: application.veteran.serviceName ||
application.claimant.name,
},
})
: application;
// Copy over preparer data if the claimant is the applicant.
const populatePreparerData = application =>
!isAuthorizedAgent({ application })
? merge({}, application, {
applicant: {
mailingAddress: application.claimant.address,
name: application.claimant.name,
},
})
: application;
// Copy over veteran data if a sponsor is filling out the form
const populateVeteranData = application =>
merge({}, application, {
veteran: {
serviceName:
// eslint-disable-next-line no-nested-ternary
application.veteran.serviceName === undefined
? application.veteran.currentName
: application.veteran.serviceName.first === undefined
? application.veteran.currentName
: application.veteran.serviceName ||
application.veteran.currentName,
},
applicant: {
applicantEmail: application.claimant.email,
applicantPhoneNumber: application.claimant.phoneNumber,
},
});
const application = [
populateSponsorData,
populatePreparerData,
populateVeteranData,
filterViewFields,
].reduce((result, func) => func(result), form.data.application);
// const formCopy = set('application', application, Object.assign({}, form));
// const formData = transformForSubmit(formConfig, formCopy);
return JSON.stringify({ application }, stringifyFormReplacer);
/* Transformation for multiple applicants.
*
* const matchClaimant = name => a => formatName(a.claimant.name) === name;
*
* formCopy.applications = formCopy.applications.map(application => {
* // Fill in veteran info that veterans didn't need to enter separately.
* if (isVeteran(application)) {
* return merge({}, application, {
* veteran: {
* address: application.claimant.address,
* currentName: application.claimant.name,
* dateOfBirth: application.claimant.dateOfBirth,
* ssn: application.claimant.ssn,
* isDeceased: 'no'
* }
* });
* }
*
* // Fill in veteran info in each application
* // where the sponsor is another claimant.
* const sponsorName = application['view:sponsor'];
* if (sponsorName !== 'Other') {
* const veteranApplication = form.applications.find(matchClaimant(sponsorName));
* const veteran = set('isDeceased', 'no', veteranApplication.veteran);
* return set('veteran', veteran, application);
* }
*
* return application;
* });
*
* // Fill in applicant info in each application
* // if the applicant is another claimant.
* const applicantName = form['view:preparer'];
* if (applicantName !== 'Other') {
* const applicantApplication = form.applications.find(matchClaimant(applicantName));
* const { address, email, name, phoneNumber } = applicantApplication.claimant;
* formCopy.applications = formCopy.applications.map(application => set('applicant', {
* applicantEmail: email,
* applicantPhoneNumber: phoneNumber,
* applicantRelationshipToClaimant: application.claimant.ssn === applicantApplication.claimant.ssn ? 'Self' : 'Authorized Agent/Rep',
* completingReason: '',
* mailingAddress: address,
* name
* }, application));
* }
*
*/
}
export const fullMaidenNameUI = merge({}, fullNameUI, {
first: { 'ui:title': 'First name' },
middle: { 'ui:title': 'Middle name' },
last: { 'ui:title': 'Last name' },
maiden: { 'ui:title': 'Maiden name' },
'ui:order': ['first', 'middle', 'last', 'suffix', 'maiden'],
});
class SSNWidget extends React.Component {
constructor(props) {
super(props);
this.state = { val: props.value };
}
handleChange = val => {
// Insert dashes if they are missing.
// Keep if value is valid and formatted with dashes.
// Set empty value to undefined.
const formattedSSN =
(val && /^\d{9}$/.test(val)
? `${val.substr(0, 3)}-${val.substr(3, 2)}-${val.substr(5)}`
: val) || undefined;
this.setState({ val }, () => {
this.props.onChange(formattedSSN);
});
};
render() {
return (
<TextWidget
{...this.props}
value={this.state.val}
onChange={this.handleChange}
/>
);
}
}
// Modify default uiSchema for SSN to insert any missing dashes.
export const ssnDashesUI = environment.isProduction()
? merge({}, ssnUI, { 'ui:widget': SSNWidget })
: ssnUI;
export const veteranUI = {
militaryServiceNumber: {
'ui:title':
'Military Service number (if it’s different than your Social Security number)',
'ui:errorMessages': {
pattern: 'Your Military Service number must be between 4 to 9 characters',
},
},
vaClaimNumber: {
'ui:title': 'VA claim number (if known)',
'ui:errorMessages': {
pattern: 'Your VA claim number must be between 8 to 9 digits',
},
},
placeOfBirth: {
'ui:title': 'Place of birth (City, State, or Territory)',
},
gender: {
'ui:title': 'What’s your sex?',
'ui:widget': 'radio',
},
maritalStatus: {
'ui:title': 'What’s your marital status?',
'ui:widget': 'radio',
'ui:options': {
labels: {
single: 'Single',
separated: 'Separated',
married: 'Married',
divorced: 'Divorced',
widowed: 'Widowed',
},
},
},
race: {
'ui:field': RaceEthnicityReviewField,
'ui:title':
'Which categories best describe you? (You may check more than one)',
isSpanishHispanicLatino: {
'ui:title': 'Hispanic or Latino',
},
notSpanishHispanicLatino: {
'ui:title': 'Not Hispanic or Latino',
},
isAmericanIndianOrAlaskanNative: {
'ui:title': 'American Indian or Alaskan Native',
},
isBlackOrAfricanAmerican: {
'ui:title': 'Black or African American',
},
isNativeHawaiianOrOtherPacificIslander: {
'ui:title': 'Native Hawaiian or other Pacific Islander',
},
isAsian: {
'ui:title': 'Asian',
},
isWhite: {
'ui:title': 'White',
},
'ui:validations': [
// require at least one value to be true/checked
(errors, fields) => {
if (!Object.values(fields).some(val => val === true)) {
errors.addError('Please provide a response');
}
},
],
'ui:options': {
showFieldLabel: true,
},
},
militaryStatus: {
'ui:title':
'Current military status (You can add more service history information later in this application.)',
'ui:options': {
labels: {
A: 'Active Duty',
I: 'Death Related to Inactive Duty Training',
D: 'Died on Active Duty',
S: 'Reserve/National Guard',
R: 'Retired',
E: 'Retired Active Duty',
O: 'Retired Reserve/National Guard',
V: 'Veteran',
X: 'Other',
},
},
},
};
export const serviceRecordsUI = {
'ui:title': 'Service period(s)',
'ui:options': {
viewField: ServicePeriodView,
itemName: 'Service period',
keepInPageOnReview: true,
useDlWrap: true,
generateIndividualItemHeaders: true,
useHeaderStyling: true,
},
items: {
'ui:order': [
'serviceBranch',
'highestRank',
'dateRange',
'dischargeType',
'nationalGuardState',
],
serviceBranch: autosuggest.uiSchema('Branch of service', null, {
'ui:options': {
labels: serviceLabels,
},
}),
dateRange: dateRangeUI(
'Service start date',
'Service end date',
'Service start date must be after end date',
),
dischargeType: {
'ui:title': 'Discharge character of service',
'ui:options': {
labels: {
1: 'Honorable',
2: 'General',
3: 'Entry Level Separation/Uncharacterized',
4: 'Other Than Honorable',
5: 'Bad Conduct',
6: 'Dishonorable',
7: 'Other',
},
},
},
highestRank: {
'ui:title': 'Highest rank attained',
},
nationalGuardState: {
'ui:title': 'State (for National Guard Service only)',
'ui:options': {
hideIf: (formData, index) =>
!['AG', 'NG'].includes(
formData.application.veteran.serviceRecords[index].serviceBranch,
),
},
},
},
};
export const militaryNameUI = {
application: {
veteran: {
'view:hasServiceName': {
'ui:title': 'Did you serve under another name?',
'ui:widget': 'yesNo',
},
serviceName: merge({}, nonRequiredFullNameUI, {
'ui:options': {
expandUnder: 'view:hasServiceName',
},
}),
},
},
};
export function getCemeteries() {
return fetch(`${environment.API_URL}/v0/preneeds/cemeteries`, {
credentials: 'include',
headers: {
'X-Key-Inflection': 'camel',
'Source-App-Name': window.appName,
},
})
.then(res => {
if (!res.ok) {
return Promise.reject(res);
}
return res.json();
})
.then(res =>
res.data.map(item => ({
label: item.attributes.name,
id: item.id,
})),
)
.catch(res => {
if (res instanceof Error) {
Sentry.captureException(res);
Sentry.captureMessage('vets_preneed_cemeteries_error');
}
// May change this to a reject later, depending on how we want
// to surface errors in autosuggest field
return Promise.resolve([]);
});
}
SSNWidget.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
};