Repository: department-of-veterans-affairs/va-mobile-app
Pull request #10481: Add travel pay entry point
Author: login: juancstlm-a6
Created: 2025-03-07T03:05:54Z | Merged: 2025-03-18T20:26:19Z

## Description of Change
<!-- Please include a description of the change and context. What would a code reviewer, or a future dev, 
need to know about this PR in order to understand why this PR was created? This could include dependencies 
introduced, changes in behavior, pointers to more detailed documentation. The description should be more 
than a link to an issue.  -->

- Add the entry point to the travel pay SMOC flow using the same logic as vets website uses to determine if an appointment is eligible for travel pay. 
- Adds a line on the **Past** appointments list for any appointments that match the criteria.
- Adds a `AppointmentFileTravelPayAlert` component that renders only when the appointment matches the travel pay criteria

## Screenshots/Video
<details><summary>Before/after: </summary>

| Before | After | 
| --- | --- |
|<img width="514" alt="Screenshot 2025-03-07 at 9 21 58 AM" src="https://github.com/user-attachments/assets/57a4ac73-6897-4556-bc2d-e84f104279f9" /><img width="514" alt="Screenshot 2025-03-07 at 9 22 08 AM" src="https://github.com/user-attachments/assets/22434be4-b8c8-4218-a675-fa65adc10bbb" />|<img width="514" alt="Screenshot 2025-03-07 at 9 09 37 AM" src="https://github.com/user-attachments/assets/c6348538-47e9-4cd8-b2e4-436e4755bb57" /><img width="514" alt="Screenshot 2025-03-07 at 9 09 32 AM" src="https://github.com/user-attachments/assets/c2930231-83ea-4e39-a0de-a0bd84ed55b4" />|
|<img width="519" alt="Screenshot 2025-03-07 at 9 28 57 AM" src="https://github.com/user-attachments/assets/ef74b0bb-6642-45ef-b203-4e21c175bb81" /><img width="519" alt="Screenshot 2025-03-07 at 9 28 51 AM" src="https://github.com/user-attachments/assets/cff16f29-1136-4061-92ec-e88ec8e73a0c" />|<img width="519" alt="Screenshot 2025-03-07 at 9 27 03 AM" src="https://github.com/user-attachments/assets/708e766e-45a7-4e2a-9217-ddb1cdeb3f01" /><img width="519" alt="Screenshot 2025-03-07 at 9 27 08 AM" src="https://github.com/user-attachments/assets/c534dba2-657c-4096-851b-7413fa639c46" />|
</details>

## Testing
<!-- What testing was done to verify the changes (local/unit)? What testing remains? Note edge cases, or special
situations that could not be tested during development. -->

- [x] Tested on iOS <!-- simulator is fine -->
- [x] Tested on Android <!-- simulator is fine -->

## Reviewer Validations
<!-- What should reviewers look for? Copy/paste Acceptance Criteria from ticket -->

## PR Checklist
<!-- Engineer: make sure all these items are checked off before requesting a review -->
  **Reviewer:** Confirm the items below as you review

