From 7d6149a18f884dc7287bf61c0ed500f2e696e0ab Mon Sep 17 00:00:00 2001 From: allgandaf Date: Sun, 8 Mar 2026 01:07:52 +0530 Subject: [PATCH 01/17] Phase 3: Remove flat fallbacks and nest travel writes under TRAVEL_US MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove flat-format fallback from getCardSettings() — Phase 2 guarantees all settings are nested under program keys (CURRENT/US/GB) - Nest travel invoicing optimistic writes under TRAVEL_US key instead of writing at root level, preventing cross-program data pollution - Remove duplicate root-level isEnabled in deactivateTravelInvoicing - Update tests to reflect new behavior (undefined instead of flat root) --- src/libs/CardUtils.ts | 15 +++----- src/libs/actions/TravelInvoicing.ts | 59 ++++++++++++++++++----------- tests/unit/CardUtilsTest.ts | 16 ++++---- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 7eb65b2788b8..1e5469cab6d6 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1100,18 +1100,13 @@ function getCardSettings(cardSettings: OnyxEntry, feedCou }; if (feedCountry) { - return getMergedProgramSettings(feedCountry) ?? cardSettings; + return getMergedProgramSettings(feedCountry); } - // Auto-detect: try known card programs in priority order so callers that - // don't pass feedCountry still get the right program sub-object when the - // backend sends nested settings (Phase 2 of fixing shared Onyx key). - const result = getMergedProgramSettings(CONST.COUNTRY.US) ?? getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? getMergedProgramSettings(CONST.COUNTRY.GB); - if (result) { - return result; - } - - return cardSettings; + // Auto-detect: try known card programs in priority order. + // Phase 2 guarantees all settings are nested under program keys + // (CURRENT for legacy domains, US/GB for new ones). + return getMergedProgramSettings(CONST.COUNTRY.US) ?? getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? getMergedProgramSettings(CONST.COUNTRY.GB); } function isCardPendingIssue(card?: Card) { diff --git a/src/libs/actions/TravelInvoicing.ts b/src/libs/actions/TravelInvoicing.ts index ba58cb833cf7..0f176460ed2b 100644 --- a/src/libs/actions/TravelInvoicing.ts +++ b/src/libs/actions/TravelInvoicing.ts @@ -90,9 +90,11 @@ function setTravelInvoicingSettlementAccount(policyID: string, workspaceAccountI onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - paymentBankAccountID: settlementBankAccountID, - previousPaymentBankAccountID, - monthlySettlementDate, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + paymentBankAccountID: settlementBankAccountID, + previousPaymentBankAccountID, + monthlySettlementDate, + }, isLoading: true, pendingFields: { paymentBankAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -109,9 +111,11 @@ function setTravelInvoicingSettlementAccount(policyID: string, workspaceAccountI onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - paymentBankAccountID: settlementBankAccountID, - previousPaymentBankAccountID: null, - monthlySettlementDate, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + paymentBankAccountID: settlementBankAccountID, + previousPaymentBankAccountID: null, + monthlySettlementDate, + }, isLoading: false, pendingFields: { paymentBankAccountID: null, @@ -128,10 +132,11 @@ function setTravelInvoicingSettlementAccount(policyID: string, workspaceAccountI onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - // Keep the attempted value visible (grayed out) until error is dismissed - paymentBankAccountID: settlementBankAccountID, - previousPaymentBankAccountID, - monthlySettlementDate, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + paymentBankAccountID: settlementBankAccountID, + previousPaymentBankAccountID, + monthlySettlementDate, + }, isLoading: false, pendingFields: { paymentBankAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -157,8 +162,10 @@ function setTravelInvoicingSettlementAccount(policyID: string, workspaceAccountI */ function clearTravelInvoicingSettlementAccountErrors(workspaceAccountID: number, paymentBankAccountID: number | null) { Onyx.merge(getTravelInvoicingCardSettingsKey(workspaceAccountID), { - paymentBankAccountID, - previousPaymentBankAccountID: null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + paymentBankAccountID, + previousPaymentBankAccountID: null, + }, pendingFields: { paymentBankAccountID: null, }, @@ -184,8 +191,10 @@ function updateTravelInvoiceSettlementFrequency(workspaceAccountID: number, freq onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - monthlySettlementDate, - previousMonthlySettlementDate: currentMonthlySettlementDate, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + monthlySettlementDate, + previousMonthlySettlementDate: currentMonthlySettlementDate, + }, pendingFields: { monthlySettlementDate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, @@ -201,8 +210,10 @@ function updateTravelInvoiceSettlementFrequency(workspaceAccountID: number, freq onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - monthlySettlementDate, - previousMonthlySettlementDate: null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + monthlySettlementDate, + previousMonthlySettlementDate: null, + }, pendingFields: { monthlySettlementDate: null, }, @@ -218,8 +229,10 @@ function updateTravelInvoiceSettlementFrequency(workspaceAccountID: number, freq onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - monthlySettlementDate, - previousMonthlySettlementDate: currentMonthlySettlementDate ?? null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + monthlySettlementDate, + previousMonthlySettlementDate: currentMonthlySettlementDate ?? null, + }, pendingFields: { monthlySettlementDate: null, }, @@ -243,8 +256,10 @@ function updateTravelInvoiceSettlementFrequency(workspaceAccountID: number, freq */ function clearTravelInvoicingSettlementFrequencyErrors(workspaceAccountID: number, monthlySettlementDate: Date | null | undefined) { Onyx.merge(getTravelInvoicingCardSettingsKey(workspaceAccountID), { - monthlySettlementDate: monthlySettlementDate ?? null, - previousMonthlySettlementDate: null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + monthlySettlementDate: monthlySettlementDate ?? null, + previousMonthlySettlementDate: null, + }, pendingFields: { monthlySettlementDate: null, }, @@ -326,7 +341,6 @@ function deactivateTravelInvoicing(policyID: string, workspaceAccountID: number) onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - isEnabled: false, [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { isEnabled: false, }, @@ -341,10 +355,10 @@ function deactivateTravelInvoicing(policyID: string, workspaceAccountID: number) onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - pendingAction: null, [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { isEnabled: false, }, + pendingAction: null, }, }, ]; @@ -354,7 +368,6 @@ function deactivateTravelInvoicing(policyID: string, workspaceAccountID: number) onyxMethod: Onyx.METHOD.MERGE, key: cardSettingsKey, value: { - isEnabled: true, [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { isEnabled: true, }, diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 9500f71ee4ed..a0888de6eed8 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -3420,14 +3420,14 @@ describe('CardUtils', () => { expect(getCardSettings(null as unknown as undefined)).toBeUndefined(); }); - it('should return flat root when feedCountry is not provided and no nested keys exist', () => { + it('should return undefined when no nested keys exist and feedCountry is not provided', () => { const result = getCardSettings(flatSettings); - expect(result).toBe(flatSettings); + expect(result).toBeUndefined(); }); - it('should return flat root when feedCountry is undefined and no nested keys exist', () => { + it('should return undefined when no nested keys exist and feedCountry is undefined', () => { const result = getCardSettings(flatSettings, undefined); - expect(result).toBe(flatSettings); + expect(result).toBeUndefined(); }); it('should return merged root + nested when feedCountry matches a nested key', () => { @@ -3438,9 +3438,9 @@ describe('CardUtils', () => { expect(result?.domainName).toBe('example.com'); }); - it('should fall back to root when feedCountry key does not exist', () => { + it('should return undefined when feedCountry key does not exist', () => { const result = getCardSettings(nestedSettings, 'CA'); - expect(result).toBe(nestedSettings); + expect(result).toBeUndefined(); }); it('should return merged root + TRAVEL_US when feedCountry is TRAVEL_US', () => { @@ -3450,9 +3450,9 @@ describe('CardUtils', () => { expect(result?.domainName).toBe('example.com'); }); - it('should not return primitive values as nested settings', () => { + it('should return undefined for primitive values as feedCountry', () => { const result = getCardSettings(nestedSettings, 'limit'); - expect(result).toBe(nestedSettings); + expect(result).toBeUndefined(); }); it('should auto-detect US program when no feedCountry is provided', () => { From 1364b0188c205f012dc43eb66814219eea418d1f Mon Sep 17 00:00:00 2001 From: allgandaf Date: Sun, 8 Mar 2026 01:21:43 +0530 Subject: [PATCH 02/17] Nest Expensify Card optimistic writes under feedCountry program key --- src/libs/CardUtils.ts | 18 +++++ src/libs/actions/Card.ts | 65 ++++++++++++------- .../ReconciliationAccountSettingsPage.tsx | 5 +- .../WorkspaceSettlementAccountPage.tsx | 5 +- .../WorkspaceSettlementFrequencyPage.tsx | 5 +- 5 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 1e5469cab6d6..6c8ad04396ea 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1084,6 +1084,23 @@ function isExpensifyCardFullySetUp(policy?: OnyxEntry, cardSettings?: On return !!(policy?.areExpensifyCardsEnabled && cardSettings?.paymentBankAccountID); } +/** + * Detects which card program key exists in the card settings object. + * Returns the first matching program key (US, CURRENT, or GB), or undefined if none found. + * Used to determine the correct nested key for optimistic writes. + */ +function getCardFeedCountry(cardSettings: OnyxEntry): string | undefined { + if (!cardSettings) { + return undefined; + } + + const programKeys = [CONST.COUNTRY.US, CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT, CONST.COUNTRY.GB]; + return programKeys.find((key) => { + const value = cardSettings[key as keyof typeof cardSettings]; + return value && typeof value === 'object' && !Array.isArray(value); + }); +} + function getCardSettings(cardSettings: OnyxEntry, feedCountry?: string): ExpensifyCardSettingsBase | undefined { if (!cardSettings) { return undefined; @@ -1434,6 +1451,7 @@ export { hasIssuedExpensifyCard, isExpensifyCardFullySetUp, getCardSettings, + getCardFeedCountry, filterAllInactiveCards, filterInactiveCards, isCardPendingIssue, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 257414b57eb2..cc2f20e88fa6 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -645,16 +645,22 @@ function revealVirtualCardDetails(cardID: number, validateCode: string): Promise }); } -function updateSettlementFrequency(workspaceAccountID: number, settlementFrequency: ValueOf, currentFrequency?: Date) { +function updateSettlementFrequency( + workspaceAccountID: number, + feedCountry: string | undefined, + settlementFrequency: ValueOf, + currentFrequency?: Date, +) { const monthlySettlementDate = settlementFrequency === CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY ? null : new Date(); + const settlementValue = feedCountry ? {[feedCountry]: {monthlySettlementDate}} : {monthlySettlementDate}; + const failureValue = feedCountry ? {[feedCountry]: {monthlySettlementDate: currentFrequency}} : {monthlySettlementDate: currentFrequency}; + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, - value: { - monthlySettlementDate, - }, + value: settlementValue, }, ]; @@ -662,9 +668,7 @@ function updateSettlementFrequency(workspaceAccountID: number, settlementFrequen { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, - value: { - monthlySettlementDate, - }, + value: settlementValue, }, ]; @@ -672,9 +676,7 @@ function updateSettlementFrequency(workspaceAccountID: number, settlementFrequen { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, - value: { - monthlySettlementDate: currentFrequency, - }, + value: failureValue, }, ]; @@ -686,19 +688,41 @@ function updateSettlementFrequency(workspaceAccountID: number, settlementFrequen API.write(WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY, parameters, {optimisticData, successData, failureData}); } -function updateSettlementAccount(domainName: string, workspaceAccountID: number, policyID: string, settlementBankAccountID?: number, currentSettlementBankAccountID?: number) { +function updateSettlementAccount( + domainName: string, + workspaceAccountID: number, + policyID: string, + feedCountry: string | undefined, + settlementBankAccountID?: number, + currentSettlementBankAccountID?: number, +) { if (!settlementBankAccountID) { return; } + const optimisticValue = feedCountry + ? {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: true} + : {paymentBankAccountID: settlementBankAccountID, isLoading: true}; + + const successValue = feedCountry ? {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: false} : {paymentBankAccountID: settlementBankAccountID, isLoading: false}; + + const failureValue = feedCountry + ? { + [feedCountry]: {paymentBankAccountID: currentSettlementBankAccountID}, + isLoading: false, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + } + : { + paymentBankAccountID: currentSettlementBankAccountID, + isLoading: false, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }; + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, - value: { - paymentBankAccountID: settlementBankAccountID, - isLoading: true, - }, + value: optimisticValue, }, ]; @@ -706,10 +730,7 @@ function updateSettlementAccount(domainName: string, workspaceAccountID: number, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, - value: { - paymentBankAccountID: settlementBankAccountID, - isLoading: false, - }, + value: successValue, }, ]; @@ -717,11 +738,7 @@ function updateSettlementAccount(domainName: string, workspaceAccountID: number, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, - value: { - paymentBankAccountID: currentSettlementBankAccountID, - isLoading: false, - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }, + value: failureValue, }, ]; diff --git a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx index 0cae6b5a8f6d..464661270f68 100644 --- a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx @@ -12,7 +12,7 @@ import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {getConnectionNameFromRouteParam} from '@libs/AccountingUtils'; import {getLastFourDigits} from '@libs/BankAccountUtils'; -import {getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils'; +import {getCardFeedCountry, getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getDomainNameForPolicy} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; @@ -43,6 +43,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); + const feedCountry = getCardFeedCountry(cardSettings); const settings = getCardSettings(cardSettings); const paymentBankAccountID = settings?.paymentBankAccountID; @@ -74,7 +75,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting }, [policyID, backTo, connection]); const selectBankAccount = (newBankAccountID?: number) => { - updateSettlementAccount(domainName, defaultFundID, policyID, newBankAccountID, paymentBankAccountID); + updateSettlementAccount(domainName, defaultFundID, policyID, feedCountry, newBankAccountID, paymentBankAccountID); goBack(); }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx index 051740721fd6..50cee4545175 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx @@ -15,7 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getRouteParamForConnection} from '@libs/AccountingUtils'; import {openPolicyAccountingPage} from '@libs/actions/PolicyConnections'; import {getLastFourDigits} from '@libs/BankAccountUtils'; -import {getCardSettings, getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils'; +import {getCardFeedCountry, getCardSettings, getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getDomainNameForPolicy} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; @@ -45,6 +45,7 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const [bankAccountsList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); + const feedCountry = getCardFeedCountry(cardSettings); const settings = getCardSettings(cardSettings); const [continuousReconciliation] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION}${defaultFundID}`); const [reconciliationConnection] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION}${defaultFundID}`); @@ -107,7 +108,7 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP const listOptions: BankAccountListItem[] = eligibleBankAccountsOptions.length > 0 ? eligibleBankAccountsOptions : [fallbackBankAccountOption]; const handleSelectAccount = (value: number) => { - updateSettlementAccountCard(domainName, defaultFundID, policyID, value, paymentBankAccountID); + updateSettlementAccountCard(domainName, defaultFundID, policyID, feedCountry, value, paymentBankAccountID); Navigation.goBack(); }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx index d3f442dce982..3b635c288eaf 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx @@ -10,7 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateSettlementFrequency as updateSettlementFrequencyUtil} from '@libs/actions/Card'; -import {getCardSettings} from '@libs/CardUtils'; +import {getCardFeedCountry, getCardSettings} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -29,6 +29,7 @@ function WorkspaceSettlementFrequencyPage({route}: WorkspaceSettlementFrequencyP const defaultFundID = useDefaultFundID(policyID); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); + const feedCountry = getCardFeedCountry(cardSettings); const settings = getCardSettings(cardSettings); const shouldShowMonthlyOption = settings?.isMonthlySettlementAllowed ?? false; @@ -58,7 +59,7 @@ function WorkspaceSettlementFrequencyPage({route}: WorkspaceSettlementFrequencyP }, [translate, shouldShowMonthlyOption, selectedFrequency]); const updateSettlementFrequency = (value: ValueOf) => { - updateSettlementFrequencyUtil(defaultFundID, value, settings?.monthlySettlementDate); + updateSettlementFrequencyUtil(defaultFundID, feedCountry, value, settings?.monthlySettlementDate); }; return ( From 512f521ad38f72ecd921fd9155319a6580fa91f0 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 10 Mar 2026 00:21:49 +0530 Subject: [PATCH 03/17] Fix useDefaultFundID test to use nested card settings format --- tests/unit/PolicyUtilsTest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 8f0a8ec4c8e5..1b36893d1800 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -272,7 +272,9 @@ describe('PolicyUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}2`, policy); await Onyx.set(`${ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED}2`, lastSelectedExpensifyCardFeed); await Onyx.set(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${lastSelectedExpensifyCardFeed}`, { - paymentBankAccountID: 1234, + [CONST.COUNTRY.US]: { + paymentBankAccountID: 1234, + }, }); const {result} = renderHook(() => useDefaultFundID(policy.id)); From ca112a76879b3088961bcc8f8eb5a32d6667540e Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 10 Mar 2026 00:23:20 +0530 Subject: [PATCH 04/17] Fix TravelInvoicing tests to expect nested TRAVEL_US structure --- tests/unit/TravelInvoicingTest.ts | 48 ++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/tests/unit/TravelInvoicingTest.ts b/tests/unit/TravelInvoicingTest.ts index 2e925d6b9ab8..fb3fc1eee74b 100644 --- a/tests/unit/TravelInvoicingTest.ts +++ b/tests/unit/TravelInvoicingTest.ts @@ -46,8 +46,10 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - paymentBankAccountID: settlementBankAccountID, - previousPaymentBankAccountID, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ + paymentBankAccountID: settlementBankAccountID, + previousPaymentBankAccountID, + }), isLoading: true, pendingFields: expect.objectContaining({ paymentBankAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -62,8 +64,10 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - paymentBankAccountID: settlementBankAccountID, - previousPaymentBankAccountID: null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ + paymentBankAccountID: settlementBankAccountID, + previousPaymentBankAccountID: null, + }), isLoading: false, pendingFields: expect.objectContaining({ paymentBankAccountID: null, @@ -78,8 +82,10 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - paymentBankAccountID: settlementBankAccountID, - previousPaymentBankAccountID, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ + paymentBankAccountID: settlementBankAccountID, + previousPaymentBankAccountID, + }), isLoading: false, pendingFields: expect.objectContaining({ paymentBankAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -102,8 +108,10 @@ describe('TravelInvoicing', () => { clearTravelInvoicingSettlementAccountErrors(workspaceAccountID, restoredAccountID); expect(spyOnyxMerge).toHaveBeenCalledWith(cardSettingsKey, { - paymentBankAccountID: restoredAccountID, - previousPaymentBankAccountID: null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + paymentBankAccountID: restoredAccountID, + previousPaymentBankAccountID: null, + }, pendingFields: { paymentBankAccountID: null, }, @@ -121,8 +129,10 @@ describe('TravelInvoicing', () => { clearTravelInvoicingSettlementFrequencyErrors(workspaceAccountID, monthlySettlementDate); expect(spyOnyxMerge).toHaveBeenCalledWith(cardSettingsKey, { - monthlySettlementDate: monthlySettlementDate ?? null, - previousMonthlySettlementDate: null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + monthlySettlementDate: monthlySettlementDate ?? null, + previousMonthlySettlementDate: null, + }, pendingFields: { monthlySettlementDate: null, }, @@ -156,8 +166,10 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - monthlySettlementDate: mockDate, - previousMonthlySettlementDate: currentMonthlySettlementDate, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ + monthlySettlementDate: mockDate, + previousMonthlySettlementDate: currentMonthlySettlementDate, + }), pendingFields: expect.objectContaining({ monthlySettlementDate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }), @@ -171,8 +183,10 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - monthlySettlementDate: mockDate, - previousMonthlySettlementDate: null, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ + monthlySettlementDate: mockDate, + previousMonthlySettlementDate: null, + }), pendingFields: expect.objectContaining({ monthlySettlementDate: null, }), @@ -186,8 +200,10 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - monthlySettlementDate: mockDate, - previousMonthlySettlementDate: currentMonthlySettlementDate, + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ + monthlySettlementDate: mockDate, + previousMonthlySettlementDate: currentMonthlySettlementDate, + }), pendingFields: expect.objectContaining({ monthlySettlementDate: null, }), From 9a5ac55b26345b32facd991ccbb73d9d0bfe341c Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 10 Mar 2026 00:43:37 +0530 Subject: [PATCH 05/17] Make feedCountry required and remove flat write fallbacks in Card actions --- src/libs/actions/Card.ts | 35 ++++++------------- .../ReconciliationAccountSettingsPage.tsx | 3 ++ .../WorkspaceSettlementAccountPage.tsx | 3 ++ .../WorkspaceSettlementFrequencyPage.tsx | 3 ++ 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index cc2f20e88fa6..62cc89e98ac2 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -645,16 +645,11 @@ function revealVirtualCardDetails(cardID: number, validateCode: string): Promise }); } -function updateSettlementFrequency( - workspaceAccountID: number, - feedCountry: string | undefined, - settlementFrequency: ValueOf, - currentFrequency?: Date, -) { +function updateSettlementFrequency(workspaceAccountID: number, feedCountry: string, settlementFrequency: ValueOf, currentFrequency?: Date) { const monthlySettlementDate = settlementFrequency === CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY ? null : new Date(); - const settlementValue = feedCountry ? {[feedCountry]: {monthlySettlementDate}} : {monthlySettlementDate}; - const failureValue = feedCountry ? {[feedCountry]: {monthlySettlementDate: currentFrequency}} : {monthlySettlementDate: currentFrequency}; + const settlementValue = {[feedCountry]: {monthlySettlementDate}}; + const failureValue = {[feedCountry]: {monthlySettlementDate: currentFrequency}}; const optimisticData: Array> = [ { @@ -692,7 +687,7 @@ function updateSettlementAccount( domainName: string, workspaceAccountID: number, policyID: string, - feedCountry: string | undefined, + feedCountry: string, settlementBankAccountID?: number, currentSettlementBankAccountID?: number, ) { @@ -700,23 +695,15 @@ function updateSettlementAccount( return; } - const optimisticValue = feedCountry - ? {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: true} - : {paymentBankAccountID: settlementBankAccountID, isLoading: true}; + const optimisticValue = {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: true}; - const successValue = feedCountry ? {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: false} : {paymentBankAccountID: settlementBankAccountID, isLoading: false}; + const successValue = {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: false}; - const failureValue = feedCountry - ? { - [feedCountry]: {paymentBankAccountID: currentSettlementBankAccountID}, - isLoading: false, - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - } - : { - paymentBankAccountID: currentSettlementBankAccountID, - isLoading: false, - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }; + const failureValue = { + [feedCountry]: {paymentBankAccountID: currentSettlementBankAccountID}, + isLoading: false, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }; const optimisticData: Array> = [ { diff --git a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx index 464661270f68..2fa7896e1817 100644 --- a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx @@ -75,6 +75,9 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting }, [policyID, backTo, connection]); const selectBankAccount = (newBankAccountID?: number) => { + if (!feedCountry) { + return; + } updateSettlementAccount(domainName, defaultFundID, policyID, feedCountry, newBankAccountID, paymentBankAccountID); goBack(); }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx index 50cee4545175..d4c5cbfad39f 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx @@ -108,6 +108,9 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP const listOptions: BankAccountListItem[] = eligibleBankAccountsOptions.length > 0 ? eligibleBankAccountsOptions : [fallbackBankAccountOption]; const handleSelectAccount = (value: number) => { + if (!feedCountry) { + return; + } updateSettlementAccountCard(domainName, defaultFundID, policyID, feedCountry, value, paymentBankAccountID); Navigation.goBack(); }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx index 3b635c288eaf..ce8f78f215d0 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx @@ -59,6 +59,9 @@ function WorkspaceSettlementFrequencyPage({route}: WorkspaceSettlementFrequencyP }, [translate, shouldShowMonthlyOption, selectedFrequency]); const updateSettlementFrequency = (value: ValueOf) => { + if (!feedCountry) { + return; + } updateSettlementFrequencyUtil(defaultFundID, feedCountry, value, settings?.monthlySettlementDate); }; From b9f06c850559cd467f99977266746de514cd7672 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 10 Mar 2026 01:05:09 +0530 Subject: [PATCH 06/17] Fix deactivateTravelInvoicing test to remove root-level isEnabled expectations --- tests/unit/TravelInvoicingTest.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/TravelInvoicingTest.ts b/tests/unit/TravelInvoicingTest.ts index fb3fc1eee74b..247bc3582de8 100644 --- a/tests/unit/TravelInvoicingTest.ts +++ b/tests/unit/TravelInvoicingTest.ts @@ -296,7 +296,6 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - isEnabled: false, [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ isEnabled: false, }), @@ -320,7 +319,6 @@ describe('TravelInvoicing', () => { expect.objectContaining({ key: cardSettingsKey, value: expect.objectContaining({ - isEnabled: true, [CONST.TRAVEL.PROGRAM_TRAVEL_US]: expect.objectContaining({ isEnabled: true, }), From 7c03735c7331f66021ee57c6b978babe1f11fb7f Mon Sep 17 00:00:00 2001 From: allgandaf Date: Wed, 11 Mar 2026 13:47:21 +0530 Subject: [PATCH 07/17] Read previousPaymentBankAccountID and previousMonthlySettlementDate from travelSettings instead of raw cardSettings --- .../workspace/travel/WorkspaceTravelInvoicingSection.tsx | 4 ++-- .../travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx index 5c4efd80bbf4..efa18642c72f 100644 --- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx @@ -269,7 +269,7 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec clearTravelInvoicingSettlementAccountErrors(workspaceAccountID, cardSettings?.previousPaymentBankAccountID ?? null)} + onClose={() => clearTravelInvoicingSettlementAccountErrors(workspaceAccountID, travelSettings?.previousPaymentBankAccountID ?? null)} errorRowStyles={styles.mh2half} errorRowTextStyles={styles.mr3} > @@ -287,7 +287,7 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec clearTravelInvoicingSettlementFrequencyErrors(workspaceAccountID, cardSettings?.previousMonthlySettlementDate)} + onClose={() => clearTravelInvoicingSettlementFrequencyErrors(workspaceAccountID, travelSettings?.previousMonthlySettlementDate)} errorRowStyles={styles.mh2half} errorRowTextStyles={styles.mr3} > diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx index 5ba950f46fcd..1203dfce00b2 100644 --- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx @@ -88,7 +88,7 @@ function WorkspaceTravelInvoicingSettlementAccountPage({route}: WorkspaceTravelI return; } - const previousPaymentBankAccountID = cardSettings?.previousPaymentBankAccountID ?? cardSettings?.paymentBankAccountID; + const previousPaymentBankAccountID = travelSettings?.previousPaymentBankAccountID ?? travelSettings?.paymentBankAccountID; setTravelInvoicingSettlementAccount(policyID, workspaceAccountID, value, previousPaymentBankAccountID); Navigation.goBack(); }; From c23804181ff8a029bf3815f853f837eb13e530ec Mon Sep 17 00:00:00 2001 From: allgandaf Date: Thu, 12 Mar 2026 13:38:30 +0530 Subject: [PATCH 08/17] Address review: rewrite comments, restore flat fallback for legacy domains --- src/libs/CardUtils.ts | 14 +++++++++----- tests/unit/CardUtilsTest.ts | 10 ++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 3e58f4273d04..2a7c8cc49ce7 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1120,7 +1120,7 @@ function getCardSettings(cardSettings: OnyxEntry, feedCou const programSettings = cardSettings[programKey as keyof typeof cardSettings]; if (programSettings && typeof programSettings === 'object' && !Array.isArray(programSettings)) { // Nested program values take precedence — they are the authoritative source for - // program-specific fields once the backend sends the full nested format (Phase 2). + // program-specific fields (e.g. paymentBankAccountID, monthlySettlementDate). return {...cardSettings, ...(programSettings as ExpensifyCardSettingsBase)} as ExpensifyCardSettingsBase; } return undefined; @@ -1130,10 +1130,14 @@ function getCardSettings(cardSettings: OnyxEntry, feedCou return getMergedProgramSettings(feedCountry); } - // Auto-detect: try known card programs in priority order. - // Phase 2 guarantees all settings are nested under program keys - // (CURRENT for legacy domains, US/GB for new ones). - return getMergedProgramSettings(CONST.COUNTRY.US) ?? getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? getMergedProgramSettings(CONST.COUNTRY.GB); + // Auto-detect: try known card programs in priority order, then fall + // back to the flat root for legacy domains that haven't been migrated. + return ( + getMergedProgramSettings(CONST.COUNTRY.US) ?? + getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? + getMergedProgramSettings(CONST.COUNTRY.GB) ?? + (cardSettings as ExpensifyCardSettingsBase) + ); } function isCardPendingIssue(card?: Card) { diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index d70844210fb1..73cdb77c4a8a 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -3432,14 +3432,16 @@ describe('CardUtils', () => { expect(getCardSettings(null as unknown as undefined)).toBeUndefined(); }); - it('should return undefined when no nested keys exist and feedCountry is not provided', () => { + it('should fall back to flat root when no nested keys exist and feedCountry is not provided', () => { const result = getCardSettings(flatSettings); - expect(result).toBeUndefined(); + expect(result?.paymentBankAccountID).toBe(12345); + expect(result?.limit).toBe(50000); }); - it('should return undefined when no nested keys exist and feedCountry is undefined', () => { + it('should fall back to flat root when no nested keys exist and feedCountry is undefined', () => { const result = getCardSettings(flatSettings, undefined); - expect(result).toBeUndefined(); + expect(result?.paymentBankAccountID).toBe(12345); + expect(result?.limit).toBe(50000); }); it('should return merged root + nested when feedCountry matches a nested key', () => { From 0ae0bb59b71faa4a587a1d1200fa8b3696fe545a Mon Sep 17 00:00:00 2001 From: allgandaf Date: Thu, 12 Mar 2026 13:38:48 +0530 Subject: [PATCH 09/17] Pass feedCountry to getCardSettings() in settlement pages --- .../reconciliation/ReconciliationAccountSettingsPage.tsx | 2 +- .../workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx | 2 +- .../expensifyCard/WorkspaceSettlementFrequencyPage.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx index 2fa7896e1817..cca4f20d1a02 100644 --- a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx @@ -44,7 +44,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); const feedCountry = getCardFeedCountry(cardSettings); - const settings = getCardSettings(cardSettings); + const settings = getCardSettings(cardSettings, feedCountry); const paymentBankAccountID = settings?.paymentBankAccountID; const selectedBankAccount = useMemo(() => bankAccountList?.[paymentBankAccountID?.toString() ?? ''], [paymentBankAccountID, bankAccountList]); diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx index d4c5cbfad39f..b165e30e0c93 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx @@ -46,7 +46,7 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP const [bankAccountsList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); const feedCountry = getCardFeedCountry(cardSettings); - const settings = getCardSettings(cardSettings); + const settings = getCardSettings(cardSettings, feedCountry); const [continuousReconciliation] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION}${defaultFundID}`); const [reconciliationConnection] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION}${defaultFundID}`); const isUkEuCurrencySupported = useExpensifyCardUkEuSupported(policyID); diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx index ce8f78f215d0..fcbe187d9c66 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx @@ -30,7 +30,7 @@ function WorkspaceSettlementFrequencyPage({route}: WorkspaceSettlementFrequencyP const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); const feedCountry = getCardFeedCountry(cardSettings); - const settings = getCardSettings(cardSettings); + const settings = getCardSettings(cardSettings, feedCountry); const shouldShowMonthlyOption = settings?.isMonthlySettlementAllowed ?? false; const selectedFrequency = settings?.monthlySettlementDate ? CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.MONTHLY : CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY; From b259976d2cac6d146cf8789c590008a149ed8248 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Thu, 12 Mar 2026 13:46:16 +0530 Subject: [PATCH 10/17] Restore flat fallback in getCardSettings for legacy domains --- src/libs/CardUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 2a7c8cc49ce7..61d5daf2c234 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1130,8 +1130,9 @@ function getCardSettings(cardSettings: OnyxEntry, feedCou return getMergedProgramSettings(feedCountry); } - // Auto-detect: try known card programs in priority order, then fall - // back to the flat root for legacy domains that haven't been migrated. + // Auto-detect: try known card programs in priority order. + // Newer domains have settings nested under US/GB, legacy ones under + // CURRENT. Fall back to the flat root for domains the backend sends as-is. return ( getMergedProgramSettings(CONST.COUNTRY.US) ?? getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? From eb3a3b756c9272dbf5f18ac101d63ca4d5028a2e Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 16 Mar 2026 11:46:36 +0530 Subject: [PATCH 11/17] Add CardProgramKey type, rename getCardFeedCountry \u2192 getCardProgramKey, remove as-casts --- src/libs/CardUtils.ts | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 65ba454febfb..ebf9a36a1c4b 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1105,45 +1105,55 @@ function isExpensifyCardFullySetUp(policy?: OnyxEntry, cardSettings?: On return !!(policy?.areExpensifyCardsEnabled && cardSettings?.paymentBankAccountID); } +/** + * The set of valid card program keys used to key nested settings in ExpensifyCardSettings. + * 'US' and 'GB' are geo-based programs, 'CURRENT' is the legacy pre-2024 US program, + * and 'TRAVEL_US' is the travel invoicing program. These map directly to the keys + * the backend nests card settings under. + */ +type CardProgramKey = typeof CONST.COUNTRY.US | typeof CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT | typeof CONST.COUNTRY.GB | typeof CONST.TRAVEL.PROGRAM_TRAVEL_US; + /** * Detects which card program key exists in the card settings object. * Returns the first matching program key (US, CURRENT, or GB), or undefined if none found. * Used to determine the correct nested key for optimistic writes. */ -function getCardFeedCountry(cardSettings: OnyxEntry): string | undefined { +function getCardProgramKey(cardSettings: OnyxEntry): CardProgramKey | undefined { if (!cardSettings) { return undefined; } - const programKeys = [CONST.COUNTRY.US, CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT, CONST.COUNTRY.GB]; + const programKeys: CardProgramKey[] = [CONST.COUNTRY.US, CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT, CONST.COUNTRY.GB]; return programKeys.find((key) => { - const value = cardSettings[key as keyof typeof cardSettings]; - return value && typeof value === 'object' && !Array.isArray(value); + const value = cardSettings[key]; + return value !== null && typeof value === 'object' && !Array.isArray(value); }); } -function getCardSettings(cardSettings: OnyxEntry, feedCountry?: string): ExpensifyCardSettingsBase | undefined { +function getCardSettings(cardSettings: OnyxEntry, programKey?: CardProgramKey): ExpensifyCardSettingsBase | undefined { if (!cardSettings) { return undefined; } - const getMergedProgramSettings = (programKey: string): ExpensifyCardSettingsBase | undefined => { - const programSettings = cardSettings[programKey as keyof typeof cardSettings]; + const getMergedProgramSettings = (key: CardProgramKey): ExpensifyCardSettingsBase | undefined => { + const programSettings = cardSettings[key]; if (programSettings && typeof programSettings === 'object' && !Array.isArray(programSettings)) { // Nested program values take precedence — they are the authoritative source for // program-specific fields (e.g. paymentBankAccountID, monthlySettlementDate). - return {...cardSettings, ...(programSettings as ExpensifyCardSettingsBase)} as ExpensifyCardSettingsBase; + return {...cardSettings, ...programSettings} as ExpensifyCardSettingsBase; } return undefined; }; - if (feedCountry) { - return getMergedProgramSettings(feedCountry); + if (programKey) { + return getMergedProgramSettings(programKey); } // Auto-detect: try known card programs in priority order. - // Newer domains have settings nested under US/GB, legacy ones under - // CURRENT. Fall back to the flat root for domains the backend sends as-is. + // Newer domains nest settings under US/GB, legacy ones under CURRENT. + // The flat root fallback supports domains that the backend still sends without nesting + // (e.g. older accounts that haven't been migrated). Writes always go to the nested key + // (via getCardProgramKey), so this flat path is read-only display fallback only. return ( getMergedProgramSettings(CONST.COUNTRY.US) ?? getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? @@ -1490,7 +1500,7 @@ export { hasIssuedExpensifyCard, isExpensifyCardFullySetUp, getCardSettings, - getCardFeedCountry, + getCardProgramKey, filterAllInactiveCards, filterInactiveCards, isCardPendingIssue, @@ -1527,4 +1537,4 @@ export { isExpiredCard, }; -export type {CompanyCardFeedIcons, CompanyCardBankIcons}; +export type {CompanyCardFeedIcons, CompanyCardBankIcons, CardProgramKey}; From 0900bbb6b871af67d181e6eb415690e9ff22af99 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 16 Mar 2026 11:48:40 +0530 Subject: [PATCH 12/17] Type updateSettlementFrequency and updateSettlementAccount with CardProgramKey --- src/libs/actions/Card.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 62cc89e98ac2..7f848f18db17 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -22,6 +22,7 @@ import type { UpdateExpensifyCardTitleParams, } from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import type {CardProgramKey} from '@libs/CardUtils'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; @@ -645,7 +646,12 @@ function revealVirtualCardDetails(cardID: number, validateCode: string): Promise }); } -function updateSettlementFrequency(workspaceAccountID: number, feedCountry: string, settlementFrequency: ValueOf, currentFrequency?: Date) { +function updateSettlementFrequency( + workspaceAccountID: number, + feedCountry: CardProgramKey, + settlementFrequency: ValueOf, + currentFrequency?: Date, +) { const monthlySettlementDate = settlementFrequency === CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY ? null : new Date(); const settlementValue = {[feedCountry]: {monthlySettlementDate}}; @@ -687,7 +693,7 @@ function updateSettlementAccount( domainName: string, workspaceAccountID: number, policyID: string, - feedCountry: string, + feedCountry: CardProgramKey, settlementBankAccountID?: number, currentSettlementBankAccountID?: number, ) { From 468768f20f76e58e01d4a328212287187859a723 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 16 Mar 2026 11:51:33 +0530 Subject: [PATCH 13/17] Update callers to use getCardProgramKey, rename feedCountry \u2192 programKey, add Log.alert on missing key --- .../ReconciliationAccountSettingsPage.tsx | 12 +++++++----- .../expensifyCard/WorkspaceSettlementAccountPage.tsx | 12 +++++++----- .../WorkspaceSettlementFrequencyPage.tsx | 12 +++++++----- tests/unit/CardUtilsTest.ts | 6 ++++-- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx index cca4f20d1a02..517506ed80e1 100644 --- a/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx @@ -12,7 +12,8 @@ import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {getConnectionNameFromRouteParam} from '@libs/AccountingUtils'; import {getLastFourDigits} from '@libs/BankAccountUtils'; -import {getCardFeedCountry, getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils'; +import {getCardProgramKey, getCardSettings, getEligibleBankAccountsForCard} from '@libs/CardUtils'; +import Log from '@libs/Log'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getDomainNameForPolicy} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; @@ -43,8 +44,8 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); - const feedCountry = getCardFeedCountry(cardSettings); - const settings = getCardSettings(cardSettings, feedCountry); + const programKey = getCardProgramKey(cardSettings); + const settings = getCardSettings(cardSettings, programKey); const paymentBankAccountID = settings?.paymentBankAccountID; const selectedBankAccount = useMemo(() => bankAccountList?.[paymentBankAccountID?.toString() ?? ''], [paymentBankAccountID, bankAccountList]); @@ -75,10 +76,11 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting }, [policyID, backTo, connection]); const selectBankAccount = (newBankAccountID?: number) => { - if (!feedCountry) { + if (!programKey) { + Log.alert('[ReconciliationAccountSettingsPage] selectBankAccount called without a detected card program key'); return; } - updateSettlementAccount(domainName, defaultFundID, policyID, feedCountry, newBankAccountID, paymentBankAccountID); + updateSettlementAccount(domainName, defaultFundID, policyID, programKey, newBankAccountID, paymentBankAccountID); goBack(); }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx index b165e30e0c93..c9406a46f5c6 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx @@ -15,7 +15,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getRouteParamForConnection} from '@libs/AccountingUtils'; import {openPolicyAccountingPage} from '@libs/actions/PolicyConnections'; import {getLastFourDigits} from '@libs/BankAccountUtils'; -import {getCardFeedCountry, getCardSettings, getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils'; +import {getCardProgramKey, getCardSettings, getEligibleBankAccountsForCard, getEligibleBankAccountsForUkEuCard} from '@libs/CardUtils'; +import Log from '@libs/Log'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getDomainNameForPolicy} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; @@ -45,8 +46,8 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const [bankAccountsList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); - const feedCountry = getCardFeedCountry(cardSettings); - const settings = getCardSettings(cardSettings, feedCountry); + const programKey = getCardProgramKey(cardSettings); + const settings = getCardSettings(cardSettings, programKey); const [continuousReconciliation] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION}${defaultFundID}`); const [reconciliationConnection] = useOnyx(`${ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION}${defaultFundID}`); const isUkEuCurrencySupported = useExpensifyCardUkEuSupported(policyID); @@ -108,10 +109,11 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP const listOptions: BankAccountListItem[] = eligibleBankAccountsOptions.length > 0 ? eligibleBankAccountsOptions : [fallbackBankAccountOption]; const handleSelectAccount = (value: number) => { - if (!feedCountry) { + if (!programKey) { + Log.alert('[WorkspaceSettlementAccountPage] handleSelectAccount called without a detected card program key'); return; } - updateSettlementAccountCard(domainName, defaultFundID, policyID, feedCountry, value, paymentBankAccountID); + updateSettlementAccountCard(domainName, defaultFundID, policyID, programKey, value, paymentBankAccountID); Navigation.goBack(); }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx index fcbe187d9c66..b2f178b5f1c4 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx @@ -10,7 +10,8 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateSettlementFrequency as updateSettlementFrequencyUtil} from '@libs/actions/Card'; -import {getCardFeedCountry, getCardSettings} from '@libs/CardUtils'; +import {getCardProgramKey, getCardSettings} from '@libs/CardUtils'; +import Log from '@libs/Log'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -29,8 +30,8 @@ function WorkspaceSettlementFrequencyPage({route}: WorkspaceSettlementFrequencyP const defaultFundID = useDefaultFundID(policyID); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); - const feedCountry = getCardFeedCountry(cardSettings); - const settings = getCardSettings(cardSettings, feedCountry); + const programKey = getCardProgramKey(cardSettings); + const settings = getCardSettings(cardSettings, programKey); const shouldShowMonthlyOption = settings?.isMonthlySettlementAllowed ?? false; const selectedFrequency = settings?.monthlySettlementDate ? CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.MONTHLY : CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY; @@ -59,10 +60,11 @@ function WorkspaceSettlementFrequencyPage({route}: WorkspaceSettlementFrequencyP }, [translate, shouldShowMonthlyOption, selectedFrequency]); const updateSettlementFrequency = (value: ValueOf) => { - if (!feedCountry) { + if (!programKey) { + Log.alert('[WorkspaceSettlementFrequencyPage] updateSettlementFrequency called without a detected card program key'); return; } - updateSettlementFrequencyUtil(defaultFundID, feedCountry, value, settings?.monthlySettlementDate); + updateSettlementFrequencyUtil(defaultFundID, programKey, value, settings?.monthlySettlementDate); }; return ( diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 13cd929da7b3..2fdff8b4d5b8 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -3592,7 +3592,8 @@ describe('CardUtils', () => { }); it('should return undefined when feedCountry key does not exist', () => { - const result = getCardSettings(nestedSettings, 'CA'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = getCardSettings(nestedSettings, 'CA' as any); expect(result).toBeUndefined(); }); @@ -3604,7 +3605,8 @@ describe('CardUtils', () => { }); it('should return undefined for primitive values as feedCountry', () => { - const result = getCardSettings(nestedSettings, 'limit'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = getCardSettings(nestedSettings, 'limit' as any); expect(result).toBeUndefined(); }); From d8123a59509832fcb6f3fde0a04d3e4f450d0fc3 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Mon, 16 Mar 2026 11:59:05 +0530 Subject: [PATCH 14/17] Fix test casts: use unknown as CardProgramKey instead of any --- tests/unit/CardUtilsTest.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 2fdff8b4d5b8..945b355a454b 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -65,6 +65,7 @@ import { splitMaskedCardNumber, supportsPINManagementFeatures, } from '@src/libs/CardUtils'; +import type {CardProgramKey} from '@src/libs/CardUtils'; import DateUtils from '@src/libs/DateUtils'; import type { BankAccountList, @@ -3592,8 +3593,7 @@ describe('CardUtils', () => { }); it('should return undefined when feedCountry key does not exist', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = getCardSettings(nestedSettings, 'CA' as any); + const result = getCardSettings(nestedSettings, 'CA' as unknown as CardProgramKey); expect(result).toBeUndefined(); }); @@ -3605,8 +3605,7 @@ describe('CardUtils', () => { }); it('should return undefined for primitive values as feedCountry', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = getCardSettings(nestedSettings, 'limit' as any); + const result = getCardSettings(nestedSettings, 'limit' as unknown as CardProgramKey); expect(result).toBeUndefined(); }); From 37e842da311a38f1084f437ab69b8fddb31c96c0 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 17 Mar 2026 01:47:29 +0530 Subject: [PATCH 15/17] Rename feedCountry to programKey in updateSettlementFrequency and updateSettlementAccount --- src/libs/actions/Card.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 7f848f18db17..556905a54f31 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -648,14 +648,14 @@ function revealVirtualCardDetails(cardID: number, validateCode: string): Promise function updateSettlementFrequency( workspaceAccountID: number, - feedCountry: CardProgramKey, + programKey: CardProgramKey, settlementFrequency: ValueOf, currentFrequency?: Date, ) { const monthlySettlementDate = settlementFrequency === CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY ? null : new Date(); - const settlementValue = {[feedCountry]: {monthlySettlementDate}}; - const failureValue = {[feedCountry]: {monthlySettlementDate: currentFrequency}}; + const settlementValue = {[programKey]: {monthlySettlementDate}}; + const failureValue = {[programKey]: {monthlySettlementDate: currentFrequency}}; const optimisticData: Array> = [ { @@ -693,7 +693,7 @@ function updateSettlementAccount( domainName: string, workspaceAccountID: number, policyID: string, - feedCountry: CardProgramKey, + programKey: CardProgramKey, settlementBankAccountID?: number, currentSettlementBankAccountID?: number, ) { @@ -701,12 +701,12 @@ function updateSettlementAccount( return; } - const optimisticValue = {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: true}; + const optimisticValue = {[programKey]: {paymentBankAccountID: settlementBankAccountID}, isLoading: true}; - const successValue = {[feedCountry]: {paymentBankAccountID: settlementBankAccountID}, isLoading: false}; + const successValue = {[programKey]: {paymentBankAccountID: settlementBankAccountID}, isLoading: false}; const failureValue = { - [feedCountry]: {paymentBankAccountID: currentSettlementBankAccountID}, + [programKey]: {paymentBankAccountID: currentSettlementBankAccountID}, isLoading: false, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }; From 3d7681395a5cfe65574f8025a3a1a4237d88f904 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 17 Mar 2026 11:23:16 +0530 Subject: [PATCH 16/17] Fix typecheck: cast feedCountry to CardProgramKey --- src/libs/CardUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index ab5ac10d66f6..644a75bbdce4 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1449,7 +1449,7 @@ function getCardCurrency(card?: OnyxEntry, cardSettings?: OnyxEntry Date: Tue, 17 Mar 2026 11:27:26 +0530 Subject: [PATCH 17/17] Rename feedCountry local var to programKey in getCardCurrency --- src/libs/CardUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 644a75bbdce4..e8ae7a8a045b 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1449,21 +1449,21 @@ function getCardCurrency(card?: OnyxEntry, cardSettings?: OnyxEntry