diff --git a/src/CONST/index.ts b/src/CONST/index.ts index b4db81c116559..5dac4a2ed5090 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7933,7 +7933,7 @@ const CONST = { * The Travel Invoicing feed type constant. * This feed is used for Travel Invoicing cards which are separate from regular Expensify Cards. */ - PROGRAM_TRAVEL_US: 'TRAVEL_US', + PROGRAM_TRAVEL_US: 'PROGRAM_TRAVEL_US', }, LAST_PAYMENT_METHOD: { LAST_USED: 'lastUsed', diff --git a/src/pages/workspace/travel/PolicyTravelPage.tsx b/src/pages/workspace/travel/PolicyTravelPage.tsx index 42221a4b2d1e6..50f9e8260b053 100644 --- a/src/pages/workspace/travel/PolicyTravelPage.tsx +++ b/src/pages/workspace/travel/PolicyTravelPage.tsx @@ -72,11 +72,12 @@ function WorkspaceTravelPage({ const step = getTravelStep(policy, travelSettings, isBetaEnabled(CONST.BETAS.IS_TRAVEL_VERIFIED), policies, currentUserLogin); const mainContent = (() => { + // TODO: Remove this conditional when Travel Invoicing feature is fully implemented + if (isTravelInvoicingEnabled) { + return ; + } switch (step) { case CONST.TRAVEL.STEPS.BOOK_OR_MANAGE_YOUR_TRIP: - if (isTravelInvoicingEnabled) { - return ; - } return ; case CONST.TRAVEL.STEPS.REVIEWING_REQUEST: return ; diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx index 5aa6eed4db2a0..6d9c69c3941e0 100644 --- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React from 'react'; import {View} from 'react-native'; import AnimatedSubmitButton from '@components/AnimatedSubmitButton'; import MenuItem from '@components/MenuItem'; @@ -12,12 +12,13 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; import {openExternalLink} from '@libs/actions/Link'; -import {clearTravelInvoicingSettlementAccountErrors} from '@libs/actions/TravelInvoicing'; +import {clearTravelInvoicingSettlementAccountErrors, setTravelInvoicingSettlementAccount} from '@libs/actions/TravelInvoicing'; import {getLastFourDigits} from '@libs/BankAccountUtils'; +import {getEligibleBankAccountsForCard} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; +import {hasInProgressUSDVBBA, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES} from '@libs/ReimbursementAccountUtils'; import { - getIsTravelInvoicingEnabled, getTravelInvoicingCardSettingsKey, getTravelLimit, getTravelSettlementAccount, @@ -30,7 +31,6 @@ import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/Toggl import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import BookOrManageYourTrip from './BookOrManageYourTrip'; import CentralInvoicingLearnHow from './CentralInvoicingLearnHow'; import CentralInvoicingSubtitleWrapper from './CentralInvoicingSubtitleWrapper'; @@ -50,16 +50,15 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec const {isExecuting, singleExecution} = useSingleExecution(); const icons = useMemoizedLazyExpensifyIcons(['LuggageWithLines', 'NewWindow']); - const [isCentralInvoicingEnabled, setIsCentralInvoicingEnabled] = useState(true); - // For Travel Invoicing, we use a travel-specific card settings key // The format is: private_expensifyCardSettings_{workspaceAccountID}_{feedType} // where feedType is PROGRAM_TRAVEL_US for Travel Invoicing const [cardSettings] = useOnyx(getTravelInvoicingCardSettingsKey(workspaceAccountID), {canBeMissing: true}); + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true}); // Use pure selectors to derive state - const isTravelInvoicingEnabled = getIsTravelInvoicingEnabled(cardSettings); const hasSettlementAccount = hasTravelInvoicingSettlementAccount(cardSettings); const travelSpend = getTravelSpend(cardSettings); const travelLimit = getTravelLimit(cardSettings); @@ -79,8 +78,41 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec // Get any errors from the settlement account update const hasSettlementAccountError = Object.keys(cardSettings?.errors ?? {}).length > 0; + // Bank account eligibility for toggle handler + const isSetupUnfinished = hasInProgressUSDVBBA(reimbursementAccount?.achData); + const eligibleBankAccounts = getEligibleBankAccountsForCard(bankAccountList); + + /** + * Handle toggle change for Central Invoicing. + * When turning ON, triggers bank account flow if needed. + * When turning OFF, unassigns the settlement account. + */ + const handleToggle = (isEnabled: boolean) => { + if (!isEnabled) { + // Turning OFF - unassign the settlement account by setting paymentBankAccountID to 0 + setTravelInvoicingSettlementAccount(policyID, workspaceAccountID, 0, cardSettings?.paymentBankAccountID); + return; + } + + // Check if user is on a public domain - Travel Invoicing requires a private domain + if (account?.isFromPublicDomain) { + Navigation.navigate(ROUTES.TRAVEL_PUBLIC_DOMAIN_ERROR.getRoute(Navigation.getActiveRoute())); + return; + } + + // Turning ON - check if bank account setup is needed + if (!eligibleBankAccounts.length || isSetupUnfinished) { + // No bank accounts - start add bank account flow + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_TRAVEL.getRoute(policyID))); + return; + } + + // Bank accounts exist - go to settlement account selection + Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_ACCOUNT.getRoute(policyID)); + }; + const getCentralInvoicingSubtitle = () => { - if (!isCentralInvoicingEnabled) { + if (!hasSettlementAccount) { return } />; } return ; @@ -91,8 +123,8 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec title: translate('workspace.moreFeatures.travel.travelInvoicing.centralInvoicingSection.title'), subtitle: getCentralInvoicingSubtitle(), switchAccessibilityLabel: translate('workspace.moreFeatures.travel.travelInvoicing.centralInvoicingSection.subtitle'), - isActive: isCentralInvoicingEnabled, - onToggle: (isEnabled: boolean) => setIsCentralInvoicingEnabled(isEnabled), + isActive: hasSettlementAccount, + onToggle: handleToggle, // pendingAction: policy?.pendingFields?.autoReporting ?? policy?.pendingFields?.autoReportingFrequency, // errors: getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING), // onCloseError: () => clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING), @@ -185,17 +217,12 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec ); - // If Travel Invoicing is not enabled or no settlement account is configured - // show the BookOrManageYourTrip component as fallback - if (!isTravelInvoicingEnabled || !hasSettlementAccount) { - return ; - } - return ( <>
diff --git a/tests/ui/WorkspaceTravelInvoicingSectionTest.tsx b/tests/ui/WorkspaceTravelInvoicingSectionTest.tsx index 4f3a73fb74d4c..4ff078401df04 100644 --- a/tests/ui/WorkspaceTravelInvoicingSectionTest.tsx +++ b/tests/ui/WorkspaceTravelInvoicingSectionTest.tsx @@ -81,7 +81,7 @@ describe('WorkspaceTravelInvoicingSection', () => { }); describe('When Travel Invoicing is not configured', () => { - it('should show BookOrManageYourTrip when card settings are not available', async () => { + it('should render sections when card settings are not available', async () => { // Given no Travel Invoicing card settings exist await act(async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, mockPolicy); @@ -94,13 +94,15 @@ describe('WorkspaceTravelInvoicingSection', () => { // Wait for component to render await waitForBatchedUpdatesWithAct(); - // Then the fallback component should be visible (BookOrManageYourTrip) - expect(screen.getByText('Book or manage your trip')).toBeTruthy(); + // Then the Travel Booking section should be visible + expect(screen.getByText('Travel booking')).toBeTruthy(); + // And the Central Invoicing section should be visible + expect(screen.getByText('Central invoicing')).toBeTruthy(); }); - it('should show BookOrManageYourTrip when paymentBankAccountID is not set', async () => { + it('should render sections when paymentBankAccountID is not set', async () => { // Given Travel Invoicing card settings exist but without paymentBankAccountID - const travelInvoicingKey = `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${WORKSPACE_ACCOUNT_ID}_TRAVEL_US` as OnyxKey; + const travelInvoicingKey = `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${WORKSPACE_ACCOUNT_ID}_${CONST.TRAVEL.PROGRAM_TRAVEL_US}` as OnyxKey; await act(async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, mockPolicy); @@ -116,13 +118,15 @@ describe('WorkspaceTravelInvoicingSection', () => { await waitForBatchedUpdatesWithAct(); - // Then the fallback component should be visible - expect(screen.getByText('Book or manage your trip')).toBeTruthy(); + // Then the Travel Booking section should be visible + expect(screen.getByText('Travel booking')).toBeTruthy(); + // And the Central Invoicing section should be visible + expect(screen.getByText('Central invoicing')).toBeTruthy(); }); }); describe('When Travel Invoicing is configured', () => { - const travelInvoicingKey = `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${WORKSPACE_ACCOUNT_ID}_TRAVEL_US` as OnyxKey; + const travelInvoicingKey = `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${WORKSPACE_ACCOUNT_ID}_${CONST.TRAVEL.PROGRAM_TRAVEL_US}` as OnyxKey; const bankAccountKey = ONYXKEYS.BANK_ACCOUNT_LIST; it('should render the section title when card settings are properly configured', async () => { diff --git a/tests/unit/TravelInvoicingUtilsTest.ts b/tests/unit/TravelInvoicingUtilsTest.ts index 74486947467de..99ad12a64a13c 100644 --- a/tests/unit/TravelInvoicingUtilsTest.ts +++ b/tests/unit/TravelInvoicingUtilsTest.ts @@ -12,8 +12,8 @@ import type ExpensifyCardSettings from '@src/types/onyx/ExpensifyCardSettings'; describe('TravelInvoicingUtils', () => { describe('PROGRAM_TRAVEL_US constant', () => { - it('Should be defined as TRAVEL_US', () => { - expect(CONST.TRAVEL.PROGRAM_TRAVEL_US).toBe('TRAVEL_US'); + it('Should be defined as PROGRAM_TRAVEL_US', () => { + expect(CONST.TRAVEL.PROGRAM_TRAVEL_US).toBe('PROGRAM_TRAVEL_US'); }); });