- [x] PR is connected to issue(s)
- [x] Tests are included to cover this change (when possible)
- [x] No magic strings (All string unions follow the [Union -> Constant](https://github.com/department-of-veterans-affairs/va-mobile-app/blob/develop/VAMobile/src/constants/common.ts) type pattern)
- [x] No secrets or API keys are checked in
- [x] All imports are absolute (no relative imports)
- [x] New functions and Redux work have proper TSDoc annotations

## For QA

[Run a build for this branch](https://github.com/department-of-veterans-affairs/va-mobile-app/actions/workflows/on_demand_build.yml)



Diff:
diff --git a/VAMobile/src/api/types/AppointmentData.ts b/VAMobile/src/api/types/AppointmentData.ts
index a69a813f7f4..8f191a10a15 100644
--- a/VAMobile/src/api/types/AppointmentData.ts
+++ b/VAMobile/src/api/types/AppointmentData.ts
@@ -105,6 +105,21 @@ export type AppointmentLocation = {
   code?: string
 }
 
+export type TravelPayClaim = {
+  id: string
+  claimNumber: string
+  claimStatus: string
+  appointmentDateTime: string
+  facilityName: string
+  createdOn: string
+  modifiedOn: string
+}
+
+export type AppointmentTravelPayClaim = {
+  metadata: { status: number; success: boolean; message: string }
+  claim?: TravelPayClaim
+}
+
 export type AppointmentTimeZone =
   | 'Pacific/Honolulu'
   | 'America/Anchorage'
@@ -176,6 +191,7 @@ export type AppointmentAttributes = {
   bestTimeToCall?: Array<string>
   vetextId: string
   physicalLocation?: string
+  travelPayClaim?: AppointmentTravelPayClaim
 }
 
 export type AppointmentData = {
diff --git a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/InPersonVAAppointment.tsx b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/InPersonVAAppointment.tsx
index 22192cbad1d..f6d8f3bb42a 100644
--- a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/InPersonVAAppointment.tsx
+++ b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/InPersonVAAppointment.tsx
@@ -1,16 +1,10 @@
 import React from 'react'
 
-import { Button } from '@department-of-veterans-affairs/mobile-component-library'
 import { UseMutateFunction } from '@tanstack/react-query'
 
 import { AppointmentAttributes } from 'api/types'
 import { Box, TextArea } from 'components'
-import {
-  AppointmentDetailsSubType,
-  AppointmentDetailsSubTypeConstants,
-  AppointmentDetailsTypeConstants,
-} from 'utils/appointments'
-import { useRouteNavigation } from 'utils/hooks'
+import { AppointmentDetailsSubType, AppointmentDetailsTypeConstants } from 'utils/appointments'
 import { featureEnabled } from 'utils/remoteConfig'
 
 import {
@@ -44,22 +38,12 @@ function InPersonVAAppointment({
   cancelAppointment,
 }: InPersonVAAppointmentProps) {
   const type = AppointmentDetailsTypeConstants.InPersonVA
-  const navigateTo = useRouteNavigation()
 
   return (
     <Box>
       <TextArea>
         <AppointmentDetailsModality attributes={attributes} subType={subType} type={type} />
         <AppointmentDateAndTime attributes={attributes} subType={subType} />
-        {subType === AppointmentDetailsSubTypeConstants.Past && (
-          <Box>
-            <Button
-              onPress={() => navigateTo('SubmitTravelPayClaimScreen', { appointmentDateTime: attributes.startDateUtc })}
-              label="Submit travel claim"
-              a11yLabel="Submit travel claim"
-            />
-          </Box>
-        )}
         {featureEnabled('useOldLinkComponent') ? (
           <DEPRECATED_AppointmentCalendarButton
             appointmentID={appointmentID}
diff --git a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert.test.tsx b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert.test.tsx
new file mode 100644
index 00000000000..090b732b6aa
--- /dev/null
+++ b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert.test.tsx
@@ -0,0 +1,294 @@
+import React from 'react'
+
+import { t } from 'i18next'
+import { DateTime } from 'luxon'
+
+import {
+  AppointmentAttributes,
+  AppointmentStatus,
+  AppointmentStatusConstants,
+  AppointmentTravelPayClaim,
+  AppointmentType,
+  AppointmentTypeConstants,
+} from 'api/types'
+import AppointmentFileTravelPayAlert from 'screens/HealthScreen/Appointments/AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert'
+import { context, fireEvent, render, screen } from 'testUtils'
+
+const mockNavigationSpy = jest.fn()
+jest.mock('utils/hooks', () => {
+  const original = jest.requireActual('utils/hooks')
+  return {
+    ...original,
+    useRouteNavigation: () => mockNavigationSpy,
+  }
+})
+
+const baseAppointmentAttributes: AppointmentAttributes = {
+  appointmentType: AppointmentTypeConstants.VA,
+  status: AppointmentStatusConstants.BOOKED,
+  bestTimeToCall: undefined, //pending appointments
+  cancelId: '12',
+  comment: 'Please arrive 20 minutes before the start of your appointment',
+  friendlyLocationName: 'Johnson Clinic suite 100',
+  healthcareProvider: 'Larry Bird',
+  location: {
+    name: 'VA Long Beach Healthcare System',
+    address: {
+      street: '5901 East 7th Street',
+      city: 'Long Beach',
+      state: 'CA',
+      zipCode: '90822',
+    },
+    phone: {
+      areaCode: '123',
+      number: '456-7890',
+      extension: '',
+    },
+    url: '',
+    code: '123 code',
+  },
+  minutesDuration: 60,
+  patientEmail: undefined, // pending appointments
+  patientPhoneNumber: undefined, // pending appointments
+  physicalLocation: '123 San Jacinto Ave, San Jacinto, CA 92583',
+  proposedTimes: undefined, // pending appointments
+  reason: 'Running a Fever',
+  startDateUtc: '2021-02-06T19:53:14.000+00:00',
+  statusDetail: null, // canceled appointments
+  timeZone: 'America/Los_Angeles',
+  typeOfCare: 'General check up',
+  startDateLocal: '2021-02-06T18:53:14.000-01:00',
+  healthcareService: undefined,
+  serviceCategoryName: null,
+  practitioner: undefined,
+  phoneOnly: false,
+  isCovidVaccine: false,
+  isPending: false,
+  vetextId: '600;3210206',
+}
+
+type createProps = {
+  startDateUtc?: AppointmentAttributes['startDateUtc']
+  status: AppointmentStatus
+  travelPayClaim?: AppointmentTravelPayClaim
+  isPending: AppointmentAttributes['isPending']
+  appointmentType: AppointmentType
+  phoneOnly: AppointmentAttributes['phoneOnly']
+}
+
+const travelPayClaimData: AppointmentTravelPayClaim = {
+  metadata: { status: 200, message: 'Data retrieved successfully.', success: true },
+}
+
+const mockStartDateUtc = DateTime.utc().toISO()
+
+const createTestAppointmentAttributes = ({
+  startDateUtc = mockStartDateUtc,
+  travelPayClaim,
+  ...rest
+}: createProps): AppointmentAttributes => {
+  const { timeZone } = baseAppointmentAttributes
+  // Convert the UTC date to the local date
+  const startDateLocal = new Date(startDateUtc).toLocaleString('en-US', { timeZone })
+  return { ...baseAppointmentAttributes, ...rest, startDateUtc, startDateLocal, travelPayClaim }
+}
+
+const claimExamAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.VA,
+  isPending: false,
+  phoneOnly: false,
+  travelPayClaim: travelPayClaimData,
+})
+
+const communityCareAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.COMMUNITY_CARE,
+  isPending: false,
+  phoneOnly: false,
+  travelPayClaim: travelPayClaimData,
+})
+
+const inPersonVAAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.VA,
+  isPending: false,
+  phoneOnly: false,
+  travelPayClaim: travelPayClaimData,
+})
+
+const phoneAppointmentAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.VA,
+  isPending: false,
+  phoneOnly: true,
+  travelPayClaim: travelPayClaimData,
+})
+
+const videoAtlasAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.VA_VIDEO_CONNECT_ATLAS,
+  isPending: false,
+  phoneOnly: false,
+  travelPayClaim: travelPayClaimData,
+})
+
+const videoGFEAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.VA_VIDEO_CONNECT_GFE,
+  isPending: false,
+  phoneOnly: false,
+  travelPayClaim: travelPayClaimData,
+})
+
+const videoHomeAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.VA_VIDEO_CONNECT_HOME,
+  isPending: false,
+  phoneOnly: false,
+  travelPayClaim: travelPayClaimData,
+})
+
+const videoOnsiteAttributes = createTestAppointmentAttributes({
+  status: AppointmentStatusConstants.BOOKED,
+  appointmentType: AppointmentTypeConstants.VA_VIDEO_CONNECT_ONSITE,
+  isPending: false,
+  phoneOnly: false,
+  travelPayClaim: travelPayClaimData,
+})
+
+const tests = [
+  { attributes: claimExamAttributes, expectedResult: true, testName: 'Claim Exam' },
+  { attributes: communityCareAttributes, expectedResult: false, testName: 'Community Care' },
+  { attributes: inPersonVAAttributes, expectedResult: true, testName: 'In Person VA' },
+  { attributes: phoneAppointmentAttributes, expectedResult: false, testName: 'Phone' },
+  { attributes: videoAtlasAttributes, expectedResult: true, testName: 'Video Atlas' },
+  { attributes: videoGFEAttributes, expectedResult: false, testName: 'Video GFE' },
+  { attributes: videoHomeAttributes, expectedResult: false, testName: 'Video Home' },
+  { attributes: videoOnsiteAttributes, expectedResult: true, testName: 'Video On Site' },
+]
+
+context('AppointmentFileTravelPayAlert', () => {
+  const initializeTestInstance = (attributes: AppointmentAttributes) => {
+    render(<AppointmentFileTravelPayAlert attributes={attributes} />)
+  }
+
+  it('should initialize correctly', async () => {
+    initializeTestInstance(inPersonVAAttributes)
+    expect(screen.getByText(t('travelPay.fileClaimAlert.header'))).toBeTruthy()
+    expect(screen.getByText(t('travelPay.fileClaimAlert.description', { count: 29, days: 29 }))).toBeTruthy()
+    expect(screen.getByText(t('travelPay.fileClaimAlert.button'))).toBeTruthy()
+  })
+
+  it('should NOT render if no more days left to file', async () => {
+    const attributes = createTestAppointmentAttributes({
+      status: AppointmentStatusConstants.BOOKED,
+      appointmentType: AppointmentTypeConstants.VA,
+      startDateUtc: DateTime.utc().minus({ days: 45 }).toISO(),
+      isPending: false,
+      phoneOnly: false,
+      travelPayClaim: travelPayClaimData,
+    })
+    initializeTestInstance(attributes)
+    expect(screen.queryByTestId('appointmentFileTravelPayAlert')).toBeNull()
+  })
+
+  it('should NOT render if no travel pay claim', async () => {
+    const attributes = createTestAppointmentAttributes({
+      status: AppointmentStatusConstants.BOOKED,
+      appointmentType: AppointmentTypeConstants.VA,
+      travelPayClaim: undefined,
+      isPending: false,
+      phoneOnly: false,
+    })
+    initializeTestInstance(attributes)
+    expect(screen.queryByTestId('appointmentFileTravelPayAlert')).toBeNull()
+  })
+
+  it('should render if zero days left to file', async () => {
+    const attributes = createTestAppointmentAttributes({
+      status: AppointmentStatusConstants.BOOKED,
+      appointmentType: AppointmentTypeConstants.VA,
+      startDateUtc: DateTime.now().minus({ days: 30 }).toUTC().toISO(),
+      isPending: false,
+      phoneOnly: false,
+      travelPayClaim: travelPayClaimData,
+    })
+    initializeTestInstance(attributes)
+    expect(screen.getByText(t('travelPay.fileClaimAlert.description', { count: 0, days: 0 }))).toBeTruthy()
+    expect(screen.getByTestId('appointmentFileTravelPayAlert')).toBeTruthy()
+  })
+
+  it('should NOT render if appointment is canceled', async () => {
+    const attributes = createTestAppointmentAttributes({
+      status: AppointmentStatusConstants.CANCELLED,
+      appointmentType: AppointmentTypeConstants.VA,
+      isPending: false,
+      phoneOnly: false,
+      travelPayClaim: travelPayClaimData,
+    })
+    initializeTestInstance(attributes)
+    expect(screen.queryByTestId('appointmentFileTravelPayAlert')).toBeNull()
+  })
+
+  it('should NOT render if appointment is pending', async () => {
+    const attributes = createTestAppointmentAttributes({
+      status: AppointmentStatusConstants.SUBMITTED,
+      appointmentType: AppointmentTypeConstants.VA,
+      isPending: true,
+      phoneOnly: false,
+      travelPayClaim: travelPayClaimData,
+    })
+    initializeTestInstance(attributes)
+    expect(screen.queryByTestId('appointmentFileTravelPayAlert')).toBeNull()
+  })
+
+  tests.forEach(({ attributes, expectedResult, testName }) => {
+    it(`should ${expectedResult ? '' : 'NOT '}render for ${testName}`, async () => {
+      initializeTestInstance(attributes)
+      if (expectedResult) {
+        expect(screen.getByTestId('appointmentFileTravelPayAlert')).toBeTruthy()
+      } else {
+        expect(screen.queryByTestId('appointmentFileTravelPayAlert')).toBeNull()
+      }
+    })
+  })
+
+  it('should navigate to the travel pay flow when the "File claim" button is clicked', async () => {
+    const attributes = createTestAppointmentAttributes({
+      status: AppointmentStatusConstants.BOOKED,
+      appointmentType: AppointmentTypeConstants.VA,
+      isPending: false,
+      phoneOnly: false,
+      travelPayClaim: travelPayClaimData,
+    })
+    initializeTestInstance(attributes)
+    fireEvent(screen.getByText(t('travelPay.fileClaimAlert.button')), 'press')
+    expect(mockNavigationSpy).toHaveBeenCalledWith('SubmitTravelPayClaimScreen', {
+      appointmentDateTime: attributes.startDateUtc,
+    })
+  })
+
+  it('should not render if a claim has already been filed', async () => {
+    const attributes = createTestAppointmentAttributes({
+      status: AppointmentStatusConstants.BOOKED,
+      appointmentType: AppointmentTypeConstants.VA,
+      isPending: false,
+      phoneOnly: false,
+      travelPayClaim: {
+        ...travelPayClaimData,
+        claim: {
+          id: '1234',
+          claimNumber: 'string',
+          claimStatus: 'In Process',
+          appointmentDateTime: '2024-01-01T16:45:34.465Z',
+          facilityName: 'Cheyenne VA Medical Center',
+          createdOn: '2024-03-22T21:22:34.465Z',
+          modifiedOn: '2024-01-01T16:44:34.465Z',
+        },
+      },
+    })
+    initializeTestInstance(attributes)
+    expect(screen.queryByTestId('appointmentFileTravelPayAlert')).toBeNull()
+  })
+})
diff --git a/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert.tsx b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert.tsx
new file mode 100644
index 00000000000..dc3201253ca
--- /dev/null
+++ b/VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+
+import { Alert } from '@department-of-veterans-affairs/mobile-component-library'
+
+import { AppointmentAttributes } from 'api/types'
+import { Box } from 'components'
+import { NAMESPACE } from 'constants/namespaces'
+import { getDaysLeftToFileTravelPay, isEligibleForTravelPay } from 'utils/appointments'
+import { useRouteNavigation, useTheme } from 'utils/hooks'
+
+type AppointmentFileTravelPayAlertProps = {
+  attributes: AppointmentAttributes
+}
+
+function AppointmentFileTravelPayAlert({ attributes }: AppointmentFileTravelPayAlertProps) {
+  const { t } = useTranslation(NAMESPACE.COMMON)
+  const theme = useTheme()
+  const navigateTo = useRouteNavigation()
+
+  const eligibleForTravelPay = isEligibleForTravelPay(attributes)
+  const daysLeftToFile = getDaysLeftToFileTravelPay(attributes.startDateUtc)
+
+  if (!eligibleForTravelPay || daysLeftToFile < 0) {
+    return null
+  }
+
+  return (
+    <Box mb={theme.dimensions.standardMarginBetween}>
+      <Alert
+        variant="info"
+        description={t('travelPay.fileClaimAlert.description', { count: daysLeftToFile, days: daysLeftToFile })}
+        header={t('travelPay.fileClaimAlert.header')}
+        primaryButton={{
+          label: t('travelPay.fileClaimAlert.button'),
+          onPress: () => {
+            navigateTo('SubmitTravelPayClaimScreen', { appointmentDateTime: attributes.startDateUtc })
+          },
+        }}
+        testID="appointmentFileTravelPayAlert"
+      />
+    </Box>
+  )
+}
+
+export default AppointmentFileTravelPayAlert
diff --git a/VAMobile/src/screens/HealthScreen/Appointments/PastAppointments/PastAppointmentDetails.tsx b/VAMobile/src/screens/HealthScreen/Appointments/PastAppointments/PastAppointmentDetails.tsx
index 4873e2cc04f..f0759292d46 100644
--- a/VAMobile/src/screens/HealthScreen/Appointments/PastAppointments/PastAppointmentDetails.tsx
+++ b/VAMobile/src/screens/HealthScreen/Appointments/PastAppointments/PastAppointmentDetails.tsx
@@ -27,6 +27,7 @@ import {
   VideoHomeAppointment,
   VideoVAAppointment,
 } from '../AppointmentTypeComponents'
