department-of-veterans-affairs/vets-website

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

Summary

Maintainability
F
4 days
Test Coverage
/* eslint-disable camelcase */
/**
 * Functions related to fetching Apppointment data and pulling information from that data
 * @module services/Appointment
 */
import moment from 'moment-timezone';
import { recordEvent } from '@department-of-veterans-affairs/platform-monitoring/exports';
import {
  getAppointment,
  getAppointments,
  postAppointment,
  putAppointment,
} from '../vaos';
import { mapToFHIRErrors } from '../utils';
import {
  APPOINTMENT_TYPES,
  APPOINTMENT_STATUS,
  VIDEO_TYPES,
  GA_PREFIX,
} from '../../utils/constants';
import { formatFacilityAddress, getFacilityPhone } from '../location';
import {
  transformVAOSAppointment,
  transformVAOSAppointments,
} from './transformers';
import { captureError } from '../../utils/error';
import { resetDataLayer } from '../../utils/events';
import {
  getTimezoneAbbrByFacilityId,
  getTimezoneByFacilityId,
  getTimezoneAbbrFromApi,
  getTimezoneNameFromAbbr,
  getUserTimezone,
  getUserTimezoneAbbr,
  stripDST,
} from '../../utils/timezone';
import { getProviderName } from '../../utils/appointment';

const CONFIRMED_APPOINTMENT_TYPES = new Set([
  APPOINTMENT_TYPES.ccAppointment,
  APPOINTMENT_TYPES.vaAppointment,
]);

// Appointments in these "HIDDEN_SET"s should not be shown in appointment lists at all
export const FUTURE_APPOINTMENTS_HIDDEN_SET = new Set(['NO-SHOW', 'DELETED']);
const DEFAULT_VIDEO_STATUS = 'FUTURE';

const PAST_APPOINTMENTS_HIDDEN_SET = new Set([
  'FUTURE',
  'DELETED',
  null,
  '<null>',
  'Deleted',
]);

// We want to throw an error for any partial results errors from MAS,
// but some sites in staging always errors. So, keep those in a list to
// ignore errors from
// const BAD_STAGING_SITES = new Set(['556', '612']);
// function hasPartialResults(response) {
//   return (
//     response.errors?.length > 0 &&
//     (environment.isProduction() ||
//       response.errors.some(err => !BAD_STAGING_SITES.has(err.source)))
//   );
// }

// Sort the requested appointments, latest appointments appear at the top of the list.
function apptRequestSort(a, b) {
  return new Date(b.created).getTime() - new Date(a.created).getTime();
}

/**
 * Fetch the logged in user's confirmed appointments that fall between a startDate and endDate
 *
 * @export
 * @async
 * @param {String} startDate Date in YYYY-MM-DD format
 * @param {String} endDate Date in YYYY-MM-DD format
 * @param {Boolean} useV2VA Toggle fetching VA appointments via VAOS api services version 2
 * @param {Boolean} useV2CC Toggle fetching CC appointments via VAOS api services version 2
 * @returns {Appointment[]} A FHIR searchset of booked Appointment resources
 */
export async function fetchAppointments({ startDate, endDate, avs = false }) {
  try {
    const appointments = [];
    const allAppointments = await getAppointments(
      startDate,
      endDate,
      ['booked', 'arrived', 'fulfilled', 'cancelled'],
      avs,
    );

    const filteredAppointments = allAppointments.data.filter(appt => {
      return !appt.requestedPeriods;
    });

    appointments.push(...transformVAOSAppointments(filteredAppointments), {
      meta: allAppointments.backendSystemFailures,
    });

    return appointments;
  } catch (e) {
    if (e.errors) {
      throw mapToFHIRErrors(e.errors);
    }

    throw e;
  }
}

/**
 * Fetch the logged in user's pending appointments that fall between a startDate and endDate
 *
 * @export
 * @async
 * @param {String} startDate Date in YYYY-MM-DD format
 * @param {String} endDate Date in YYYY-MM-DD format
 * @returns {Appointment[]} A FHIR searchset of pending Appointment resources
 */
