From d1575b7d9cd7224fdc1d8ea7870e0ffc761e6e5a Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 13 Apr 2026 09:53:17 +0200 Subject: [PATCH 01/11] Fix issue: Home - Connect to accounting task is checked off when connection fails #87730 --- .../hooks/useGettingStartedItems.ts | 10 ++++- .../unit/hooks/useGettingStartedItems.test.ts | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 75d7d723c4b1..7aa967a764a9 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -4,7 +4,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {hasAccountingConnections, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getValidConnectedIntegration, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; import {enablePolicyCategories} from '@userActions/Policy/Category'; import {enableCompanyCards, enablePolicyConnections, enablePolicyRules} from '@userActions/Policy/Policy'; @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; +import type {ConnectionName} from '@src/types/onyx/Policy'; type GettingStartedItem = { key: string; @@ -82,7 +83,12 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { items.push({ key: 'connectAccounting', label: translate('homePage.gettingStartedSection.connectAccounting', {integrationName}), - isComplete: hasAccountingConnections(policy), + // Use getValidConnectedIntegration for currently-healthy connections. Additionally check + // successfulDate so that a task completed by a once-working integration stays checked + // even if the connection later breaks (e.g. auth expires). successfulDate is never + // cleared by subsequent error syncs due to Onyx deep-merge semantics. + isComplete: + !!getValidConnectedIntegration(policy, [reportedIntegration as ConnectionName]) || !!policy?.connections?.[reportedIntegration as ConnectionName]?.lastSync?.successfulDate, route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), isFeatureEnabled: policy.areConnectionsEnabled, enableFeature: () => enablePolicyConnections(activePolicyID, true, false), diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index c563d01d112d..96976fd706a3 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -269,6 +269,47 @@ describe('useGettingStartedItems', () => { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, data: {}, + lastSync: {isConnected: true}, + }, + } as Policy['connections'], + }, + }); + + const {result} = renderHook(() => useGettingStartedItems()); + + const connectItem = result.current.items.find((item) => item.key === 'connectAccounting'); + expect(connectItem?.isComplete).toBe(true); + }); + + it('should not be completed when the initial connection attempt failed', async () => { + await setupManageTeamScenario({ + accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, + policy: { + connections: { + [CONST.POLICY.CONNECTIONS.NAME.QBO]: { + config: {}, + data: {}, + lastSync: {isConnected: false}, + }, + } as Policy['connections'], + }, + }); + + const {result} = renderHook(() => useGettingStartedItems()); + + const connectItem = result.current.items.find((item) => item.key === 'connectAccounting'); + expect(connectItem?.isComplete).toBe(false); + }); + + it('should stay completed when a previously successful connection later breaks', async () => { + await setupManageTeamScenario({ + accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, + policy: { + connections: { + [CONST.POLICY.CONNECTIONS.NAME.QBO]: { + config: {}, + data: {}, + lastSync: {isConnected: false, successfulDate: '2024-01-01'}, }, } as Policy['connections'], }, From 38fc883b67dbbc5d23f2d84cab87370625ca9e76 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 14 Apr 2026 12:00:03 +0200 Subject: [PATCH 02/11] Refactor isDirectConnect variable logic --- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + .../hooks/useGettingStartedItems.ts | 19 ++++++++----------- 11 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 562881c135ee..444fcd963081 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1033,6 +1033,7 @@ const translations: TranslationDeepObject = { title: 'Erste Schritte', createWorkspace: 'Workspace erstellen', connectAccounting: ({integrationName}: {integrationName: string}) => `Mit ${integrationName} verbinden`, + connectAccountingDefault: 'Mit Buchhaltung verbinden', customizeCategories: 'Buchhaltungskategorien anpassen', linkCompanyCards: 'Firmenkarten verknüpfen', setupRules: 'Ausgabelimits einrichten', diff --git a/src/languages/en.ts b/src/languages/en.ts index bb3359169c1a..39fd78e6b409 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1083,6 +1083,7 @@ const translations = { title: 'Getting started', createWorkspace: 'Create a workspace', connectAccounting: ({integrationName}: {integrationName: string}) => `Connect to ${integrationName}`, + connectAccountingDefault: 'Connect to accounting', customizeCategories: 'Customize accounting categories', linkCompanyCards: 'Link company cards', setupRules: 'Set up spend rules', diff --git a/src/languages/es.ts b/src/languages/es.ts index c5d180f627ab..0475fa12552d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -968,6 +968,7 @@ const translations: TranslationDeepObject = { title: 'Primeros pasos', createWorkspace: 'Crear un espacio de trabajo', connectAccounting: ({integrationName}: {integrationName: string}) => `Conectar con ${integrationName}`, + connectAccountingDefault: 'Conectar a contabilidad', customizeCategories: 'Personalizar categorías contables', linkCompanyCards: 'Vincular tarjetas corporativas', setupRules: 'Configurar reglas de gasto', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 94e116020874..19a14f31af58 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1049,6 +1049,7 @@ const translations: TranslationDeepObject = { title: 'Premiers pas', createWorkspace: 'Créer un espace de travail', connectAccounting: ({integrationName}: {integrationName: string}) => `Se connecter à ${integrationName}`, + connectAccountingDefault: 'Connecter à la comptabilité', customizeCategories: 'Personnaliser les catégories comptables', linkCompanyCards: 'Lier des cartes d’entreprise', setupRules: 'Configurer les règles de dépense', diff --git a/src/languages/it.ts b/src/languages/it.ts index 4e3f1d08de42..de8a5b828aae 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1046,6 +1046,7 @@ const translations: TranslationDeepObject = { title: 'Per iniziare', createWorkspace: 'Crea uno spazio di lavoro', connectAccounting: ({integrationName}: {integrationName: string}) => `Connetti a ${integrationName}`, + connectAccountingDefault: 'Connetti alla contabilità', customizeCategories: 'Personalizza le categorie contabili', linkCompanyCards: 'Collega carte aziendali', setupRules: 'Configura le regole di spesa', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 228e9d37a34d..890139a96091 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1029,6 +1029,7 @@ const translations: TranslationDeepObject = { title: 'はじめに', createWorkspace: 'ワークスペースを作成', connectAccounting: ({integrationName}: {integrationName: string}) => `${integrationName}に接続する`, + connectAccountingDefault: '会計ソフトに接続', customizeCategories: '会計カテゴリをカスタマイズする', linkCompanyCards: '会社カードを連携', setupRules: '支出ルールを設定', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 4e0bad97ba01..4376291040ef 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1045,6 +1045,7 @@ const translations: TranslationDeepObject = { title: 'Aan de slag', createWorkspace: 'Maak een werkruimte', connectAccounting: ({integrationName}: {integrationName: string}) => `Verbind met ${integrationName}`, + connectAccountingDefault: 'Verbind met boekhouding', customizeCategories: 'Boekhoudcategorieën aanpassen', linkCompanyCards: 'Bedrijfspassen koppelen', setupRules: 'Uitgavenregels instellen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index e0afd753dc80..c1e7788fdab2 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1046,6 +1046,7 @@ const translations: TranslationDeepObject = { title: 'Pierwsze kroki', createWorkspace: 'Utwórz przestrzeń roboczą', connectAccounting: ({integrationName}: {integrationName: string}) => `Połącz z ${integrationName}`, + connectAccountingDefault: 'Połącz z księgowością', customizeCategories: 'Dostosuj kategorie księgowe', linkCompanyCards: 'Połącz firmowe karty', setupRules: 'Skonfiguruj zasady wydatków', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 4eed9ac82355..25fdad9d3ccb 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1044,6 +1044,7 @@ const translations: TranslationDeepObject = { title: 'Introdução', createWorkspace: 'Criar um workspace', connectAccounting: ({integrationName}: {integrationName: string}) => `Conectar ao ${integrationName}`, + connectAccountingDefault: 'Conectar à contabilidade', customizeCategories: 'Personalizar categorias contábeis', linkCompanyCards: 'Vincular cartões corporativos', setupRules: 'Configurar regras de gasto', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 038d18646407..543512b72815 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1012,6 +1012,7 @@ const translations: TranslationDeepObject = { title: '入门', createWorkspace: '创建工作区', connectAccounting: ({integrationName}: {integrationName: string}) => `连接到 ${integrationName}`, + connectAccountingDefault: '连接会计系统', customizeCategories: '自定义会计类别', linkCompanyCards: '关联公司卡', setupRules: '设置消费规则', diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 7aa967a764a9..ecb66b454a1e 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -4,7 +4,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getValidConnectedIntegration, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getValidConnectedIntegration, hasAccountingConnections, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; import {enablePolicyCategories} from '@userActions/Policy/Category'; import {enableCompanyCards, enablePolicyConnections, enablePolicyRules} from '@userActions/Policy/Policy'; @@ -12,7 +12,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; -import type {ConnectionName} from '@src/types/onyx/Policy'; type GettingStartedItem = { key: string; @@ -76,19 +75,17 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { route: shouldUseNarrowLayout ? ROUTES.WORKSPACE_INITIAL.getRoute(activePolicyID, Navigation.getActiveRoute()) : ROUTES.WORKSPACE_OVERVIEW.getRoute(activePolicyID), }); - const isDirectConnect = !!reportedIntegration && DIRECT_CONNECT_INTEGRATIONS.has(reportedIntegration); + const isDirectConnect = policy.areConnectionsEnabled && !hasAccountingConnections(policy); if (isDirectConnect) { - const integrationName = CONST.ONBOARDING_ACCOUNTING_MAPPING[reportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] ?? String(reportedIntegration); + const integrationName = + reportedIntegration && DIRECT_CONNECT_INTEGRATIONS.has(reportedIntegration) + ? (CONST.ONBOARDING_ACCOUNTING_MAPPING[reportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] ?? String(reportedIntegration)) + : undefined; items.push({ key: 'connectAccounting', - label: translate('homePage.gettingStartedSection.connectAccounting', {integrationName}), - // Use getValidConnectedIntegration for currently-healthy connections. Additionally check - // successfulDate so that a task completed by a once-working integration stays checked - // even if the connection later breaks (e.g. auth expires). successfulDate is never - // cleared by subsequent error syncs due to Onyx deep-merge semantics. - isComplete: - !!getValidConnectedIntegration(policy, [reportedIntegration as ConnectionName]) || !!policy?.connections?.[reportedIntegration as ConnectionName]?.lastSync?.successfulDate, + label: integrationName ? translate('homePage.gettingStartedSection.connectAccounting', {integrationName}) : translate('homePage.gettingStartedSection.connectAccountingDefault'), + isComplete: !!getValidConnectedIntegration(policy), route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), isFeatureEnabled: policy.areConnectionsEnabled, enableFeature: () => enablePolicyConnections(activePolicyID, true, false), From 0b936406da7eadfb751dd3aced77bec8cc398c4f Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 14 Apr 2026 15:09:15 +0200 Subject: [PATCH 03/11] Fix connect accounting task never completing and handle missing reportedIntegration --- .../hooks/useGettingStartedItems.ts | 15 +++++++------ .../unit/hooks/useGettingStartedItems.test.ts | 21 ++++++++++++++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index ecb66b454a1e..853425540e5e 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -4,7 +4,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getValidConnectedIntegration, hasAccountingConnections, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getValidConnectedIntegration, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; import {enablePolicyCategories} from '@userActions/Policy/Category'; import {enableCompanyCards, enablePolicyConnections, enablePolicyRules} from '@userActions/Policy/Policy'; @@ -75,17 +75,16 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { route: shouldUseNarrowLayout ? ROUTES.WORKSPACE_INITIAL.getRoute(activePolicyID, Navigation.getActiveRoute()) : ROUTES.WORKSPACE_OVERVIEW.getRoute(activePolicyID), }); - const isDirectConnect = policy.areConnectionsEnabled && !hasAccountingConnections(policy); + const isDirectConnect = !!reportedIntegration && DIRECT_CONNECT_INTEGRATIONS.has(reportedIntegration); - if (isDirectConnect) { - const integrationName = - reportedIntegration && DIRECT_CONNECT_INTEGRATIONS.has(reportedIntegration) - ? (CONST.ONBOARDING_ACCOUNTING_MAPPING[reportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] ?? String(reportedIntegration)) - : undefined; + if (isDirectConnect || !reportedIntegration) { + const integrationName = isDirectConnect + ? (CONST.ONBOARDING_ACCOUNTING_MAPPING[reportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] ?? String(reportedIntegration)) + : undefined; items.push({ key: 'connectAccounting', label: integrationName ? translate('homePage.gettingStartedSection.connectAccounting', {integrationName}) : translate('homePage.gettingStartedSection.connectAccountingDefault'), - isComplete: !!getValidConnectedIntegration(policy), + isComplete: !!getValidConnectedIntegration(policy) || Object.values(policy?.connections ?? {}).some((conn) => !!conn?.lastSync?.successfulDate), route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), isFeatureEnabled: policy.areConnectionsEnabled, enableFeature: () => enablePolicyConnections(activePolicyID, true, false), diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index 96976fd706a3..803cd8b3563d 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -329,10 +329,29 @@ describe('useGettingStartedItems', () => { const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories'); expect(categoriesItem).toBeUndefined(); }); + + it('should show generic "Connect to accounting" when reportedIntegration is not set (e.g. cache cleared)', async () => { + await setupManageTeamScenario(); + + const {result} = renderHook(() => useGettingStartedItems()); + + const connectItem = result.current.items.find((item) => item.key === 'connectAccounting'); + expect(connectItem).toBeDefined(); + expect(connectItem?.label).toContain('connectAccountingDefault'); + }); + + it('should not show the categories row when reportedIntegration is not set', async () => { + await setupManageTeamScenario(); + + const {result} = renderHook(() => useGettingStartedItems()); + + const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories'); + expect(categoriesItem).toBeUndefined(); + }); }); describe('row 2b - Customize accounting categories', () => { - const categoriesIntegrations = ['sap', 'oracle', 'microsoftDynamics', 'other', 'none', null]; + const categoriesIntegrations = ['sap', 'oracle', 'microsoftDynamics', 'other', 'none']; it.each(categoriesIntegrations)('should show "Customize accounting categories" when accounting choice is %s', async (accounting) => { await setupManageTeamScenario({accounting}); From 090f873acb1bc7415982aaf580e26dc0910022b9 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 14 Apr 2026 15:28:48 +0200 Subject: [PATCH 04/11] Address review comments for Home - Connect to accounting task --- .../hooks/useGettingStartedItems.ts | 4 ++-- .../unit/hooks/useGettingStartedItems.test.ts | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 853425540e5e..14328ebe07b4 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -4,7 +4,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getValidConnectedIntegration, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getValidConnectedIntegration, hasAccountingConnections, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; import {enablePolicyCategories} from '@userActions/Policy/Category'; import {enableCompanyCards, enablePolicyConnections, enablePolicyRules} from '@userActions/Policy/Policy'; @@ -77,7 +77,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { const isDirectConnect = !!reportedIntegration && DIRECT_CONNECT_INTEGRATIONS.has(reportedIntegration); - if (isDirectConnect || !reportedIntegration) { + if (isDirectConnect || (!reportedIntegration && hasAccountingConnections(policy))) { const integrationName = isDirectConnect ? (CONST.ONBOARDING_ACCOUNTING_MAPPING[reportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] ?? String(reportedIntegration)) : undefined; diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index 803cd8b3563d..b8b643f33588 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -330,8 +330,18 @@ describe('useGettingStartedItems', () => { expect(categoriesItem).toBeUndefined(); }); - it('should show generic "Connect to accounting" when reportedIntegration is not set (e.g. cache cleared)', async () => { - await setupManageTeamScenario(); + it('should show generic "Connect to accounting" when reportedIntegration is not set but a connection already exists (e.g. cache cleared after connecting)', async () => { + await setupManageTeamScenario({ + policy: { + connections: { + [CONST.POLICY.CONNECTIONS.NAME.QBO]: { + config: {}, + data: {}, + lastSync: {isConnected: true}, + }, + } as Policy['connections'], + }, + }); const {result} = renderHook(() => useGettingStartedItems()); @@ -340,13 +350,13 @@ describe('useGettingStartedItems', () => { expect(connectItem?.label).toContain('connectAccountingDefault'); }); - it('should not show the categories row when reportedIntegration is not set', async () => { + it('should show "Customize accounting categories" when reportedIntegration is not set and no connections exist (e.g. cache cleared before connecting)', async () => { await setupManageTeamScenario(); const {result} = renderHook(() => useGettingStartedItems()); const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories'); - expect(categoriesItem).toBeUndefined(); + expect(categoriesItem).toBeDefined(); }); }); From 4d1329cbc19995e25c06418b2f8a668a2ee38a23 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 14 Apr 2026 20:41:32 +0200 Subject: [PATCH 05/11] Check if features are enabled to display them --- .../hooks/useGettingStartedItems.ts | 19 ++++----- .../unit/hooks/useGettingStartedItems.test.ts | 42 ++++++++----------- .../GettingStartedSectionTest.tsx | 18 +++++--- 3 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 14328ebe07b4..04b7c7471d07 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -4,14 +4,13 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getValidConnectedIntegration, hasAccountingConnections, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getValidConnectedIntegration, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; -import {enablePolicyCategories} from '@userActions/Policy/Category'; -import {enableCompanyCards, enablePolicyConnections, enablePolicyRules} from '@userActions/Policy/Policy'; +import {enableCompanyCards} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; type GettingStartedItem = { key: string; @@ -77,7 +76,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { const isDirectConnect = !!reportedIntegration && DIRECT_CONNECT_INTEGRATIONS.has(reportedIntegration); - if (isDirectConnect || (!reportedIntegration && hasAccountingConnections(policy))) { + if (policy?.areAccountingEnabled) { const integrationName = isDirectConnect ? (CONST.ONBOARDING_ACCOUNTING_MAPPING[reportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] ?? String(reportedIntegration)) : undefined; @@ -86,17 +85,15 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { label: integrationName ? translate('homePage.gettingStartedSection.connectAccounting', {integrationName}) : translate('homePage.gettingStartedSection.connectAccountingDefault'), isComplete: !!getValidConnectedIntegration(policy) || Object.values(policy?.connections ?? {}).some((conn) => !!conn?.lastSync?.successfulDate), route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), - isFeatureEnabled: policy.areConnectionsEnabled, - enableFeature: () => enablePolicyConnections(activePolicyID, true, false), }); - } else { + } + + if (policy?.areCategoriesEnabled) { items.push({ key: 'customizeCategories', label: translate('homePage.gettingStartedSection.customizeCategories'), isComplete: hasCustomCategories(policyCategories), route: ROUTES.WORKSPACE_CATEGORIES.getRoute(activePolicyID), - isFeatureEnabled: policy.areCategoriesEnabled, - enableFeature: () => enablePolicyCategories({policy, categories: policyCategories ?? {}, tags: {}, reports: [], transactionsAndViolations: {}}, true, false), }); } @@ -115,8 +112,6 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { label: translate('homePage.gettingStartedSection.setupRules'), isComplete: hasNonDefaultRules(policy), route: ROUTES.WORKSPACE_RULES.getRoute(activePolicyID), - isFeatureEnabled: policy.areRulesEnabled, - enableFeature: () => enablePolicyRules(policy, true, false), }); } diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index b8b643f33588..c8659d8cae3d 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -231,7 +231,7 @@ describe('useGettingStartedItems', () => { ]; it.each(directConnectIntegrations)('should show "Connect to $name" when user selected $key in onboarding', async ({key, name}) => { - await setupManageTeamScenario({accounting: key}); + await setupManageTeamScenario({accounting: key, policy: {areAccountingEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -241,7 +241,7 @@ describe('useGettingStartedItems', () => { }); it('should navigate to workspace accounting route', async () => { - await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO}); + await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areAccountingEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -252,7 +252,7 @@ describe('useGettingStartedItems', () => { it('should be not completed when workspace has no accounting connection', async () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, - policy: {connections: undefined}, + policy: {areAccountingEnabled: true, connections: undefined}, }); const {result} = renderHook(() => useGettingStartedItems()); @@ -265,6 +265,7 @@ describe('useGettingStartedItems', () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: { + areAccountingEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -285,6 +286,7 @@ describe('useGettingStartedItems', () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: { + areAccountingEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -305,6 +307,7 @@ describe('useGettingStartedItems', () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: { + areAccountingEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -322,7 +325,7 @@ describe('useGettingStartedItems', () => { }); it('should not show the categories row when showing the connect row', async () => { - await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO}); + await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areAccountingEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -333,6 +336,7 @@ describe('useGettingStartedItems', () => { it('should show generic "Connect to accounting" when reportedIntegration is not set but a connection already exists (e.g. cache cleared after connecting)', async () => { await setupManageTeamScenario({ policy: { + areAccountingEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -351,7 +355,7 @@ describe('useGettingStartedItems', () => { }); it('should show "Customize accounting categories" when reportedIntegration is not set and no connections exist (e.g. cache cleared before connecting)', async () => { - await setupManageTeamScenario(); + await setupManageTeamScenario({policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -364,7 +368,7 @@ describe('useGettingStartedItems', () => { const categoriesIntegrations = ['sap', 'oracle', 'microsoftDynamics', 'other', 'none']; it.each(categoriesIntegrations)('should show "Customize accounting categories" when accounting choice is %s', async (accounting) => { - await setupManageTeamScenario({accounting}); + await setupManageTeamScenario({accounting, policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -373,7 +377,7 @@ describe('useGettingStartedItems', () => { }); it('should navigate to workspace categories route', async () => { - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -382,7 +386,7 @@ describe('useGettingStartedItems', () => { }); it('should not show the connect accounting row when showing the categories row', async () => { - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -391,7 +395,7 @@ describe('useGettingStartedItems', () => { }); it('should be not completed when workspace has only default categories', async () => { - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -415,7 +419,7 @@ describe('useGettingStartedItems', () => { }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${POLICY_ID}`, customCategories); - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -511,18 +515,6 @@ describe('useGettingStartedItems', () => { expect(rulesItem).toBeUndefined(); }); - it('should have isFeatureEnabled=true when rules feature is enabled', async () => { - await setupManageTeamScenario({ - accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, - policy: {areRulesEnabled: true}, - }); - - const {result} = renderHook(() => useGettingStartedItems()); - - const rulesItem = result.current.items.find((item) => item.key === 'setupRules'); - expect(rulesItem?.isFeatureEnabled).toBe(true); - }); - it('should not be included in items when rules feature is not enabled', async () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, @@ -602,7 +594,7 @@ describe('useGettingStartedItems', () => { it('should return items in the correct order: createWorkspace, accounting/categories, companyCards, rules', async () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, - policy: {areCompanyCardsEnabled: true, areRulesEnabled: true}, + policy: {areAccountingEnabled: true, areCompanyCardsEnabled: true, areRulesEnabled: true}, }); const {result} = renderHook(() => useGettingStartedItems()); @@ -614,7 +606,7 @@ describe('useGettingStartedItems', () => { it('should return items in the correct order with categories instead of connect', async () => { await setupManageTeamScenario({ accounting: 'none', - policy: {areCompanyCardsEnabled: true, areRulesEnabled: true}, + policy: {areCategoriesEnabled: true, areCompanyCardsEnabled: true, areRulesEnabled: true}, }); const {result} = renderHook(() => useGettingStartedItems()); @@ -626,7 +618,7 @@ describe('useGettingStartedItems', () => { it('should contain three rows when areRulesEnabled is false', async () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, - policy: {areCompanyCardsEnabled: false, areRulesEnabled: false}, + policy: {areAccountingEnabled: true, areCompanyCardsEnabled: false, areRulesEnabled: false}, }); const {result} = renderHook(() => useGettingStartedItems()); diff --git a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx index 2977e2672188..4cca2e2eb0bb 100644 --- a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx +++ b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx @@ -43,6 +43,8 @@ async function setManageTeamUserState(overrides?: { integration?: string | null; areCompanyCardsEnabled?: boolean; areRulesEnabled?: boolean; + areAccountingEnabled?: boolean; + areCategoriesEnabled?: boolean; hasAccountingConnection?: boolean; hasCustomCategories?: boolean; hasCompanyCardConnection?: boolean; @@ -65,6 +67,8 @@ async function setManageTeamUserState(overrides?: { role: CONST.POLICY.ROLE.ADMIN, areCompanyCardsEnabled: overrides?.areCompanyCardsEnabled ?? true, areRulesEnabled: overrides?.areRulesEnabled ?? true, + areAccountingEnabled: overrides?.areAccountingEnabled, + areCategoriesEnabled: overrides?.areCategoriesEnabled, }; if (overrides?.hasAccountingConnection) { @@ -214,7 +218,7 @@ describe('GettingStartedSection', () => { }); it('shows "Connect to [system]" row for QBO integration', async () => { - await setManageTeamUserState({integration: 'quickbooksOnline'}); + await setManageTeamUserState({integration: 'quickbooksOnline', areAccountingEnabled: true}); renderGettingStartedSection(); @@ -222,7 +226,7 @@ describe('GettingStartedSection', () => { }); it('shows "Connect to [system]" row for Xero integration', async () => { - await setManageTeamUserState({integration: 'xero'}); + await setManageTeamUserState({integration: 'xero', areAccountingEnabled: true}); renderGettingStartedSection(); @@ -230,7 +234,7 @@ describe('GettingStartedSection', () => { }); it('shows "Customize accounting categories" for non-direct-connect integrations', async () => { - await setManageTeamUserState({integration: 'other'}); + await setManageTeamUserState({integration: 'other', areCategoriesEnabled: true}); renderGettingStartedSection(); @@ -239,7 +243,7 @@ describe('GettingStartedSection', () => { }); it('shows "Customize accounting categories" when no integration is selected', async () => { - await setManageTeamUserState({integration: 'none'}); + await setManageTeamUserState({integration: 'none', areCategoriesEnabled: true}); renderGettingStartedSection(); @@ -281,6 +285,7 @@ describe('GettingStartedSection', () => { it('renders rows in the expected order: workspace, accounting, cards, rules', async () => { await setManageTeamUserState({ integration: 'quickbooksOnline', + areAccountingEnabled: true, areCompanyCardsEnabled: true, areRulesEnabled: true, }); @@ -314,6 +319,7 @@ describe('GettingStartedSection', () => { it('accounting row is checked when workspace has a successful connection', async () => { await setManageTeamUserState({ integration: 'quickbooksOnline', + areAccountingEnabled: true, hasAccountingConnection: true, }); @@ -336,7 +342,7 @@ describe('GettingStartedSection', () => { }); it('navigates to workspace accounting when "Connect to [system]" row is pressed', async () => { - await setManageTeamUserState({integration: 'quickbooksOnline'}); + await setManageTeamUserState({integration: 'quickbooksOnline', areAccountingEnabled: true}); renderGettingStartedSection(); @@ -347,7 +353,7 @@ describe('GettingStartedSection', () => { }); it('navigates to workspace categories when "Customize categories" row is pressed', async () => { - await setManageTeamUserState({integration: 'other'}); + await setManageTeamUserState({integration: 'other', areCategoriesEnabled: true}); renderGettingStartedSection(); From 6249b4b4df198821f3d3176e50746685c23e1428 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 15 Apr 2026 13:44:14 +0200 Subject: [PATCH 06/11] Fix Home - Set up spend rules task not completing on rule changes --- src/CONST/index.ts | 9 ++++ src/libs/PolicyUtils.ts | 49 ++++++++++++++++--- .../hooks/useGettingStartedItems.ts | 4 +- .../GettingStartedSectionTest.tsx | 2 +- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 81c50c890eb3..4b01b77625de 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3796,6 +3796,15 @@ const CONST = { DEFAULT_MAX_EXPENSE_AMOUNT: 200000, DEFAULT_MAX_AMOUNT_NO_RECEIPT: 2500, DEFAULT_MAX_AMOUNT_NO_ITEMIZED_RECEIPT: 7500, + DEFAULT_PROHIBITED_EXPENSES: { + alcohol: false, + hotelIncidentals: false, + gambling: true, + tobacco: false, + adultEntertainment: true, + }, + DEFAULT_BILLABLE: false, + DEFAULT_REIMBURSABLE: true, DEFAULT_TAG_LIST: { Tag: { name: 'Tag', diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index ce8f511615b1..72cfd093f89b 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -692,14 +692,12 @@ function hasCustomCategories(policyCategories: OnyxEntry): boo } /** - * Checks if a policy has any non-default rules configured. - * Defaults are: no approval/expense/coding rules and no custom rules text. + * Checks if a policy has any rules configured (structured rules, individual expense limits, or prohibited expenses). */ -function hasNonDefaultRules(policy: OnyxEntry): boolean { +function hasConfiguredRules(policy: OnyxEntry): boolean { if (!policy) { return false; } - const hasCustomRules = !!policy.customRules && policy.customRules.trim().length > 0; const {rules} = policy; @@ -707,7 +705,46 @@ function hasNonDefaultRules(policy: OnyxEntry): boolean { const hasExpenseRules = !!rules?.expenseRules && rules.expenseRules.length > 0; const hasCodingRules = !!rules?.codingRules && Object.keys(rules.codingRules).length > 0; - return hasCustomRules || hasApprovalRules || hasExpenseRules || hasCodingRules; + const hasModifiedMaxExpenseAmount = + !!policy.maxExpenseAmount && policy.maxExpenseAmount !== CONST.DISABLED_MAX_EXPENSE_VALUE && policy.maxExpenseAmount !== CONST.POLICY.DEFAULT_MAX_EXPENSE_AMOUNT; + const hasModifiedMaxExpenseAge = !!policy.maxExpenseAge && policy.maxExpenseAge !== CONST.DISABLED_MAX_EXPENSE_VALUE && policy.maxExpenseAge !== CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE; + const hasModifiedMaxExpenseAmountNoReceipt = + !!policy.maxExpenseAmountNoReceipt && + policy.maxExpenseAmountNoReceipt !== CONST.DISABLED_MAX_EXPENSE_VALUE && + policy.maxExpenseAmountNoReceipt !== CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_RECEIPT; + const hasModifiedMaxExpenseAmountNoItemizedReceipt = + !!policy.maxExpenseAmountNoItemizedReceipt && + policy.maxExpenseAmountNoItemizedReceipt !== CONST.DISABLED_MAX_EXPENSE_VALUE && + policy.maxExpenseAmountNoItemizedReceipt !== CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_ITEMIZED_RECEIPT; + + const hasModifiedBillable = !!policy.defaultBillable; + const hasModifiedReimbursable = policy.defaultReimbursable === false; + const hasEReceiptsEnabled = !!policy.eReceipts; + const hasRequireCompanyCardsEnabled = !!policy.requireCompanyCardsEnabled; + + const {prohibitedExpenses} = policy; + const hasModifiedProhibitedExpenses = + !!prohibitedExpenses && + Object.entries(CONST.POLICY.DEFAULT_PROHIBITED_EXPENSES).some(([key, defaultValue]) => { + const value = prohibitedExpenses[key as keyof typeof CONST.POLICY.DEFAULT_PROHIBITED_EXPENSES]; + return value !== undefined && value !== defaultValue; + }); + + return ( + hasCustomRules || + hasApprovalRules || + hasExpenseRules || + hasCodingRules || + hasModifiedMaxExpenseAmount || + hasModifiedMaxExpenseAge || + hasModifiedMaxExpenseAmountNoReceipt || + hasModifiedMaxExpenseAmountNoItemizedReceipt || + hasModifiedBillable || + hasModifiedReimbursable || + hasEReceiptsEnabled || + hasRequireCompanyCardsEnabled || + hasModifiedProhibitedExpenses + ); } /** @@ -2132,7 +2169,7 @@ export { getTagLists, hasTags, hasCustomCategories, - hasNonDefaultRules, + hasConfiguredRules, getTaxByID, getUnitRateValue, getRateDisplayValue, diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 04b7c7471d07..aa1bb731a8fc 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -4,7 +4,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getValidConnectedIntegration, hasCustomCategories, hasNonDefaultRules, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getValidConnectedIntegration, hasConfiguredRules, hasCustomCategories, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; import {enableCompanyCards} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -110,7 +110,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { items.push({ key: 'setupRules', label: translate('homePage.gettingStartedSection.setupRules'), - isComplete: hasNonDefaultRules(policy), + isComplete: hasConfiguredRules(policy), route: ROUTES.WORKSPACE_RULES.getRoute(activePolicyID), }); } diff --git a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx index 4cca2e2eb0bb..235203032033 100644 --- a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx +++ b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx @@ -48,7 +48,7 @@ async function setManageTeamUserState(overrides?: { hasAccountingConnection?: boolean; hasCustomCategories?: boolean; hasCompanyCardConnection?: boolean; - hasNonDefaultRules?: boolean; + hasConfiguredRules?: boolean; trialStartDate?: string; }) { const trialStart = overrides?.trialStartDate ?? '2026-03-01'; From 81349ae20e528fedc0cf8e676637d40ad661fd58 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 15 Apr 2026 16:55:50 +0200 Subject: [PATCH 07/11] Update useGettingStartedItems tests for areConnectionsEnabled condition --- .../hooks/useGettingStartedItems.ts | 17 +++++++--- .../unit/hooks/useGettingStartedItems.test.ts | 34 +++++++++---------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index aa1bb731a8fc..768965259dbf 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -4,7 +4,15 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getValidConnectedIntegration, hasConfiguredRules, hasCustomCategories, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import { + getValidConnectedIntegration, + hasAccountingFeatureConnection, + hasConfiguredRules, + hasCustomCategories, + isPaidGroupPolicy, + isPendingDeletePolicy, + isPolicyAdmin, +} from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; import {enableCompanyCards} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -45,6 +53,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${activePolicyID}`); const [allCardFeeds] = useCardFeeds(activePolicyID); + const isAccountingEnabled = !!policy?.areConnectionsEnabled || hasAccountingFeatureConnection(policy); const emptyResult: UseGettingStartedItemsResult = {shouldShowSection: false, items: []}; @@ -76,7 +85,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { const isDirectConnect = !!reportedIntegration && DIRECT_CONNECT_INTEGRATIONS.has(reportedIntegration); - if (policy?.areAccountingEnabled) { + if (isAccountingEnabled) { const integrationName = isDirectConnect ? (CONST.ONBOARDING_ACCOUNTING_MAPPING[reportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] ?? String(reportedIntegration)) : undefined; @@ -86,9 +95,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { isComplete: !!getValidConnectedIntegration(policy) || Object.values(policy?.connections ?? {}).some((conn) => !!conn?.lastSync?.successfulDate), route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), }); - } - - if (policy?.areCategoriesEnabled) { + } else { items.push({ key: 'customizeCategories', label: translate('homePage.gettingStartedSection.customizeCategories'), diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index c8659d8cae3d..5ee2fb847f9b 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -231,7 +231,7 @@ describe('useGettingStartedItems', () => { ]; it.each(directConnectIntegrations)('should show "Connect to $name" when user selected $key in onboarding', async ({key, name}) => { - await setupManageTeamScenario({accounting: key, policy: {areAccountingEnabled: true}}); + await setupManageTeamScenario({accounting: key, policy: {areConnectionsEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -241,7 +241,7 @@ describe('useGettingStartedItems', () => { }); it('should navigate to workspace accounting route', async () => { - await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areAccountingEnabled: true}}); + await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areConnectionsEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -252,7 +252,7 @@ describe('useGettingStartedItems', () => { it('should be not completed when workspace has no accounting connection', async () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, - policy: {areAccountingEnabled: true, connections: undefined}, + policy: {areConnectionsEnabled: true, connections: undefined}, }); const {result} = renderHook(() => useGettingStartedItems()); @@ -265,7 +265,7 @@ describe('useGettingStartedItems', () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: { - areAccountingEnabled: true, + areConnectionsEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -286,7 +286,7 @@ describe('useGettingStartedItems', () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: { - areAccountingEnabled: true, + areConnectionsEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -307,7 +307,7 @@ describe('useGettingStartedItems', () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: { - areAccountingEnabled: true, + areConnectionsEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -325,7 +325,7 @@ describe('useGettingStartedItems', () => { }); it('should not show the categories row when showing the connect row', async () => { - await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areAccountingEnabled: true}}); + await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areConnectionsEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -336,7 +336,7 @@ describe('useGettingStartedItems', () => { it('should show generic "Connect to accounting" when reportedIntegration is not set but a connection already exists (e.g. cache cleared after connecting)', async () => { await setupManageTeamScenario({ policy: { - areAccountingEnabled: true, + areConnectionsEnabled: true, connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {}, @@ -355,7 +355,7 @@ describe('useGettingStartedItems', () => { }); it('should show "Customize accounting categories" when reportedIntegration is not set and no connections exist (e.g. cache cleared before connecting)', async () => { - await setupManageTeamScenario({policy: {areCategoriesEnabled: true}}); + await setupManageTeamScenario({}); const {result} = renderHook(() => useGettingStartedItems()); @@ -368,7 +368,7 @@ describe('useGettingStartedItems', () => { const categoriesIntegrations = ['sap', 'oracle', 'microsoftDynamics', 'other', 'none']; it.each(categoriesIntegrations)('should show "Customize accounting categories" when accounting choice is %s', async (accounting) => { - await setupManageTeamScenario({accounting, policy: {areCategoriesEnabled: true}}); + await setupManageTeamScenario({accounting}); const {result} = renderHook(() => useGettingStartedItems()); @@ -377,7 +377,7 @@ describe('useGettingStartedItems', () => { }); it('should navigate to workspace categories route', async () => { - await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); + await setupManageTeamScenario({accounting: 'none'}); const {result} = renderHook(() => useGettingStartedItems()); @@ -386,7 +386,7 @@ describe('useGettingStartedItems', () => { }); it('should not show the connect accounting row when showing the categories row', async () => { - await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); + await setupManageTeamScenario({accounting: 'none'}); const {result} = renderHook(() => useGettingStartedItems()); @@ -395,7 +395,7 @@ describe('useGettingStartedItems', () => { }); it('should be not completed when workspace has only default categories', async () => { - await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); + await setupManageTeamScenario({accounting: 'none'}); const {result} = renderHook(() => useGettingStartedItems()); @@ -419,7 +419,7 @@ describe('useGettingStartedItems', () => { }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${POLICY_ID}`, customCategories); - await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); + await setupManageTeamScenario({accounting: 'none'}); const {result} = renderHook(() => useGettingStartedItems()); @@ -594,7 +594,7 @@ describe('useGettingStartedItems', () => { it('should return items in the correct order: createWorkspace, accounting/categories, companyCards, rules', async () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, - policy: {areAccountingEnabled: true, areCompanyCardsEnabled: true, areRulesEnabled: true}, + policy: {areConnectionsEnabled: true, areCompanyCardsEnabled: true, areRulesEnabled: true}, }); const {result} = renderHook(() => useGettingStartedItems()); @@ -606,7 +606,7 @@ describe('useGettingStartedItems', () => { it('should return items in the correct order with categories instead of connect', async () => { await setupManageTeamScenario({ accounting: 'none', - policy: {areCategoriesEnabled: true, areCompanyCardsEnabled: true, areRulesEnabled: true}, + policy: {areCompanyCardsEnabled: true, areRulesEnabled: true}, }); const {result} = renderHook(() => useGettingStartedItems()); @@ -618,7 +618,7 @@ describe('useGettingStartedItems', () => { it('should contain three rows when areRulesEnabled is false', async () => { await setupManageTeamScenario({ accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, - policy: {areAccountingEnabled: true, areCompanyCardsEnabled: false, areRulesEnabled: false}, + policy: {areConnectionsEnabled: true, areCompanyCardsEnabled: false, areRulesEnabled: false}, }); const {result} = renderHook(() => useGettingStartedItems()); From 08a3e9bc8de429981dfa8f14eae901e2865ae9fe Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 15 Apr 2026 17:25:43 +0200 Subject: [PATCH 08/11] Update useGettingStartedItems tests for categories isFeatureEnabled condition --- .../hooks/useGettingStartedItems.ts | 3 ++ .../unit/hooks/useGettingStartedItems.test.ts | 33 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 768965259dbf..d2a085bcbe84 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -2,6 +2,7 @@ import useCardFeeds from '@hooks/useCardFeeds'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import {enablePolicyCategories} from '@libs/actions/Policy/Category'; import {hasCompanyCardFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import { @@ -101,6 +102,8 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { label: translate('homePage.gettingStartedSection.customizeCategories'), isComplete: hasCustomCategories(policyCategories), route: ROUTES.WORKSPACE_CATEGORIES.getRoute(activePolicyID), + isFeatureEnabled: policy.areCategoriesEnabled, + enableFeature: () => enablePolicyCategories({policy, categories: policyCategories ?? {}, tags: {}, reports: [], transactionsAndViolations: {}}, true, false), }); } diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index 5ee2fb847f9b..d651d6b8ffb5 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -355,7 +355,7 @@ describe('useGettingStartedItems', () => { }); it('should show "Customize accounting categories" when reportedIntegration is not set and no connections exist (e.g. cache cleared before connecting)', async () => { - await setupManageTeamScenario({}); + await setupManageTeamScenario({policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -368,7 +368,7 @@ describe('useGettingStartedItems', () => { const categoriesIntegrations = ['sap', 'oracle', 'microsoftDynamics', 'other', 'none']; it.each(categoriesIntegrations)('should show "Customize accounting categories" when accounting choice is %s', async (accounting) => { - await setupManageTeamScenario({accounting}); + await setupManageTeamScenario({accounting, policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -376,8 +376,27 @@ describe('useGettingStartedItems', () => { expect(categoriesItem).toBeDefined(); }); + it('should have isFeatureEnabled=true when categories feature is enabled', async () => { + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); + + const {result} = renderHook(() => useGettingStartedItems()); + + const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories'); + expect(categoriesItem?.isFeatureEnabled).toBe(true); + }); + + it('should have isFeatureEnabled=false when categories feature is not enabled', async () => { + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: false}}); + + const {result} = renderHook(() => useGettingStartedItems()); + + const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories'); + expect(categoriesItem).toBeDefined(); + expect(categoriesItem?.isFeatureEnabled).toBe(false); + }); + it('should navigate to workspace categories route', async () => { - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -386,7 +405,7 @@ describe('useGettingStartedItems', () => { }); it('should not show the connect accounting row when showing the categories row', async () => { - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -395,7 +414,7 @@ describe('useGettingStartedItems', () => { }); it('should be not completed when workspace has only default categories', async () => { - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -419,7 +438,7 @@ describe('useGettingStartedItems', () => { }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${POLICY_ID}`, customCategories); - await setupManageTeamScenario({accounting: 'none'}); + await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}}); const {result} = renderHook(() => useGettingStartedItems()); @@ -606,7 +625,7 @@ describe('useGettingStartedItems', () => { it('should return items in the correct order with categories instead of connect', async () => { await setupManageTeamScenario({ accounting: 'none', - policy: {areCompanyCardsEnabled: true, areRulesEnabled: true}, + policy: {areCategoriesEnabled: true, areCompanyCardsEnabled: true, areRulesEnabled: true}, }); const {result} = renderHook(() => useGettingStartedItems()); From 83b5c8c20573984c17dc0c78c42ed56392e53bc1 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 15 Apr 2026 17:32:20 +0200 Subject: [PATCH 09/11] Update useGettingStartedItems tests for connectAccounting isFeatureEnabled condition --- .../hooks/useGettingStartedItems.ts | 4 ++- .../unit/hooks/useGettingStartedItems.test.ts | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index d2a085bcbe84..57a7d35abe75 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -15,7 +15,7 @@ import { isPolicyAdmin, } from '@libs/PolicyUtils'; import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod'; -import {enableCompanyCards} from '@userActions/Policy/Policy'; +import {enableCompanyCards, enablePolicyConnections} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -95,6 +95,8 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { label: integrationName ? translate('homePage.gettingStartedSection.connectAccounting', {integrationName}) : translate('homePage.gettingStartedSection.connectAccountingDefault'), isComplete: !!getValidConnectedIntegration(policy) || Object.values(policy?.connections ?? {}).some((conn) => !!conn?.lastSync?.successfulDate), route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), + isFeatureEnabled: policy.areConnectionsEnabled, + enableFeature: () => enablePolicyConnections(activePolicyID, true, false), }); } else { items.push({ diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index d651d6b8ffb5..b65ef75e9fc6 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -362,6 +362,37 @@ describe('useGettingStartedItems', () => { const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories'); expect(categoriesItem).toBeDefined(); }); + + it('should have isFeatureEnabled=true when accounting connections feature is enabled', async () => { + await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areConnectionsEnabled: true}}); + + const {result} = renderHook(() => useGettingStartedItems()); + + const connectItem = result.current.items.find((item) => item.key === 'connectAccounting'); + expect(connectItem?.isFeatureEnabled).toBe(true); + }); + + it('should have isFeatureEnabled=false when accounting connections feature is not enabled but an existing connection makes the row visible', async () => { + await setupManageTeamScenario({ + accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, + policy: { + areConnectionsEnabled: false, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.QBO]: { + config: {}, + data: {}, + lastSync: {isConnected: true}, + }, + } as Policy['connections'], + }, + }); + + const {result} = renderHook(() => useGettingStartedItems()); + + const connectItem = result.current.items.find((item) => item.key === 'connectAccounting'); + expect(connectItem).toBeDefined(); + expect(connectItem?.isFeatureEnabled).toBe(false); + }); }); describe('row 2b - Customize accounting categories', () => { From d08c492791501cb09be612602430184032c1dec8 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 16 Apr 2026 08:06:59 +0200 Subject: [PATCH 10/11] Refactor useConfiguredRules to early return when found changed rule, fix method tests --- src/libs/PolicyUtils.ts | 77 +++++++++++-------- .../GettingStartedSectionTest.tsx | 2 +- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 72cfd093f89b..b73c93030fe4 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -698,52 +698,63 @@ function hasConfiguredRules(policy: OnyxEntry): boolean { if (!policy) { return false; } - const hasCustomRules = !!policy.customRules && policy.customRules.trim().length > 0; + + if (!!policy.customRules && policy.customRules.trim().length > 0) { + return true; + } const {rules} = policy; - const hasApprovalRules = !!rules?.approvalRules && rules.approvalRules.length > 0; - const hasExpenseRules = !!rules?.expenseRules && rules.expenseRules.length > 0; - const hasCodingRules = !!rules?.codingRules && Object.keys(rules.codingRules).length > 0; - - const hasModifiedMaxExpenseAmount = - !!policy.maxExpenseAmount && policy.maxExpenseAmount !== CONST.DISABLED_MAX_EXPENSE_VALUE && policy.maxExpenseAmount !== CONST.POLICY.DEFAULT_MAX_EXPENSE_AMOUNT; - const hasModifiedMaxExpenseAge = !!policy.maxExpenseAge && policy.maxExpenseAge !== CONST.DISABLED_MAX_EXPENSE_VALUE && policy.maxExpenseAge !== CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE; - const hasModifiedMaxExpenseAmountNoReceipt = + if (!!rules?.approvalRules && rules.approvalRules.length > 0) { + return true; + } + if (!!rules?.expenseRules && rules.expenseRules.length > 0) { + return true; + } + if (!!rules?.codingRules && Object.keys(rules.codingRules).length > 0) { + return true; + } + + if (!!policy.maxExpenseAmount && policy.maxExpenseAmount !== CONST.DISABLED_MAX_EXPENSE_VALUE && policy.maxExpenseAmount !== CONST.POLICY.DEFAULT_MAX_EXPENSE_AMOUNT) { + return true; + } + if (!!policy.maxExpenseAge && policy.maxExpenseAge !== CONST.DISABLED_MAX_EXPENSE_VALUE && policy.maxExpenseAge !== CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE) { + return true; + } + if ( !!policy.maxExpenseAmountNoReceipt && policy.maxExpenseAmountNoReceipt !== CONST.DISABLED_MAX_EXPENSE_VALUE && - policy.maxExpenseAmountNoReceipt !== CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_RECEIPT; - const hasModifiedMaxExpenseAmountNoItemizedReceipt = + policy.maxExpenseAmountNoReceipt !== CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_RECEIPT + ) { + return true; + } + if ( !!policy.maxExpenseAmountNoItemizedReceipt && policy.maxExpenseAmountNoItemizedReceipt !== CONST.DISABLED_MAX_EXPENSE_VALUE && - policy.maxExpenseAmountNoItemizedReceipt !== CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_ITEMIZED_RECEIPT; + policy.maxExpenseAmountNoItemizedReceipt !== CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_ITEMIZED_RECEIPT + ) { + return true; + } - const hasModifiedBillable = !!policy.defaultBillable; - const hasModifiedReimbursable = policy.defaultReimbursable === false; - const hasEReceiptsEnabled = !!policy.eReceipts; - const hasRequireCompanyCardsEnabled = !!policy.requireCompanyCardsEnabled; + if (policy.defaultBillable) { + return true; + } + if (policy.defaultReimbursable === false) { + return true; + } + if (policy.eReceipts) { + return true; + } + if (policy.requireCompanyCardsEnabled) { + return true; + } const {prohibitedExpenses} = policy; - const hasModifiedProhibitedExpenses = + return ( !!prohibitedExpenses && Object.entries(CONST.POLICY.DEFAULT_PROHIBITED_EXPENSES).some(([key, defaultValue]) => { const value = prohibitedExpenses[key as keyof typeof CONST.POLICY.DEFAULT_PROHIBITED_EXPENSES]; return value !== undefined && value !== defaultValue; - }); - - return ( - hasCustomRules || - hasApprovalRules || - hasExpenseRules || - hasCodingRules || - hasModifiedMaxExpenseAmount || - hasModifiedMaxExpenseAge || - hasModifiedMaxExpenseAmountNoReceipt || - hasModifiedMaxExpenseAmountNoItemizedReceipt || - hasModifiedBillable || - hasModifiedReimbursable || - hasEReceiptsEnabled || - hasRequireCompanyCardsEnabled || - hasModifiedProhibitedExpenses + }) ); } diff --git a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx index 235203032033..84d7d63c8796 100644 --- a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx +++ b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx @@ -67,7 +67,7 @@ async function setManageTeamUserState(overrides?: { role: CONST.POLICY.ROLE.ADMIN, areCompanyCardsEnabled: overrides?.areCompanyCardsEnabled ?? true, areRulesEnabled: overrides?.areRulesEnabled ?? true, - areAccountingEnabled: overrides?.areAccountingEnabled, + areConnectionsEnabled: overrides?.areAccountingEnabled, areCategoriesEnabled: overrides?.areCategoriesEnabled, }; From 9d9285d97dcc6ed88d39aa938c4c451bc37f7f1b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 16 Apr 2026 12:04:12 +0200 Subject: [PATCH 11/11] Add tests for hasConfiguredRules --- tests/unit/PolicyUtilsTest.ts | 192 ++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 954426aaa7a5..b7419f585cc2 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -26,6 +26,7 @@ import { getTagListByOrderWeight, getUberConnectionErrorDirectlyFromPolicy, getUnitRateValue, + hasConfiguredRules, hasDependentTags, hasDynamicExternalWorkflow, hasIndependentTags, @@ -2449,4 +2450,195 @@ describe('PolicyUtils', () => { expect(hasIndependentTags(policy, undefined)).toBe(false); }); }); + + describe('hasConfiguredRules', () => { + it('returns false when policy is undefined', () => { + expect(hasConfiguredRules(undefined)).toBe(false); + }); + + it('returns false when policy has no rules configured', () => { + expect(hasConfiguredRules({} as Policy)).toBe(false); + }); + + describe('customRules', () => { + it('returns true when customRules is non-empty', () => { + expect(hasConfiguredRules({customRules: 'some rule'} as Policy)).toBe(true); + }); + + it('returns false when customRules is an empty string', () => { + expect(hasConfiguredRules({customRules: ''} as Policy)).toBe(false); + }); + + it('returns false when customRules is only whitespace', () => { + expect(hasConfiguredRules({customRules: ' '} as Policy)).toBe(false); + }); + }); + + describe('rules.approvalRules', () => { + it('returns true when approvalRules has items', () => { + const policy = {rules: {approvalRules: [{id: '1', applyWhen: [], approver: 'approver@test.com'}]}} as unknown as Policy; + expect(hasConfiguredRules(policy)).toBe(true); + }); + + it('returns false when approvalRules is empty', () => { + expect(hasConfiguredRules({rules: {approvalRules: []}} as unknown as Policy)).toBe(false); + }); + }); + + describe('rules.expenseRules', () => { + it('returns true when expenseRules has items', () => { + const policy = { + rules: { + expenseRules: [ + { + id: '1', + applyWhen: [], + tax: {field_id_TAX: {externalID: 'TAX_US'}}, + }, + ], + }, + } as unknown as Policy; + expect(hasConfiguredRules(policy)).toBe(true); + }); + + it('returns false when expenseRules is empty', () => { + expect(hasConfiguredRules({rules: {expenseRules: []}} as unknown as Policy)).toBe(false); + }); + }); + + describe('rules.codingRules', () => { + it('returns true when codingRules has entries', () => { + const policy = { + rules: { + codingRules: { + rule1: { + ruleID: 'rule1', + filters: {left: 'merchant', operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, right: 'Starbucks'}, + }, + }, + }, + } as unknown as Policy; + expect(hasConfiguredRules(policy)).toBe(true); + }); + + it('returns false when codingRules is empty', () => { + expect(hasConfiguredRules({rules: {codingRules: {}}} as unknown as Policy)).toBe(false); + }); + }); + + describe('maxExpenseAmount', () => { + it('returns true when maxExpenseAmount is set to a non-default value', () => { + expect(hasConfiguredRules({maxExpenseAmount: 500000} as Policy)).toBe(true); + }); + + it('returns false when maxExpenseAmount is the default value', () => { + expect(hasConfiguredRules({maxExpenseAmount: CONST.POLICY.DEFAULT_MAX_EXPENSE_AMOUNT} as Policy)).toBe(false); + }); + + it('returns false when maxExpenseAmount is the disabled value', () => { + expect(hasConfiguredRules({maxExpenseAmount: CONST.DISABLED_MAX_EXPENSE_VALUE} as Policy)).toBe(false); + }); + }); + + describe('maxExpenseAge', () => { + it('returns true when maxExpenseAge is set to a non-default value', () => { + expect(hasConfiguredRules({maxExpenseAge: 30} as Policy)).toBe(true); + }); + + it('returns false when maxExpenseAge is the default value', () => { + expect(hasConfiguredRules({maxExpenseAge: CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE} as Policy)).toBe(false); + }); + + it('returns false when maxExpenseAge is the disabled value', () => { + expect(hasConfiguredRules({maxExpenseAge: CONST.DISABLED_MAX_EXPENSE_VALUE} as Policy)).toBe(false); + }); + }); + + describe('maxExpenseAmountNoReceipt', () => { + it('returns true when maxExpenseAmountNoReceipt is set to a non-default value', () => { + expect(hasConfiguredRules({maxExpenseAmountNoReceipt: 5000} as Policy)).toBe(true); + }); + + it('returns false when maxExpenseAmountNoReceipt is the default value', () => { + expect(hasConfiguredRules({maxExpenseAmountNoReceipt: CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_RECEIPT} as Policy)).toBe(false); + }); + + it('returns false when maxExpenseAmountNoReceipt is the disabled value', () => { + expect(hasConfiguredRules({maxExpenseAmountNoReceipt: CONST.DISABLED_MAX_EXPENSE_VALUE} as Policy)).toBe(false); + }); + }); + + describe('maxExpenseAmountNoItemizedReceipt', () => { + it('returns true when maxExpenseAmountNoItemizedReceipt is set to a non-default value', () => { + expect(hasConfiguredRules({maxExpenseAmountNoItemizedReceipt: 10000} as Policy)).toBe(true); + }); + + it('returns false when maxExpenseAmountNoItemizedReceipt is the default value', () => { + expect(hasConfiguredRules({maxExpenseAmountNoItemizedReceipt: CONST.POLICY.DEFAULT_MAX_AMOUNT_NO_ITEMIZED_RECEIPT} as Policy)).toBe(false); + }); + + it('returns false when maxExpenseAmountNoItemizedReceipt is the disabled value', () => { + expect(hasConfiguredRules({maxExpenseAmountNoItemizedReceipt: CONST.DISABLED_MAX_EXPENSE_VALUE} as Policy)).toBe(false); + }); + }); + + describe('defaultBillable', () => { + it('returns true when defaultBillable is true', () => { + expect(hasConfiguredRules({defaultBillable: true} as Policy)).toBe(true); + }); + + it('returns false when defaultBillable is false', () => { + expect(hasConfiguredRules({defaultBillable: false} as Policy)).toBe(false); + }); + }); + + describe('defaultReimbursable', () => { + it('returns true when defaultReimbursable is false', () => { + expect(hasConfiguredRules({defaultReimbursable: false} as Policy)).toBe(true); + }); + + it('returns false when defaultReimbursable is true', () => { + expect(hasConfiguredRules({defaultReimbursable: true} as Policy)).toBe(false); + }); + }); + + describe('eReceipts', () => { + it('returns true when eReceipts is true', () => { + expect(hasConfiguredRules({eReceipts: true} as Policy)).toBe(true); + }); + + it('returns false when eReceipts is false', () => { + expect(hasConfiguredRules({eReceipts: false} as Policy)).toBe(false); + }); + }); + + describe('requireCompanyCardsEnabled', () => { + it('returns true when requireCompanyCardsEnabled is true', () => { + expect(hasConfiguredRules({requireCompanyCardsEnabled: true} as Policy)).toBe(true); + }); + + it('returns false when requireCompanyCardsEnabled is false', () => { + expect(hasConfiguredRules({requireCompanyCardsEnabled: false} as Policy)).toBe(false); + }); + }); + + describe('prohibitedExpenses', () => { + it('returns true when a prohibitedExpenses value differs from its default', () => { + // alcohol defaults to false — setting it to true triggers the rule + expect(hasConfiguredRules({prohibitedExpenses: {alcohol: true}} as Policy)).toBe(true); + }); + + it('returns true when gambling is disabled (differs from default true)', () => { + expect(hasConfiguredRules({prohibitedExpenses: {gambling: false}} as Policy)).toBe(true); + }); + + it('returns false when prohibitedExpenses matches all defaults', () => { + expect(hasConfiguredRules({prohibitedExpenses: {...CONST.POLICY.DEFAULT_PROHIBITED_EXPENSES}} as Policy)).toBe(false); + }); + + it('returns false when prohibitedExpenses is an empty object', () => { + expect(hasConfiguredRules({prohibitedExpenses: {}} as Policy)).toBe(false); + }); + }); + }); });