department-of-veterans-affairs/vets-website

View on GitHub
src/applications/claims-status/actions/index.js

Summary

Maintainability
F
1 wk
Test Coverage
import React from 'react';
import * as Sentry from '@sentry/browser';

import { recordEvent } from '@department-of-veterans-affairs/platform-monitoring/exports';
import { apiRequest } from '@department-of-veterans-affairs/platform-utilities/exports';
import environment from '@department-of-veterans-affairs/platform-utilities/environment';
import localStorage from 'platform/utilities/storage/localStorage';

import { getErrorStatus, UNKNOWN_STATUS } from '../utils/appeals-v2-helpers';
import {
  makeAuthRequest,
  roundToNearest,
  buildDateFormatter,
} from '../utils/helpers';
import { mockApi } from '../tests/e2e/fixtures/mocks/mock-api';
import manifest from '../manifest.json';
import { canUseMocks } from '../constants';
import {
  ADD_FILE,
  BACKEND_SERVICE_ERROR,
  CANCEL_UPLOAD,
  CLEAR_ADDITIONAL_EVIDENCE_NOTIFICATION,
  CLEAR_CLAIM_DETAIL,
  CLEAR_NOTIFICATION,
  DONE_UPLOADING,
  FETCH_APPEALS_ERROR,
  FETCH_APPEALS_PENDING,
  FETCH_APPEALS_SUCCESS,
  FETCH_CLAIMS_ERROR,
  FETCH_CLAIMS_PENDING,
  FETCH_CLAIMS_SUCCESS,
  FETCH_STEM_CLAIMS_ERROR,
  FETCH_STEM_CLAIMS_PENDING,
  FETCH_STEM_CLAIMS_SUCCESS,
  GET_CLAIM_DETAIL,
  RECORD_NOT_FOUND_ERROR,
  REMOVE_FILE,
  RESET_UPLOADS,
  SET_ADDITIONAL_EVIDENCE_NOTIFICATION,
  SET_CLAIM_DETAIL,
  SET_CLAIMS_UNAVAILABLE,
  SET_DECISION_REQUEST_ERROR,
  SET_DECISION_REQUESTED,
  SET_FIELDS_DIRTY,
  SET_LAST_PAGE,
  SET_NOTIFICATION,
  SET_PROGRESS,
  SET_UNAUTHORIZED,
  SET_UPLOAD_ERROR,
  SET_UPLOADER,
  SET_UPLOADING,
  SUBMIT_DECISION_REQUEST,
  UPDATE_FIELD,
  USER_FORBIDDEN_ERROR,
  VALIDATION_ERROR,
} from './types';

export const getClaimLetters = async () => {
  return apiRequest('/claim_letters');
};

export function setNotification(message) {
  return {
    type: SET_NOTIFICATION,
    message,
  };
}

export function setAdditionalEvidenceNotification(message) {
  return {
    type: SET_ADDITIONAL_EVIDENCE_NOTIFICATION,
    message,
  };
}

function fetchAppealsSuccess(response) {
  const appeals = response.data;
  return {
    type: FETCH_APPEALS_SUCCESS,
    appeals,
  };
}

export function getAppealsV2() {
  return dispatch => {
    dispatch({ type: FETCH_APPEALS_PENDING });
    return apiRequest('/appeals')
      .then(appeals => dispatch(fetchAppealsSuccess(appeals)))
      .catch(error => {
        const status = getErrorStatus(error);
        const action = { type: '' };
        switch (status) {
          case '403':
            action.type = USER_FORBIDDEN_ERROR;
            break;
          case '404':
            action.type = RECORD_NOT_FOUND_ERROR;
            break;
          case '422':
            action.type = VALIDATION_ERROR;
            break;
          case '502':
            action.type = BACKEND_SERVICE_ERROR;
            break;
          default:
            action.type = FETCH_APPEALS_ERROR;
            break;
        }
        Sentry.withScope(scope => {
          scope.setFingerprint(['{{default}}', status]);
          Sentry.captureException(`vets_appeals_v2_err_get_appeals ${status}`);
        });
        return dispatch(action);
      });
  };
}