export async function getAppointmentRequests({ startDate, endDate }) {
  try {
    const appointments = await getAppointments(startDate, endDate, [
      'proposed',
      'cancelled',
    ]);

    const requestsWithoutAppointments = appointments.data.filter(
      appt => !!appt.requestedPeriods,
    );

    requestsWithoutAppointments.sort(apptRequestSort);

    const transformRequests = transformVAOSAppointments(
      requestsWithoutAppointments,
    );

    transformRequests.push({
      meta: appointments.backendSystemFailures,
    });

    return transformRequests;
  } catch (e) {
    if (e.errors) {
      throw mapToFHIRErrors(e.errors);
    }

    throw e;
  }
}

/**
 * Fetches a single appointment request and transforms it into our Appointment type
 *
 * @export
 * @async
 * @param {string} id Appointment request id
 * @returns {Appointment} An Appointment object for the given request id
 */
export async function fetchRequestById({ id }) {
  try {
    const appointment = await getAppointment(id);

    return transformVAOSAppointment(appointment);
  } catch (e) {
    if (e.errors) {
      throw mapToFHIRErrors(e.errors);
    }

    throw e;
  }
}
/**
 * Fetches a booked appointment based on the id and type provided
 *
 * @export
 * @async
 * @param {string} id MAS or community care booked appointment id
 * @param {Boolean} useV2 Toggle fetching VA or CC appointment via VAOS api services version 2
 * @returns {Appointment} A transformed appointment with the given id
 */
export async function fetchBookedAppointment({ id, avs = true }) {
  try {
    const appointment = await getAppointment(id, avs);
    return transformVAOSAppointment(appointment);
  } catch (e) {
    if (e.errors) {
      throw mapToFHIRErrors(e.errors);
    }

    throw e;
  }
}

/**
 * Returns whether or not the appointment is VA phone appointment
 *
 * @export
 * @param {Object} appointment A FHIR appointment resource
 * @returns {Boolean} Whether or not the appointment is by phone
 */
export function isVAPhoneAppointment(appointment) {
  return appointment?.vaos.isPhoneAppointment;
}

/**
 * Returns true if the appointment is a video appointment
 * where the Veteran needs to go to a clinic, rather than stay at home
 *
 * @export
 * @param {Appointment} appointment
 * @returns {boolean} True if appointment is a clinic or store forward appointment
 */
export function isClinicVideoAppointment(appointment) {
  return (
    appointment?.videoData.kind === VIDEO_TYPES.clinic ||
    appointment?.videoData.kind === VIDEO_TYPES.storeForward
  );
}

/**
 * Returns true if the appointment is a video appointment
 * where the Veteran uses a VA furnished device
 *
 * @export
 * @param {Appointment} appointment
 * @returns {boolean} True if appointment is a video appointment that uses a VA furnished device
 */
export function isGfeVideoAppointment(appointment) {
  const patientHasMobileGfe =
    appointment.videoData.extension?.patientHasMobileGfe;

  return (
    (appointment?.videoData.kind === VIDEO_TYPES.mobile ||
      appointment?.videoData.kind === VIDEO_TYPES.adhoc) &&
    (!appointment?.videoData.isAtlas && patientHasMobileGfe)
  );
}

/**
 * Returns true if the appointment is a video appointment
 * at an ATLAS location
 *
 * @export
 * @param {Appointment} appointment
 * @returns {boolean} True if appointment is a video appointment at ATLAS location
 */
export function isAtlasVideoAppointment(appointment) {
  return appointment?.videoData.isAtlas;
}

/**
 * Returns the location ID of a VA appointment (in person or video)
 *
 * @export
 * @param {Appointment} appointment A FHIR appointment resource
 * @returns {string} The location id where the VA appointment is located
 */
