Skip to content
Open
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
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6182,6 +6182,7 @@ const translations = {
invalidDateRangeError: 'The start date must be before the end date',
enabled: 'Consolidated Travel Billing enabled!',
enabledDescription: 'All travel spend on this workspace will now be centralized in a monthly bill.',
depositOnly: 'Deposit only',
},
personalDetailsDescription: 'In order to book travel, please enter your legal name as it appears on your government-issued ID.',
},
Expand Down
23 changes: 23 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {
BankAccount,
BankAccountList,
Card,
CardFeeds,
Expand Down Expand Up @@ -512,6 +513,26 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry<BankAccountL
);
}

/**
* Checks whether a verified bank account can only receive deposits (not enabled for debits).
*/
function isDepositOnlyBankAccount(bankAccount: BankAccount | undefined): boolean {
return !!bankAccount?.accountData?.defaultCredit && !bankAccount?.accountData?.allowDebit && !isBankAccountPartiallySetup(bankAccount?.accountData?.state);
}

/**
* Returns the bank accounts that can settle Consolidated Travel Billing. Includes the standard card-eligible
* accounts, plus verified deposit-only accounts when the workspace can settle by invoice instead of an ACH debit.
*/
function getEligibleBankAccountsForTravelInvoicing(bankAccountsList: OnyxEntry<BankAccountList>, canUseDepositOnlyAccounts: boolean) {
const eligibleBankAccounts = getEligibleBankAccountsForCard(bankAccountsList);
if (!canUseDepositOnlyAccounts || !bankAccountsList) {
return eligibleBankAccounts;
}
const depositOnlyBankAccounts = Object.values(bankAccountsList).filter(isDepositOnlyBankAccount);
return [...eligibleBankAccounts, ...depositOnlyBankAccounts];
}