+import AppointmentFileTravelPayAlert from '../AppointmentTypeComponents/SharedComponents/AppointmentFileTravelPayAlert'
 
 type PastAppointmentDetailsProps = StackScreenProps<HealthStackParamList, 'PastAppointmentDetails'>
 
@@ -81,6 +82,7 @@ function PastAppointmentDetails({ route, navigation }: PastAppointmentDetailsPro
       backLabel={t('appointments')}
       backLabelOnPress={navigation.goBack}
       title={t('details')}>
+      <AppointmentFileTravelPayAlert attributes={attributes} />
       {isPhoneAppointment ? (
         <PhoneAppointment
           appointmentID={appointment.id}
diff --git a/VAMobile/src/store/api/demo/mocks/appointments.json b/VAMobile/src/store/api/demo/mocks/appointments.json
index 203ff325133..adc5796cd90 100644
--- a/VAMobile/src/store/api/demo/mocks/appointments.json
+++ b/VAMobile/src/store/api/demo/mocks/appointments.json
@@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -95,7 +102,14 @@
             "patientEmail": "christopher.ray3@va.gov",
             "bestTimeToCall": ["Afternoon", "Evening"],
             "friendlyLocationName": "Denver VA Medical Center",
-            "serviceCategoryName": null
+            "serviceCategoryName": null,
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -145,7 +159,14 @@
             "patientEmail": "Judy.morrison@id.me",
             "bestTimeToCall": ["Evening"],
             "friendlyLocationName": null,
-            "serviceCategoryName": null
+            "serviceCategoryName": null,
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -187,7 +208,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "Data retrieved successfully.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -229,7 +257,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -278,7 +313,14 @@
             ],
             "isPending": true,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
 
@@ -321,7 +363,14 @@
             "reason": "Reason test",
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "Data retrieved successfully.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -363,7 +412,14 @@
             "reason": "Reason test",
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -415,7 +471,14 @@
             ],
             "patientPhoneNumber": "413-757-5329",
             "patientEmail": "Judy.morrison@id.me",
-            "bestTimeToCall": ["Afternoon"]
+            "bestTimeToCall": ["Afternoon"],
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -457,7 +520,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -499,7 +569,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -551,7 +628,14 @@
             ],
             "patientPhoneNumber": "413-757-5329",
             "patientEmail": "Judy.morrison@id.me",
