department-of-veterans-affairs/vets-website

View on GitHub
src/applications/financial-status-report/utils/helpers.js

Summary

Maintainability
D
2 days
Test Coverage
import { parse, addDays, format, isAfter, isFuture, isValid } from 'date-fns';
import { toggleValues } from '~/platform/site-wide/feature-toggles/selectors';
import FEATURE_FLAG_NAMES from '~/platform/utilities/feature-toggles/featureFlagNames';
import { formatDateLong } from 'platform/utilities/date';
import { deductionCodes } from '../constants/deduction-codes';
import { ignoreFields } from '../constants/ignoreFields';

export const fsrWizardFeatureToggle = state => {
  return toggleValues(state)[
    FEATURE_FLAG_NAMES.showFinancialStatusReportWizard
  ];
};

export const fsrFeatureToggle = state => {
  return toggleValues(state)[FEATURE_FLAG_NAMES.showFinancialStatusReport];
};

export const reviewPageNavigationFeatureToggle = state => {
  return toggleValues(state)[
    FEATURE_FLAG_NAMES.financialStatusReportReviewPageNavigation
  ];
};

export const fsrConfirmationEmailToggle = state =>
  toggleValues(state)[FEATURE_FLAG_NAMES.fsrConfirmationEmail];

export const allEqual = arr => arr.every(val => val === arr[0]);

export const isNumber = value => {
  const pattern = /^\d*$/; // This pattern ensures only whole numbers
  return pattern.test(value);
};

/**
 * Helper function to format date strings with only month and year
 *
 * @param {string} dateString - e.g. '2021-01-XX' or '2021-01'
 * @returns {string} e.g. '01/2021'
 */
export const monthYearFormatter = dateString => {
  if (!dateString) return '';

  // Replace any '-XX' legacy markers with '-01'
  const safeDate = dateString.replace(/-XX$/, '-01');

  // If it’s only "YYYY-MM" (length 7), parse it as year-month
  let parsedDate;
  if (safeDate.length === 7) {
    parsedDate = parse(safeDate, 'yyyy-MM', new Date());
  } else {
    // Otherwise assume "YYYY-MM-dd"
    parsedDate = parse(safeDate, 'yyyy-MM-dd', new Date());
  }

  // If parsed successfully, format as "MM/yyyy"
  return isValid(parsedDate) ? format(parsedDate, 'MM/yyyy') : '';
};

export const endDate = (date, days) => {
  if (!date) return '';
  const parsed = new Date(date);
  return isValid(parsed) ? formatDateLong(addDays(parsed, days)) : '';
};

export const currency = amount => {
  const formatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    minimumFractionDigits: 2,
  });
  const value =
    typeof amount === 'number'
      ? amount
      : parseFloat(amount?.replaceAll(/[^0-9.-]/g, '') ?? 0);
  return formatter.format(value);
};

const hasProperty = (arr, key) => {
  return arr.filter(item => item[key]).length > 0 ?? false;
};

export const sumValues = (arr, key) => {
  const isArrValid = Array.isArray(arr) && arr.length && hasProperty(arr, key);
  if (!isArrValid) return 0;
  return arr.reduce(
    (acc, item) =>
      acc + (item[key] ? Number(item[key]?.replaceAll(/[^0-9.-]/g, '')) : 0),
    0,
  );
};

export const filterReduceByName = (deductions, filters) => {
  if (!deductions?.length) return 0;
  return deductions
    .filter(({ name = '' }) => filters.includes(name))
    .reduce(
      (acc, curr) =>
        acc + Number(curr.amount?.replaceAll(/[^0-9.-]/g, '') ?? 0),
      0,
    );
};

export const otherDeductionsName = (deductions, filters) => {
  if (!deductions.length) return '';
  return deductions
    .filter(({ name = '' }) => !filters.includes(name))
    .map(({ name }) => name)
    .join(', ');
};

export const otherDeductionsAmt = (deductions, filters) => {
  if (!deductions.length) return 0;
  return deductions
    .filter(({ name = '' }) => name && !filters.includes(name))
    .reduce(
      (acc, curr) =>
        acc + Number(curr.amount?.replaceAll(/[^0-9.-]/g, '') ?? 0),
      0,
    );
};

