department-of-veterans-affairs/vets-website

View on GitHub
src/applications/personalization/profile/util/index.js

Summary

Maintainability
C
1 day
Test Coverage
import cloneDeep from 'lodash/cloneDeep';
import recordEvent from '~/platform/monitoring/record-event';
import { apiRequest } from '~/platform/utilities/api';

export const BASE_DIRECT_DEPOSIT_ERROR_KEYS = {
  ACCOUNT_FLAGGED_FOR_FRAUD: '.account.number.fraud',
  PAYMENT_RESTRICTIONS_PRESENT: '.restriction.indicators.present',
  ROUTING_NUMBER_FLAGGED_FOR_FRAUD: '.routing.number.fraud',
  ROUTING_NUMBER_INVALID_CHECKSUM: '.routing.number.invalid.checksum',
  ROUTING_NUMBER_INVALID: '.routing.number.invalid',
  DAY_PHONE_NUMBER_INVALID: '.day.phone.number.invalid',
  DAY_PHONE_AREA_INVALID: '.day.area.number.invalid',
  NIGHT_PHONE_NUMBER_INVALID: '.night.phone.number.invalid',
  NIGHT_PHONE_AREA_INVALID: '.night.area.number.invalid',
  MAILING_ADDRESS_INVALID: '.mailing.address.invalid',
  UNSPECIFIED_ERROR: '.unspecified.error',
  GENERIC_ERROR: '.generic.error',
};

// error keys for profile/direct_deposits/disability_compensations endpoint
// easier to export and use than importing one by one constants
// add cnp to the front of each base error key
export const LIGHTHOUSE_ERROR_KEYS = Object.keys(
  BASE_DIRECT_DEPOSIT_ERROR_KEYS,
).reduce((acc, key) => {
  acc[key] = `cnp.payment${BASE_DIRECT_DEPOSIT_ERROR_KEYS[key]}`;
  return acc;
}, {});

export const DIRECT_DEPOSIT_ERROR_KEYS = Object.keys(
  BASE_DIRECT_DEPOSIT_ERROR_KEYS,
).reduce((acc, key) => {
  acc[key] = `direct.deposit${BASE_DIRECT_DEPOSIT_ERROR_KEYS[key]}`;
  return acc;
}, {});

const OTHER_ERROR_GA_KEY = 'other-error';

export async function getData(apiRoute, options) {
  try {
    const response = await apiRequest(apiRoute, options);
    return response.data.attributes;
  } catch (error) {
    return { error };
  }
}

const hasLighthouseErrorText = (errors, errorKey, errorText) => {
  return errors.some(
    err =>
      err?.code === errorKey &&
      err?.detail?.toLowerCase().includes(errorText.toLowerCase()),
  );
};

const hasLighthouseErrorKey = (errors, errorKey) => {
  return errors.some(err => err?.code === errorKey);
};

const hasErrorMessage = (errors, errorKey, errorText) => {
  if (errorText) {
    return hasLighthouseErrorText(errors, errorKey, errorText);
  }
  return hasLighthouseErrorKey(errors, errorKey);
};

export const hasErrorCombos = ({
  errors,
  errorKeys = [],
  errorTexts = [],
} = {}) => {
  return errorKeys.some(errorKey => {
    if (errorTexts.length > 0) {
      return errorTexts.some(errorText =>
        hasErrorMessage(errors, errorKey, errorText),
      );
    }
    return hasErrorMessage(errors, errorKey);
  });
};

export const hasAccountFlaggedError = errors => {
  return hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.ACCOUNT_FLAGGED_FOR_FRAUD,
      DIRECT_DEPOSIT_ERROR_KEYS.ACCOUNT_FLAGGED_FOR_FRAUD,
    ],
  });
};

export const hasRoutingNumberFlaggedError = errors =>
  hasErrorCombos({
    errors,
    errorKeys: [LIGHTHOUSE_ERROR_KEYS.ROUTING_NUMBER_FLAGGED_FOR_FRAUD],
  });

// the cases for invalid routing number include:
// - invalid routing number checksum error code
// - invalid routing number error code
// - unspecified error code with error text 'Invalid Routing Number' in the error detail
// - generic error code with error text 'Invalid Routing Number' in the error detail
export const hasInvalidRoutingNumberError = errors =>
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.ROUTING_NUMBER_INVALID_CHECKSUM,
      LIGHTHOUSE_ERROR_KEYS.ROUTING_NUMBER_INVALID,
      DIRECT_DEPOSIT_ERROR_KEYS.ROUTING_NUMBER_INVALID_CHECKSUM,
      DIRECT_DEPOSIT_ERROR_KEYS.ROUTING_NUMBER_INVALID,
    ],
  }) ||
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.UNSPECIFIED_ERROR,
      LIGHTHOUSE_ERROR_KEYS.GENERIC_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.UNSPECIFIED_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.GENERIC_ERROR,
    ],
    errorTexts: ['Invalid Routing Number'],
  });

