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
19 changes: 16 additions & 3 deletions src/libs/CopyPolicySettingsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,19 @@ function isCopyPolicySettingsPartEnabledOnSource(part: Part, context: CopyPolicy
}

/**
* The selected parts that the Collect (Team) targets can't access on their current plan, as
* determined by `canPolicyAccessFeature` (the single source of truth for which features require a
* Control plan). Returns empty when there are no Collect targets.
* Parts that always require a Control target, regardless of what `canPolicyAccessFeature` says about
* the underlying feature toggle. Collect can turn Rules on for the handful of items it supports, but
* it can't receive a Control workspace's Rules configuration: both parts copy Control-only policy
* fields (`maxExpenseAmount`, `eReceipts`, `preventSelfApproval`, `glCodes`, ...) that the backend
* rejects on a Collect target, so the copy has to be gated behind the Upgrade step instead.
*/
const CONTROL_ONLY_COPY_PARTS = new Set<Part>(['rules', 'codingRules']);

/**
* The selected parts that the Collect (Team) targets can't receive on their current plan. Most parts
* defer to `canPolicyAccessFeature` (the single source of truth for which features require a Control
* plan). `CONTROL_ONLY_COPY_PARTS` covers the parts that need Control to be copied even when the
* target could enable the feature itself. Returns empty when there are no Collect targets.
*/
function getControlOnlySelectedParts(targetPolicies: ReadonlyArray<Policy | undefined>, selectedParts: readonly Part[], sourcePolicy?: Policy | null): Part[] {
const collectTargets = targetPolicies.filter((policy): policy is Policy => isCollectPolicy(policy));
Expand All @@ -319,6 +329,9 @@ function getControlOnlySelectedParts(targetPolicies: ReadonlyArray<Policy | unde
const hasInvoiceFields =
isInvoiceFieldsEnabled(sourcePolicy ?? undefined) || Object.values(sourcePolicy?.fieldList ?? {}).some((field) => field.target === CONST.REPORT_FIELD_TARGETS.INVOICE);
return selectedParts.filter((part) => {
if (CONTROL_ONLY_COPY_PARTS.has(part)) {
return true;
}
const featureName = part === 'invoices' && hasInvoiceFields ? CONST.POLICY.MORE_FEATURES.ARE_INVOICE_FIELDS_ENABLED : PART_TO_POLICY_FEATURE[part];
if (!featureName) {
return false;
Expand Down
28 changes: 26 additions & 2 deletions tests/unit/CopyPolicySettingsUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,8 +456,25 @@ describe('CopyPolicySettingsUtils', () => {
expect(getControlOnlySelectedParts([collectTarget(1)], ['categories', 'tags'] as Part[])).toEqual([]);
});

it('does not treat rules as Control-only, since Collect can access them', () => {
expect(getControlOnlySelectedParts([collectTarget(1)], ['rules'] as Part[])).toEqual([]);
it('treats rules as Control-only, since a Collect target cannot receive a Control rules configuration', () => {
// Given a Collect target and the rules part selected
// When the Control-only parts are computed
// Then rules is returned, so the Upgrade step is offered instead of a copy the backend rejects
expect(getControlOnlySelectedParts([collectTarget(1)], ['rules'] as Part[])).toEqual(['rules']);
});

it('treats coding rules as Control-only, since they copy the same Control-only policy fields', () => {
// Given a Collect target and the codingRules part selected
// When the Control-only parts are computed
// Then codingRules is returned, because it maps to the same rules configuration as the rules part
expect(getControlOnlySelectedParts([collectTarget(1)], ['codingRules'] as Part[])).toEqual(['codingRules']);
});

it('does not treat rules as Control-only when every target is already Control', () => {
// Given only a Control target
// When the Control-only parts are computed
// Then nothing is returned, so the flow goes straight to Confirm
expect(getControlOnlySelectedParts([controlTarget(1)], ['rules', 'codingRules'] as Part[])).toEqual([]);
});

it('treats invoices as Control-only when source policy has invoice fields enabled', () => {
Expand Down Expand Up @@ -507,6 +524,13 @@ describe('CopyPolicySettingsUtils', () => {
it('is false when no Control-only part is selected', () => {
expect(shouldShowCopyPolicySettingsUpgradeStep([collectTarget(1)], ['categories'] as Part[])).toBe(false);
});

it('is true when rules target a Collect workspace', () => {
// Given a Collect target and the rules part selected
// When the upgrade step gate is evaluated
// Then it is true, so Select settings routes to Upgrade rather than Confirm
expect(shouldShowCopyPolicySettingsUpgradeStep([collectTarget(1)], ['rules'] as Part[])).toBe(true);
});
});
});
});
Loading