function fetchClaimsSuccess(claims) {
  return {
    type: FETCH_CLAIMS_SUCCESS,
    claims,
  };
}

const recordClaimsAPIEvent = ({ startTime, success, error }) => {
  const event = {
    event: 'api_call',
    'api-name': 'GET claims',
    'api-status': success ? 'successful' : 'failed',
  };
  if (error) {
    event['error-key'] = error;
  }
  if (startTime) {
    const apiLatencyMs = roundToNearest({
      interval: 5000,
      value: Date.now() - startTime,
    });
    event['api-latency-ms'] = apiLatencyMs;
  }

  // There is a difference between the way that custom dimensions
  // and metrics are dealt with in UA (Universal Analytics) vs in
  // GA4. In UA, we push keys with dashes ('-') but in GA4 the object
  // keys must be delimited with ('_'). So we should just include
  // both versions for the applicable keys
  Object.keys(event).forEach(key => {
    if (key.includes('-')) {
      const newKey = key.replace(/-/g, '_');
      event[newKey] = event[key];
    }
  });

  recordEvent(event);
  if (event['error-key']) {
    recordEvent({
      'error-key': undefined,
    });
  }
};

export const getClaims = () => {
  return dispatch => {
    const startTimeMillis = Date.now();
    dispatch({ type: FETCH_CLAIMS_PENDING });

    return apiRequest('/benefits_claims')
      .then(res => {
        recordClaimsAPIEvent({
          startTime: startTimeMillis,
          success: true,
        });

        dispatch(fetchClaimsSuccess(res.data));
      })
      .catch(error => {
        const errorCode = getErrorStatus(error);
        if (errorCode && errorCode !== UNKNOWN_STATUS) {
          Sentry.withScope(scope => {
            scope.setFingerprint(['{{default}}', errorCode]);
            Sentry.captureException(
              `lighthouse_claims_err_get_claims ${errorCode}`,
            );
          });
        }

        // This onError callback will be called with a null response arg when
        // the API takes too long to return data
        const errorDetail =
          error === null ? '504 Timed out - API took too long' : errorCode;
        recordClaimsAPIEvent({
          startTime: startTimeMillis,
          success: false,
          error: errorDetail,
        });

        return dispatch({ type: FETCH_CLAIMS_ERROR });
      });
  };
};

export const getClaim = (id, navigate) => {
  return dispatch => {
    dispatch({ type: GET_CLAIM_DETAIL });

    return apiRequest(`/benefits_claims/${id}`)
      .then(res => {
        dispatch({
          type: SET_CLAIM_DETAIL,
          claim: res.data,
        });
      })
      .catch(error => {
        if (error.status !== 404 || !navigate) {
          return dispatch({
            type: SET_CLAIMS_UNAVAILABLE,
            error: error.message,
          });
        }

        return navigate('/your-claims', { replace: true });
      });
  };
};

export const clearClaim = () => ({ type: CLEAR_CLAIM_DETAIL });

export function submitRequest(id, cstClaimPhasesEnabled = false) {
  return dispatch => {
    dispatch({
      type: SUBMIT_DECISION_REQUEST,
    });

    if (canUseMocks()) {
      dispatch({ type: SET_DECISION_REQUESTED });
      dispatch(
        setNotification({
          title: 'Request received',
          body:
            'Thank you. We have your claim request and will make a decision.',
        }),
      );
      return Promise.resolve();
    }

    return makeAuthRequest(
      `/v0/evss_claims/${id}/request_decision`,
      { method: 'POST' },
      dispatch,
      () => {
        dispatch({ type: SET_DECISION_REQUESTED });
        if (cstClaimPhasesEnabled) {
          dispatch(
            setNotification({
              title: 'We received your evidence waiver',
              body:
                'Thank you. We’ll move your claim to the next step as soon as possible.',
            }),
          );
        } else {
          dispatch(
            setNotification({
              title: 'Request received',
              body:
                'Thank you. We have your claim request and will make a decision.',
            }),
          );
        }
      },
      error => {
        dispatch({ type: SET_DECISION_REQUEST_ERROR, error });
      },
    );
  };
}