export function getVAAppointmentLocationId(appointment) {
  if (appointment === undefined) return null;

  if (
    appointment?.vaos.isVideo &&
    appointment?.vaos.appointmentType === APPOINTMENT_TYPES.vaAppointment &&
    !isClinicVideoAppointment(appointment)
  ) {
    // 612 doesn't exist in the facilities api, but it's a valid VistA site
    // So, we want to show the facility information for the actual parent location
    // in that system, which is 612A4. This is really only visible for at home
    // video appointments, as the facility we direct users to in order to cancel
    if (appointment.location.vistaId === '612') {
      return '612A4';
    }

    return appointment?.location.vistaId;
  }

  return appointment?.location.stationId;
}
/**
 * Returns the patient telecom info in a VA appointment
 *
 * @export
 * @param {Appointment} appointment A FHIR appointment resource
 * @param {string} type A FHIR telecom type id
 * @returns {string} The patient telecom value
 */
export function getPatientTelecom(appointment, type) {
  return appointment?.contact?.telecom.find(t => t.type === type)?.value;
}

/**
 * Returns whether or not the facility has a COVID vaccine phone line
 *
 * @export
 * @param {Object} facility A facility resource
 * @returns {Boolean} Whether or not the facility has a COVID vaccine phone line
 */
export function hasValidCovidPhoneNumber(facility) {
  return !!facility?.telecom?.find(tele => tele.system === 'covid')?.value;
}

/**
 * Checks to see if an appointment should be shown in the past appointment
 * list
 *
 * @param {Appointment} appt A FHIR appointment resource
 * @returns {boolean} Whether or not the appt should be shown
 */
export function isValidPastAppointment(appt) {
  return (
    CONFIRMED_APPOINTMENT_TYPES.has(appt.vaos.appointmentType) &&
    appt.status !== APPOINTMENT_STATUS.cancelled &&
    // Show confirmed appointments that don't have vista statuses in the exclude
    // list
    (!PAST_APPOINTMENTS_HIDDEN_SET.has(appt.description) ||
      // Show video appointments that have the default FUTURE status,
      // since we can't infer anything about the video appt from that status
      (appt.videoData?.isVideo && appt.description === DEFAULT_VIDEO_STATUS) ||
      // Some CC appointments can have a null status because they're not from VistA
      // And we want to show those
      (appt.vaos.appointmentType === APPOINTMENT_TYPES.ccAppointment &&
        !appt.description))
  );
}

/**
 * Returns true if the given Appointment is a confirmed appointment
 * or a request that still needs processing
 *
 * @export
 * @param {Appointment} appt The FHIR Appointment to check
 * @returns {boolean} Whether or not the appointment is a valid upcoming
 *  appointment or request
 */
export function isUpcomingAppointmentOrRequest(appt) {
  if (CONFIRMED_APPOINTMENT_TYPES.has(appt.vaos.appointmentType)) {
    const apptDateTime = moment(appt.start);

    return (
      !appt.vaos.isPastAppointment &&
      !FUTURE_APPOINTMENTS_HIDDEN_SET.has(appt.description) &&
      apptDateTime.isValid() &&
      apptDateTime.isBefore(moment().add(395, 'days'))
    );
  }

  const today = moment().startOf('day');
  const hasValidDate = appt.requestedPeriod.some(period => {
    const momentStart = moment(period.start);
    const momentEnd = moment(period.end);
    return (
      momentStart.isValid() && momentStart.isAfter(today) && momentEnd.isValid()
    );
  });

  return (
    !appt.vaos.isExpressCare &&
    (appt.status === APPOINTMENT_STATUS.proposed ||
      appt.status === APPOINTMENT_STATUS.pending ||
      (appt.status === APPOINTMENT_STATUS.cancelled && hasValidDate))
  );
}
/**
 * Returns cancelled and pending requests, which should be visible to users
 *
 * @export
 * @param {Appointment} appt The appointment to check
 * @returns {Boolean} If the appointment should be shown or not
 */
export function isPendingOrCancelledRequest(appt) {
  return (
    !appt.vaos?.isExpressCare &&
    (appt.status === APPOINTMENT_STATUS.proposed ||
      appt.status === APPOINTMENT_STATUS.pending ||
      appt.status === APPOINTMENT_STATUS.cancelled)
  );
}

