Skip to content

Commit

Permalink
Sa 102293 kicker dod notification (#33929)
Browse files Browse the repository at this point in the history
* Adds mebKickerNotificationEnabled to Original Claims form and copies it to formData

* Refactors additional COnsiderations component to not use the old feature flags

* Hides questions with new kicker notification turned On and displays alert if eligibility is detected

* Implement kicker widget to display notification based on API data

* Adds fallback for accessing kicker fields from claimant object

* Adds try catch so that when properties are undefined on introduction page the application can still excecute until the values are fetched on the next page

* Fix validation issue by treating street2 as a string, handling null/undefined values

* Remove unused prefill transformer functions

* Fix race condition with feature flags: only prefill kicker data when mebKickerNotificationEnabled is true

* Adds conditional logic for preventing fetching of exclusion periods when mebKickerNotificationEnabled is true

* Prevent fetching exclusion periods when mebKickerNotificationEnabled is true and remove mebKickerNotificationEnabled logic gate references

* Fix redundant fetch of personal info when Chapter33 is selected

- Optimized useEffect to ensure personal info is only fetched once when a benefit is selected.
- Prevents duplicate calls to getPersonalInfo by checking if the chosen benefit has changed.
- Ensured previousChosenBenefit is updated after each fetch to avoid unnecessary API calls.

* More refactoring

* removes commenting out of moment removal debug

* Fix: Safely handle falsy values for kicker eligibility

* Adds custom notification message to submission if eligibility is detected

* Updates abbreviation and capitalization of Defense

* updates prefill transformer to include only one version of the function

* feat(test): Improve prefillTransformer coverage for mebKickerNotificationEnabled and address fallback
	•	Add scenarios testing mebKickerNotificationEnabled both ON and OFF
	•	Verify eligibility flags for active/reserve kickers populate correctly
	•	Expand tests for domestic vs. international address fallback
	•	Ensure missing phone fields are handled gracefully

* fix(tests): Ensure sixHundredDollarBuyUp key is present to pass exclusion message test
	•	Added sixHundredDollarBuyUp: undefined to the test data for createAdditionalConsiderations
	•	Allows the function to produce 'N/A' for buy-up, matching the test’s expected output
	•	Fixes the failing unit test in form-submit-transform.unit.spec.js
  • Loading branch information
bradbergeron-us authored Feb 6, 2025
1 parent 9be31b3 commit ca07dfb
Show file tree
Hide file tree
Showing 11 changed files with 723 additions and 984 deletions.
72 changes: 72 additions & 0 deletions src/applications/my-education-benefits/components/KickerWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';

/**
* KickerWidget checks formData for mebKickerNotificationEnabled plus
* `eligibleForActiveDutyKicker` or `eligibleForReserveKicker`.
* If conditions are met, it displays a <va-alert> for that kicker type.
*/
const KickerWidget = ({
formData,
kickerType,
eligibleForActiveDutyKicker,
eligibleForReserveKicker,
}) => {
if (!formData) {
return null;
}

const { mebKickerNotificationEnabled } = formData;

// If the global flag is off, we skip
if (!mebKickerNotificationEnabled) {
return null;
}

// Check the actual kicker type
if (kickerType === 'activeDuty' && eligibleForActiveDutyKicker) {
return (
<va-alert>
Department of Defense data shows you are potentially eligible for an
active duty kicker
</va-alert>
);
}

if (kickerType === 'reserve' && eligibleForReserveKicker) {
return (
<va-alert>
Department of Defense data shows you are potentially eligible for a
reserve kicker
</va-alert>
);
}

return null;
};

KickerWidget.propTypes = {
eligibleForActiveDutyKicker: PropTypes.bool,
eligibleForReserveKicker: PropTypes.bool,
formData: PropTypes.object,
kickerType: PropTypes.oneOf(['activeDuty', 'reserve']),
};

KickerWidget.defaultProps = {
kickerType: 'activeDuty',
};

const mapStateToProps = state => {
const claimant = state?.data?.formData?.data?.attributes?.claimant || {};
const { eligibleForActiveDutyKicker, eligibleForReserveKicker } =
claimant || {};

return {
formData: state?.form?.data,
eligibleForActiveDutyKicker,
eligibleForReserveKicker,
};
};

export default connect(mapStateToProps)(KickerWidget);
Original file line number Diff line number Diff line change
@@ -1,59 +1,58 @@
import React from 'react';
import ExclusionPeriodsWidget from '../../../../components/ExclusionPeriodsWidget';
import KickerWidget from '../../../../components/KickerWidget';
import { formFields } from '../../../../constants';
import { formPages } from '../../../../helpers';

/**
* Determines the "Question X of Y" text for Additional Considerations pages.
*/
function additionalConsiderationsQuestionTitleText(
order,
rudisillFlag,
meb160630Automation,
chosenBenefit, // Include chosenBenefit here
chosenBenefit,
pageName,
mebKickerNotificationEnabled,
) {
let pageNumber;
let totalPages;
// Handle when rudisillFlag is true and meb160630Automation is false (5 questions)
if (rudisillFlag && !meb160630Automation) {
if (mebKickerNotificationEnabled) {
const pageOrder = {
'active-duty-kicker': 1,
'reserve-kicker': 2,
'academy-commission': 3,
'rotc-commission': 4,
'loan-payment': 5,
'six-hundred-dollar-buy-up': 3,
};
pageNumber = pageOrder[pageName] || order;
totalPages = 5;
} else {
// Handle when meb160630Automation is true, but chosenBenefit is NOT chapter30 (still show 5 questions)
const pageOrder = {
'active-duty-kicker': 1,
'reserve-kicker': 2,
'academy-commission': 3,
'rotc-commission': 4,
'loan-payment': 5,
'additional-contributions': 6, // This question only appears for chapter30
};
// Show 6 questions only if meb160630Automation is enabled AND chosenBenefit is 'chapter30'
pageNumber = pageOrder[pageName] || order;
totalPages = meb160630Automation && chosenBenefit === 'chapter30' ? 6 : 5;
const pageNumber = pageOrder[pageName] || order;
const totalPages = chosenBenefit === 'chapter30' ? 3 : 2;
return `Question ${pageNumber} of ${totalPages}`;
}

const legacyPageOrder = {
'active-duty-kicker': 1,
'reserve-kicker': 2,
'academy-commission': 3,
'rotc-commission': 4,
'loan-payment': 5,
'six-hundred-dollar-buy-up': 6,
};
const pageNumber = legacyPageOrder[pageName] || order;
const totalPages = chosenBenefit === 'chapter30' ? 6 : 5;
return `Question ${pageNumber} of ${totalPages}`;
}
// Function to render the question title on the form

/**
* Renders the heading for each Additional Considerations question.
*/
function additionalConsiderationsQuestionTitle(
order,
rudisillFlag,
meb160630Automation,
chosenBenefit,
pageName,
mebKickerNotificationEnabled,
) {
const titleText = additionalConsiderationsQuestionTitleText(
order,
rudisillFlag,
meb160630Automation,
chosenBenefit,
pageName,
mebKickerNotificationEnabled,
);

return (
<>
<h3 className="meb-additional-considerations-title meb-form-page-only">
Expand All @@ -69,65 +68,86 @@ function additionalConsiderationsQuestionTitle(
</>
);
}
// Template for additional considerations pages

/**
* Template function for Additional Considerations "page"
*/
function AdditionalConsiderationTemplate(page, formField, options = {}) {
const { title, additionalInfo } = page;
const additionalInfoViewName = `view:${page.name}AdditionalInfo`;

const displayTypeMapping = {
[formFields.federallySponsoredAcademy]: 'Academy',
[formFields.seniorRotcCommission]: 'ROTC',
[formFields.loanPayment]: 'LRP',
};
const displayType = displayTypeMapping[formField] || '';

// Prepare the UI snippet that will be displayed on this page
let additionalInfoView;
const uiDescription = (
<>
{options.includeExclusionWidget && (
<ExclusionPeriodsWidget displayType={displayType} />
)}
{additionalInfo && (
<>
<br />
<va-additional-info trigger={additionalInfo.trigger}>
<p>{additionalInfo.info}</p>
</va-additional-info>
</>
)}
</>
);
if (additionalInfo || options.includeExclusionWidget) {
if (
additionalInfo ||
options.includeExclusionWidget ||
options.kickerNotificationAlert
) {
const uiDescription = (
<>
{/* Conditionally render the KickerWidget */}
{options.kickerNotificationAlert && (
<>
{page.name === 'active-duty-kicker' && (
<KickerWidget kickerType="activeDuty" />
)}
{page.name === 'reserve-kicker' && (
<KickerWidget kickerType="reserve" />
)}
</>
)}

{/* Conditionally render the ExclusionPeriodsWidget */}
{options.includeExclusionWidget && (
<ExclusionPeriodsWidget displayType={displayType} />
)}

{/* Render additional info if available */}
{additionalInfo && (
<>
<br />
<va-additional-info trigger={additionalInfo.trigger}>
<p>{additionalInfo.info}</p>
</va-additional-info>
</>
)}
</>
);
additionalInfoView = {
[additionalInfoViewName]: {
'ui:description': uiDescription,
},
};
}

return {
path: page.name,
title: data => {
const rudisillFlag = data?.dgiRudisillHideBenefitsSelectionStep;
const meb160630Automation = data?.meb160630Automation;
const chosenBenefit = data?.formData?.chosenBenefit;
const mebKickerFlag = data?.formData?.mebKickerNotificationEnabled;
return additionalConsiderationsQuestionTitleText(
page.order,
rudisillFlag,
meb160630Automation,
chosenBenefit,
page.name,
mebKickerFlag,
);
},
uiSchema: {
'ui:description': data => {
const rudisillFlag =
data.formData?.dgiRudisillHideBenefitsSelectionStep;
const meb160630Automation = data?.formData?.meb160630Automation;
const chosenBenefit = data?.formData?.chosenBenefit;
const mebKickerFlag = data?.formData?.mebKickerNotificationEnabled;
return additionalConsiderationsQuestionTitle(
page.order,
rudisillFlag,
meb160630Automation,
chosenBenefit,
page.name,
mebKickerFlag,
);
},
[formFields[formField]]: {
Expand All @@ -152,50 +172,67 @@ function AdditionalConsiderationTemplate(page, formField, options = {}) {
},
};
}
// Define the additional considerations pages with correct dependencies

/**
* Final export: Additional Considerations config object
*/
const additionalConsiderations33 = {
// 1) Active Duty Kicker
[formPages.additionalConsiderations.activeDutyKicker.name]: {
...AdditionalConsiderationTemplate(
formPages.additionalConsiderations.activeDutyKicker,
formFields.activeDutyKicker,
{ kickerNotificationAlert: true },
),
depends: formData => formData.dgiRudisillHideBenefitsSelectionStep,
},

// 2) Reserve Kicker
[formPages.additionalConsiderations.reserveKicker.name]: {
...AdditionalConsiderationTemplate(
formPages.additionalConsiderations.reserveKicker,
formFields.selectedReserveKicker,
{ kickerNotificationAlert: true },
),
depends: formData => formData.dgiRudisillHideBenefitsSelectionStep,
},

// 3) Academy Commission
[formPages.additionalConsiderations.militaryAcademy.name]: {
...AdditionalConsiderationTemplate(
formPages.additionalConsiderations.militaryAcademy,
formFields.federallySponsoredAcademy,
{ includeExclusionWidget: true },
),
depends: formData => !formData.mebKickerNotificationEnabled,
},

// 4) Senior ROTC
[formPages.additionalConsiderations.seniorRotc.name]: {
...AdditionalConsiderationTemplate(
formPages.additionalConsiderations.seniorRotc,
formFields.seniorRotcCommission,
{ includeExclusionWidget: true },
),
depends: formData => !formData.mebKickerNotificationEnabled,
},

// 5) Loan Payment
[formPages.additionalConsiderations.loanPayment.name]: {
...AdditionalConsiderationTemplate(
formPages.additionalConsiderations.loanPayment,
formFields.loanPayment,
{ includeExclusionWidget: true },
),
depends: formData => !formData.mebKickerNotificationEnabled,
},

// 6) $600 Buy-Up (chapter30)
[formPages.additionalConsiderations.sixHundredDollarBuyUp.name]: {
...AdditionalConsiderationTemplate(
formPages.additionalConsiderations.sixHundredDollarBuyUp,
formFields.sixHundredDollarBuyUp,
),
depends: formData =>
formData?.chosenBenefit === 'chapter30' && formData?.meb160630Automation,
depends: formData => formData?.chosenBenefit === 'chapter30',
},
};

export default additionalConsiderations33;
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,37 @@ const mailingAddress33 = {
'ui:title': 'Street address line 2',
'ui:validations': [
(errors, field) => {
if (field?.length > 40) {
// If field is provided and contains only whitespace
if (field && isOnlyWhitespace(field)) {
errors.addError('Please enter a valid street address line 2');
} else if (field?.length > 40) {
errors.addError('maximum of 40 characters');
}
},
],
'ui:options': {
updateSchema: (formData, schema) => {
const addressData = get(
['view:mailingAddress', 'address'],
formData,
);

// Make sure street2 is treated as a string (even if it's null or undefined)
if (addressData.street2 == null) {
addressData.street2 = ''; // Set to empty string to avoid validation errors
}

// If no value is provided, skip validation
if (!addressData.street2) {
return {
...schema,
minLength: 0,
};
}

return schema;
},
},
},
city: {
'ui:errorMessages': {
Expand Down
Loading

0 comments on commit ca07dfb

Please sign in to comment.