department-of-veterans-affairs/vets-website

View on GitHub
src/applications/vaos/services/patient/index.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * Functions related to patient specific information
 * @module services/Patient
 */
import environment from '@department-of-veterans-affairs/platform-utilities/environment';
import { recordEligibilityFailure, recordVaosError } from '../../utils/events';
import { captureError } from '../../utils/error';
import { ELIGIBILITY_REASONS } from '../../utils/constants';
import { promiseAllFromObject } from '../../utils/data';
import { getAvailableHealthcareServices } from '../healthcare-service';
import { getPatientEligibility, getPatientRelationships } from '../vaos';
import { getLongTermAppointmentHistoryV2 } from '../appointment';
import { transformPatientRelationships } from './transformers';

function createErrorHandler(errorKey) {
  return data => {
    captureError(data, true);
    recordVaosError(`eligibility-${errorKey}`);
    return new Error('Eligibility error');
  };
}

const PRIMARY_CARE = '323';
const MENTAL_HEALTH = '502';

function checkEligibilityReason(ineligibilityReasons, ineligibilityType) {
  return !Array.isArray(ineligibilityReasons)
    ? true
    : !ineligibilityReasons.some(reason => {
        const { code } = reason.coding[0];
        return code === ineligibilityType;
      });
}

const VAOS_SERVICE_PATIENT_HISTORY = 'patient-history-insufficient';
const VAOS_SERVICE_REQUEST_LIMIT = 'facility-request-limit-exceeded';

/**
 * Returns patient based eligibility checks for specified request or direct types
 *
 * @export
 * @param {Object} params
 * @param {TypeOfCare} params.typeOfCare Type of care object,
 * @param {Location} params.location Location of where patient should have eligibility checked,
 * @param {'direct'|'request'|null} [params.type=null] The type to check eligibility for. By default,
 *   will check both
 * }
 * @param {boolean} [params.useV2=false] Use the v2 apis when making eligibility calls
 * @returns {PatientEligibility} Patient eligibility data
 */
export async function fetchPatientEligibility({
  typeOfCare,
  location,
  type = null,
}) {
  const checks = {};
  if (type !== 'request') {
    checks.direct = getPatientEligibility(
      location.id,
      typeOfCare.idV2,
      'direct',
    ).catch(createErrorHandler(`direct-check-metadata-error`));
  }

  if (type !== 'direct') {
    checks.request = getPatientEligibility(
      location.id,
      typeOfCare.idV2,
      'request',
    ).catch(createErrorHandler(`request-check-metadata-error`));
  }

  const results = await promiseAllFromObject(checks);
  const output = { direct: null, request: null };

  if (results.direct instanceof Error) {
    output.direct = new Error('Direct scheduling eligibility check error');
  } else if (results.direct) {
    output.direct = {
      eligible: results.direct.eligible,
      hasRequiredAppointmentHistory: checkEligibilityReason(
        results.direct.ineligibilityReasons,
        VAOS_SERVICE_PATIENT_HISTORY,
      ),
    };
  }

  if (results.request instanceof Error) {
    output.request = new Error('Request eligibility check error');
  } else if (results.request) {
    output.request = {
      eligible: results.request.eligible,
      hasRequiredAppointmentHistory: checkEligibilityReason(
        results.request.ineligibilityReasons,
        VAOS_SERVICE_PATIENT_HISTORY,
      ),
      isEligibleForNewAppointmentRequest: checkEligibilityReason(
        results.request.ineligibilityReasons,
        VAOS_SERVICE_REQUEST_LIMIT,
      ),
    };
  }

  return output;
}

/**
 * Fetch the logged in user's patient/provider relationships
 *
 * @export
 * @async
 * @param {Object} params
 * @param {TypeOfCare} params.typeOfCare Type of care object for which to check patient relationships
 * @param {Location} params.location Location of where patient should have relationships checked,
 * @returns {Array<PatientProviderRelationship} Returns an array of PatientProviderRelationship objects
 */

export async function fetchPatientRelationships() {
  // TODO:  https://github.com/department-of-veterans-affairs/va.gov-team/issues/98864
  // Once we are aware of the data that we need and where this needs to be called
  // in the flow, we need to add { typeOfCare, location } as a passed attribute.
  //
  // export async function fetchPatientRelationships({ typeOfCare, location })
  //
  // const data = await getPatientRelationships(location.id, typeOfCare.idV2);
  //
  // Currently this will fetch all patient provider relationships for the logged
  // in user.

  try {
    const data = await getPatientRelationships();
    return transformPatientRelationships(data || []);
  } catch (e) {
    return null;
  }
}

function locationSupportsDirectScheduling(location, typeOfCare) {
  return (
    // this check is included due to old two step facilities page
    location.legacyVAR.directSchedulingSupported ||
    location.legacyVAR.settings?.[typeOfCare.id]?.direct.enabled
  );
}