-            "bestTimeToCall": ["Afternoon"]
+            "bestTimeToCall": ["Afternoon"],
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
 
@@ -594,7 +678,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": null
+            "typeOfCare": null,
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -636,7 +727,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -688,7 +786,14 @@
             ],
             "patientPhoneNumber": "413-757-5329",
             "patientEmail": "Judy.morrison@id.me",
-            "bestTimeToCall": ["Afternoon"]
+            "bestTimeToCall": ["Afternoon"],
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -732,7 +837,14 @@
             "isCovidVaccine": false,
             "serviceCategoryName": "COMPENSATION & PENSION",
             "friendlyLocationName": "Fort Collins VA Clinic",
-            "typeOfCare": "Audiology and speech (including hearing aid support)"
+            "typeOfCare": "Audiology and speech (including hearing aid support)",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -776,7 +888,14 @@
             "isCovidVaccine": false,
             "serviceCategoryName": "COMPENSATION & PENSION",
             "friendlyLocationName": "Fort Collins VA Clinic",
-            "typeOfCare": "Audiology and speech (including hearing aid support)"
+            "typeOfCare": "Audiology and speech (including hearing aid support)",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "Data retrieved successfully.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -836,7 +955,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "Data retrieved successfully.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -896,7 +1022,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -956,7 +1089,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1003,7 +1143,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1050,7 +1197,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1097,7 +1251,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1144,7 +1305,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1191,7 +1359,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1238,7 +1413,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1285,7 +1467,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1332,7 +1521,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1379,7 +1575,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1426,7 +1629,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1473,7 +1683,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1520,7 +1737,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1567,7 +1791,14 @@
             "patientPhoneNumber": "(666) 666-6666",
             "patientEmail": "Vilasini.reddy@va.gov",
             "bestTimeToCall": ["Morning"],
