Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 4 additions & 3 deletions src/pages/workspace/travel/PolicyTravelPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <WorkspaceTravelInvoicingSection policyID={policyID} />;
}
switch (step) {
case CONST.TRAVEL.STEPS.BOOK_OR_MANAGE_YOUR_TRIP:
if (isTravelInvoicingEnabled) {
return <WorkspaceTravelInvoicingSection policyID={policyID} />;
}
return <BookOrManageYourTrip policyID={policyID} />;
case CONST.TRAVEL.STEPS.REVIEWING_REQUEST:
return <ReviewingRequest />;
Expand Down
59 changes: 43 additions & 16 deletions src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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';

Expand All @@ -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);
Expand All @@ -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 <CentralInvoicingSubtitleWrapper htmlComponent={<CentralInvoicingLearnHow />} />;
}
return <CentralInvoicingSubtitleWrapper />;
Expand All @@ -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),
Expand Down Expand Up @@ -185,17 +217,12 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec
</Section>
);

// If Travel Invoicing is not enabled or no settlement account is configured
// show the BookOrManageYourTrip component as fallback
if (!isTravelInvoicingEnabled || !hasSettlementAccount) {
return <BookOrManageYourTrip policyID={policyID} />;
}

return (
<>
<Section
title={translate('workspace.moreFeatures.travel.travelInvoicing.travelBookingSection.title')}
subtitle={translate('workspace.moreFeatures.travel.travelInvoicing.travelBookingSection.subtitle')}
subtitleStyles={styles.mb6}
isCentralPane
subtitleMuted
>
Expand Down
20 changes: 12 additions & 8 deletions tests/ui/WorkspaceTravelInvoicingSectionTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/TravelInvoicingUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down
Loading