/**
 * Returns true if the given Appointment is a confirmed appointment
 *
 * @export
 * @param {Appointment} appt The FHIR Appointment to check
 * @returns {boolean} Whether or not the appointment is a valid upcoming
 *  appointment
 */
export function isUpcomingAppointment(appt) {
  if (CONFIRMED_APPOINTMENT_TYPES.has(appt.vaos.appointmentType)) {
    const apptDateTime = moment(appt.start);

    return (
      !appt.vaos.isPastAppointment &&
      !FUTURE_APPOINTMENTS_HIDDEN_SET.has(appt.description) &&
      apptDateTime.isValid() &&
      apptDateTime.isAfter(moment().startOf('day')) &&
      apptDateTime.isBefore(
        moment()
          .endOf('day')
          .add(395, 'days'),
      )
    );
  }

  return false;
}

/**
 * Sort method for past appointments
 * @param {Appointment} a A FHIR appointment resource
 * @param {Appointment} b A FHIR appointment resource
 */
export function sortByDateDescending(a, b) {
  return moment(a.start).isAfter(moment(b.start)) ? -1 : 1;
}

/**
 * Sort method for upcoming appointments
 * @param {Appointment} a A FHIR appointment resource
 * @param {Appointment} b A FHIR appointment resource
 */
export function sortByDateAscending(a, b) {
  return moment(a.start).isBefore(moment(b.start)) ? -1 : 1;
}

/**
 * Sort method for appointments requests
 * @param {Appointment} a A FHIR appointment resource
 * @param {Appointment} b A FHIR appointment resource
 */
export function sortByCreatedDateDescending(a, b) {
  return moment(a.created).isAfter(moment(b.created)) ? -1 : 1;
}

/**
 * Sort method for future appointment requests
 * @param {Appointment} a A FHIR appointment resource
 * @param {Appointment} b A FHIR appointment resource
 */
export function sortUpcoming(a, b) {
  if (
    CONFIRMED_APPOINTMENT_TYPES.has(a.vaos.appointmentType) !==
    CONFIRMED_APPOINTMENT_TYPES.has(b.vaos.appointmentType)
  ) {
    return CONFIRMED_APPOINTMENT_TYPES.has(a.vaos.appointmentType) ? -1 : 1;
  }

  if (CONFIRMED_APPOINTMENT_TYPES.has(a.vaos.appointmentType)) {
    return moment(a.start).isBefore(moment(b.start)) ? -1 : 1;
  }

  const typeOfCareA = a.type?.coding?.[0]?.display;
  const typeOfCareB = b.type?.coding?.[0]?.display;

  // If type of care is the same, return the one with the sooner date
  if (typeOfCareA === typeOfCareB) {
    return moment(a.requestedPeriod[0].start).isBefore(
      moment(b.requestedPeriod[0].start),
    )
      ? -1
      : 1;
  }

  // Otherwise, return sorted alphabetically by appointmentType
  return typeOfCareA.toLowerCase() < typeOfCareB.toLowerCase() ? -1 : 1;
}

/**
 * Sort method for appointment messages
 * @param {Object} a Message object
 * @param {Object} b Message object
 */
export function sortMessages(a, b) {
  return moment(a.attributes.date).isBefore(b.attributes.date) ? -1 : 1;
}

/**
 * Method to check for home video appointment
 * @param {Appointment} appointment A FHIR appointment resource
 * @return {boolean} Returns whether or not the appointment is a home video appointment.
 */
export function isVideoHome(appointment) {
  const { isAtlas, kind } = appointment?.videoData || {};
  return (
    !isAtlas && (kind === VIDEO_TYPES.mobile || kind === VIDEO_TYPES.adhoc)
  );
}

/**
 * Get the name of the first preferred community care provider, or generic text
 *
 * @param {Appointment} appointment An appointment object
 * @return {String} Returns the community care provider name
 */
export function getPreferredCommunityCareProviderName(appointment) {
  const provider = appointment?.preferredCommunityCareProviders?.[0];

  if (provider) {
    return provider.practiceName || provider.providerName;
  }

  return 'Community care';
}

/**
 * Groups appointments by month into an array of objects with appointment start
 * date as the key.
 * Assumes appointments are already sorted
 *
 * @export
 * @param {Appointment[]} appointments List of FHIR appointments
 * @returns {Array} An array of objects grouped by month
 */
export function groupAppointmentsByMonth(appointments) {
  if (appointments.length === 0) {
    return [];
  }

  return appointments.reduce((previous, current) => {
    const key = moment(current.start).format('YYYY-MM');
    // eslint-disable-next-line no-param-reassign
    previous[key] = previous[key] || [];
    previous[key].push(current);
    return previous;
  }, {});
}

/**
 * Creates an appointment through the v2 api and transforms the result
 * back into our Appointment format
 *
 * @export
 * @param {Object} params
 * @param {VAOSAppointment} params.appointment The appointment to send
 * @returns {Appointment} The created appointment
 */
export async function createAppointment({ appointment }) {
  const result = await postAppointment(appointment);

  return transformVAOSAppointment(result);
}

const eventPrefix = `${GA_PREFIX}-cancel-appointment-submission`;

/**
 * Cancels an appointment or request
 *
 * @export
 * @param {Object} params
 * @param {Appointment} params.appointment The appointment to cancel
 * @returns {?Appointment} Returns either null or the updated appointment data
 */
export async function cancelAppointment({ appointment }) {
  const additionalEventData = {
    appointmentType:
      appointment.status === APPOINTMENT_STATUS.proposed
        ? 'pending'
        : 'confirmed',
    facilityType: appointment.vaos?.isCommunityCare ? 'cc' : 'va',
  };

  recordEvent({
    event: eventPrefix,
    ...additionalEventData,
  });

  try {
    const updatedAppointment = await putAppointment(appointment.id, {
      status: APPOINTMENT_STATUS.cancelled,
    });

    recordEvent({
      event: `${eventPrefix}-successful`,
      ...additionalEventData,
    });
    resetDataLayer();

    return transformVAOSAppointment(updatedAppointment);
  } catch (e) {
    captureError(e, true);
    recordEvent({
      event: `${eventPrefix}-failed`,
      ...additionalEventData,
    });
    resetDataLayer();

    throw e;
  }
}

export function isInPersonVAAppointment(appointment) {
  const { isCommunityCare, isVideo } = appointment?.vaos || {};
  const isPhone = isVAPhoneAppointment(appointment);

  return !isVideo && !isCommunityCare && !isPhone;
}

/**
 * Get scheduled appointment information needed for generating
 * an .ics file.
 *
 * @export
 * @param {Appointment} Appointment object, facility object
 * @param {Facility} Facility object
 * @returns An object containing appointment information.
 */
