Skip to content
Draft
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
4 changes: 2 additions & 2 deletions src/components/TransactionItemRow/DataCells/TypeCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import {isTravelCard} from '@libs/CardUtils';
import {isTravelCardTransaction} from '@libs/CardUtils';
import {getExpenseTypeTranslationKey, getTransactionType, isExpensifyCardTransaction, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';

import variables from '@styles/variables';
Expand Down Expand Up @@ -73,7 +73,7 @@ function TypeCell({transactionItem, shouldUseNarrowLayout, shouldShowTooltip}: T
const type = getTransactionType(transactionItem, card);
const isExpensifyCard = isExpensifyCardTransaction(transactionItem);
const isManagedCard = isManagedCardTransaction(transactionItem);
const isTravelInvoicingCard = isTravelCard(card);
const isTravelInvoicingCard = isTravelCardTransaction(transactionItem.feedCountry, card);
const isPendingExpensifyCardTransaction = isExpensifyCard && isPending(transactionItem);
const pendingIcon = isTravelInvoicingCard ? expensifyIcons.CreditCardWithPlaneHourglass : expensifyIcons.ExpensifyCardHourglass;
const typeIcon = isPendingExpensifyCardTransaction ? pendingIcon : getTypeIcon(expensifyIcons, type, isExpensifyCard, isManagedCard, isTravelInvoicingCard);
Expand Down
2 changes: 1 addition & 1 deletion src/components/TransactionItemRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ function TransactionItemRow({

const description = getDescription(transactionItem);
const exchangeRateMessage = getExchangeRate(transactionItem, report?.currency ?? policy?.outputCurrency);
const cardName = getCompanyCardDescription(translate, transactionItem?.cardName, transactionItem?.cardID, nonPersonalAndWorkspaceCards);
const cardName = getCompanyCardDescription(translate, transactionItem?.cardName, transactionItem?.cardID, nonPersonalAndWorkspaceCards, transactionItem?.feedCountry);
const isUnreported = transactionItem.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const shouldShowAttendees = (isUnreported ? !!isAttendeesEnabledForMovingPolicy : shouldShowAttendeesUtils(CONST.IOU.TYPE.SUBMIT, policy)) && transactionAttendees.length > 0;

Expand Down
25 changes: 18 additions & 7 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,17 +248,18 @@ function getCardDescriptionForSearchTable(card: Card, translate: LocalizedTransl
* Returns the formatted card name for a company card. Returns an empty string
* if the card is not a real card, but a cash expense
*/
function getCompanyCardDescription(translate: LocalizedTranslate, transactionCardName?: string, cardID?: number, cards?: CardList) {
function getCompanyCardDescription(translate: LocalizedTranslate, transactionCardName?: string, cardID?: number, cards?: CardList, feedCountry?: string) {
const formattedTransactionCardName = transactionCardName === CONST.EXPENSE.TYPE.CASH_CARD_NAME ? '' : transactionCardName;
const card = cardID ? cards?.[cardID] : undefined;

if (!cardID || !cards?.[cardID]) {
return formattedTransactionCardName;
// feedCountry travels with the transaction, so a travel card belonging to another member (absent from the viewer's card
// list) still shows the localized travel name instead of the server string.
if (isTravelCardTransaction(feedCountry, card)) {
return translate('cardTransactions.travelInvoicing');
}

const card = cards[cardID];

if (isTravelCard(card)) {
return translate('cardTransactions.travelInvoicing');
if (!card) {
return formattedTransactionCardName;
}

if (isExpensifyCard(card)) {
Expand Down Expand Up @@ -1757,6 +1758,15 @@ function isTravelCard(card: Card | undefined): boolean {
return card?.nameValuePairs?.feedCountry === CONST.TRAVEL.PROGRAM_TRAVEL_US;
}

/**
* A transaction is on a travel card when the backend stamps its feedCountry, which travels with the transaction so the icon
* resolves even for another member's card that isn't in the viewer's own card list. Falls back to the card object for old
* cached transactions that predate the feedCountry field.
*/
function isTravelCardTransaction(feedCountry: string | undefined, card: Card | undefined): boolean {
return feedCountry === CONST.TRAVEL.PROGRAM_TRAVEL_US || isTravelCard(card);
}

/**
* Gets displayable Expensify cards, filtering out inactive cards and grouping combo cards
* (physical + virtual pairs) so only the physical card is shown per domain.
Expand Down Expand Up @@ -1979,6 +1989,7 @@ export {
getBankName,
isSelectedFeedExpired,
isTravelCard,
isTravelCardTransaction,
getCompanyFeeds,
hasCompanyCardFeeds,
isPersonalCardBrokenConnection,
Expand Down
1 change: 1 addition & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@
if (TRANSACTION_REQUIRED_PROPERTIES.includes(key) && isEmptyValue(value)) {
throw SyntaxError('debug.missingValue');
}
switch (key) {

Check failure on line 977 in src/libs/DebugUtils.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Switch is not exhaustive. Cases not matched: "feedCountry"

Check failure on line 977 in src/libs/DebugUtils.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Switch is not exhaustive. Cases not matched: "feedCountry"
case 'reportID':
case 'reportName':
case 'currency':
Expand Down Expand Up @@ -1153,6 +1153,7 @@
bank: CONST.RED_BRICK_ROAD_PENDING_ACTION,
liabilityType: CONST.RED_BRICK_ROAD_PENDING_ACTION,
cardName: CONST.RED_BRICK_ROAD_PENDING_ACTION,
feedCountry: CONST.RED_BRICK_ROAD_PENDING_ACTION,
cardNumber: CONST.RED_BRICK_ROAD_PENDING_ACTION,
managedCard: CONST.RED_BRICK_ROAD_PENDING_ACTION,
posted: CONST.RED_BRICK_ROAD_PENDING_ACTION,
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The display name of the purchaser card, if any */
cardName?: string;

/** The Expensify Card program the card belongs to (e.g. `TRAVEL_US`), used to derive the travel icon without needing the card in the viewer's own list */
feedCountry?: string;

/** The masked PAN of the purchaser card, if any */
cardNumber?: string;

Expand Down
29 changes: 29 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
isExpiredCard,
isMatchingCard,
isPersonalCard,
isTravelCardTransaction,
isUkEuExpensifyCard,
lastFourNumbersFromCardName,
maskCardNumber,
Expand Down Expand Up @@ -3280,6 +3281,34 @@ describe('CardUtils', () => {
const description = getCompanyCardDescription(mockTranslate, 'Expensify Card - 6909', 99999, travelCardList);
expect(description).toBe('Travel invoicing');
});

it("should return 'Travel invoicing' for another member's travel card that isn't in the viewer's card list", () => {
const description = getCompanyCardDescription(mockTranslate, 'Expensify Card - 6909', 99999, undefined, CONST.TRAVEL.PROGRAM_TRAVEL_US);
expect(description).toBe('Travel invoicing');
});
});

describe('isTravelCardTransaction', () => {
it("returns true from the transaction's feedCountry even when the card isn't in the viewer's list", () => {
expect(isTravelCardTransaction(CONST.TRAVEL.PROGRAM_TRAVEL_US, undefined)).toBe(true);
});

it('falls back to the card when the transaction has no feedCountry', () => {
const travelCardList = createMock<CardList>({
'99999': {
cardID: 99999,
bank: CONST.EXPENSIFY_CARD.BANK,
nameValuePairs: {
feedCountry: CONST.TRAVEL.PROGRAM_TRAVEL_US,
},
},
});
expect(isTravelCardTransaction(undefined, travelCardList['99999'])).toBe(true);
});

it('returns false for a non-travel transaction with no card', () => {
expect(isTravelCardTransaction(CONST.COUNTRY.US, undefined)).toBe(false);
});
});

describe('Expensify card sort comparator', () => {
Expand Down
Loading