export const nameStr = (socialSecurity, compensation, education, addlInc) => {
  const benefitTypes = [];
  if (socialSecurity) {
    benefitTypes.push('Social Security');
  }
  if (compensation) {
    benefitTypes.push('Disability Compensation');
  }
  if (education) {
    benefitTypes.push('Education');
  }
  const vetAddlNames = addlInc?.map(({ name }) => name) ?? [];
  const otherIncNames = [...benefitTypes, ...vetAddlNames];

  return otherIncNames?.map(item => item).join(', ') ?? '';
};

// safeNumber will return 0 if input is null, undefined, or NaN
export const safeNumber = input => {
  if (!input) return 0;
  const num = Number(input.replaceAll(/[^0-9.-]/g, ''));
  return Number.isNaN(num) ? 0 : num;
};

export const fsrReasonDisplay = resolutionOption => {
  switch (resolutionOption) {
    case 'monthly':
      return 'Extended monthly payments';
    case 'waiver':
      return 'Waiver';
    case 'compromise':
      return 'Compromise';
    default:
      return '';
  }
};

export const getFsrReason = debts => {
  const reasons = debts.map(({ resolutionOption }) =>
    fsrReasonDisplay(resolutionOption),
  );
  const uniqReasons = [...new Set(reasons)];

  return uniqReasons.join(', ');
};

export const getAmountCanBePaidTowardDebt = debts => {
  return debts
    .filter(item => item.resolutionComment !== undefined)
    .reduce(
      (acc, debt) =>
        acc + Number(debt.resolutionComment?.replaceAll(/[^0-9.-]/g, '') ?? 0),
      0,
    );
};

export const mergeAdditionalComments = (additionalComments, expenses) => {
  const individualExpenses = expenses
    ?.map(expense => `${expense.name} (${currency(expense.amount)})`)
    .join(', ');

  const individualExpensesStr = `Individual expense amount: ${individualExpenses}`;

  return individualExpenses
    ? `${
        additionalComments !== undefined ? additionalComments : ''
      }\n${individualExpensesStr}`
    : additionalComments;
};

export const getTotalAssets = ({ assets, questions }) => {
  const totOtherAssets = sumValues(assets.otherAssets, 'amount');
  const totRecVehicles = Number(
    assets?.recVehicleAmount?.replaceAll(/[^0-9.-]/g, '') ?? 0,
  );
  const totVehicles = questions?.hasVehicle
    ? sumValues(assets.automobiles, 'resaleValue')
    : 0;
  const realEstate = Number(
    assets.realEstateValue?.replaceAll(/[^0-9.-]/g, '') ?? 0,
  );

  const totAssets = sumValues(assets.monetaryAssets, 'amount');

  return totVehicles + totRecVehicles + totOtherAssets + realEstate + totAssets;
};

export const getEmploymentHistory = ({ questions, personalData }) => {
  const { employmentHistory } = personalData;
  let history = [];

  const defaultObj = {
    veteranOrSpouse: '',
    occupationName: '',
    from: '',
    to: '',
    present: false,
    employerName: '',
    employerAddress: {
      addresslineOne: '',
      addresslineTwo: '',
      addresslineThree: '',
      city: '',
      stateOrProvince: '',
      zipOrPostalCode: '',
      countryName: '',
    },
  };

  if (questions.vetIsEmployed) {
    const { employmentRecords } = employmentHistory.veteran;
    const vetEmploymentHistory = employmentRecords.map(employment => ({
      ...defaultObj,
      veteranOrSpouse: 'VETERAN',
      occupationName: employment.type,
      from: monthYearFormatter(employment.from),
      to: employment.isCurrent ? '' : monthYearFormatter(employment.to),
      present: employment.isCurrent ? employment.isCurrent : false,
      employerName: employment.employerName,
    }));
    history = [...history, ...vetEmploymentHistory];
  }

  if (questions.spouseIsEmployed) {
    const { spEmploymentRecords } = employmentHistory.spouse;
    const spouseEmploymentHistory = spEmploymentRecords.map(employment => ({
      ...defaultObj,
      veteranOrSpouse: 'SPOUSE',
      occupationName: employment.type,
      from: monthYearFormatter(employment.from),
      to: employment.isCurrent ? '' : monthYearFormatter(employment.to),
      present: employment.isCurrent ? employment.isCurrent : false,
      employerName: employment.employerName,
    }));
    history = [...history, ...spouseEmploymentHistory];
  }

  return history;
};