export function getCalendarData({ appointment, facility }) {
  let data = {};
  const isAtlas = appointment?.videoData.isAtlas;
  const isHome = isVideoHome(appointment);
  const videoKind = appointment?.videoData.kind;
  const isVideo = appointment?.vaos.isVideo;
  const isCommunityCare = appointment?.vaos.isCommunityCare;
  const isPhone = isVAPhoneAppointment(appointment);
  // const isInPersonVAAppointment = !isVideo && !isCommunityCare && !isPhone;
  const signinText =
    'Sign in to https://va.gov/health-care/schedule-view-va-appointments/appointments to get details about this appointment';

  if (isPhone) {
    data = {
      summary: 'Phone appointment',
      providerName: facility?.name,
      location: formatFacilityAddress(facility),
      text: `A provider will call you at ${moment
        .parseZone(appointment.start)
        .format('h:mm a')}`,
      phone: getFacilityPhone(facility),
      additionalText: [signinText],
    };
  } else if (isInPersonVAAppointment(appointment)) {
    data = {
      summary: `Appointment at ${facility?.name || 'the VA'}`,
      location: formatFacilityAddress(facility),
      text: `You have a health care appointment at ${facility?.name ||
        'a VA location.'}`,
      phone: getFacilityPhone(facility),
      additionalText: [signinText],
    };
  } else if (isCommunityCare) {
    let { practiceName } = appointment.communityCareProvider || {};
    const providerName = getProviderName(appointment);
    let summary = 'Community care appointment';
    practiceName = practiceName?.trim().length ? practiceName : '';
    if (!!practiceName || !!providerName) {
      // order of the name appearing on the calendar title is important to match the display screen name
      summary =
        appointment.version === 1
          ? `Appointment at ${providerName || practiceName}`
          : `Appointment at ${providerName[0] || practiceName}`;
    }
    data = {
      summary,
      providerName:
        providerName !== undefined ? `${providerName || practiceName}` : null,
      location: formatFacilityAddress(appointment?.communityCareProvider),
      text:
        'You have a health care appointment with a community care provider. Please don’t go to your local VA health facility.',
      phone: getFacilityPhone(appointment?.communityCareProvider),
      additionalText: [signinText],
    };
  } else if (isVideo) {
    const providerName = appointment.videoData?.providers
      ? appointment.videoData.providers[0]?.display
      : '';
    const providerText = providerName
      ? `You'll be meeting with ${providerName}`
      : '';

    if (isHome) {
      data = {
        summary: 'VA Video Connect appointment',
        text:
          'You can join this meeting up to 30 minutes before the start time.',
        location: 'VA Video Connect at home',
        additionalText: [signinText],
      };
    } else if (isAtlas) {
      const { atlasLocation } = appointment.videoData;

      if (atlasLocation?.address) {
        data = {
          summary: `VA Video Connect appointment at an ATLAS facility`,
          location: formatFacilityAddress(atlasLocation),
          text: 'Join this video meeting from this ATLAS (non-VA) location:',
          additionalText: [
            `Your appointment code is ${
              appointment.videoData.atlasConfirmationCode
            }. Use this code to find your appointment on the computer at the ATLAS facility.`,
          ],
        };

        if (providerName)
          data.additionalText.push(`You'll be meeting with ${providerName}`);
      }
    } else if (isClinicVideoAppointment(appointment)) {
      data = {
        summary: `VA Video Connect appointment at ${facility?.name ||
          'a VA location'}`,
        providerName: facility?.name,
        location: formatFacilityAddress(facility),
        text: `You need to join this video meeting from${
          facility
            ? ':'
            : ' the VA location where you scheduled the appointment.'
        }`,
        phone: getFacilityPhone(facility),
      };

      if (providerName) data.additionalText = [providerText, signinText];
      else data.additionalText = [signinText];
    } else if (videoKind === VIDEO_TYPES.gfe) {
      data = {
        summary: 'VA Video Connect appointment using a VA device',
        location: '',
        text: 'Join this video meeting using a device provided by VA.',
      };

      if (providerName) data.additionalText = [providerText];
    }
  }

  return data;
}

/**
 * Returns an object with timezone identifiers for a given appointment
 *
 * @export
 * @param {Appointment} appointment The appointment to get a timezone for
 * @returns {Object} An object with:
 *   - identifier: The full timezone identifier (like America/New_York)
 *   - abbreviation: The timezone abbreviation (e.g. ET)
 *   - description: The written out description (e.g. Eastern time)
 */
export function getAppointmentTimezone(appointment) {
  // Appointments with timezone included in api
  if (appointment?.timezone) {
    const abbreviation = getTimezoneAbbrFromApi(appointment);

    return {
      identifier: appointment.timezone,
      abbreviation,
      description: getTimezoneNameFromAbbr(abbreviation),
    };
  }
  // Fallback to getting Timezone from facility Id and hardcoded timezone Json.
  if (appointment?.location?.vistaId) {
    const locationId =
      appointment?.location.stationId || appointment?.location.vistaId;
    const abbreviation = getTimezoneAbbrByFacilityId(locationId);

    return {
      identifier: moment.tz
        .zone(getTimezoneByFacilityId(locationId))
        ?.abbr(appointment?.start),
      abbreviation,
      description: getTimezoneNameFromAbbr(abbreviation),
    };
  }

  // Everything else will use the local timezone
  const abbreviation = stripDST(getUserTimezoneAbbr());
  return {
    identifier: getUserTimezone(),
    abbreviation,
    description: getTimezoneNameFromAbbr(abbreviation),
  };
}

