Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,10 +817,15 @@ function shouldFilterExpensifyTeam(policyOwner: string | undefined, currentUserL
* Creates a selector for useOnyx that computes the filtered member count.
* Returns a primitive number to prevent unnecessary re-renders when unrelated personal details change.
*/
function createFilteredMemberCountSelector(employeeList: PolicyEmployeeList | undefined, policyOwner: string | undefined, currentUserLogin: string | undefined) {
function createFilteredMemberCountSelector(
employeeList: PolicyEmployeeList | undefined,
policyOwner: string | undefined,
currentUserLogin: string | undefined,
personalDetailsByLogins: PersonalDetailsByLogin,
) {
return (personalDetails: PersonalDetailsList | undefined): number => {
const shouldFilter = shouldFilterExpensifyTeam(policyOwner, currentUserLogin);
const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(employeeList, undefined, false, false);
const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(employeeList, personalDetailsByLogins, false, false);

return Object.keys(policyMemberEmailsToAccountIDs).reduce((count, email) => {
const accountID = policyMemberEmailsToAccountIDs[email];
Expand Down
5 changes: 4 additions & 1 deletion src/libs/actions/IOU/TrackExpense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2348,6 +2348,7 @@ function shareTrackedExpense(trackedExpenseParams: TrackedExpenseParams) {
accountantParams,
currentUser,
reportActionsList,
personalDetailsByLogins,
} = trackedExpenseParams;
const {accountID: currentUserAccountID} = currentUser;

Expand Down Expand Up @@ -2405,7 +2406,7 @@ function shareTrackedExpense(trackedExpenseParams: TrackedExpenseParams) {

const policyEmployeeList = policyParams?.policy?.employeeList;
if (policyParams.policy && !policyEmployeeList?.[accountantEmail]) {
const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policyEmployeeList, undefined, false, false));
const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policyEmployeeList, personalDetailsByLogins, false, false));
const {
optimisticData: addAccountantToWorkspaceOptimisticData,
successData: addAccountantToWorkspaceSuccessData,
Expand Down Expand Up @@ -2505,6 +2506,7 @@ function trackExpense(params: CreateTrackExpenseParams) {
currentUserLocalCurrency,
getCurrencyDecimals,
rules,
personalDetailsByLogins,
} = params;
const {accountID: currentUserAccountIDParam, email: currentUserEmailParam = ''} = currentUser;
const {participant, payeeAccountID, payeeEmail} = participantParams;
Expand Down Expand Up @@ -2826,6 +2828,7 @@ function trackExpense(params: CreateTrackExpenseParams) {
accountantParams,
currentUser: {accountID: currentUserAccountIDParam, email: currentUserEmailParam},
reportActionsList,
personalDetailsByLogins,
};
shareTrackedExpense(trackedExpenseParams);
break;
Expand Down
2 changes: 2 additions & 0 deletions src/libs/actions/IOU/types/CreateTrackExpenseParams.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import type {PersonalDetailsByLogin} from '@components/PersonalDetailsByLoginProvider';

import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList';

Expand Down Expand Up @@ -53,6 +54,7 @@ type CreateTrackExpenseParams = {
isDraftChatReport: boolean;
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'];
rules: OnyxCollection<OnyxTypes.Rule>;
personalDetailsByLogins?: PersonalDetailsByLogin;
};

export type {CreateTrackExpenseParams, TrackExpenseAccountantParams};
3 changes: 3 additions & 0 deletions src/libs/actions/IOU/types/TrackedExpenseParams.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type {PersonalDetailsByLogin} from '@components/PersonalDetailsByLoginProvider';

import type {CreateWorkspaceParams} from '@libs/API/parameters';

import type {BuildPolicyDataKeys, CurrentUser} from '@userActions/Policy/Policy';
Expand Down Expand Up @@ -75,6 +77,7 @@ type TrackedExpenseParams = {
isDistanceRequest?: boolean;
currentUser: CurrentUser;
reportActionsList: OnyxCollection<OnyxTypes.ReportActions>;
personalDetailsByLogins?: PersonalDetailsByLogin;
};

export type {TrackedExpenseParams, TrackedExpensePolicyParams, TrackedExpenseTransactionParams, TrackedExpenseReportInformation, BuildOnyxDataForTrackExpenseKeys};
5 changes: 4 additions & 1 deletion src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ReportExportType} from '@components/ButtonWithDropdownMenu/types';
import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider';
import type {PersonalDetailsByLogin} from '@components/PersonalDetailsByLoginProvider';

import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList';
import type PolicyData from '@hooks/usePolicyData/types';
Expand Down Expand Up @@ -315,6 +316,7 @@ type DuplicatePolicyDataOptions = {
file?: File | CustomRNImageManipulatorResult;
policyCategories?: PolicyCategories;
localCurrency: string;
personalDetailsByLogins?: PersonalDetailsByLogin;
};

type SetWorkspaceReimbursementActionParams = {
Expand Down Expand Up @@ -3592,6 +3594,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp
localCurrency,
currentUserAccountID,
currentUserEmail,
personalDetailsByLogins,
} = options;

const {
Expand All @@ -3611,7 +3614,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp

const outputCurrency = isOverviewOptionSelected && policy?.outputCurrency ? policy?.outputCurrency : localCurrency;

const policyMemberAccountIDs = isMemberOptionSelected ? Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, undefined, false, false)) : [];
const policyMemberAccountIDs = isMemberOptionSelected ? Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, personalDetailsByLogins, false, false)) : [];
const {customUnitID: distanceCustomUnitID, customUnitRateID} = buildOptimisticDistanceRateCustomUnits(outputCurrency);
const perDiemCustomUnitID = generateCustomUnitID();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useParentReportAction from '@hooks/useParentReportAction';
import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport';
import useParticipantsPolicyTags from '@hooks/useParticipantsPolicyTags';
import usePermissions from '@hooks/usePermissions';
import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin';
import useReportTransactions from '@hooks/useReportTransactions';
import useTransactionsByID from '@hooks/useTransactionsByID';

Expand Down Expand Up @@ -217,6 +218,8 @@ function useExpenseSubmission(params: UseExpenseSubmissionParams) {
onExpenseWriteWillStart,
} = params;

const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {}));