export const sortStatementsByDate = statements => {
  return statements.sort(
    (a, b) =>
      new Date(b.pSStatementDateOutput) - new Date(a.pSStatementDateOutput),
  );
};

// Returns name of debt depending on the type
export const getDebtName = debt => {
  return debt.debtType === 'COPAY'
    ? debt.station.facilityName
    : deductionCodes[debt.deductionCode] || debt.benefitType;
};

/**
 * Helper function to determine if date value is valid starting date:
 * - date is in the past or today
 * - date is not in the future
 *
 * @param {string} date - date string in ISO format ('YYYY-MM' or 'YYYY-MM-DD')
 * @returns {boolean} true if date meets requirements above
 */
export const isValidStartDate = date => {
  if (!date) return false;

  // Ensure the date is in a valid format (either YYYY-MM-DD or YYYY-MM)
  const isProperFormat = date.length === 10 || date.length === 7;
  if (!isProperFormat) return false;

  // If only "YYYY-MM" is provided, append "-01" to make it complete
  const safeDate = date.length === 7 ? `${date}-01` : date;

  // Parse the date and check its validity
  const parsedDate = new Date(safeDate.replace(/-/g, '/'));

  const year = parsedDate.getFullYear();
  if (year < 1900) return false;

  // Check that it's a real date, and not in the future
  return isValid(parsedDate) && !isFuture(parsedDate);
};

/**
 * Helper function to determine if date value is valid ending date:
 * - ending date is not in the future
 * - ending date is after start date
 *
 * @param {string} startDate - date string in ISO format ('YYYY-MM' or 'YYYY-MM-DD')
 * @param {string} endingDate - date string in ISO format ('YYYY-MM' or 'YYYY-MM-DD')
 * @returns {boolean} true if date meets requirements above
 */

export const isValidEndDate = (startDate, endingDate) => {
  if (!startDate || !endingDate) return false;

  // Ensure both dates are in a valid format
  const isProperStartFormat = startDate.length === 10 || startDate.length === 7;
  const isProperEndFormat = endingDate.length === 10 || endingDate.length === 7;

  if (!isProperStartFormat || !isProperEndFormat) return false;

  // Append "-01" to incomplete dates this applies to months and days
  const safeStart = startDate.length === 7 ? `${startDate}-01` : startDate;
  const safeEnd = endingDate.length === 7 ? `${endingDate}-01` : endingDate;

  // Parse the dates
  const parsedStart = new Date(safeStart.replace(/-/g, '/'));
  const parsedEnd = new Date(safeEnd.replace(/-/g, '/'));

  if (!isValid(parsedEnd) || !isValid(parsedStart)) return false;

  const year = parsedEnd.getFullYear();
  if (year < 1900) return false;

  // Ensure the end date is not in the future and is after the start date
  return !isFuture(parsedEnd) && isAfter(parsedEnd, parsedStart);
};

/**
 * Generates a unique key based on the given data fields and an optional index.
 */
export const generateUniqueKey = (data, fields, index = null) => {
  if (data === null || !fields.length) {
    return `default-key-${index}`;
  }
  const keyParts = fields.map(field => data[field] ?? 'undefined');
  if (index !== null) {
    keyParts.push(index);
  }
  return keyParts.join('-');
};

export const firstLetterLowerCase = str => {
  if (!str || str.length === 0) return '';
  // Check if the string is in the ignoreFields array
  if (ignoreFields.includes(str)) {
    return str;
  }
  return str.charAt(0).toLowerCase() + str.slice(1);
};

export const setDocumentTitle = title => {
  document.title = `${title} | FSR (VA Form 5655) | Veterans Affairs`;
};