export const getLongTermAppointmentHistoryV2 = ((chunks = 1) => {
  const batch = [];
  let promise = null;

  return () => {
    if (!promise || navigator.userAgent === 'node.js') {
      // Creating an array of start and end dates for each chunk
      const ranges = Array.from(Array(chunks).keys()).map(i => {
        return {
          start: moment()
            .startOf('day')
            .subtract(i + 1, 'year')
            .utc()
            .format(),

          end: moment()
            .startOf('day')
            .subtract(i, 'year')
            .utc()
            .format(),
        };
      });

      // There are three chunks with date ranges from the array created above.
      // We're trying to run them serially, because we want to be careful about
      // overloading the upstream service, so Promise.all doesn't fit here.
      promise = ranges.reduce(async (prev, curr) => {
        // NOTE: This is the secret sauce to run the fetch requests sequentially.
        // Doing an 'await' on a non promise wraps it into a promise that is then awaited.
        // In this case, the initial value of previous is set to an empty array.
        //
        // NOTE: fetchAppointments will run concurrently without this await 1st!
        await prev;

        // Next, fetch the appointments which will be chained which the previous await
        const p1 = await fetchAppointments({
          startDate: curr.start,
          endDate: curr.end,
          useV2VA: true,
          useV2CC: true,
        });
        batch.push(p1);
        return Promise.resolve([...batch].flat());
      }, []);
    }

    return promise;
  };
})(3);

/**
 * Function to return appointment date. Date is return with conversion to locale
 * timezone.
 *
 * @export
 * @param {*} appointment
 * @returns Appointment date
 */
export function getAppointmentDate(appointment) {
  return moment.parseZone(appointment.start);
}

export function groupAppointmentByDay(appointments) {
  return appointments.reduce((previous, current) => {
    const key = moment(current.start).format('YYYY-MM-DD');
    // eslint-disable-next-line no-param-reassign
    previous[key] = previous[key] || [];
    previous[key].push(current);
    return previous;
  }, {});
}

export function getLink({ featureBreadcrumbUrlUpdate, appointment }) {
  const { isCommunityCare, isPastAppointment } = appointment.vaos;

  if (!featureBreadcrumbUrlUpdate) {
    return isCommunityCare
      ? `${isPastAppointment ? '/past' : ''}/cc/${appointment.id}`
      : `${isPastAppointment ? '/past' : ''}/va/${appointment.id}`;
  }
  return `${isPastAppointment ? 'past' : ''}/${appointment.id}`;
}

export function getPractitionerName(appointment) {
  const { practitioners } = appointment;

  if (!practitioners?.length) return null;

  const practitioner = practitioners[0];
  const { name } = practitioner;

  return `${name.given.toString().replaceAll(',', ' ')} ${name.family}`;
}

export function getVideoAppointmentLocationText(appointment) {
  const { isAtlas } = appointment.videoData;
  const videoKind = appointment.videoData.kind;
  let desc = 'Video appointment at home';

  if (isAtlas) {
    desc = 'Video appointment at an ATLAS location';
  } else if (isClinicVideoAppointment(appointment)) {
    desc = 'Video appointment at a VA location';
  } else if (videoKind === VIDEO_TYPES.gfe) {
    desc = 'Video with VA device';
  }

  return desc;
}

// TODO: Verify if isCanceledConfirmed can be used
export function isCanceled(appointment) {
  return appointment.status === APPOINTMENT_STATUS.cancelled;
}

export function getLabelText(appointment) {
  const appointmentDate = getAppointmentDate(appointment);

  return `Details for ${
    isCanceled(appointment) ? 'canceled ' : ''
  }appointment on ${appointmentDate.format('dddd, MMMM D h:mm a')}`;
}