diff --git a/src/hooks/useParticipantSubmission.ts b/src/hooks/useParticipantSubmission.ts index ed2969ea5bbb..fada87794eb8 100644 --- a/src/hooks/useParticipantSubmission.ts +++ b/src/hooks/useParticipantSubmission.ts @@ -53,7 +53,6 @@ const policyMapper = (policy: OnyxEntry): OnyxEntry => role: policy.role, owner: policy.owner, outputCurrency: policy.outputCurrency, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, customUnits: policy.customUnits, }; diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index ef0a6a41fc57..ddcc1d55a8a2 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -27,7 +27,6 @@ const policyMapper = (policy: OnyxEntry): OnyxEntry => owner: policy.owner, connections: policy.connections, outputCurrency: policy.outputCurrency, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, isJoinRequestPending: policy.isJoinRequestPending, pendingAction: policy.pendingAction, errors: policy.errors, diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index bd52aa48f06d..3d0cf2d30102 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -140,7 +140,7 @@ const ONYX_KEY_EXPORT_RULES: Record = { maskList: ['merchant', 'description', 'comment'], }, [ONYXKEYS.COLLECTION.POLICY]: { - allowList: ['id', 'type', 'role', 'outputCurrency', 'isPolicyExpenseChatEnabled', 'areCategoriesEnabled', 'areTagsEnabled'], + allowList: ['id', 'type', 'role', 'outputCurrency', 'areCategoriesEnabled', 'areTagsEnabled'], maskList: ['name', 'avatar'], }, [ONYXKEYS.USER_WALLET]: { diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index d3a9c99bd46e..48128269d283 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -174,10 +174,9 @@ function isSplitAction( const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const isManager = (report.managerID ?? CONST.DEFAULT_NUMBER_ID) === currentUserAccountID; const isOpenReport = isOpenReportUtils(report); - const isPolicyExpenseChat = !!policy?.isPolicyExpenseChatEnabled; const userIsPolicyMember = isPolicyMember(policy, currentUserLogin); - if (!(userIsPolicyMember && isPolicyExpenseChat)) { + if (!(userIsPolicyMember && isGroupPolicy(policy))) { return false; } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index c23b9434e491..3d1dc35bdba0 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -356,26 +356,26 @@ function getInvoicePrimaryWorkspace(activePolicy: OnyxEntry, activeAdmin } /** - * Check if the user has any active free policies (aka workspaces) + * Check if the user has any active group workspaces. */ -function hasActiveChatEnabledPolicies(policies: Array> | OnyxCollection, includeOnlyAdminPolicies = false): boolean { - const chatEnabledPolicies = Object.values(policies ?? {}).filter( - (policy) => policy?.isPolicyExpenseChatEnabled && (!includeOnlyAdminPolicies || policy.role === CONST.POLICY.ROLE.ADMIN), +function hasActiveGroupPolicies(policies: Array> | OnyxCollection, includeOnlyAdminPolicies = false): boolean { + const groupPolicies = Object.values(policies ?? {}).filter( + (policy) => PolicyUtils.isGroupPolicyByType(policy?.type) && (!includeOnlyAdminPolicies || policy?.role === CONST.POLICY.ROLE.ADMIN), ); - if (chatEnabledPolicies.length === 0) { + if (groupPolicies.length === 0) { return false; } - if (chatEnabledPolicies.some((policy) => !policy?.pendingAction)) { + if (groupPolicies.some((policy) => !policy?.pendingAction)) { return true; } - if (chatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) { + if (groupPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) { return true; } - if (chatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { + if (groupPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) { return false; } @@ -486,7 +486,7 @@ function deleteWorkspace(params: DeleteWorkspaceActionParams) { key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED}${policyID}`, value: null, }, - ...(!hasActiveChatEnabledPolicies(filteredPolicies, true) + ...(!hasActiveGroupPolicies(filteredPolicies, true) ? [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2586,7 +2586,6 @@ function createDraftInitialWorkspace({ role: CONST.POLICY.ROLE.ADMIN, owner: currentUserEmail, ownerAccountID: currentUserAccountID, - isPolicyExpenseChatEnabled: true, areCategoriesEnabled: true, approver: currentUserEmail, areCompanyCardsEnabled: true, @@ -2812,7 +2811,6 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData) => units: policy.units, }; -type PolicySelector = Pick; +type PolicySelector = Pick; const policyMapper = (policy: OnyxEntry): PolicySelector | undefined => { if (!policy) { @@ -288,7 +288,6 @@ const policyMapper = (policy: OnyxEntry): PolicySelector | undefined => type: policy.type, role: policy.role, id: policy.id, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, pendingAction: policy.pendingAction, avatarURL: policy.avatarURL, name: policy.name, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index af781143b61e..c0f1fec9a344 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -2880,8 +2880,8 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** The custom units data for this policy */ customUnits?: Record; - /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ - isPolicyExpenseChatEnabled: boolean; + /** Deprecated legacy flag for policy expense chat compatibility. */ + isPolicyExpenseChatEnabled?: boolean; /** Whether the auto reporting is enabled */ autoReporting?: boolean; diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index e3d7bb005efc..569e3913b751 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -216,7 +216,6 @@ describe('actions/Policy', () => { expect(policy?.arePerDiemRatesEnabled).toBe(false); expect(policy?.approvalMode).toBe(CONST.POLICY.APPROVAL_MODE.BASIC); expect(policy?.approver).toBe(ESH_EMAIL); - expect(policy?.isPolicyExpenseChatEnabled).toBe(true); expect(policy?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(policy?.employeeList).toEqual({[ESH_EMAIL]: {email: ESH_EMAIL, submitsTo: ESH_EMAIL, errors: {}, role: CONST.POLICY.ROLE.ADMIN}}); expect(policy?.mccGroup).toBeDefined(); @@ -437,7 +436,6 @@ describe('actions/Policy', () => { expect(policy?.arePerDiemRatesEnabled).toBe(true); expect(policy?.approvalMode).toBe(fakePolicy.approvalMode); expect(policy?.approver).toBe(fakePolicy.approver); - expect(policy?.isPolicyExpenseChatEnabled).toBe(fakePolicy.isPolicyExpenseChatEnabled); expect(policy?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(policy?.employeeList).toEqual(fakePolicy.employeeList); expect(policy?.mccGroup).toBe(fakePolicy.mccGroup); @@ -4343,6 +4341,98 @@ describe('actions/Policy', () => { expect(activePolicyID).toBe(mostRecentlyCreatedGroupPolicy.id); }); + it('should clear reimbursement account errors when no admin group workspace remains after deleting a workspace', async () => { + const reimbursementAccountError = {}; + const policyToDelete = createRandomPolicy(0, CONST.POLICY.TYPE.TEAM); + policyToDelete.pendingAction = null; + policyToDelete.role = CONST.POLICY.ROLE.ADMIN; + + const personalPolicy = createRandomPolicy(1, CONST.POLICY.TYPE.PERSONAL); + personalPolicy.pendingAction = null; + personalPolicy.role = CONST.POLICY.ROLE.ADMIN; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyToDelete.id}`, policyToDelete); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${personalPolicy.id}`, personalPolicy); + await Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: reimbursementAccountError}); + await waitForBatchedUpdates(); + + mockFetch.pause(); + + Policy.deleteWorkspace({ + policies: { + [`${ONYXKEYS.COLLECTION.POLICY}${policyToDelete.id}`]: policyToDelete, + [`${ONYXKEYS.COLLECTION.POLICY}${personalPolicy.id}`]: personalPolicy, + }, + policyID: policyToDelete.id, + personalPolicyID: personalPolicy.id, + activePolicyID: undefined, + policyName: policyToDelete.name, + lastAccessedWorkspacePolicyID: undefined, + policyCardFeeds: undefined, + lastSelectedFeed: undefined, + lastSelectedExpensifyCardFeed: undefined, + reportsToArchive: [], + transactionViolations: undefined, + reimbursementAccountError, + lastUsedPaymentMethods: undefined, + localeCompare: TestHelper.localeCompare, + currentUserAccountID: ESH_ACCOUNT_ID, + accountIDToLogin: {}, + }); + await waitForBatchedUpdates(); + + const reimbursementAccount = await getOnyxValue(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + expect(reimbursementAccount?.errors).toBeUndefined(); + + await mockFetch.resume?.(); + }); + + it('should keep reimbursement account errors when another admin group workspace remains after deleting a workspace', async () => { + const reimbursementAccountError = {}; + const policyToDelete = createRandomPolicy(0, CONST.POLICY.TYPE.TEAM); + policyToDelete.pendingAction = null; + policyToDelete.role = CONST.POLICY.ROLE.ADMIN; + + const remainingGroupPolicy = createRandomPolicy(1, CONST.POLICY.TYPE.SUBMIT); + remainingGroupPolicy.pendingAction = null; + remainingGroupPolicy.role = CONST.POLICY.ROLE.ADMIN; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyToDelete.id}`, policyToDelete); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${remainingGroupPolicy.id}`, remainingGroupPolicy); + await Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: reimbursementAccountError}); + await waitForBatchedUpdates(); + + mockFetch.pause(); + + Policy.deleteWorkspace({ + policies: { + [`${ONYXKEYS.COLLECTION.POLICY}${policyToDelete.id}`]: policyToDelete, + [`${ONYXKEYS.COLLECTION.POLICY}${remainingGroupPolicy.id}`]: remainingGroupPolicy, + }, + policyID: policyToDelete.id, + personalPolicyID: undefined, + activePolicyID: undefined, + policyName: policyToDelete.name, + lastAccessedWorkspacePolicyID: undefined, + policyCardFeeds: undefined, + lastSelectedFeed: undefined, + lastSelectedExpensifyCardFeed: undefined, + reportsToArchive: [], + transactionViolations: undefined, + reimbursementAccountError, + lastUsedPaymentMethods: undefined, + localeCompare: TestHelper.localeCompare, + currentUserAccountID: ESH_ACCOUNT_ID, + accountIDToLogin: {}, + }); + await waitForBatchedUpdates(); + + const reimbursementAccount = await getOnyxValue(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + expect(reimbursementAccount?.errors).toEqual(reimbursementAccountError); + + await mockFetch.resume?.(); + }); + it('should reset lastAccessedWorkspacePolicyID when deleting the last accessed workspace', async () => { const policyToDelete = createRandomPolicy(0, CONST.POLICY.TYPE.TEAM); const lastAccessedWorkspacePolicyID = policyToDelete.id; @@ -7196,7 +7286,6 @@ describe('actions/Policy', () => { expect(policyDraft?.role).toBe(CONST.POLICY.ROLE.ADMIN); expect(policyDraft?.outputCurrency).toBe('USD'); expect(policyDraft?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(policyDraft?.isPolicyExpenseChatEnabled).toBe(true); expect(policyDraft?.areCategoriesEnabled).toBe(true); expect(policyDraft?.areCompanyCardsEnabled).toBe(true); expect(policyDraft?.areExpensifyCardsEnabled).toBe(false); diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index f438b3195ca7..3d274bb63b12 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -4703,7 +4703,7 @@ describe('getSecondaryTransactionThreadActions', () => { expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(false); }); - it('includes the SPLIT option if the current user belongs to the workspace', async () => { + it('includes the SPLIT option for a group policy', async () => { const report = createMock({ reportID: REPORT_ID, policyID: POLICY_ID, @@ -4725,7 +4725,7 @@ describe('getSecondaryTransactionThreadActions', () => { const policy = createMock({ id: POLICY_ID, type: CONST.POLICY.TYPE.TEAM, - isPolicyExpenseChatEnabled: true, + isPolicyExpenseChatEnabled: false, employeeList: { [EMPLOYEE_EMAIL]: {email: EMPLOYEE_EMAIL, role: CONST.POLICY.ROLE.USER}, [ADMIN_EMAIL]: {email: ADMIN_EMAIL, role: CONST.POLICY.ROLE.ADMIN}, @@ -4858,7 +4858,7 @@ describe('getSecondaryTransactionThreadActions', () => { expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SPLIT)).toBe(false); }); - it('does not include the SPLIT option if the policy is not expense chat enabled', async () => { + it('does not include the SPLIT option for a personal policy', async () => { const report = createMock({ reportID: REPORT_ID, policyID: POLICY_ID, @@ -4879,14 +4879,15 @@ describe('getSecondaryTransactionThreadActions', () => { const policy = createMock({ id: POLICY_ID, - type: CONST.POLICY.TYPE.TEAM, - isPolicyExpenseChatEnabled: false, + type: CONST.POLICY.TYPE.PERSONAL, + isPolicyExpenseChatEnabled: true, employeeList: { [EMPLOYEE_EMAIL]: {email: EMPLOYEE_EMAIL, role: CONST.POLICY.ROLE.USER}, [ADMIN_EMAIL]: {email: ADMIN_EMAIL, role: CONST.POLICY.ROLE.ADMIN}, }, role: CONST.POLICY.ROLE.ADMIN, }); + jest.mocked(jest.requireMock('@libs/PolicyUtils').isGroupPolicy).mockReturnValueOnce(false); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 6076e278cce7..f36ca6b25172 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -6388,7 +6388,6 @@ describe('ReportUtils', () => { type: CONST.POLICY.TYPE.CORPORATE, owner: '', outputCurrency: CONST.CURRENCY.USD, - isPolicyExpenseChatEnabled: false, employeeList: { 'lagertha2@vikings.net': { email: 'lagertha2@vikings.net', diff --git a/tests/unit/useCreateReportTest.tsx b/tests/unit/useCreateReportTest.tsx index f7bba133a1cc..7aed632c96da 100644 --- a/tests/unit/useCreateReportTest.tsx +++ b/tests/unit/useCreateReportTest.tsx @@ -39,10 +39,9 @@ jest.mock('@libs/PolicyUtils', () => { const CONSTANTS = jest.requireActual<{default: typeof CONST}>('@src/CONST').default; return { getDefaultChatEnabledPolicy: jest.fn((policies: Array>, activePolicy: OnyxEntry) => { - // Mirror the real helper: prefer activePolicy if it's a paid group with chat enabled, otherwise the single non-personal candidate. + // Mirror the real helper: prefer activePolicy when it is a group workspace from the provided create-report candidates; otherwise use the only candidate. if ( activePolicy && - activePolicy.isPolicyExpenseChatEnabled && (activePolicy.type === CONSTANTS.POLICY.TYPE.TEAM || activePolicy.type === CONSTANTS.POLICY.TYPE.CORPORATE || activePolicy.type === CONSTANTS.POLICY.TYPE.SUBMIT) ) { return activePolicy; @@ -86,7 +85,6 @@ function makePaidPolicy(id = POLICY_ID): Policy { name: 'Test Workspace', role: CONST.POLICY.ROLE.ADMIN, type: CONST.POLICY.TYPE.TEAM, - isPolicyExpenseChatEnabled: true, owner: 'test@test.com', ownerAccountID: 1, outputCurrency: 'USD',