export function submit5103(id, trackedItemId, cstClaimPhasesEnabled = false) {
  return dispatch => {
    dispatch({
      type: SUBMIT_DECISION_REQUEST,
    });

    const body = JSON.stringify({
      trackedItemId: Number(trackedItemId) || null,
    });

    makeAuthRequest(
      `/v0/benefits_claims/${id}/submit5103`,
      { method: 'POST', body },
      dispatch,
      () => {
        dispatch({ type: SET_DECISION_REQUESTED });
        if (cstClaimPhasesEnabled) {
          dispatch(
            setNotification({
              title: 'We received your evidence waiver',
              body:
                'Thank you. We’ll move your claim to the next step as soon as possible.',
            }),
          );
        } else {
          dispatch(
            setNotification({
              title: 'Request received',
              body:
                'Thank you. We have your claim request and will make a decision.',
            }),
          );
        }
      },
      error => {
        dispatch({ type: SET_DECISION_REQUEST_ERROR, error });
      },
    );
  };
}
// END lighthouse_migration

export function resetUploads() {
  return {
    type: RESET_UPLOADS,
  };
}

export function addFile(files, { isEncrypted = false } = {}) {
  return {
    type: ADD_FILE,
    files,
    isEncrypted,
  };
}

export function removeFile(index) {
  return {
    type: REMOVE_FILE,
    index,
  };
}

function calcProgress(totalFiles, totalSize, filesComplete, bytesComplete) {
  const ratio = 0.8;

  return (
    (filesComplete / totalFiles) * (1 - ratio) +
    (bytesComplete / totalSize) * ratio
  );
}

export function clearNotification() {
  return {
    type: CLEAR_NOTIFICATION,
  };
}

export function clearAdditionalEvidenceNotification() {
  return {
    type: CLEAR_ADDITIONAL_EVIDENCE_NOTIFICATION,
  };
}

