Skip to content

[$175] Workspace - Rules settings are not applied when copying workspace settings #102447

Description

@MelvinBot

Reproducible in staging?: Yes
Reproducible in production?: Yes
Issue reported by: lanitochka17 (Applause), during deploy QA of Copy workspace settings one target at a time in BulkCopyPolicySettings. That PR did not cause this bug. It also happens in production.
App Component: Workspace Settings

Action Performed:

Preconditions: The account has 3 workspaces. Workspace A has a tag, a tax rate, and Rules enabled with "Require company card for all purchases" turned on.

  1. Log in and go to Settings > Workspaces.
  2. On Workspace A's row, open the three-dot menu and select Copy settings.
  3. Select Workspace B and Workspace C as the targets, then continue.
  4. Select settings to copy, including Rules, then continue.
  5. Review the confirmation screen and confirm the copy.
  6. Open Workspace B and Workspace C.

Expected Result:

All selected settings are applied to Workspace B and Workspace C, including Rules settings such as "Require company card for all purchases."

Actual Result:

Rules settings are not applied to Workspace B and Workspace C.

Workaround:

Unknown

Screenshots/Videos

Bug7271788_1790617752505.Recording__1215.mp4

Possibly related

Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 28, 2026
  2. MelvinBot commented on Sep 28, 2026

    @MelvinBot
    ContributorAuthor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When you copy workspace settings with Rules selected, the Rules setting "Require company cards for all purchases" is not applied to the target workspaces.

    What is the root cause of that problem?

    This toggle is stored in policy.requireCompanyCardsEnabled (IndividualExpenseRulesSectionRevamp.tsx:139). The Copy settings flow leaves that field out of the rules part in three places:

    1. Optimistic copy: PARTS_TO_POLICY_FIELDS.rules lists 17 fields, but requireCompanyCardsEnabled is not one of them. As a result, buildPolicyFieldPatch never writes it to the target policies (CopyPolicySettings.ts:48-66).
    2. Backend copy: CopyPolicySettings sends only part names (parts: 'rules,...'), so the backend decides which fields to copy (CopyPolicySettings.ts:589-595). The App list was built to match the backend list (CopyPolicySettings: add missing rules and categories fields). That suggests the backend also skips this field.
    3. Settings summary: getWorkspaceRules does not list "Require company cards" in the Rules row description on the Select settings step (duplicate/utils.ts:15-75). By contrast, hasConfiguredRules does check this field (PolicyUtils.ts:1338).

    feat: add require company cards toggle to rules revamp added the toggle to the Rules page but did not update the copy mapping.

    What changes do you think we should make in order to solve the problem?

    1. Add requireCompanyCardsEnabled to PARTS_TO_POLICY_FIELDS.rules in CopyPolicySettings.ts. The target policies then get the value optimistically, with the right pending-field handling.
    2. In getWorkspaceRules, add "Require company cards for all purchases" (workspace.rules.individualExpenseRules.requireCompanyCard) to the summary when policy.requireCompanyCardsEnabled is true.
    3. Update the rules field list in tests/actions/CopyPolicySettingsTest.ts, and add a test that checks the target policies end up with requireCompanyCardsEnabled: true.

    The backend copy must also carry this field for the rules part. Without that, the server response will reset the toggle after the optimistic update. An internal engineer needs to confirm and handle that part.

    What alternative solutions did you explore? (Optional)

    The Rules page has other settings that the rules part also doesn't copy, such as receipt visibility (isReceiptVisibilityPublic) and requiresTag. We could add them in the same PR if the backend copies them too.

    Verification notes
    • Verified in App code: the field name, the missing field in PARTS_TO_POLICY_FIELDS.rules and getWorkspaceRules, and that the App sends only part names. The App treats every target the same way, so nothing skips the rules part for Control targets.
    • Not verified: backend behavior. It is inferred from the App list being built to match the backend list. If other Rules fields (for example, the max expense amount) also failed to copy in the video, a separate backend problem exists.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102447#issuecomment-5878848071) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 28, 2026
  4. melvin-bot commented on Sep 28, 2026

    @melvin-bot

    Current assignee @situchan is eligible for the External assigner, not assigning anyone new.

  5. changed the title [-]Workspace - Rules settings are not applied when copying workspace settings[/-] [+][$175] Workspace - Rules settings are not applied when copying workspace settings[/+] on Sep 28, 2026
  6. melvin-bot commented on Sep 28, 2026

    @melvin-bot
  7. bonnhatnguyen commented on Sep 28, 2026

    @bonnhatnguyen
    ## Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When copying workspace settings from a source workspace with Rules enabled and "Require company card for all purchases" turned ON to target workspaces, the "Require company card for all purchases" rule is not applied to the target workspaces. Additionally, on the "Select settings" step of the Copy Settings flow, "Require company cards" is omitted from the Rules row description.


    What is the root cause of that problem?

    There are two distinct root causes stemming from PR #100241 (feat: add require company cards toggle to rules revamp), which introduced policy.requireCompanyCardsEnabled and PublicReceiptVisibilityToggle to the Rules page without updating the Copy Policy Settings subsystem:

    1. Missing from PARTS_TO_POLICY_FIELDS.rules in src/libs/actions/Policy/CopyPolicySettings.ts:
      PARTS_TO_POLICY_FIELDS.rules (lines 48–66) defines the exhaustive array of Onyx Policy fields copied when part === 'rules':

      rules: [
          'areRulesEnabled',
          'maxExpenseAmount',
          'maxExpenseAge',
          'maxExpenseAmountNoReceipt',
          'maxExpenseAmountNoItemizedReceipt',
          'defaultBillable',
          'defaultReimbursable',
          'prohibitedExpenses',
          'eReceipts',
          'isAttendeeTrackingEnabled',
          'preventSelfApproval',
          'disabledFields',
          'glCodes',
          'showTagGLCodes',
          'shouldShowAutoApprovalOptions',
          'shouldShowAutoReimbursementLimitOption',
          'customRules',
      ],

      requireCompanyCardsEnabled (as well as isReceiptVisibilityPublic) is omitted from this list.
      Consequently:

      • In buildPolicyFieldPatch (lines 425–438), the iteration over PARTS_TO_POLICY_FIELDS.rules never reads or patches sourcePolicy.requireCompanyCardsEnabled onto the target policy.
      • In buildPendingFields (lines 484–492) and buildClearedPendingFields (lines 494–502), pendingFields.requireCompanyCardsEnabled is never registered for optimistic Red Brick Road (RBR) feedback.
      • The optimistic update payload for ONYXKEYS.COLLECTION.POLICY leaves requireCompanyCardsEnabled untouched on target workspaces.
    2. Missing from getWorkspaceRules in src/pages/workspace/duplicate/utils.ts:
      In getWorkspaceRules (lines 15–58), the function builds the summary list of active rules displayed on CopyPolicySettingsSelectFeaturesPage.tsx and WorkspaceDuplicateSelectFeaturesForm.tsx.
      While PolicyUtils.hasConfiguredRules checks policy.requireCompanyCardsEnabled (line 1338), getWorkspaceRules does not check policy?.requireCompanyCardsEnabled or policy?.isReceiptVisibilityPublic.
      As a result, the "Select settings" UI summary never displays "Require company cards for all purchases" to the user before they initiate the copy.

    3. Backend sync:
      In CopyPolicySettings.ts, write(WRITE_COMMANDS.COPY_POLICY_SETTINGS, ...) passes only parts: 'rules,overview,...'. The backend processes the copy by part name, referencing the server-side rule bundle that corresponds to the client's PARTS_TO_POLICY_FIELDS.rules. Adding requireCompanyCardsEnabled (and isReceiptVisibilityPublic) ensures full client-side optimistic synchronization and parity with the backend schema (as previously completed in PR CopyPolicySettings: add missing rules and categories fields #93936).


    What changes do you think we should make in order to solve the problem?

    1. In src/libs/actions/Policy/CopyPolicySettings.ts:
      Add 'requireCompanyCardsEnabled' and 'isReceiptVisibilityPublic' to PARTS_TO_POLICY_FIELDS.rules:

      diff --git a/src/libs/actions/Policy/CopyPolicySettings.ts b/src/libs/actions/Policy/CopyPolicySettings.ts
      --- a/src/libs/actions/Policy/CopyPolicySettings.ts
      +++ b/src/libs/actions/Policy/CopyPolicySettings.ts
      @@ -57,6 +57,8 @@ const PARTS_TO_POLICY_FIELDS = {
               'defaultReimbursable',
               'prohibitedExpenses',
               'eReceipts',
      +        'requireCompanyCardsEnabled',
      +        'isReceiptVisibilityPublic',
               'isAttendeeTrackingEnabled',
               'preventSelfApproval',
               'disabledFields',
    2. In src/pages/workspace/duplicate/utils.ts (getWorkspaceRules):
      Add description items for requireCompanyCardsEnabled and isReceiptVisibilityPublic:

      diff --git a/src/pages/workspace/duplicate/utils.ts b/src/pages/workspace/duplicate/utils.ts
      --- a/src/pages/workspace/duplicate/utils.ts
      +++ b/src/pages/workspace/duplicate/utils.ts
      @@ -43,6 +43,12 @@ function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextP
           if (policy?.eReceipts) {
               total.push(translate('workspace.rules.individualExpenseRules.eReceipts'));
           }
      +    if (policy?.requireCompanyCardsEnabled) {
      +        total.push(translate('workspace.rules.individualExpenseRules.requireCompanyCard'));
      +    }
      +    if (policy?.isReceiptVisibilityPublic) {
      +        total.push(translate('workspace.rules.individualExpenseRules.publicReceiptVisibility'));
      +    }
           if (policy?.isAttendeeTrackingEnabled) {
               total.push(translate('workspace.rules.individualExpenseRules.attendeeTracking'));
           }
    3. Automated Unit Tests:
      In tests/actions/CopyPolicySettingsTest.ts:

      • Add 'requireCompanyCardsEnabled' and 'isReceiptVisibilityPublic' to the parameterized test cases under PARTS_TO_POLICY_FIELDS.rules (lines 142–158).
      • Add a dedicated unit test asserting:
        it('copies requireCompanyCardsEnabled and isReceiptVisibilityPublic when copying rules', () => {
            const sourcePolicy = makeSourcePolicy({requireCompanyCardsEnabled: true, isReceiptVisibilityPublic: true});
            const targetPolicy = makeTargetPolicy({requireCompanyCardsEnabled: false, isReceiptVisibilityPublic: false});
        
            const {optimisticData, successData} = buildCopyPolicySettingsData(sourcePolicy, [targetPolicy], ['rules'], {}, {});
        
            const policy = getOptimisticPolicy(optimisticData);
            expect(policy?.requireCompanyCardsEnabled).toBe(true);
            expect(policy?.isReceiptVisibilityPublic).toBe(true);
            expect(policy?.pendingFields?.requireCompanyCardsEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE);
            expect(policy?.pendingFields?.isReceiptVisibilityPublic).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE);
        
            const targetSuccess = successData.find((entry) => entry.key === POLICY_KEY && entry.onyxMethod === Onyx.METHOD.MERGE);
            const successPatch = getMergedPolicyPatch(targetSuccess);
            expect(successPatch?.pendingFields?.requireCompanyCardsEnabled).toBeNull();
            expect(successPatch?.pendingFields?.isReceiptVisibilityPublic).toBeNull();
        });

    What alternative solutions did you explore? (Optional)

    • Copying requireCompanyCardsEnabled only if cards are enabled on the target policy:
      Rejected. A target workspace may enable Expensify/Company cards after the settings copy. The policy rule definition itself should be faithfully mirrored in the policy data model just like other rule toggles (preventSelfApproval, defaultBillable), while row interaction locks in IndividualExpenseRulesSectionRevamp.tsx handle missing card preconditions gracefully.

    Contributor details
    Your Expensify account email: bonnhatnguyen0@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0139ed4098bdc50182

  8. melvin-bot commented on Sep 28, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  9. bconnnnn commented on Sep 29, 2026

    @bconnnnn

    I traced this to the Rules copy allowlist.

    requireCompanyCardsEnabled is a real Rules field and the Rules page reads/writes it, but it isn’t included in PARTS_TO_POLICY_FIELDS.rules in CopyPolicySettings.ts. So the target policy’s optimistic copy never gets that value.

    I’d add requireCompanyCardsEnabled to the Rules copy contract and cover both directions in tests:

    • source true -> target becomes true
    • source false -> target that was true gets cleared

    I’d also verify the backend Rules allowlist uses the same field set. The frontend optimistic state and persisted copy need to stay in sync.

    If the copy-settings summary is meant to enumerate the individual Rules being copied, I’d include this setting there too.

  10. bconnnnn commented on Sep 29, 2026

    @bconnnnn

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When copying workspace settings with Rules selected, the source workspace's Require company cards for all purchases setting is not applied to the target workspaces, which retain their prior value.

    What is the root cause of that problem?

    The App models this toggle as policy.requireCompanyCardsEnabled, but PARTS_TO_POLICY_FIELDS.rules in CopyPolicySettings.ts does not include that field. This is a confirmed App-side gap in the optimistic patch: selecting Rules will not optimistically copy or mark this property pending. The Rules copy summary in duplicate/utils.ts also does not list this toggle.

    The App request sends selected part names such as rules, not individual policy fields. I cannot confirm from this repository whether the server-side CopyPolicySettings handler also omits requireCompanyCardsEnabled; that needs to be checked to explain why the persisted target value remains unchanged after the server response.

    What changes do you think we should make in order to solve the problem?

    1. Add requireCompanyCardsEnabled to the rules field mapping so the optimistic target-policy update includes it.
    2. Include this setting in the Rules copy summary when enabled, so the review step accurately shows what will be copied.
    3. Verify that the server handler copies the same field for the rules part; update its mapping too if it is missing. The client-side allowlist alone cannot guarantee persistence.

    What tests should be added?

    • Extend CopyPolicySettingsTest.ts to cover copying the field both from true to false and false to true, including its pending state.
    • Test that the Rules copy summary includes the company-card requirement when enabled.
    • Verify the reported multi-workspace flow on staging, then reload both targets to confirm the server response preserves the copied value.

    What alternative solutions did you explore? (Optional)

    Changing only the Rules summary would improve visibility but would not copy the field. Changing only the optimistic client map could make the UI look correct temporarily while a server response restores the old value, so both sides need to be aligned.

    Concrete App-side patch

    Add the missing field to the Rules copy allowlist in src/libs/actions/Policy/CopyPolicySettings.ts:

    rules: [
        'areRulesEnabled',
        'requireCompanyCardsEnabled',
        // existing Rules fields...
    ],

    If the copy-settings summary is intended to list enabled Rules, add:

    if (policy?.requireCompanyCardsEnabled) {
        total.push(translate('workspace.rules.individualExpenseRules.requireCompanyCard'));
    }

    Then check the server-side CopyPolicySettings allowlist before calling this complete: the App sends the part name rules, so this client patch alone cannot prove the persisted target value is copied. Test both true→true and true→false, and reload the target workspace to verify the server response.

  11. bconnnnn commented on Sep 30, 2026

    @bconnnnn

    @MelvinBot @situchan I added the App-side Rules allowlist/summary patch and called out the server-side copy check needed to confirm the persisted value. I’m available to implement and verify the fix. Could you review the updated proposal?

  12. melvin-bot commented on Oct 1, 2026

    @melvin-bot

    @situchan Whoops! This issue is 2 days overdue. Let's get this updated quick!

  13. melvin-bot commented on Oct 5, 2026

    @melvin-bot

    @situchan 6 days overdue. This is scarier than being forced to listen to Vogon poetry!

  14. situchan commented on Oct 7, 2026

    @situchan
    Contributor

    Reviewing

  15. situchan commented on Oct 8, 2026

    @situchan
    Contributor

    @MelvinBot implement this for testing

  16. MelvinBot commented on Oct 8, 2026

    @MelvinBot
    ContributorAuthor

    🤖 Draft PR created: #103410
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    The PR adds requireCompanyCardsEnabled to the Rules copy fields and to the Rules summary on the Select settings step, plus unit tests. The related Jest suites and typecheck pass.

    The backend must also copy this field for the rules part. If it doesn't, the server response will reset the toggle after the optimistic update. An internal engineer should confirm that before QA.


    view run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions