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
1 change: 0 additions & 1 deletion src/hooks/useParticipantSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ const policyMapper = (policy: OnyxEntry<Policy>): OnyxEntry<Policy> =>
role: policy.role,
owner: policy.owner,
outputCurrency: policy.outputCurrency,
isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled,
customUnits: policy.customUnits,
};

Expand Down
1 change: 0 additions & 1 deletion src/hooks/useSearchTypeMenuSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const policyMapper = (policy: OnyxEntry<Policy>): OnyxEntry<Policy> =>
owner: policy.owner,
connections: policy.connections,
outputCurrency: policy.outputCurrency,
isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled,
isJoinRequestPending: policy.isJoinRequestPending,
pendingAction: policy.pendingAction,
errors: policy.errors,
Expand Down
2 changes: 1 addition & 1 deletion src/libs/ExportOnyxState/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const ONYX_KEY_EXPORT_RULES: Record<string, ExportRule> = {
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]: {
Expand Down
3 changes: 1 addition & 2 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
22 changes: 9 additions & 13 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,26 +356,26 @@ function getInvoicePrimaryWorkspace(activePolicy: OnyxEntry<Policy>, 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<OnyxEntry<PolicySelector>> | OnyxCollection<PolicySelector>, includeOnlyAdminPolicies = false): boolean {
const chatEnabledPolicies = Object.values(policies ?? {}).filter(
(policy) => policy?.isPolicyExpenseChatEnabled && (!includeOnlyAdminPolicies || policy.role === CONST.POLICY.ROLE.ADMIN),
function hasActiveGroupPolicies(policies: Array<OnyxEntry<PolicySelector>> | OnyxCollection<PolicySelector>, 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;
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2586,7 +2586,6 @@ function createDraftInitialWorkspace({
role: CONST.POLICY.ROLE.ADMIN,
owner: currentUserEmail,
ownerAccountID: currentUserAccountID,
isPolicyExpenseChatEnabled: true,
areCategoriesEnabled: true,
approver: currentUserEmail,
areCompanyCardsEnabled: true,
Expand Down Expand Up @@ -2812,7 +2811,6 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
role: getRoleForCallerOnNewPolicy(isSubmitWorkspace, makeMeAdmin, policyOwnerEmail, currentUserEmailParam),
owner: policyOwnerEmail || currentUserEmailParam,
ownerAccountID: policyOwnerEmail ? (PersonalDetailsUtils.getPersonalDetailByEmail(policyOwnerEmail)?.accountID ?? currentUserAccountIDParam) : currentUserAccountIDParam,
isPolicyExpenseChatEnabled: true,
outputCurrency,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
autoReporting: true,
Expand Down Expand Up @@ -3349,7 +3347,6 @@ function createDraftWorkspace({
role: CONST.POLICY.ROLE.ADMIN,
owner: currentUserEmail,
ownerAccountID: currentUserAccountID,
isPolicyExpenseChatEnabled: true,
outputCurrency,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
autoReporting: true,
Expand Down Expand Up @@ -4435,7 +4432,6 @@ function createWorkspaceFromIOUPayment({
role: CONST.POLICY.ROLE.ADMIN,
owner: currentUserEmail,
ownerAccountID: currentUserAccountID,
isPolicyExpenseChatEnabled: true,

// Setting the new workspace currency to the currency of the iouReport
outputCurrency: iouReport?.currency ?? CONST.CURRENCY.USD,
Expand Down
1 change: 0 additions & 1 deletion src/libs/actions/TeachersUnite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ function addSchoolPrincipal(
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
id: policyID,
isPolicyExpenseChatEnabled: true,
type: CONST.POLICY.TYPE.CORPORATE,
name: policyName,
role: CONST.POLICY.ROLE.USER,
Expand Down
3 changes: 1 addition & 2 deletions src/selectors/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const policyTimeTrackingSelector = (policy: OnyxEntry<Policy>) =>
units: policy.units,
};

type PolicySelector = Pick<Policy, 'type' | 'role' | 'isPolicyExpenseChatEnabled' | 'pendingAction' | 'avatarURL' | 'name' | 'id' | 'areInvoicesEnabled'>;
type PolicySelector = Pick<Policy, 'type' | 'role' | 'pendingAction' | 'avatarURL' | 'name' | 'id' | 'areInvoicesEnabled'>;

const policyMapper = (policy: OnyxEntry<Policy>): PolicySelector | undefined => {
if (!policy) {
Expand All @@ -288,7 +288,6 @@ const policyMapper = (policy: OnyxEntry<Policy>): PolicySelector | undefined =>
type: policy.type,
role: policy.role,
id: policy.id,
isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled,
pendingAction: policy.pendingAction,
avatarURL: policy.avatarURL,
name: policy.name,
Expand Down
4 changes: 2 additions & 2 deletions src/types/onyx/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2880,8 +2880,8 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The custom units data for this policy */
customUnits?: Record<string, CustomUnit>;

/** 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;
Comment thread
fedirjh marked this conversation as resolved.

/** Whether the auto reporting is enabled */
autoReporting?: boolean;
Expand Down
95 changes: 92 additions & 3 deletions tests/actions/PolicyTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 6 additions & 5 deletions tests/unit/ReportSecondaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Report>({
reportID: REPORT_ID,
policyID: POLICY_ID,
Expand All @@ -4725,7 +4725,7 @@ describe('getSecondaryTransactionThreadActions', () => {
const policy = createMock<Policy>({
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},
Expand Down Expand Up @@ -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<Report>({
reportID: REPORT_ID,
policyID: POLICY_ID,
Expand All @@ -4879,14 +4879,15 @@ describe('getSecondaryTransactionThreadActions', () => {

const policy = createMock<Policy>({
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<typeof PolicyUtils>('@libs/PolicyUtils').isGroupPolicy).mockReturnValueOnce(false);

await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
Expand Down
1 change: 0 additions & 1 deletion tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 1 addition & 3 deletions tests/unit/useCreateReportTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ jest.mock('@libs/PolicyUtils', () => {
const CONSTANTS = jest.requireActual<{default: typeof CONST}>('@src/CONST').default;
return {
getDefaultChatEnabledPolicy: jest.fn((policies: Array<OnyxEntry<Policy>>, activePolicy: OnyxEntry<Policy>) => {
// 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;
Expand Down Expand Up @@ -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',
Expand Down
Loading