function locationSupportsRequests(location, typeOfCare) {
  return (
    // this check is included due to old two step facilities page
    location.legacyVAR.requestSupported ||
    location.legacyVAR.settings?.[typeOfCare.id]?.request.enabled
  );
}

function hasMatchingClinics(
  clinics,
  pastAppointments,
  featureClinicFilter = false,
) {
  return clinics?.some(
    clinic =>
      !!pastAppointments.find(appt => {
        const clinicIds = clinic.id.split('_');
        if (appt.version === 2 && featureClinicFilter) {
          return (
            clinic.stationId === appt.location.stationId &&
            clinicIds[1] === appt.location.clinicId &&
            clinic.patientDirectScheduling === true
          );
        }
        // TODO remove lines since v0 returns pre-filtered direct schedule clinics
        // if (appt.version === 1 && featureClinicFilter) {
        //   return (
        //     clinicIds[0] === appt.facilityId &&
        //     clinicIds[1] === appt.clinicId &&
        //     clinic.patientDirectScheduling === 'Y'
        //   );
        // }
        return (
          clinicIds[0] === appt.facilityId && clinicIds[1] === appt.clinicId
        );
      }),
  );
}

/*
 * This function logs information about eligibility to the console, to help with
 * testing in non-production environments
 */
/* istanbul ignore next */
function logEligibilityExplanation(
  location,
  typeOfCare,
  { request, direct, directReasons, requestReasons },
) {
  if (environment.isProduction() || navigator.userAgent === 'node.js') {
    return;
  }
  const reasonMapping = {
    [ELIGIBILITY_REASONS.notEnabled]:
      'The FE has disabled direct scheduling via feature toggle',
    [ELIGIBILITY_REASONS.notSupported]:
      'Disabled in VATS for this location and type of care',
    [ELIGIBILITY_REASONS.noRecentVisit]:
      'The var-resources visited in past months service indicated that there has not been a recent visit',
    [ELIGIBILITY_REASONS.overRequestLimit]:
      'The var-resources request limit service indicated that there are too many outstanding requests',
    [ELIGIBILITY_REASONS.noClinics]:
      'The var-resources clinics service did not return any clinics',
    [ELIGIBILITY_REASONS.noMatchingClinics]:
      'The FE could not find any of the clinics returned by var-resources in the past 24 months of appointments',
    [ELIGIBILITY_REASONS.error]:
      'There were errors trying to determine eligibility',
  };

  try {
    /* eslint-disable no-console */
    console.log('----');
    console.log(
      `%cEligibility checks for location ${location.id} and type of care ${
        typeOfCare.id
      }`,
      'font-weight: bold',
    );

    if (!direct) {
      console.log('%cUser not eligible for direct scheduling:', 'color: red');
      directReasons.map(reason => reasonMapping[reason]).forEach(message => {
        console.log(`  ${message}`);
      });
    } else {
      console.log('%cUser passed checks for direct scheduling', 'color: green');
    }

    if (!request) {
      console.log('%cUser not eligible for requests:', 'color: red');
      requestReasons.map(reason => reasonMapping[reason]).forEach(message => {
        console.log(`  ${message}`);
      });
    } else {
      console.log('%cUser passed checks for requests', 'color: green');
    }
  } catch (e) {
    captureError(e);
  }
  /* eslint-enable no-console */
}

/**
 * Checks eligibility for new appointment flow and returns
 * results, plus clinics and past appointments fetched along the way
 *
 * @export
 * @async
 * @param {Object} params
 * @param {TypeOfCare} params.typeOfCare Type of care object for the currently chosen type of care
 * @param {Location} params.location The current location to check eligibility against
 * @param {boolean} params.directSchedulingEnabled If direct scheduling is currently enabled
 * @param {boolean} [params.useV2=false] Use the v2 apis when making eligibility calls
 * @param {boolean} [params.featureClinicFilter=false] feature flag to filter clinics based on VATS
 * @returns {FlowEligibilityReturnData} Eligibility results, plus clinics and past appointments
 *   so that they can be cache and reused later
 */
export async function fetchFlowEligibilityAndClinics({
  typeOfCare,
  location,
  directSchedulingEnabled,
  useV2 = false,
  featureClinicFilter = false,
}) {
  const directSchedulingAvailable =
    locationSupportsDirectScheduling(location, typeOfCare) &&
    directSchedulingEnabled;

  const apiCalls = {
    patientEligibility: fetchPatientEligibility({
      typeOfCare,
      location,
      type: !directSchedulingAvailable ? 'request' : null,
    }),
  };

  // location contains legacyVAR that contains patientHistoryRequired
  const directTypeOfCareSettings =
    location.legacyVAR.settings?.[typeOfCare.id]?.direct;

  // We don't want to make unnecessary api calls if DS is turned off
  if (directSchedulingAvailable) {
    apiCalls.clinics = getAvailableHealthcareServices({
      facilityId: location.id,
      typeOfCare,
    }).catch(createErrorHandler('direct-available-clinics-error'));
    // Primary care and mental health is exempt from past appt history requirement
    const isDirectAppointmentHistoryRequired = featureClinicFilter
      ? typeOfCare.id !== PRIMARY_CARE &&
        typeOfCare.id !== MENTAL_HEALTH &&
        directTypeOfCareSettings.patientHistoryRequired === true
      : typeOfCare.id !== PRIMARY_CARE && typeOfCare.id !== MENTAL_HEALTH;

    if (isDirectAppointmentHistoryRequired) {
      apiCalls.pastAppointments = getLongTermAppointmentHistoryV2().catch(
        createErrorHandler('direct-no-matching-past-clinics-error'),
      );
    }
  }

  // This waits for all the api calls we're running in parallel to finish
  // It does not have a try/catch because all errors in the calls are caught
  // and resolved, so that we can still provide users a path forward if enough
  // checks succeeded
  const results = await promiseAllFromObject(apiCalls);

  const eligibility = {
    direct: directSchedulingEnabled,
    directReasons: !directSchedulingEnabled
      ? [ELIGIBILITY_REASONS.notEnabled]
      : [],
    request: true,
    requestReasons: [],
  };

  // This is going through all of our request related checks and setting
  // to false if we fail any of them. Order is important here, because the UI
  // will only be able to show one reason, the first one
  if (!locationSupportsRequests(location, typeOfCare)) {
    eligibility.request = false;
    eligibility.requestReasons.push(ELIGIBILITY_REASONS.notSupported);
  } else if (results.patientEligibility.request instanceof Error) {
    eligibility.request = false;
    eligibility.requestReasons.push(ELIGIBILITY_REASONS.error);
  } else {
    if (!results.patientEligibility.request.hasRequiredAppointmentHistory) {
      eligibility.request = false;
      eligibility.requestReasons.push(ELIGIBILITY_REASONS.noRecentVisit);
      recordEligibilityFailure('request-past-visits');
    }

    if (
      !results.patientEligibility.request.isEligibleForNewAppointmentRequest
    ) {
      eligibility.request = false;
      eligibility.requestReasons.push(ELIGIBILITY_REASONS.overRequestLimit);
      recordEligibilityFailure('request-exceeded-outstanding-requests');
    }
  }

  // Similar to above, but for direct scheduling
  // v2 needs to filter clinics
  if (useV2 && featureClinicFilter) {
    results.clinics = results?.clinics?.filter(
      clinic => clinic.patientDirectScheduling === true,
    );
  }

  if (!locationSupportsDirectScheduling(location, typeOfCare)) {
    eligibility.direct = false;
    eligibility.directReasons.push(ELIGIBILITY_REASONS.notSupported);
  } else if (
    results.patientEligibility.direct instanceof Error ||
    results.clinics instanceof Error ||
    results.pastAppointments instanceof Error
  ) {
    eligibility.direct = false;
    eligibility.directReasons.push(ELIGIBILITY_REASONS.error);
  } else if (directSchedulingEnabled) {
    if (!results.patientEligibility.direct.hasRequiredAppointmentHistory) {
      eligibility.direct = false;
      eligibility.directReasons.push(ELIGIBILITY_REASONS.noRecentVisit);
      recordEligibilityFailure(
        'direct-check-past-visits',
        typeOfCare?.id,
        location?.id,
      );
    }

    if (!results.clinics.length) {
      eligibility.direct = false;
      eligibility.directReasons.push(ELIGIBILITY_REASONS.noClinics);
      recordEligibilityFailure(
        'direct-available-clinics',
        typeOfCare?.id,
        location?.id,
      );
    }

    if (featureClinicFilter) {
      // v2 uses boolean while v0 uses Yes/No string for patientHistoryRequired
      const enable = useV2 ? true : 'Yes';
      if (
        typeOfCare.id !== PRIMARY_CARE &&
        typeOfCare.id !== MENTAL_HEALTH &&
        directTypeOfCareSettings.patientHistoryRequired === enable &&
        !hasMatchingClinics(
          results.clinics,
          results.pastAppointments,
          featureClinicFilter,
        )
      ) {
        eligibility.direct = false;
        eligibility.directReasons.push(ELIGIBILITY_REASONS.noMatchingClinics);
        recordEligibilityFailure('direct-no-matching-past-clinics');
      }
    } else if (
      typeOfCare.id !== PRIMARY_CARE &&
      typeOfCare.id !== MENTAL_HEALTH &&
      !hasMatchingClinics(
        results.clinics,
        results.pastAppointments,
        featureClinicFilter,
      )
    ) {
      eligibility.direct = false;
      eligibility.directReasons.push(ELIGIBILITY_REASONS.noMatchingClinics);
      recordEligibilityFailure('direct-no-matching-past-clinics');
    }
  }

  logEligibilityExplanation(location, typeOfCare, eligibility);

  return {
    eligibility,
    // it feels sort of hackish to return these along with our main
    // eligibility calcs, but we want to cache them for future use
    clinics: results.clinics,
    pastAppointments: results.pastAppointments,
  };
}