-            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center"
+            "friendlyLocationName": "DAYTSHR - Dayton VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1614,7 +1845,14 @@
             "patientPhoneNumber": null,
             "patientEmail": null,
             "bestTimeToCall": null,
-            "friendlyLocationName": "CHYSHR-Cheyenne VA Medical Center"
+            "friendlyLocationName": "CHYSHR-Cheyenne VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "Data retrieved successfully.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1661,7 +1899,14 @@
             "patientPhoneNumber": null,
             "patientEmail": null,
             "bestTimeToCall": null,
-            "friendlyLocationName": "CHYSHR-Cheyenne VA Medical Center"
+            "friendlyLocationName": "CHYSHR-Cheyenne VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         },
         {
@@ -1713,7 +1958,14 @@
             "patientPhoneNumber": "(703) 652-0000",
             "patientEmail": "samatha.girla@va.gov",
             "bestTimeToCall": ["Afternoon", "Evening", "Morning"],
-            "friendlyLocationName": "CHYSHR-Cheyenne VA Medical Center"
+            "friendlyLocationName": "CHYSHR-Cheyenne VA Medical Center",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
+            }
           }
         }
       ],
diff --git a/VAMobile/src/translations/en/common.json b/VAMobile/src/translations/en/common.json
index ac372cd16a3..f081f8600e0 100644
--- a/VAMobile/src/translations/en/common.json
+++ b/VAMobile/src/translations/en/common.json
@@ -1413,6 +1413,42 @@
   "termsAndConditions.toAccept.a11yLabel": "To accept the current terms and conditions, please go to the My Healthy Vet website:  ",
   "text.raw": "{{text}}",
   "thisFacilityWillContactYou": "This facility will contact you if we need more information about your request.",
+  "travelPay.fileClaimAlert.header": "File a mileage-only travel claim",
+  "travelPay.fileClaimAlert.description": "You have {{days}} day to file a travel claim for this appointment.",
+  "travelPay.fileClaimAlert.description_plural": "You have {{days}} days to file a travel claim for this appointment.",
+  "travelPay.fileClaimAlert.button": "File travel claim",
+  "travelPay.cancelClaim.title": "Cancel travel claim?",
+  "travelPay.cancelClaim.cancel": "Cancel claim",
+  "travelPay.cancelClaim.continue": "Continue claim",
+  "travelPay.daysToFile": "{{days}} day to file travel claim",
+  "travelPay.daysToFile_plural": "{{days}} days to file travel claim",
+  "travelPay.mileageQuestion": "Are you claiming only mileage?",
+  "travelPay.mileageQualifier": "If you need to submit receipts for other expenses, like tolls, meals, or lodging, you can't file a claim using this app.",
+  "travelPay.vehicleQuestion": "Did you travel in your own vehicle?",
+  "travelPay.vehicleQualifier": "If you traveled by bus, train, taxi, or other authorized public transportation, you can't file a claim using this app.",
+  "travelPay.addressQuestion": "Did you travel from your home address?",
+  "travelPay.addressQualifier": "If you traveled from a different address, you can't file a claim using this app.",
+  "travelPay.addressPOBox": "You can't file a claim if your home address is a Post Office box.",
+  "travelPay.noAddressText": "We don't have a home address on file for you and can't file a claim in the app.",
+  "travelPay.reviewTitle": "Review your travel claim",
+  "travelPay.reviewText": "Confirm the information is correct before you submit your claim.",
+  "travelPay.cannotSubmitThisType": "We can't file this type of claim in the app",
+  "travelPay.referToPortal": "But you can file a claim using the Beneficiary Travel Self Service System (BTSSS) or VA Form 10-3542.",
+  "travelPay.youCanStillFile": "You can still file a claim within 30 days of this appointment these other ways:",
+  "travelPay.youCanStillFile.bulletOne": "Online 24/7 through the Beneficiary Travel Self Service System (BTSSS), or",
+  "travelPay.youCanStillFile.bulletTwo": "VA Form 10-3542 by mail, fax, email, or in person.",
+  "travelPay.helpTitle": "Need help?",
+  "travelPay.helpText": "Call the BTSSS call center. We're here Monday through Friday, 8:00 a.m. to 8:00 p.m. ET.",
+  "travelPay.helpTextTwo": "Or you can call your VA health facility's Beneficiary Travel contact.",
+  "travelPay.phone": "855-574-8292",
+  "travelPay.phone.a11yHint": "8555748292",
+  "travelPay.error.title": "We couldn't file your claim",
+  "travelPay.error.text": "We're sorry. Something went wrong on our end. We couldn't file your travel reimbursement claim right now.",
+  "travelPay.success.title": "We're processing your claim",
+  "travelPay.success.text": "Your travel reimbursement claim number is {{claimId}}",
+  "travelPay.success.nextTitle": "What happens next?",
+  "travelPay.success.nextText": "You can check the status of your claim by going to the appointment in the app.",
+  "travelPay.success.nextText2": "If your claim is approved, we'll deposit your reimbursement through electronic funds transfer (EFT), unless we've made other payment arrangements with you.",
   "tryAgain": "Try again",
   "upcoming": "upcoming",
   "upcomingAppointmentDetails.addToCalendarA11yHint": "Add this appointment to your device's calendar",