// TODO: remove this function when Lighthouse feature toggle is removed
export function submitFiles(claimId, trackedItem, files) {
  let filesComplete = 0;
  let bytesComplete = 0;
  let hasError = false;
  const totalSize = files.reduce((sum, file) => sum + file.file.size, 0);
  const totalFiles = files.length;
  const trackedItemId = trackedItem ? trackedItem.id : null;

  recordEvent({
    event: 'claims-upload-start',
  });

  return dispatch => {
    dispatch(clearNotification());
    dispatch(clearAdditionalEvidenceNotification());
    dispatch({
      type: SET_UPLOADING,
      uploading: true,
    });
    dispatch({
      type: SET_PROGRESS,
      progress: 0,
    });
    require.ensure(
      [],
      require => {
        const csrfTokenStored = localStorage.getItem('csrfToken');
        const { FineUploaderBasic } = require('fine-uploader/lib/core');
        const uploader = new FineUploaderBasic({
          request: {
            endpoint: `${
              environment.API_URL
            }/v0/evss_claims/${claimId}/documents`,
            inputName: 'file',
            customHeaders: {
              'Source-App-Name': manifest.entryName,
              'X-Key-Inflection': 'camel',
              'X-CSRF-Token': csrfTokenStored,
            },
          },
          cors: {
            expected: true,
            sendCredentials: true,
          },
          multiple: false,
          callbacks: {
            onAllComplete: () => {
              if (canUseMocks()) {
                dispatch({ type: DONE_UPLOADING });
                dispatch(
                  setNotification({
                    title: 'We have your evidence',
                    body: (
                      <span>
                        Thank you for sending us{' '}
                        {trackedItem
                          ? trackedItem.displayName
                          : 'additional evidence'}
                        . We will associate it with your record in a matter of
                        days. If the submitted evidence impacts the status of
                        your claim, then you will see that change within 30 days
                        of submission.
                        <br />
                        Note: It may take a few minutes for your uploaded file
                        to show here. If you don’t see your file, please try
                        refreshing the page.
                      </span>
                    ),
                  }),
                );
                return;
              }

              if (!hasError) {
                recordEvent({
                  event: 'claims-upload-success',
                });
                dispatch({
                  type: DONE_UPLOADING,
                });
                dispatch(
                  setNotification({
                    title: 'We have your evidence',
                    body: (
                      <span>
                        Thank you for sending us{' '}
                        {trackedItem
                          ? trackedItem.displayName
                          : 'additional evidence'}
                        . We will associate it with your record in a matter of
                        days. If the submitted evidence impacts the status of
                        your claim, then you will see that change within 30 days
                        of submission.
                        <br />
                        Note: It may take a few minutes for your uploaded file
                        to show here. If you don’t see your file, please try
                        refreshing the page.
                      </span>
                    ),
                  }),
                );
              } else {
                recordEvent({
                  event: 'claims-upload-failure',
                });
                dispatch({
                  type: SET_UPLOAD_ERROR,
                });
                dispatch(
                  setAdditionalEvidenceNotification({
                    title: `Error uploading ${hasError?.fileName || 'files'}`,
                    body:
                      hasError?.errors?.[0]?.title ||
                      'There was an error uploading your files. Please try again',
                    type: 'error',
                  }),
                );
              }
            },
            onTotalProgress: bytes => {
              bytesComplete = bytes;
              dispatch({
                type: SET_PROGRESS,
                progress: calcProgress(
                  totalFiles,
                  totalSize,
                  filesComplete,
                  bytesComplete,
                ),
              });
            },
            onComplete: () => {
              filesComplete += 1;
              dispatch({
                type: SET_PROGRESS,
                progress: calcProgress(
                  totalFiles,
                  totalSize,
                  filesComplete,
                  bytesComplete,
                ),
              });
            },
            onError: (_id, fileName, _reason, { response, status }) => {
              if (status === 401) {
                dispatch({
                  type: SET_UNAUTHORIZED,
                });
              }
              if (status < 200 || status > 299) {
                hasError = JSON.parse(response || '{}');
                hasError.fileName = fileName;
              }
            },
          },
        });
        dispatch({
          type: SET_UPLOADER,
          uploader,
        });
        dispatch({
          type: SET_PROGRESS,
          progress: filesComplete / files.length,
        });

        /* eslint-disable camelcase */
        files.forEach(({ file, docType, password }) => {
          uploader.addFiles(file, {
            tracked_item_id: trackedItemId,
            document_type: docType.value,
            password: password.value,
          });
        });
        /* eslint-enable camelcase */
      },
      'claims-uploader',
    );
  };
}

