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
13 changes: 13 additions & 0 deletions src/libs/CopyPolicySettingsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const FEATURE_ROWS = [
{part: 'invoices', labelKey: 'workspace.common.invoices'},
{part: 'travel', labelKey: 'workspace.common.travel'},
{part: 'timeTracking', labelKey: 'workspace.moreFeatures.timeTracking.title'},
{part: 'receiptPartners', labelKey: 'workspace.moreFeatures.receiptPartners.title'},
] as const satisfies readonly FeatureRow[];

type CopyPolicySettingsSourceFeatureContext = {
Expand Down Expand Up @@ -230,11 +231,22 @@ function isCopyPolicySettingsPartEnabledOnSource(part: Part, context: CopyPolicy
return !!policy?.isTravelEnabled;
case 'timeTracking':
return isTimeTrackingEnabled(policy);
case 'receiptPartners':
return !!policy?.receiptPartners?.enabled || !!policy?.receiptPartners?.uber?.organizationID;
default:
return false;
}
}

/** Subtitle for the receipt partners row when Uber is connected on the source. */
function getReceiptPartnersCopySettingsDescription(policy: Policy | undefined, translate: LocalizedTranslate): string {
const organizationName = policy?.receiptPartners?.uber?.organizationName;
if (organizationName) {
return organizationName;
}
return translate('common.enabled');
}

/** Subtitle for the time tracking row; rate is shown without currency because targets may use a different output currency. */
function getTimeTrackingCopySettingsDescription(policy: Policy | undefined, translate: LocalizedTranslate): string {
const hourlyRate = policy?.units?.time?.rate;
Expand All @@ -253,6 +265,7 @@ export {
areAllTargetsCompatibleForAccountingPart,
isCopyPolicySettingsPartEnabledOnSource,
getTimeTrackingCopySettingsDescription,
getReceiptPartnersCopySettingsDescription,
FEATURE_ROWS,
};
export type {CopyPolicySettingsSourceFeatureContext};
42 changes: 38 additions & 4 deletions src/libs/actions/Policy/CopyPolicySettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ type Part =
| 'perDiem'
| 'invoices'
| 'travel'
| 'timeTracking';
| 'timeTracking'
| 'receiptPartners';

const PARTS_TO_POLICY_FIELDS = {
overview: ['outputCurrency', 'address', 'description'],
Expand Down Expand Up @@ -58,6 +59,7 @@ const PARTS_TO_POLICY_FIELDS = {
invoices: ['areInvoicesEnabled', 'invoice'],
travel: ['isTravelEnabled', 'travelSettings'],
timeTracking: [],
receiptPartners: [],
} as const satisfies Record<Part, ReadonlyArray<keyof Policy>>;

type PolicyFieldsForPart = (typeof PARTS_TO_POLICY_FIELDS)[Part][number];
Expand Down Expand Up @@ -117,6 +119,33 @@ function buildCustomUnitsPatch(sourcePolicy: Policy, targetPolicy: Policy, isDis
return {customUnits: patch};
}

/** Replaces receiptPartners on the target; omits employees and ephemeral Uber UI state. */
function buildReceiptPartnersPatch(sourcePolicy: Policy): Pick<Policy, 'receiptPartners'> | undefined {
const sourceReceiptPartners = sourcePolicy.receiptPartners;
if (!sourceReceiptPartners) {
return undefined;
}

const sourceUber = sourceReceiptPartners.uber;
const uberPatch = sourceUber
? {
...(sourceUber.enabled !== undefined ? {enabled: sourceUber.enabled} : {}),
...(sourceUber.autoInvite !== undefined ? {autoInvite: sourceUber.autoInvite} : {}),
...(sourceUber.autoRemove !== undefined ? {autoRemove: sourceUber.autoRemove} : {}),
...(sourceUber.organizationID !== undefined ? {organizationID: sourceUber.organizationID} : {}),
...(sourceUber.organizationName !== undefined ? {organizationName: sourceUber.organizationName} : {}),
...(sourceUber.centralBillingAccountEmail !== undefined ? {centralBillingAccountEmail: sourceUber.centralBillingAccountEmail} : {}),
}
: undefined;

return {
receiptPartners: {
...(sourceReceiptPartners.enabled !== undefined ? {enabled: sourceReceiptPartners.enabled} : {}),
...(uberPatch ? {uber: uberPatch} : {}),
},
};
}

/** Merges source units.time onto the target without generating IDs. */
function buildTimeTrackingPatch(sourcePolicy: Policy): Pick<Policy, 'units'> | undefined {
const sourceTime = sourcePolicy.units?.time;
Expand All @@ -128,7 +157,7 @@ function buildTimeTrackingPatch(sourcePolicy: Policy): Pick<Policy, 'units'> | u

/**
* Returns the partial Policy patch derived from the selected `parts`, excluding fields whose
* mapping is handled separately (customUnits, timeTracking, categories, tags collection keys).
* mapping is handled separately (customUnits, timeTracking, receiptPartners, categories, tags collection keys).
*/
function buildPolicyFieldPatch(sourcePolicy: Policy, parts: Part[]): Partial<Policy> {
const patch: Partial<Policy> = {};
Expand Down Expand Up @@ -198,6 +227,7 @@ function buildCopyPolicySettingsData(
const isDistanceSelected = parts.includes('distanceRates');
const isPerDiemSelected = parts.includes('perDiem');
const isTimeTrackingSelected = parts.includes('timeTracking');
const isReceiptPartnersSelected = parts.includes('receiptPartners');
const isCodingRulesSelected = parts.includes('codingRules');
const timeTrackingPendingFields = isTimeTrackingSelected
? {
Expand All @@ -211,6 +241,8 @@ function buildCopyPolicySettingsData(
timeTrackingDefaultRate: null,
}
: {};
const receiptPartnersPendingFields = isReceiptPartnersSelected ? {receiptPartners: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE} : {};
const receiptPartnersClearedPendingFields = isReceiptPartnersSelected ? {receiptPartners: null} : {};

const sourceCategoriesKey = `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${sourcePolicy.id}` as const;
const sourceTagsKey = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${sourcePolicy.id}` as const;
Expand All @@ -233,6 +265,7 @@ function buildCopyPolicySettingsData(
const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${targetPolicy.id}` as const;
const customUnitsPatch = buildCustomUnitsPatch(sourcePolicy, targetPolicy, isDistanceSelected, isPerDiemSelected);
const timeTrackingPatch = isTimeTrackingSelected ? buildTimeTrackingPatch(sourcePolicy) : undefined;
const receiptPartnersPatch = isReceiptPartnersSelected ? buildReceiptPartnersPatch(sourcePolicy) : undefined;
const codingRulesPatch = isCodingRulesSelected
? {
rules: {
Expand Down Expand Up @@ -260,8 +293,9 @@ function buildCopyPolicySettingsData(
},
}
: {}),
...(receiptPartnersPatch ? {receiptPartners: receiptPartnersPatch.receiptPartners} : {}),
...codingRulesPatch,
pendingFields: {...targetPolicy.pendingFields, ...pendingFields, ...timeTrackingPendingFields},
pendingFields: {...targetPolicy.pendingFields, ...pendingFields, ...timeTrackingPendingFields, ...receiptPartnersPendingFields},
},
});

Expand All @@ -270,7 +304,7 @@ function buildCopyPolicySettingsData(
onyxMethod: Onyx.METHOD.MERGE,
key: policyKey,
value: {
pendingFields: {...clearedPendingFields, ...timeTrackingClearedPendingFields},
pendingFields: {...clearedPendingFields, ...timeTrackingClearedPendingFields, ...receiptPartnersClearedPendingFields},
errors: null,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
areAllTargetsAccountingCompatible,
areAllTargetsCompatibleForAccountingPart,
FEATURE_ROWS,
getReceiptPartnersCopySettingsDescription,
getTimeTrackingCopySettingsDescription,
isCopyPolicySettingsPartEnabledOnSource,
} from '@libs/CopyPolicySettingsUtils';
Expand Down Expand Up @@ -174,6 +175,8 @@ function CopyPolicySettingsSelectFeaturesPage() {
return perDiemCount > 0 ? `${perDiemCount} ${translate('workspace.common.perDiem').toLowerCase()}` : undefined;
case 'timeTracking':
return getTimeTrackingCopySettingsDescription(sourcePolicy, translate);
case 'receiptPartners':
return getReceiptPartnersCopySettingsDescription(sourcePolicy, translate);
case 'invoices':
return invoiceConfigurationText || undefined;
default:
Expand Down
2 changes: 1 addition & 1 deletion src/types/onyx/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ type UberReceiptPartner = {
/**
* form data for uber partner
*/
connectFormData: string;
connectFormData?: string;
/**
* auto invite for uber connection
*/
Expand Down
42 changes: 29 additions & 13 deletions tests/unit/CopyPolicySettingsUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import {
areAllTargetsAccountingCompatible,
areAllTargetsCompatibleForAccountingPart,
arePoliciesAccountingCompatible,
FEATURE_ROWS,
getAccountingConnectionIdentity,
getConnectionCompanyID,
getReceiptPartnersCopySettingsDescription,
getTimeTrackingCopySettingsDescription,
isCopyPolicySettingsPartEnabledOnSource,
isTargetCompatibleForAccountingPart,
} from '@libs/CopyPolicySettingsUtils';
import type {CopyPolicySettingsSourceFeatureContext} from '@libs/CopyPolicySettingsUtils';
import CONST from '@src/CONST';
import IntlStore from '@src/languages/IntlStore';
import type {Policy} from '@src/types/onyx';
import type {ConnectionName} from '@src/types/onyx/Policy';
import createRandomPolicy from '../utils/collections/policies';
import {translateLocal} from '../utils/TestHelper';

function makePolicyWithConnection(connectionName: ConnectionName, connectionPayload: Record<string, unknown>): Policy {
const base = createRandomPolicy(0, CONST.POLICY.TYPE.CORPORATE);
Expand All @@ -27,6 +29,8 @@ function makePolicyWithConnection(connectionName: ConnectionName, connectionPayl
}

describe('CopyPolicySettingsUtils', () => {
beforeAll(() => IntlStore.load(CONST.LOCALES.EN));

describe('getConnectionCompanyID', () => {
it('returns realmId for QuickBooks Online', () => {
const policy = makePolicyWithConnection(CONST.POLICY.CONNECTIONS.NAME.QBO, {config: {realmId: 'REALM-123'}});
Expand Down Expand Up @@ -188,6 +192,25 @@ describe('CopyPolicySettingsUtils', () => {
expect(isCopyPolicySettingsPartEnabledOnSource('travel', {...baseContext, policy: travelPolicy})).toBe(true);
});

it('shows receipt partners when the feature or Uber connection is enabled on the source', () => {
expect(isCopyPolicySettingsPartEnabledOnSource('receiptPartners', baseContext)).toBe(false);

const enabledOnlyPolicy = createRandomPolicy(9);
enabledOnlyPolicy.receiptPartners = {enabled: true};
expect(isCopyPolicySettingsPartEnabledOnSource('receiptPartners', {...baseContext, policy: enabledOnlyPolicy})).toBe(true);

const connectedUberPolicy = createRandomPolicy(10);
connectedUberPolicy.receiptPartners = {uber: {organizationID: 'org-123', organizationName: 'Acme Uber'}};
expect(isCopyPolicySettingsPartEnabledOnSource('receiptPartners', {...baseContext, policy: connectedUberPolicy})).toBe(true);
});

it('describes receipt partners with the connected Uber organization name', () => {
const policy = createRandomPolicy(11);
policy.receiptPartners = {enabled: true, uber: {organizationName: 'Acme Uber Org'}};

expect(getReceiptPartnersCopySettingsDescription(policy, translateLocal)).toBe('Acme Uber Org');
});

it('shows time tracking only when the feature is enabled on the source', () => {
expect(isCopyPolicySettingsPartEnabledOnSource('timeTracking', baseContext)).toBe(false);

Expand All @@ -201,27 +224,19 @@ describe('CopyPolicySettingsUtils', () => {
});

it('describes time tracking without currency when a default rate exists', () => {
const translate = ((key: string) => {
if (key === 'common.enabled') {
return 'Enabled';
}
if (key === 'workspace.moreFeatures.timeTracking.defaultHourlyRate') {
return 'Default hourly rate';
}
return key;
}) as LocalizedTranslate;
const policy = createRandomPolicy(7);
policy.units = {time: {enabled: true, rate: 75}};

expect(getTimeTrackingCopySettingsDescription(policy, translate)).toBe('Enabled, Default hourly rate: 75');
expect(getTimeTrackingCopySettingsDescription(policy, translateLocal)).toBe(
`${translateLocal('common.enabled')}, ${translateLocal('workspace.moreFeatures.timeTracking.defaultHourlyRate')}: 75`,
);
});

it('describes time tracking as enabled when no default rate is set', () => {
const translate = ((key: string) => (key === 'common.enabled' ? 'Enabled' : key)) as LocalizedTranslate;
const policy = createRandomPolicy(8);
policy.units = {time: {enabled: true}};

expect(getTimeTrackingCopySettingsDescription(policy, translate)).toBe('Enabled');
expect(getTimeTrackingCopySettingsDescription(policy, translateLocal)).toBe(translateLocal('common.enabled'));
});

it('hides distance rates when the feature flag is off even if rates exist', () => {
Expand Down Expand Up @@ -319,6 +334,7 @@ describe('CopyPolicySettingsUtils', () => {
expect(parts).toContain('invoices');
expect(parts).toContain('travel');
expect(parts).toContain('timeTracking');
expect(parts).toContain('receiptPartners');
});
});
});
Loading