// the cases for invalid address include:
// - unspecified error code with error text 'address update' in the error detail
// - generic error code with error text 'address update' in the error detail
// - mailing address invalid error code
export const hasInvalidAddressError = errors =>
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.UNSPECIFIED_ERROR,
      LIGHTHOUSE_ERROR_KEYS.GENERIC_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.UNSPECIFIED_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.GENERIC_ERROR,
    ],
    errorTexts: ['address update'],
  }) ||
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.MAILING_ADDRESS_INVALID,
      DIRECT_DEPOSIT_ERROR_KEYS.MAILING_ADDRESS_INVALID,
    ],
  });

// the cases for invalid phone number include:
// - unspecified error code with error text 'night phone number' or 'night area number' in the error detail
// - generic error code with error text 'night phone number' or 'night area number' in the error detail
// - night phone number invalid error code
// - night area number invalid error code
export const hasInvalidHomePhoneNumberError = errors =>
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.UNSPECIFIED_ERROR,
      LIGHTHOUSE_ERROR_KEYS.GENERIC_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.UNSPECIFIED_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.GENERIC_ERROR,
    ],
    errorTexts: ['night phone number', 'night area number'],
  }) ||
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.NIGHT_PHONE_NUMBER_INVALID,
      LIGHTHOUSE_ERROR_KEYS.NIGHT_PHONE_AREA_INVALID,
      DIRECT_DEPOSIT_ERROR_KEYS.NIGHT_PHONE_NUMBER_INVALID,
      DIRECT_DEPOSIT_ERROR_KEYS.NIGHT_PHONE_AREA_INVALID,
    ],
  });

// the cases for invalid phone number include:
// - unspecified error code with error text 'day phone number' or 'day area number' in the error detail
// - generic error code with error text 'day phone number' or 'day area number' in the error detail
// - day phone number invalid error code
// - day area number invalid error code
export const hasInvalidWorkPhoneNumberError = errors =>
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.UNSPECIFIED_ERROR,
      LIGHTHOUSE_ERROR_KEYS.GENERIC_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.UNSPECIFIED_ERROR,
      DIRECT_DEPOSIT_ERROR_KEYS.GENERIC_ERROR,
    ],
    errorTexts: ['day phone number', 'day area number'],
  }) ||
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.DAY_PHONE_NUMBER_INVALID,
      LIGHTHOUSE_ERROR_KEYS.DAY_PHONE_AREA_INVALID,
      DIRECT_DEPOSIT_ERROR_KEYS.DAY_PHONE_NUMBER_INVALID,
      DIRECT_DEPOSIT_ERROR_KEYS.DAY_PHONE_AREA_INVALID,
    ],
  });

// the cases for payment restriction indicators present include:
// - payment restrictions present error code
export const hasPaymentRestrictionIndicatorsError = errors =>
  hasErrorCombos({
    errors,
    errorKeys: [
      LIGHTHOUSE_ERROR_KEYS.PAYMENT_RESTRICTIONS_PRESENT,
      DIRECT_DEPOSIT_ERROR_KEYS.PAYMENT_RESTRICTIONS_PRESENT,
    ],
  });

// BEGIN TODO: remove this once the direct deposit form is updated to use single form
export const cnpDirectDepositBankInfo = apiData => {
  return apiData?.paymentAccount;
};

export const eduDirectDepositAccountNumber = apiData => {
  return apiData?.accountNumber;
};

export const isEligibleForCNPDirectDeposit = apiData => {
  const controlInfo = apiData?.controlInformation;
  return !!controlInfo?.canUpdateDirectDeposit;
};

export const isSignedUpForCNPDirectDeposit = apiData =>
  !!cnpDirectDepositBankInfo(apiData)?.accountNumber;

export const isSignedUpForEDUDirectDeposit = apiData =>
  !!eduDirectDepositAccountNumber(apiData);
// END TODO: remove this once the direct deposit form is updated to use single form

const getLighthouseErrorCode = (errors = []) => {
  // there should only be one error code in the errors array, but just in case
  const error = errors.find(err => err?.code);
  return `${error?.code || OTHER_ERROR_GA_KEY} | ${error?.detail || ''}`;
};

// Helper that creates and returns an object to pass to the recordEvent()
// function when an error occurs while trying to save/update a user's direct
// deposit for compensation and pension payment information. The value of the
// `error-key` prop will change depending on the content of the `errors` array.
export const createCNPDirectDepositAnalyticsDataObject = ({
  errors = [],
  isEnrolling = false,
} = {}) => {
  const errorCode = getLighthouseErrorCode(errors);

  return cloneDeep({
    event: 'profile-edit-failure',
    'profile-action': 'save-failure',
    'profile-section': `cnp-direct-deposit-information`,
    'error-key': `${errorCode}${isEnrolling ? '-enroll' : '-update'}`,
  });
};

export function recordApiEvent(
  { endpoint, status, method = 'GET', extraProperties = {} },
  recordAnalyticsEvent = recordEvent,
) {
  const payload = {
    event: 'api_call',
    'api-name': `${method} ${endpoint}`,
    'api-status': status,
  };

  const errorKey = extraProperties?.['error-key'];

  if (errorKey) {
    payload['error-key'] = errorKey;
  }

  recordAnalyticsEvent(payload);
}