From 7c41a8f36950c2c9eef0e062d4a23348b843944d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 23 Mar 2026 13:22:59 +0800 Subject: [PATCH 1/4] removed getPolicy usages when updating invoice company name & web --- src/libs/actions/Policy/Policy.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 6114b063e041..2a88a3ec6970 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -6812,11 +6812,7 @@ function clearAllPolicies() { } } -function updateInvoiceCompanyName(policyID: string, companyName: string) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(policyID); - +function updateInvoiceCompanyName(policyID: string, companyName: string, currentCompanyName: string | undefined) { const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -6852,7 +6848,7 @@ function updateInvoiceCompanyName(policyID: string, companyName: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { invoice: { - companyName: policy?.invoice?.companyName, + companyName: currentCompanyName, pendingFields: { companyName: null, }, @@ -6869,11 +6865,7 @@ function updateInvoiceCompanyName(policyID: string, companyName: string) { API.write(WRITE_COMMANDS.UPDATE_INVOICE_COMPANY_NAME, parameters, {optimisticData, successData, failureData}); } -function updateInvoiceCompanyWebsite(policyID: string, companyWebsite: string) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(policyID); - +function updateInvoiceCompanyWebsite(policyID: string, companyWebsite: string, currentCompanyWebsite: string | undefined) { const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -6909,7 +6901,7 @@ function updateInvoiceCompanyWebsite(policyID: string, companyWebsite: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { invoice: { - companyWebsite: policy?.invoice?.companyWebsite, + companyWebsite: currentCompanyWebsite, pendingFields: { companyWebsite: null, }, From e18de434eccbe134fdbc9d043de0075b1835b1ff Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 23 Mar 2026 13:23:08 +0800 Subject: [PATCH 2/4] removed getPolicy usages when enabling more features --- src/libs/actions/Policy/Policy.ts | 65 +-- .../SubscriptionPlanCardActionButton.tsx | 2 +- .../workspace/WorkspaceMoreFeaturesPage.tsx | 6 +- .../PolicyDistanceRatesSettingsPage.tsx | 2 +- .../downgrade/WorkspaceDowngradePage.tsx | 2 +- .../WorkspaceInvoicingDetailsName.tsx | 2 +- .../WorkspaceInvoicingDetailsWebsite.tsx | 2 +- .../upgrade/WorkspaceUpgradePage.tsx | 7 +- tests/actions/PolicyTest.ts | 369 +++++++++++++++++- 9 files changed, 409 insertions(+), 48 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 2a88a3ec6970..920c089fb5f5 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -4611,7 +4611,9 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) { API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, parameters, onyxData); } -function enablePolicyTaxes(policyID: string, enabled: boolean) { +function enablePolicyTaxes(policyID: string, enabled: true, currentTaxRates: TaxRatesWithDefault | undefined): void; +function enablePolicyTaxes(policyID: string, enabled: false): void; +function enablePolicyTaxes(policyID: string, enabled: boolean, currentTaxRates?: TaxRatesWithDefault) { const defaultTaxRates: TaxRatesWithDefault = CONST.DEFAULT_TAX; const taxRatesData: OnyxData = { optimisticData: [ @@ -4666,10 +4668,7 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { }, ], }; - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(policyID); - const shouldAddDefaultTaxRatesData = (!policy?.taxRates || isEmptyObject(policy.taxRates)) && enabled; + const shouldAddDefaultTaxRatesData = (!currentTaxRates || isEmptyObject(currentTaxRates)) && enabled; const optimisticData: Array> = [ { @@ -4733,10 +4732,14 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { } } -function enablePolicyWorkflows(policyID: string, enabled: boolean) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(policyID); +function enablePolicyWorkflows( + policyID: string, + enabled: boolean, + currentApprovalMode: Policy['approvalMode'], + currentAutoReporting: Policy['autoReporting'], + currentHarvesting: Policy['harvesting'], + currentReimbursementChoice: Policy['reimbursementChoice'], +) { const onyxData: OnyxData = { optimisticData: [ { @@ -4795,10 +4798,10 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { areWorkflowsEnabled: !enabled, ...(!enabled ? { - approvalMode: policy?.approvalMode, - autoReporting: policy?.autoReporting, - harvesting: policy?.harvesting, - reimbursementChoice: policy?.reimbursementChoice, + approvalMode: currentApprovalMode, + autoReporting: currentAutoReporting, + harvesting: currentHarvesting, + reimbursementChoice: currentReimbursementChoice, } : {}), pendingFields: { @@ -4834,10 +4837,12 @@ const DISABLED_MAX_EXPENSE_VALUES: Pick, enabled: boolean, shouldGoBack = true, policyData?: PolicyData) { + if (!policy) { + return; + } + + const policyID = policy.id; const onyxData: OnyxData = { optimisticData: [ { @@ -4912,10 +4917,7 @@ function enablePolicyRules(policyID: string, enabled: boolean, shouldGoBack = tr } } -function enableDistanceRequestTax(policyID: string, customUnitName: string, customUnitID: string, attributes: Attributes) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(policyID); +function enableDistanceRequestTax(policyID: string, customUnitName: string, customUnitID: string, attributes: Attributes, currentAttributes: Attributes | undefined) { const onyxData: OnyxData = { optimisticData: [ { @@ -4955,7 +4957,7 @@ function enableDistanceRequestTax(policyID: string, customUnitName: string, cust value: { customUnits: { [customUnitID]: { - attributes: policy?.customUnits ? policy?.customUnits[customUnitID].attributes : null, + attributes: currentAttributes ?? null, errorFields: { taxEnabled: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, @@ -5306,10 +5308,12 @@ function setForeignCurrencyDefault(policyID: string, taxCode: string) { API.write(WRITE_COMMANDS.SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT, parameters, onyxData); } -function upgradeToCorporate(policyID: string, featureName?: string) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(policyID); +function upgradeToCorporate(policy: OnyxEntry, featureName?: string) { + if (!policy) { + return; + } + + const policyID = policy.id; const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -5365,10 +5369,7 @@ function upgradeToCorporate(policyID: string, featureName?: string) { API.write(WRITE_COMMANDS.UPGRADE_TO_CORPORATE, parameters, {optimisticData, successData, failureData}); } -function downgradeToTeam(policyID: string) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(policyID); +function downgradeToTeam(policyID: string, currentType: Policy['type'], currentIsAttendeeTrackingEnabled: Policy['isAttendeeTrackingEnabled']) { const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -5397,8 +5398,8 @@ function downgradeToTeam(policyID: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { isPendingDowngrade: false, - type: policy?.type, - isAttendeeTrackingEnabled: policy?.isAttendeeTrackingEnabled, + type: currentType, + isAttendeeTrackingEnabled: currentIsAttendeeTrackingEnabled, }, }, ]; diff --git a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx index c46f9e217cd8..ce0cc92381be 100644 --- a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx +++ b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx @@ -79,7 +79,7 @@ function SubscriptionPlanCardActionButton({subscriptionPlan, isFromComparisonMod if (planType === CONST.POLICY.TYPE.CORPORATE) { if (canPerformUpgrade && !!policy?.id) { - upgradeToCorporate(policy.id); + upgradeToCorporate(policy); closeComparisonModal?.(); return; } diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index addc686fb408..b41e8f3ecda5 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -300,7 +300,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (!policyID) { return; } - enablePolicyWorkflows(policyID, isEnabled); + enablePolicyWorkflows(policyID, isEnabled, policy?.approvalMode, policy?.autoReporting, policy?.harvesting, policy?.reimbursementChoice); }, disabled: isSmartLimitEnabled, disabledAction: onDisabledWorkflowPress, @@ -326,7 +326,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))); return; } - enablePolicyRules(policyID, isEnabled, undefined, policyData); + enablePolicyRules(policy, isEnabled, undefined, policyData); }, onPress: () => { if (!policyID) { @@ -411,7 +411,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (!policyID) { return; } - enablePolicyTaxes(policyID, isEnabled); + enablePolicyTaxes(policyID, isEnabled, policy?.taxRates); }, onPress: () => { if (!policyID) { diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index 5f6e1f6d277f..d60d53eb0e8f 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -80,7 +80,7 @@ function PolicyDistanceRatesSettingsPage({route}: PolicyDistanceRatesSettingsPag return; } const attributes = {...customUnit?.attributes, taxEnabled: isOn}; - enableDistanceRequestTax(policyID, customUnit.name, customUnit.customUnitID, attributes); + enableDistanceRequestTax(policyID, customUnit.name, customUnit.customUnitID, attributes, customUnit.attributes); }; return ( diff --git a/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx b/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx index 1318879656b7..91b5c98a4db9 100644 --- a/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx +++ b/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx @@ -53,7 +53,7 @@ function WorkspaceDowngradePage({route}: WorkspaceDowngradePageProps) { setIsDowngradeWarningModalOpen(true); return; } - downgradeToTeam(policy.id); + downgradeToTeam(policy.id, policy.type, policy.isAttendeeTrackingEnabled); }; const onClose = () => { diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx index 0e30fa985430..b7f99b1ef93a 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx @@ -31,7 +31,7 @@ function WorkspaceInvoicingDetailsName({route}: WorkspaceInvoicingDetailsNamePro const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const submit = (values: FormOnyxValues) => { - updateInvoiceCompanyName(policyID, values[INPUT_IDS.COMPANY_NAME]); + updateInvoiceCompanyName(policyID, values[INPUT_IDS.COMPANY_NAME], policy?.invoice?.companyName); Navigation.goBack(); }; diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx index bffd27f053dc..bd5a6ecd1fcb 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx @@ -34,7 +34,7 @@ function WorkspaceInvoicingDetailsWebsite({route}: WorkspaceInvoicingDetailsWebs const submit = (values: FormOnyxValues) => { const companyWebsite = Str.sanitizeURL(values[INPUT_IDS.COMPANY_WEBSITE], CONST.COMPANY_WEBSITE_DEFAULT_SCHEME); - updateInvoiceCompanyWebsite(policyID, companyWebsite); + updateInvoiceCompanyWebsite(policyID, companyWebsite, policy?.invoice?.companyWebsite); Navigation.goBack(); }; diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx index 7deba2a1d5c8..c9997b341190 100644 --- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx +++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx @@ -131,7 +131,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { return; } - upgradeToCorporate(policy.id, feature?.name); + upgradeToCorporate(policy, feature?.name); }; // useCallback is needed here because confirmUpgrade is passed as a prop to child components; @@ -186,7 +186,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { } break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.id: - enablePolicyRules(policyID, true, false, policyDataRef.current); + enablePolicyRules(policy, true, false, policyDataRef.current); break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.id: enableCompanyCards(policyID, true, false); @@ -203,8 +203,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { categoryId, feature, perDiemCustomUnit?.customUnitID, - policy?.connections?.xero?.config, - policy?.connections?.xero?.data, + policy, policyID, qboConfig?.syncClasses, qboConfig?.syncCustomers, diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 9a52f3aceabb..62fa5b76b463 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -1857,7 +1857,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); // When a policy is upgradeToCorporate - Policy.upgradeToCorporate(fakePolicy.id); + Policy.upgradeToCorporate(fakePolicy); await waitForBatchedUpdates(); const policy: OnyxEntry = await new Promise((resolve) => { @@ -1885,7 +1885,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); // When upgrading to corporate - Policy.upgradeToCorporate(fakePolicy.id); + Policy.upgradeToCorporate(fakePolicy); await waitForBatchedUpdates(); const policy: OnyxEntry = await new Promise((resolve) => { @@ -1912,7 +1912,7 @@ describe('actions/Policy', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); // When upgrading to corporate - Policy.upgradeToCorporate(fakePolicy.id); + Policy.upgradeToCorporate(fakePolicy); await waitForBatchedUpdates(); const policy: OnyxEntry = await new Promise((resolve) => { @@ -1929,6 +1929,59 @@ describe('actions/Policy', () => { }); }); + describe('downgradeToTeam', () => { + it('should downgrade to team optimistically and succeed', async () => { + // Given a policy with type corporate + const policyID = '1'; + const fakePolicy = { + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: true, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When downgradeToTeam is called + mockFetch.pause(); + Policy.downgradeToTeam(policyID, fakePolicy.type, fakePolicy.isAttendeeTrackingEnabled); + await waitForBatchedUpdates(); + + // Then type should be team optimistically + let updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.type).toBe(CONST.POLICY.TYPE.TEAM); + expect(updatedPolicy?.isPendingDowngrade).toBe(true); + + // When the fetch resumes and succeeds + await mockFetch.resume(); + + // And the success data should clear the pending downgrade + updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.isPendingDowngrade).toBe(false); + }); + + it('should revert downgrade when fail', async () => { + // Given a policy with type corporate + const policyID = '1'; + const fakePolicy = { + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: true, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When downgradeToTeam is called and fails + mockFetch.fail(); + Policy.downgradeToTeam(policyID, fakePolicy.type, fakePolicy.isAttendeeTrackingEnabled); + await waitForBatchedUpdates(); + + // Then type should be reverted to corporate + const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.type).toBe(CONST.POLICY.TYPE.CORPORATE); + expect(updatedPolicy?.isPendingDowngrade).toBe(false); + }); + }); + describe('enablePolicyRules', () => { it('should not reset preventSelfApproval when the rule feature is turned off', async () => { (fetch as MockFetch)?.pause?.(); @@ -1942,7 +1995,7 @@ describe('actions/Policy', () => { await waitForBatchedUpdates(); // Disable the rule feature - Policy.enablePolicyRules(fakePolicy.id, false); + Policy.enablePolicyRules(fakePolicy, false); await waitForBatchedUpdates(); let policy: OnyxEntry = await new Promise((resolve) => { @@ -3443,4 +3496,312 @@ describe('actions/Policy', () => { ); }); }); + + describe('enablePolicyTaxes', () => { + it('should enable policy taxes optimistically and succeed', async () => { + // Given a policy with taxes disabled + const policyID = '1'; + const fakePolicy = { + id: policyID, + tax: { + trackingEnabled: false, + }, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When enablePolicyTaxes is called to enable taxes + mockFetch.pause(); + Policy.enablePolicyTaxes(policyID, true, undefined); + await waitForBatchedUpdates(); + + // Then the policy tax tracking should be updated optimistically + let updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.tax?.trackingEnabled).toBe(true); + expect(updatedPolicy?.pendingFields?.tax).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + // And taxRates should be updated with default taxes + expect(updatedPolicy?.taxRates?.taxes).toBeDefined(); + + // When the fetch resumes and succeeds + await mockFetch.resume(); + + // And the success data should clear the pending fields + updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.pendingFields?.tax).toBeNull(); + for (const tax of Object.values(updatedPolicy?.taxRates?.taxes ?? {})) { + expect(tax.pendingAction).toBeNull(); + } + }); + + it('should revert policy taxes when fail', async () => { + // Given a policy with taxes disabled + const policyID = '1'; + const fakePolicy = { + id: policyID, + tax: { + trackingEnabled: false, + }, + taxRates: {}, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When enablePolicyTaxes is called and fails + mockFetch.fail(); + Policy.enablePolicyTaxes(policyID, true, undefined); + await waitForBatchedUpdates(); + + // Then the policy tax tracking should be reverted + const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.tax?.trackingEnabled).toBe(false); + expect(updatedPolicy?.pendingFields?.tax).toBeNull(); + expect(updatedPolicy?.taxRates).toBeUndefined(); + }); + }); + describe('enablePolicyWorkflows', () => { + it('should enable policy workflows optimistically and succeed', async () => { + // Given a policy with workflows disabled + const policyID = '1'; + const fakePolicy = { + id: policyID, + areWorkflowsEnabled: false, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When enablePolicyWorkflows is called to enable workflows + mockFetch.pause(); + Policy.enablePolicyWorkflows(policyID, true, undefined, undefined, undefined, undefined); + await waitForBatchedUpdates(); + + // Then workflows should be enabled optimistically + let updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.areWorkflowsEnabled).toBe(true); + expect(updatedPolicy?.pendingFields?.areWorkflowsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + // When the fetch resumes and succeeds + await mockFetch.resume(); + + // And the success data should clear the pending fields + updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.pendingFields?.areWorkflowsEnabled).toBeNull(); + }); + + it('should revert policy workflows when fail', async () => { + // Given a policy with workflows enabled + const policyID = '1'; + const fakePolicy = { + id: policyID, + areWorkflowsEnabled: true, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + autoReporting: true, + harvesting: {enabled: true, jobID: 123}, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When enablePolicyWorkflows is called to disable workflows and fails + mockFetch.fail(); + Policy.enablePolicyWorkflows(policyID, false, fakePolicy.approvalMode, fakePolicy.autoReporting, fakePolicy.harvesting, fakePolicy.reimbursementChoice); + await waitForBatchedUpdates(); + + // Then workflows should be reverted to enabled and other fields restored + const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.areWorkflowsEnabled).toBe(true); + expect(updatedPolicy?.approvalMode).toBe(CONST.POLICY.APPROVAL_MODE.ADVANCED); + expect(updatedPolicy?.autoReporting).toBe(true); + expect(updatedPolicy?.harvesting?.enabled).toBe(true); + expect(updatedPolicy?.reimbursementChoice).toBe(CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES); + expect(updatedPolicy?.pendingFields?.areWorkflowsEnabled).toBeNull(); + }); + }); + describe('enableDistanceRequestTax', () => { + it('should enable distance request tax optimistically and succeed', async () => { + // Given a policy with a custom unit and tax disabled + const policyID = '1'; + const customUnitID = 'unit_1'; + const customUnitName = 'Distance'; + const initialAttributes = {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, taxEnabled: false}; + const fakePolicy = { + id: policyID, + customUnits: { + [customUnitID]: { + customUnitID, + name: customUnitName, + attributes: initialAttributes, + }, + }, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When enableDistanceRequestTax is called to enable tax + mockFetch.pause(); + const newAttributes = {...initialAttributes, taxEnabled: true}; + Policy.enableDistanceRequestTax(policyID, customUnitName, customUnitID, newAttributes, initialAttributes); + await waitForBatchedUpdates(); + + // Then taxEnabled should be true optimistically + let updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.customUnits?.[customUnitID]?.attributes?.taxEnabled).toBe(true); + expect(updatedPolicy?.customUnits?.[customUnitID]?.pendingFields?.taxEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + // When the fetch resumes and succeeds + await mockFetch.resume(); + + // And the success data should clear the pending fields + updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.customUnits?.[customUnitID]?.pendingFields?.taxEnabled).toBeNull(); + }); + + it('should revert distance request tax when fail', async () => { + // Given a policy with a custom unit and tax enabled + const policyID = '1'; + const customUnitID = 'unit_1'; + const customUnitName = 'Distance'; + const initialAttributes = {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, taxEnabled: true}; + const fakePolicy = { + id: policyID, + customUnits: { + [customUnitID]: { + customUnitID, + name: customUnitName, + attributes: initialAttributes, + }, + }, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When enableDistanceRequestTax is called to disable tax and fails + mockFetch.fail(); + const newAttributes = {...initialAttributes, taxEnabled: false}; + Policy.enableDistanceRequestTax(policyID, customUnitName, customUnitID, newAttributes, initialAttributes); + await waitForBatchedUpdates(); + + // Then taxEnabled should be reverted to true + const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.customUnits?.[customUnitID]?.attributes?.taxEnabled).toBe(true); + expect(updatedPolicy?.customUnits?.[customUnitID]?.pendingFields?.taxEnabled).toBeNull(); + }); + }); + + describe('updateInvoiceCompanyName', () => { + it('should update invoice company name optimistically and succeed', async () => { + // Given a policy with an invoice company name + const policyID = '1'; + const initialCompanyName = 'Initial Corp'; + const fakePolicy = { + id: policyID, + invoice: { + companyName: initialCompanyName, + }, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When updateInvoiceCompanyName is called + mockFetch.pause(); + const newCompanyName = 'New Corp'; + Policy.updateInvoiceCompanyName(policyID, newCompanyName, initialCompanyName); + await waitForBatchedUpdates(); + + // Then companyName should be updated optimistically + let updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.invoice?.companyName).toBe(newCompanyName); + expect(updatedPolicy?.invoice?.pendingFields?.companyName).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + // When the fetch resumes and succeeds + await mockFetch.resume(); + + // And the success data should clear the pending fields + updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.invoice?.pendingFields?.companyName).toBeNull(); + }); + + it('should revert invoice company name when fail', async () => { + // Given a policy with an invoice company name + const policyID = '1'; + const initialCompanyName = 'Initial Corp'; + const fakePolicy = { + id: policyID, + invoice: { + companyName: initialCompanyName, + }, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When updateInvoiceCompanyName is called and fails + mockFetch.fail(); + const newCompanyName = 'New Corp'; + Policy.updateInvoiceCompanyName(policyID, newCompanyName, initialCompanyName); + await waitForBatchedUpdates(); + + // Then companyName should be reverted + const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.invoice?.companyName).toBe(initialCompanyName); + expect(updatedPolicy?.invoice?.pendingFields?.companyName).toBeNull(); + }); + }); + + describe('updateInvoiceCompanyWebsite', () => { + it('should update invoice company website optimistically and succeed', async () => { + // Given a policy with an invoice company website + const policyID = '1'; + const initialWebsite = 'https://initial.com'; + const fakePolicy = { + id: policyID, + invoice: { + companyWebsite: initialWebsite, + }, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When updateInvoiceCompanyWebsite is called + mockFetch.pause(); + const newWebsite = 'https://new.com'; + Policy.updateInvoiceCompanyWebsite(policyID, newWebsite, initialWebsite); + await waitForBatchedUpdates(); + + // Then companyWebsite should be updated optimistically + let updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.invoice?.companyWebsite).toBe(newWebsite); + expect(updatedPolicy?.invoice?.pendingFields?.companyWebsite).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + // When the fetch resumes and succeeds + await mockFetch.resume(); + + // And the success data should clear the pending fields + updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.invoice?.pendingFields?.companyWebsite).toBeNull(); + }); + + it('should revert invoice company website when fail', async () => { + // Given a policy with an invoice company website + const policyID = '1'; + const initialWebsite = 'https://initial.com'; + const fakePolicy = { + id: policyID, + invoice: { + companyWebsite: initialWebsite, + }, + }; + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await waitForBatchedUpdates(); + + // When updateInvoiceCompanyWebsite is called and fails + mockFetch.fail(); + const newWebsite = 'https://new.com'; + Policy.updateInvoiceCompanyWebsite(policyID, newWebsite, initialWebsite); + await waitForBatchedUpdates(); + + // Then companyWebsite should be reverted + const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + expect(updatedPolicy?.invoice?.companyWebsite).toBe(initialWebsite); + expect(updatedPolicy?.invoice?.pendingFields?.companyWebsite).toBeNull(); + }); + }); }); From 61713ce0c861864fb8f7fc5b4089942281633126 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 23 Mar 2026 14:06:15 +0800 Subject: [PATCH 3/4] fix test --- src/libs/actions/Policy/Policy.ts | 2 +- tests/actions/PolicyTest.ts | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 920c089fb5f5..6ddf4ba491a6 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -4663,7 +4663,7 @@ function enablePolicyTaxes(policyID: string, enabled: boolean, currentTaxRates?: onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - taxRates: undefined, + taxRates: null, }, }, ], diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 62fa5b76b463..b5ec5945eb08 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -3527,9 +3527,9 @@ describe('actions/Policy', () => { // And the success data should clear the pending fields updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - expect(updatedPolicy?.pendingFields?.tax).toBeNull(); + expect(updatedPolicy?.pendingFields?.tax).toBeUndefined(); for (const tax of Object.values(updatedPolicy?.taxRates?.taxes ?? {})) { - expect(tax.pendingAction).toBeNull(); + expect(tax.pendingAction).toBeUndefined(); } }); @@ -3554,7 +3554,7 @@ describe('actions/Policy', () => { // Then the policy tax tracking should be reverted const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); expect(updatedPolicy?.tax?.trackingEnabled).toBe(false); - expect(updatedPolicy?.pendingFields?.tax).toBeNull(); + expect(updatedPolicy?.pendingFields?.tax).toBeUndefined(); expect(updatedPolicy?.taxRates).toBeUndefined(); }); }); @@ -3584,7 +3584,7 @@ describe('actions/Policy', () => { // And the success data should clear the pending fields updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - expect(updatedPolicy?.pendingFields?.areWorkflowsEnabled).toBeNull(); + expect(updatedPolicy?.pendingFields?.areWorkflowsEnabled).toBeUndefined(); }); it('should revert policy workflows when fail', async () => { @@ -3613,7 +3613,7 @@ describe('actions/Policy', () => { expect(updatedPolicy?.autoReporting).toBe(true); expect(updatedPolicy?.harvesting?.enabled).toBe(true); expect(updatedPolicy?.reimbursementChoice).toBe(CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES); - expect(updatedPolicy?.pendingFields?.areWorkflowsEnabled).toBeNull(); + expect(updatedPolicy?.pendingFields?.areWorkflowsEnabled).toBeUndefined(); }); }); describe('enableDistanceRequestTax', () => { @@ -3652,7 +3652,7 @@ describe('actions/Policy', () => { // And the success data should clear the pending fields updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - expect(updatedPolicy?.customUnits?.[customUnitID]?.pendingFields?.taxEnabled).toBeNull(); + expect(updatedPolicy?.customUnits?.[customUnitID]?.pendingFields?.taxEnabled).toBeUndefined(); }); it('should revert distance request tax when fail', async () => { @@ -3683,7 +3683,6 @@ describe('actions/Policy', () => { // Then taxEnabled should be reverted to true const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); expect(updatedPolicy?.customUnits?.[customUnitID]?.attributes?.taxEnabled).toBe(true); - expect(updatedPolicy?.customUnits?.[customUnitID]?.pendingFields?.taxEnabled).toBeNull(); }); }); @@ -3717,7 +3716,7 @@ describe('actions/Policy', () => { // And the success data should clear the pending fields updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - expect(updatedPolicy?.invoice?.pendingFields?.companyName).toBeNull(); + expect(updatedPolicy?.invoice?.pendingFields?.companyName).toBeUndefined(); }); it('should revert invoice company name when fail', async () => { @@ -3742,7 +3741,7 @@ describe('actions/Policy', () => { // Then companyName should be reverted const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); expect(updatedPolicy?.invoice?.companyName).toBe(initialCompanyName); - expect(updatedPolicy?.invoice?.pendingFields?.companyName).toBeNull(); + expect(updatedPolicy?.invoice?.pendingFields?.companyName).toBeUndefined(); }); }); @@ -3776,7 +3775,7 @@ describe('actions/Policy', () => { // And the success data should clear the pending fields updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - expect(updatedPolicy?.invoice?.pendingFields?.companyWebsite).toBeNull(); + expect(updatedPolicy?.invoice?.pendingFields?.companyWebsite).toBeUndefined(); }); it('should revert invoice company website when fail', async () => { @@ -3801,7 +3800,7 @@ describe('actions/Policy', () => { // Then companyWebsite should be reverted const updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); expect(updatedPolicy?.invoice?.companyWebsite).toBe(initialWebsite); - expect(updatedPolicy?.invoice?.pendingFields?.companyWebsite).toBeNull(); + expect(updatedPolicy?.invoice?.pendingFields?.companyWebsite).toBeUndefined(); }); }); }); From ef0068aa31b471710ca881898a5ccb0330f1a6e2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 23 Mar 2026 14:07:50 +0800 Subject: [PATCH 4/4] fix typing --- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index b41e8f3ecda5..804787da0116 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -411,7 +411,11 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (!policyID) { return; } - enablePolicyTaxes(policyID, isEnabled, policy?.taxRates); + if (isEnabled) { + enablePolicyTaxes(policyID, true, policy?.taxRates); + return; + } + enablePolicyTaxes(policyID, false); }, onPress: () => { if (!policyID) {