diff --git a/VAMobile/src/utils/appointments.tsx b/VAMobile/src/utils/appointments.tsx
index 31f53a9576a..6b8ad605338 100644
--- a/VAMobile/src/utils/appointments.tsx
+++ b/VAMobile/src/utils/appointments.tsx
@@ -300,6 +300,90 @@ const getListItemsForAppointments = (
   return listItems
 }
 
+const isClinicVideoAppointment = (attributes: AppointmentAttributes) => {
+  const { appointmentType } = attributes
+  return (
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ONSITE ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ATLAS
+  )
+}
+
+/**
+ * Determines if an appointment is community care.
+ *
+ * @param attributes - type AppointmentAttributes, data attrubutes of an appointment.
+ * @returns boolean - true if the appointment is community care appointment.
+ */
+const getIsCommunityCare = (attributes: AppointmentAttributes) => {
+  return attributes.appointmentType === AppointmentTypeConstants.COMMUNITY_CARE
+}
+
+/**
+ * Determines if an appointment is phone only.
+ *
+ * @param attributes - type AppointmentAttributes, data attrubutes of an appointment.
+ * @returns boolean - true if the appointment is phone only.
+ */
+const getIsPhoneOnly = (attributes: AppointmentAttributes) => {
+  return attributes.phoneOnly
+}
+
+/**
+ * Determines if an appointment is a video appointment.
+ *
+ * @param attributes - type AppointmentAttributes, data attrubutes of an appointment.
+ * @returns boolean - true if the appointment is a video appointment.
+ */
+const getIsVideo = (attributes: AppointmentAttributes) => {
+  const { appointmentType } = attributes
+  return (
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ATLAS ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ONSITE ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_GFE ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_HOME
+  )
+}
+
+/**
+ * Returns true or false if the appointment is eligible for travel pay
+ * @param attributes - type AppointmentAttributes, data attributes of an appointment
+ * @returns boolean, true if the appointment is eligible for travel pay
+ */
+export const isEligibleForTravelPay = (attributes: AppointmentAttributes) => {
+  const { status, travelPayClaim } = attributes
+  const isPast = !isAPendingAppointment(attributes)
+  const isInPerson = !getIsVideo(attributes) && !getIsCommunityCare(attributes) && !getIsPhoneOnly(attributes)
+  const isClinicVideo = isClinicVideoAppointment(attributes)
+  const isBooked = status === AppointmentStatusConstants.BOOKED
+  // if the claim data is not successful or the claim has already been filed, then the appointment is not eligible for travel pay
+  const hasNoClaim = !!travelPayClaim?.metadata.success && !travelPayClaim?.claim
+  return isPast && isBooked && (isInPerson || isClinicVideo) && hasNoClaim
+}
+
+/**
+ * Returns the number of days left to file travel pay
+ * @param startDateUtc - string, the start date of the appointment in UTC
+ * @returns number, the number of days left to file travel pay
+ */
+export const getDaysLeftToFileTravelPay = (startDateUtc: string) => {
+  const daysToFile = 30 // 30 days to file travel pay
+  const lastFileDate = DateTime.fromISO(startDateUtc).plus({ days: daysToFile })
+  return Math.floor(lastFileDate.diff(DateTime.now().toUTC(), 'days').days)
+}
+
+const getTravelPay = (attributes: AppointmentAttributes, t: TFunction, mb: number) => {
+  const daysLeftToFile = getDaysLeftToFileTravelPay(attributes.startDateUtc)
+  if (isEligibleForTravelPay(attributes) && daysLeftToFile >= 0) {
+    return {
+      text: t('travelPay.daysToFile', { count: daysLeftToFile, days: daysLeftToFile }),
+      textTag: { labelType: LabelTagTypeConstants.tagBlue },
+      mb,
+    }
+  } else {
+    return undefined
+  }
+}
+
 const getStatus = (
   isPending: boolean,
   status: AppointmentStatus,
@@ -407,6 +491,7 @@ export const getTextLinesForAppointmentListItem = (
     result = [
       getDate(startDateUtc, timeZone),
       getTime(startDateUtc, timeZone, tinyMarginBetween),
+      getTravelPay(attributes, t, condensedMarginBetween),
       getStatus(isPending, attributes.status, t, condensedMarginBetween),
       getTextLine(careText, tinyMarginBetween),
       getTextLine(healthcareProvider, tinyMarginBetween),


Reviews:
user: login: liztownd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:52:29Z
user: login: liztownd
body: Should probably have a mobile-specific person's input, but from a travel pay perspective it's looking good!

One thing to note is passing the include flag to the API call to retrieve the appts. I haven't spent much time looking at the Mobile module for appts, so can't confirm at this time, but we'll need to pass the `travel_pay_claim` in the `include` list to make sure the claims are appended when we get the appts.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:04:18Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T19:34:22Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T19:42:49Z
user: login: liztownd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T12:48:28Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:21:47Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:25:24Z
user: login: dumathane
body: I think this all looks pretty great, left a few notes I'd just like a little clarification on. Marking request changes until I hear back but then will approve.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:31:36Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:34:22Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:36:13Z
user: login: dumathane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:41:53Z
user: login: dumathane
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:42:15Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:45:41Z
user: login: juancstlm-a6
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:46:32Z
user: login: dumathane
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T19:44:56Z

Review comments:
diff_hunk: @@ -187,7 +187,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: liztownd
body: I know we really only care whether or not there's a travel claim appended to certain types of appointments, but for the record, the travelPayClaim will be appended to every appointment, regardless of type of care.
created_at: 2025-03-11T16:52:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -300,6 +300,90 @@ const getListItemsForAppointments = (
   return listItems
 }
 
+const isClinicVideoAppointment = (attributes: AppointmentAttributes) => {
+  const { appointmentType } = attributes
+  return (
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ONSITE ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ATLAS
+  )
+}
+
+/**
+ * Determines if an appointment is community care.
+ *
+ * @param attributes - type AppointmentAttributes, data attrubutes of an appointment.
+ * @returns boolean - true if the appointment is community care appointment.
+ */
+const getIsCommunityCare = (attributes: AppointmentAttributes) => {
+  return attributes.appointmentType === AppointmentTypeConstants.COMMUNITY_CARE
+}
+
+/**
+ * Determines if an appointment is phone only.
+ *
+ * @param attributes - type AppointmentAttributes, data attrubutes of an appointment.
+ * @returns boolean - true if the appointment is phone only.
+ */
+const getIsPhoneOnly = (attributes: AppointmentAttributes) => {
+  return attributes.phoneOnly
+}
+
+/**
+ * Determines if an appointment is a video appointment.
+ *
+ * @param attributes - type AppointmentAttributes, data attrubutes of an appointment.
+ * @returns boolean - true if the appointment is a video appointment.
+ */
+const getIsVideo = (attributes: AppointmentAttributes) => {
+  const { appointmentType } = attributes
+  return (
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ATLAS ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_ONSITE ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_GFE ||
+    appointmentType === AppointmentTypeConstants.VA_VIDEO_CONNECT_HOME
+  )
+}
+
+/**
+ * Returns true or false if the appointment is eligible for travel pay
+ * @param attributes - type AppointmentAttributes, data attributes of an appointment
+ * @returns boolean, true if the appointment is eligible for travel pay
+ */
+export const isEligibleForTravelPay = (attributes: AppointmentAttributes) => {
+  const { status, travelPayClaim } = attributes
+  const isPast = !isAPendingAppointment(attributes)
+  const isInPerson = !getIsVideo(attributes) && !getIsCommunityCare(attributes) && !getIsPhoneOnly(attributes)
+  const isClinicVideo = isClinicVideoAppointment(attributes)
+  const isBooked = status === AppointmentStatusConstants.BOOKED
+  // if the claim data is not successful or the claim has already been filed, then the appointment is not eligible for travel pay
+  const hasNoClaim = !!travelPayClaim?.metadata.success && !travelPayClaim?.claim
+  return isPast && isBooked && (isInPerson || isClinicVideo) && hasNoClaim
path: VAMobile/src/utils/appointments.tsx
user: login: liztownd
body: Nice! 
created_at: 2025-03-11T16:54:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +105,11 @@ export type AppointmentLocation = {
   code?: string
 }
 
+export type AppointmentTravelPayClaim = {
+  metadata: { status: number; success: boolean; message: string }
+  claim?: Record<string, unknown>
path: VAMobile/src/api/types/AppointmentData.ts
user: login: liztownd
body: Next step is to add claim details, so I wonder if it's worth it to go ahead and add a TravelPayClaim type as well and drop that in for this property? I can see an argument for either way at this point in time. 🤷‍♀️ 
created_at: 2025-03-11T16:57:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +105,11 @@ export type AppointmentLocation = {
   code?: string
 }
 
+export type AppointmentTravelPayClaim = {
+  metadata: { status: number; success: boolean; message: string }
+  claim?: Record<string, unknown>
path: VAMobile/src/api/types/AppointmentData.ts
user: login: juancstlm-a6
body: Went ahead and added it. 
created_at: 2025-03-12T19:34:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,7 +187,14 @@
             "reason": null,
             "isCovidVaccine": false,
             "serviceCategoryName": null,
-            "typeOfCare": "Mental Health"
+            "typeOfCare": "Mental Health",
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: juancstlm-a6
body: So in that case it makes sense to include it here on all the mock appointments? 
created_at: 2025-03-12T19:42:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +105,32 @@ export type AppointmentLocation = {
   code?: string
 }
 
+export type ClaimStatus =
+  | 'In Process'
path: VAMobile/src/api/types/AppointmentData.ts
user: login: liztownd
body: 2 things here:

1) these will be coming back from the API in `Sentence case` rather that `Title Case`

2) This honestly makes me nervous to make this a union type specifically calling out the existing statuses. If VTP adds a new status (which they have - they only recently added `On hold`) it will break the Mobile app when a new, undefined status comes through. Might be safer to leave this just a `string` type. (Not my favorite thing ever, but I'd rather be safe than sorry.)
created_at: 2025-03-13T12:46:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +105,32 @@ export type AppointmentLocation = {
   code?: string
 }
 
+export type ClaimStatus =
+  | 'In Process'
+  | 'Claim Submitted'
+  | 'Saved'
+  | 'Incomplete'
+  | 'Appealed'
+  | 'In Manual Review'
+  | 'Closed'
+  | 'On Hold'
+  | 'Denied'
+
+export type TravelPayClaim = {
+  id: string
+  claimNumber: string
+  claimStatus: 'In Process'
path: VAMobile/src/api/types/AppointmentData.ts
user: login: liztownd
body: This should be `ClaimStatus`.

But honestly, I'd rather keep it just a `string` type to future-proof against new statuses being added by VTP (see comment above).
created_at: 2025-03-13T12:47:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +105,32 @@ export type AppointmentLocation = {
   code?: string
 }
 
+export type ClaimStatus =
+  | 'In Process'
path: VAMobile/src/api/types/AppointmentData.ts
user: login: juancstlm-a6
body: Given that I think it makes sense to just leave it as a string type for now.
created_at: 2025-03-13T15:21:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +105,32 @@ export type AppointmentLocation = {
   code?: string
 }
 
+export type ClaimStatus =
+  | 'In Process'
+  | 'Claim Submitted'
+  | 'Saved'
+  | 'Incomplete'
+  | 'Appealed'
+  | 'In Manual Review'
+  | 'Closed'
+  | 'On Hold'
+  | 'Denied'
+
+export type TravelPayClaim = {
+  id: string
+  claimNumber: string
+  claimStatus: 'In Process'
path: VAMobile/src/api/types/AppointmentData.ts
user: login: juancstlm-a6
body: A good catch. Looks like I missed staging this change and it didn't make it through. Updated to just a string type for now
created_at: 2025-03-13T15:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,22 +38,12 @@ function InPersonVAAppointment({
   cancelAppointment,
 }: InPersonVAAppointmentProps) {
   const type = AppointmentDetailsTypeConstants.InPersonVA
-  const navigateTo = useRouteNavigation()
 
   return (
     <Box>
       <TextArea>
         <AppointmentDetailsModality attributes={attributes} subType={subType} type={type} />
         <AppointmentDateAndTime attributes={attributes} subType={subType} />
-        {subType === AppointmentDetailsSubTypeConstants.Past && (
-          <Box>
-            <Button
-              onPress={() => navigateTo('SubmitTravelPayClaimScreen', { appointmentDateTime: attributes.startDateUtc })}
-              label="Submit travel claim"
-              a11yLabel="Submit travel claim"
-            />
-          </Box>
-        )}
path: VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/InPersonVAAppointment.tsx
user: login: dumathane
body: Why all this removal?
created_at: 2025-03-18T17:01:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: dumathane
body: Aren't all of these missing the actual claim data?
created_at: 2025-03-18T17:05:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,22 +38,12 @@ function InPersonVAAppointment({
   cancelAppointment,
 }: InPersonVAAppointmentProps) {
   const type = AppointmentDetailsTypeConstants.InPersonVA
-  const navigateTo = useRouteNavigation()
 
   return (
     <Box>
       <TextArea>
         <AppointmentDetailsModality attributes={attributes} subType={subType} type={type} />
         <AppointmentDateAndTime attributes={attributes} subType={subType} />
-        {subType === AppointmentDetailsSubTypeConstants.Past && (
-          <Box>
-            <Button
-              onPress={() => navigateTo('SubmitTravelPayClaimScreen', { appointmentDateTime: attributes.startDateUtc })}
-              label="Submit travel claim"
-              a11yLabel="Submit travel claim"
-            />
-          </Box>
-        )}
path: VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/InPersonVAAppointment.tsx
user: login: dumathane
body: I think I get it now: Removing some older code and replacing this by not specifying the appt types specifically and instead doing it in the PastAppointmentDetails and relying a on a Nil return if they don't fit the criteria? Correct me if I'm wrong?
created_at: 2025-03-18T17:26:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: dumathane
body: Is this just a step in the right direction and the actual full claim detail is to come next?
created_at: 2025-03-18T17:26:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,22 +38,12 @@ function InPersonVAAppointment({
   cancelAppointment,
 }: InPersonVAAppointmentProps) {
   const type = AppointmentDetailsTypeConstants.InPersonVA
-  const navigateTo = useRouteNavigation()
 
   return (
     <Box>
       <TextArea>
         <AppointmentDetailsModality attributes={attributes} subType={subType} type={type} />
         <AppointmentDateAndTime attributes={attributes} subType={subType} />
-        {subType === AppointmentDetailsSubTypeConstants.Past && (
-          <Box>
-            <Button
-              onPress={() => navigateTo('SubmitTravelPayClaimScreen', { appointmentDateTime: attributes.startDateUtc })}
-              label="Submit travel claim"
-              a11yLabel="Submit travel claim"
-            />
-          </Box>
-        )}
path: VAMobile/src/screens/HealthScreen/Appointments/AppointmentTypeComponents/InPersonVAAppointment.tsx
user: login: juancstlm-a6
body: Correct. This was older scaffolding code that Liz added to get into the feature w/o having any of the actual logic in place to build out the screens 
created_at: 2025-03-18T17:34:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: juancstlm-a6
body: Correct. We are not really using the details at the moment and just like web only care if there is a claim or not.
created_at: 2025-03-18T17:36:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: dumathane
body: You don't currently have any sort of 'claim already submitted' or 'claim paid' indicator yet though right? That is probably also to come in the future?
created_at: 2025-03-18T17:41:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: dumathane
body: Any thoughts on indicating if someone is past the claim window to them as well like 'hey you could have but now it's too late' type situation?
created_at: 2025-03-18T17:42:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: juancstlm-a6
body: Yes, that will be done as part of another ticket. The changes in scope here are just to remove the existing feature entry point and replace it with one that includes the logic such that it matches what exists on web. 
created_at: 2025-03-18T17:45:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,7 +40,14 @@
             "vetextId": null,
             "reason": "Test",
             "isCovidVaccine": false,
-            "typeOfCare": "Podiatry"
+            "typeOfCare": "Podiatry",
+            "travelPayClaim": {
+              "metadata": {
+                "status": 200,
+                "message": "No claims found.",
+                "success": true
+              }
path: VAMobile/src/store/api/demo/mocks/appointments.json
user: login: juancstlm-a6
body: > You don't currently have any sort of 'claim already submitted' or 'claim paid' indicator yet though right? That is probably also to come in the future?

This is all part of future tickets
created_at: 2025-03-18T17:46:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: juancstlm-a6
created_at: 2025-03-12T21:58:17Z
body: > Should probably have a mobile-specific person's input, but from a travel pay perspective it's looking good!
> 
> One thing to note is passing the include flag to the API call to retrieve the appts. I haven't spent much time looking at the Mobile module for appts, so can't confirm at this time, but we'll need to pass the `travel_pay_claim` in the `include` list to make sure the claims are appended when we get the appts.

@liztownd 
Thanks for calling that out. I was going to tackle that portion in https://github.com/department-of-veterans-affairs/va.gov-team/issues/91570 I'll update it to include the details you shared here about passing the `travel_pay_claim` in the `include` param 
author_association: CONTRIBUTOR
user: login: liztownd
created_at: 2025-03-13T12:41:55Z
body: @juancstlm-a6 Heads up, I mis-spoke, the flag is actually `travel_pay_claims` with an `s`. Double checked in the `vets-website` code where the flag is being passed in:

`includeParams.push('travel_pay_claims')`

Also: It's totally fine to wait to tackle it in a later PR, just wanted to make sure to call it out. 😁 
author_association: CONTRIBUTOR