function getConnectionBankAccountsForReconciliation(connections: OnyxEntry<Partial<Connections>>, connectionName: PolicyConnectionName | undefined): Array<{id: string; name: string}> {
if (!connections || !connectionName) {
return [];
Expand Down Expand Up @@ -2029,6 +2050,8 @@ export {
getTranslationKeyForCardStatus,
maskPin,
getEligibleBankAccountsForCard,
getEligibleBankAccountsForTravelInvoicing,
isDepositOnlyBankAccount,
sortCardsByCardholderName,
isCurrencySupportedForECards,
getCardFeedIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
retryTravelCardsProvisioning,
} from '@libs/actions/TravelInvoicing';
import {getLastFourDigits} from '@libs/BankAccountUtils';
import {getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils';
import {getCardSettings, getEligibleBankAccountsForTravelInvoicing} from '@libs/CardUtils';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import Navigation from '@libs/Navigation/Navigation';
import {areTravelPersonalDetailsMissing} from '@libs/PersonalDetailsUtils';
Expand Down Expand Up @@ -147,7 +147,7 @@

// Bank account eligibility for toggle handler
const isSetupUnfinished = hasInProgressUSDVBBA(reimbursementAccount?.achData);
const eligibleBankAccounts = getEligibleBankAccountsForCard(bankAccountList);
const eligibleBankAccounts = getEligibleBankAccountsForTravelInvoicing(bankAccountList, isPayByInvoice);

Check failure on line 150 in src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find name 'isPayByInvoice'.

Check failure on line 150 in src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe argument of type error typed assigned to a parameter of type `boolean`

Check failure on line 150 in src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe argument of type error typed assigned to a parameter of type `boolean`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Define the pay-by-invoice flag before using it

In this branch, isPayByInvoice is never declared or imported in this component, and the only added use of the pay-by-invoice helper in the settlement page imports a symbol that is not exported from TravelInvoicingUtils; as a result the Travel settings code fails to compile when this file is checked. Please derive this flag from travelSettings (and add/export the helper if needed) before passing it to getEligibleBankAccountsForTravelInvoicing.

Useful? React with 👍 / 👎.


// Determine if Travel Invoicing is enabled based on isEnabled field
const isTravelInvoicingEnabled = getIsTravelInvoicingEnabled(travelSettings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

import {configureTravelInvoicingForPolicy, setTravelInvoicingSettlementAccount} from '@libs/actions/TravelInvoicing';
import {getLastFourDigits} from '@libs/BankAccountUtils';
import {getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils';
import {getCardSettings, getEligibleBankAccountsForTravelInvoicing, isDepositOnlyBankAccount} from '@libs/CardUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {getIsTravelInvoicingEnabled, getTravelInvoicingCardSettingsKey} from '@libs/TravelInvoicingUtils';
import {getIsTravelBillingPayByInvoice, getIsTravelInvoicingEnabled, getTravelInvoicingCardSettingsKey} from '@libs/TravelInvoicingUtils';

import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
Expand Down Expand Up @@ -48,7 +48,8 @@
const isSuccess = !!cardSettings?.isSuccess;
const isTravelInvoicingEnabled = getIsTravelInvoicingEnabled(travelSettings);
const paymentBankAccountID = travelSettings?.paymentBankAccountID;
const eligibleBankAccounts = getEligibleBankAccountsForCard(bankAccountsList);
const canUseDepositOnlyAccounts = getIsTravelBillingPayByInvoice(travelSettings);

Check failure on line 51 in src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe call of a type that could not be resolved

Check failure on line 51 in src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe assignment of an error typed value

Check failure on line 51 in src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe call of a type that could not be resolved

Check failure on line 51 in src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe assignment of an error typed value
const eligibleBankAccounts = getEligibleBankAccountsForTravelInvoicing(bankAccountsList, canUseDepositOnlyAccounts);

Check failure on line 52 in src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe argument of type error typed assigned to a parameter of type `boolean`

Check failure on line 52 in src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unsafe argument of type error typed assigned to a parameter of type `boolean`

const getVerificationState = () => {
if (cardOnWaitlist) {
Expand All @@ -69,12 +70,15 @@
const bankName = (bankAccount.accountData?.addressName ?? '') as BankName;
const bankAccountNumber = bankAccount.accountData?.accountNumber ?? '';
const bankAccountID = bankAccount.accountData?.bankAccountID ?? bankAccount.methodID;
const accountEndingIn = `${translate('workspace.expensifyCard.accountEndingIn')} ${getLastFourDigits(bankAccountNumber)}`;

return {
value: bankAccountID,
text: bankAccount.title,
leftElement: <BankAccountListItemLeftElement bankName={bankName} />,
alternateText: `${translate('workspace.expensifyCard.accountEndingIn')} ${getLastFourDigits(bankAccountNumber)}`,
alternateText: isDepositOnlyBankAccount(bankAccount)
? `${accountEndingIn} ${CONST.DOT_SEPARATOR} ${translate('workspace.moreFeatures.travel.travelInvoicing.depositOnly')}`
: accountEndingIn,
keyForList: bankAccountID?.toString() ?? '',
isSelected: bankAccountID === paymentBankAccountID,
};
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
getDisplayableThirdPartyCards,
getDomainByFundID,
getEligibleBankAccountsForCard,
getEligibleBankAccountsForTravelInvoicing,
getEligibleBankAccountsForUkEuCard,
getFeedNameForDisplay,
getFeedType,
Expand Down Expand Up @@ -4397,6 +4398,48 @@ describe('getEligibleBankAccountsForCard', () => {
});
});

describe('getEligibleBankAccountsForTravelInvoicing', () => {
const debitBusinessAccount: BankAccountList = {
'1': {
accountData: {type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, allowDebit: true, state: CONST.BANK_ACCOUNT.STATE.OPEN},
bankCurrency: 'USD',
bankCountry: 'US',
},
};

const depositOnlyAccount: BankAccountList = {
'2': {
accountData: {type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, allowDebit: false, defaultCredit: true, state: CONST.BANK_ACCOUNT.STATE.OPEN},
bankCurrency: 'USD',
bankCountry: 'US',
},
};

const partiallySetupDepositOnlyAccount: BankAccountList = {
'3': {
accountData: {type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, allowDebit: false, defaultCredit: true, state: CONST.BANK_ACCOUNT.STATE.SETUP},
bankCurrency: 'USD',
bankCountry: 'US',
},
};

it('excludes deposit-only accounts when deposit-only accounts are not allowed', () => {
const result = getEligibleBankAccountsForTravelInvoicing({...debitBusinessAccount, ...depositOnlyAccount}, false);
expect(result).toHaveLength(1);
expect(result.at(0)?.accountData?.allowDebit).toBe(true);
});

it('includes verified deposit-only accounts alongside debit-eligible accounts when allowed', () => {
const result = getEligibleBankAccountsForTravelInvoicing({...debitBusinessAccount, ...depositOnlyAccount}, true);
expect(result).toHaveLength(2);
});

it('always excludes partially set up deposit-only accounts', () => {
expect(getEligibleBankAccountsForTravelInvoicing(partiallySetupDepositOnlyAccount, true)).toHaveLength(0);
expect(getEligibleBankAccountsForTravelInvoicing(partiallySetupDepositOnlyAccount, false)).toHaveLength(0);
});
});

describe('getEligibleBankAccountsForUkEuCard', () => {
it('excludes partially set up accounts', () => {
const bankAccounts: BankAccountList = {
Expand Down
Loading