// START lighthouse_migration
export function submitFilesLighthouse(claimId, trackedItem, files) {
  let filesComplete = 0;
  let bytesComplete = 0;
  let hasError = false;
  const totalSize = files.reduce((sum, file) => sum + file.file.size, 0);
  const totalFiles = files.length;
  const trackedItemId = trackedItem ? trackedItem.id : null;

  recordEvent({
    event: 'claims-upload-start',
  });

  return dispatch => {
    dispatch(clearNotification());
    dispatch(clearAdditionalEvidenceNotification());
    dispatch({
      type: SET_UPLOADING,
      uploading: true,
    });
    dispatch({
      type: SET_PROGRESS,
      progress: 0,
    });
    require.ensure(
      [],
      require => {
        const csrfTokenStored = localStorage.getItem('csrfToken');
        const { FineUploaderBasic } = require('fine-uploader/lib/core');
        const uploader = new FineUploaderBasic({
          request: {
            endpoint: `${
              environment.API_URL
            }/v0/benefits_claims/${claimId}/benefits_documents`,
            inputName: 'file',
            customHeaders: {
              'Source-App-Name': manifest.entryName,
              'X-Key-Inflection': 'camel',
              'X-CSRF-Token': csrfTokenStored,
            },
          },
          cors: {
            expected: true,
            sendCredentials: true,
          },
          multiple: false,
          callbacks: {
            onAllComplete: () => {
              const now = new Date(Date.now());
              const uploadDate = buildDateFormatter()(now.toISOString());
              if (!hasError) {
                recordEvent({
                  event: 'claims-upload-success',
                });
                dispatch({
                  type: DONE_UPLOADING,
                });
                dispatch(
                  setNotification({
                    title: `We received your file upload on ${uploadDate}`,
                    body: (
                      <span>
                        If your uploaded file doesn’t appear in the Documents
                        Filed section on this page, please try refreshing the
                        page.
                      </span>
                    ),
                  }),
                );
              } else {
                recordEvent({
                  event: 'claims-upload-failure',
                });
                dispatch({
                  type: SET_UPLOAD_ERROR,
                });
                dispatch(
                  setAdditionalEvidenceNotification({
                    title: `Error uploading ${hasError?.fileName || 'files'}`,
                    body:
                      hasError?.errors?.[0]?.title ||
                      'There was an error uploading your files. Please try again',
                    type: 'error',
                  }),
                );
              }
            },
            onTotalProgress: bytes => {
              bytesComplete = bytes;
              dispatch({
                type: SET_PROGRESS,
                progress: calcProgress(
                  totalFiles,
                  totalSize,
                  filesComplete,
                  bytesComplete,
                ),
              });
            },
            onComplete: () => {
              filesComplete += 1;
              dispatch({
                type: SET_PROGRESS,
                progress: calcProgress(
                  totalFiles,
                  totalSize,
                  filesComplete,
                  bytesComplete,
                ),
              });
            },
            onError: (_id, fileName, _reason, { response, status }) => {
              if (status === 401) {
                dispatch({
                  type: SET_UNAUTHORIZED,
                });
              }
              if (status < 200 || status > 299) {
                hasError = JSON.parse(response || '{}');
                hasError.fileName = fileName;
              }
            },
          },
        });
        dispatch({
          type: SET_UPLOADER,
          uploader,
        });
        dispatch({
          type: SET_PROGRESS,
          progress: filesComplete / files.length,
        });

        /* eslint-disable camelcase */
        files.forEach(({ file, docType, password }) => {
          uploader.addFiles(file, {
            tracked_item_ids: JSON.stringify([trackedItemId]),
            document_type: docType.value,
            password: password.value,
          });
        });
        /* eslint-enable camelcase */
      },
      'claims-uploader',
    );
  };
}
// END lighthouse_migration

export function updateField(path, field) {
  return {
    type: UPDATE_FIELD,
    path,
    field,
  };
}

export function cancelUpload() {
  return (dispatch, getState) => {
    const { uploader } = getState().disability.status.uploads;
    recordEvent({
      event: 'claims-upload-cancel',
    });

    if (uploader) {
      uploader.cancelAll();
    }

    dispatch({
      type: CANCEL_UPLOAD,
    });
  };
}

export function setFieldsDirty() {
  return {
    type: SET_FIELDS_DIRTY,
  };
}

export function setLastPage(page) {
  return {
    type: SET_LAST_PAGE,
    page,
  };
}

// Add some attributes to the STEM claim to help normalize it's shape
const addAttributes = claim => ({
  ...claim,
  attributes: {
    ...claim.attributes,
    claimType: 'STEM',
    phaseChangeDate: claim.attributes.submittedAt,
  },
});

// We don't want to show STEM claims unless they were automatically denied
const automatedDenial = stemClaim => stemClaim.attributes.automatedDenial;

const getStemClaimsMock = dispatch => {
  return mockApi.getStemClaimList().then(res => {
    const stemClaims = res.data.map(addAttributes).filter(automatedDenial);

    return dispatch({
      type: FETCH_STEM_CLAIMS_SUCCESS,
      stemClaims,
    });
  });
};

export function getStemClaims() {
  return dispatch => {
    dispatch({ type: FETCH_STEM_CLAIMS_PENDING });

    if (canUseMocks()) {
      return getStemClaimsMock(dispatch);
    }

    return makeAuthRequest(
      '/v0/education_benefits_claims/stem_claim_status',
      null,
      dispatch,
      res => {
        const stemClaims = res.data.map(addAttributes).filter(automatedDenial);
        dispatch({
          type: FETCH_STEM_CLAIMS_SUCCESS,
          stemClaims,
        });
      },
      () => dispatch({ type: FETCH_STEM_CLAIMS_ERROR }),
    );
  };
}