// Localization
const {translate, toLocaleDigit, formatPhoneNumber, dateFnsLocale} = useLocalize();
const {getCurrencyDecimals, getCurrencySymbol} = useCurrencyListActions();
Expand Down Expand Up @@ -904,6 +907,7 @@ function useExpenseSubmission(params: UseExpenseSubmissionParams) {
currentUserLocalCurrency: currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD,
delegateAccountID,
rules,
personalDetailsByLogins: employeePersonalDetails,
});
}
performPostBatchCleanup({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useConfirmModal from '@hooks/useConfirmModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin';
import useThemeStyles from '@hooks/useThemeStyles';

import {setCopyPolicySettingsData} from '@libs/actions/Policy/CopyPolicySettings';
Expand Down Expand Up @@ -96,8 +97,9 @@ function CopyPolicySettingsSelectFeaturesPage() {
const sourceHasAddress = !isEmptyObject(sourcePolicy?.address);
const hasTargetWithoutAddress = targetPolicies.some((policy) => isEmptyObject(policy?.address));

const sourceEmployeePersonalDetails = usePersonalDetailsByLogins(Object.keys(sourcePolicy?.employeeList ?? {}));
const [memberCount = 0] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
selector: createFilteredMemberCountSelector(sourcePolicy?.employeeList, sourcePolicy?.owner, currentUserPersonalDetails.login),
selector: createFilteredMemberCountSelector(sourcePolicy?.employeeList, sourcePolicy?.owner, currentUserPersonalDetails.login, sourceEmployeePersonalDetails),
});
const invoiceCompany = [sourcePolicy?.invoice?.companyName, sourcePolicy?.invoice?.companyWebsite].filter(Boolean).join(', ');
const [invoiceConfigurationText = ''] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import useConfirmModal from '@hooks/useConfirmModal';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';

Expand Down Expand Up @@ -59,7 +60,8 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm
const customUnitRates: Record<string, Rate> = customUnits?.rates ?? {};
const allRates = Object.values(customUnitRates)?.filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length ?? 0;
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const totalMembersSelector = createFilteredMemberCountSelector(policy?.employeeList, policy?.owner, currentUserPersonalDetails.login);
const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {}));
const totalMembersSelector = createFilteredMemberCountSelector(policy?.employeeList, policy?.owner, currentUserPersonalDetails.login, employeePersonalDetails);
const [totalMembers = 0] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
selector: totalMembersSelector,
});
Expand Down Expand Up @@ -223,6 +225,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm
targetPolicyID: duplicateWorkspace.policyID,
welcomeNote: `${translate('workspace.duplicateWorkspace.welcomeNote')} ${duplicateWorkspace.name}`,
policyCategories: selectedItems.includes('categories') ? policyCategories : undefined,
personalDetailsByLogins: employeePersonalDetails,
parts: {
people: selectedItems.includes('members'),
reports: selectedItems.includes('reports'),
Expand Down
84 changes: 46 additions & 38 deletions tests/unit/createFilteredMemberCountSelectorTest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Onyx from 'react-native-onyx';

import type {PersonalDetailsList, PolicyEmployeeList} from '../../src/types/onyx';
import type {PersonalDetailsByLogin} from '../../src/components/PersonalDetailsByLoginProvider';
import type {PersonalDetails, PersonalDetailsList, PolicyEmployeeList} from '../../src/types/onyx';

import {createFilteredMemberCountSelector} from '../../src/libs/PolicyUtils';
import ONYXKEYS from '../../src/ONYXKEYS';
Expand All @@ -17,27 +18,42 @@ const regularUser2Email = 'user2@company.com';
const expensifyGuideEmail = 'guide@team.expensify.com';
const expensifyEmployeeEmail = 'employee@expensify.com';

const regularUser1: PersonalDetails = {
accountID: regularUser1AccountID,
login: regularUser1Email,
displayName: 'User One',
};

const regularUser2: PersonalDetails = {
accountID: regularUser2AccountID,
login: regularUser2Email,
displayName: 'User Two',
};

const expensifyGuide: PersonalDetails = {
accountID: expensifyGuideAccountID,
login: expensifyGuideEmail,
displayName: 'Expensify Guide',
};

const expensifyEmployee: PersonalDetails = {
accountID: expensifyEmployeeAccountID,
login: expensifyEmployeeEmail,
displayName: 'Expensify Employee',
};

const personalDetails: PersonalDetailsList = {
[regularUser1AccountID]: {
accountID: regularUser1AccountID,
login: regularUser1Email,
displayName: 'User One',
},
[regularUser2AccountID]: {
accountID: regularUser2AccountID,
login: regularUser2Email,
displayName: 'User Two',
},
[expensifyGuideAccountID]: {
accountID: expensifyGuideAccountID,
login: expensifyGuideEmail,
displayName: 'Expensify Guide',
},
[expensifyEmployeeAccountID]: {
accountID: expensifyEmployeeAccountID,
login: expensifyEmployeeEmail,
displayName: 'Expensify Employee',
},
[regularUser1AccountID]: regularUser1,
[regularUser2AccountID]: regularUser2,
[expensifyGuideAccountID]: expensifyGuide,
[expensifyEmployeeAccountID]: expensifyEmployee,
};

const personalDetailsByLogin: PersonalDetailsByLogin = {
[regularUser1Email]: regularUser1,
[regularUser2Email]: regularUser2,
[expensifyGuideEmail]: expensifyGuide,
[expensifyEmployeeEmail]: expensifyEmployee,
};

const employeeListWithGuide: PolicyEmployeeList = {
Expand Down Expand Up @@ -73,22 +89,20 @@ describe('createFilteredMemberCountSelector', () => {
const policyOwner = 'owner@company.com';
const currentUserLogin = regularUser1Email;

beforeEach(() => Onyx.multiSet({[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails}).then(waitForBatchedUpdates));

it('should filter out Expensify guides (team.expensify.com)', () => {
const selector = createFilteredMemberCountSelector(employeeListWithGuide, policyOwner, currentUserLogin);
const selector = createFilteredMemberCountSelector(employeeListWithGuide, policyOwner, currentUserLogin, personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(1);
});

it('should filter out Expensify employees (expensify.com)', () => {
const selector = createFilteredMemberCountSelector(employeeListWithExpensifyEmployee, policyOwner, currentUserLogin);
const selector = createFilteredMemberCountSelector(employeeListWithExpensifyEmployee, policyOwner, currentUserLogin, personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(1);
});

it('should return full count when no Expensify team members are present', () => {
const selector = createFilteredMemberCountSelector(employeeListAllRegular, policyOwner, currentUserLogin);
const selector = createFilteredMemberCountSelector(employeeListAllRegular, policyOwner, currentUserLogin, personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(2);
});
Expand All @@ -98,10 +112,8 @@ describe('createFilteredMemberCountSelector', () => {
const policyOwner = 'owner@expensify.com';
const currentUserLogin = regularUser1Email;

beforeEach(() => Onyx.multiSet({[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails}).then(waitForBatchedUpdates));

it('should NOT filter out Expensify team members', () => {
const selector = createFilteredMemberCountSelector(employeeListWithGuide, policyOwner, currentUserLogin);
const selector = createFilteredMemberCountSelector(employeeListWithGuide, policyOwner, currentUserLogin, personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(2);
});
Expand All @@ -111,38 +123,34 @@ describe('createFilteredMemberCountSelector', () => {
const policyOwner = 'owner@company.com';
const currentUserLogin = expensifyGuideEmail;

beforeEach(() => Onyx.multiSet({[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails}).then(waitForBatchedUpdates));

it('should NOT filter out Expensify team members', () => {
const selector = createFilteredMemberCountSelector(employeeListWithGuide, policyOwner, currentUserLogin);
const selector = createFilteredMemberCountSelector(employeeListWithGuide, policyOwner, currentUserLogin, personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(2);
});
});

describe('edge cases', () => {
beforeEach(() => Onyx.multiSet({[ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails}).then(waitForBatchedUpdates));

it('should return 0 when employeeList is undefined', () => {
const selector = createFilteredMemberCountSelector(undefined, 'owner@company.com', 'user@company.com');
const selector = createFilteredMemberCountSelector(undefined, 'owner@company.com', 'user@company.com', personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(0);
});

it('should return 0 when employeeList is empty', () => {
const selector = createFilteredMemberCountSelector({}, 'owner@company.com', 'user@company.com');
const selector = createFilteredMemberCountSelector({}, 'owner@company.com', 'user@company.com', personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(0);
});

it('should NOT filter when policyOwner is undefined (filtering disabled)', () => {
const selector = createFilteredMemberCountSelector(employeeListWithGuide, undefined, regularUser1Email);
const selector = createFilteredMemberCountSelector(employeeListWithGuide, undefined, regularUser1Email, personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(2);
});

it('should NOT filter when currentUserLogin is undefined (filtering disabled)', () => {
const selector = createFilteredMemberCountSelector(employeeListWithGuide, 'owner@company.com', undefined);
const selector = createFilteredMemberCountSelector(employeeListWithGuide, 'owner@company.com', undefined, personalDetailsByLogin);
const count = selector(personalDetails);
expect(count).toBe(2);
});
Expand Down
Loading