diff --git a/src/libs/CopyPolicySettingsUtils.ts b/src/libs/CopyPolicySettingsUtils.ts index c37c05ffa985..3629fc0b396e 100644 --- a/src/libs/CopyPolicySettingsUtils.ts +++ b/src/libs/CopyPolicySettingsUtils.ts @@ -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 = { @@ -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; @@ -253,6 +265,7 @@ export { areAllTargetsCompatibleForAccountingPart, isCopyPolicySettingsPartEnabledOnSource, getTimeTrackingCopySettingsDescription, + getReceiptPartnersCopySettingsDescription, FEATURE_ROWS, }; export type {CopyPolicySettingsSourceFeatureContext}; diff --git a/src/libs/actions/Policy/CopyPolicySettings.ts b/src/libs/actions/Policy/CopyPolicySettings.ts index 00fbef58e206..aa338c146810 100644 --- a/src/libs/actions/Policy/CopyPolicySettings.ts +++ b/src/libs/actions/Policy/CopyPolicySettings.ts @@ -25,7 +25,8 @@ type Part = | 'perDiem' | 'invoices' | 'travel' - | 'timeTracking'; + | 'timeTracking' + | 'receiptPartners'; const PARTS_TO_POLICY_FIELDS = { overview: ['outputCurrency', 'address', 'description'], @@ -58,6 +59,7 @@ const PARTS_TO_POLICY_FIELDS = { invoices: ['areInvoicesEnabled', 'invoice'], travel: ['isTravelEnabled', 'travelSettings'], timeTracking: [], + receiptPartners: [], } as const satisfies Record>; type PolicyFieldsForPart = (typeof PARTS_TO_POLICY_FIELDS)[Part][number]; @@ -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 | 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 | undefined { const sourceTime = sourcePolicy.units?.time; @@ -128,7 +157,7 @@ function buildTimeTrackingPatch(sourcePolicy: Policy): Pick | 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 { const patch: Partial = {}; @@ -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 ? { @@ -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; @@ -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: { @@ -260,8 +293,9 @@ function buildCopyPolicySettingsData( }, } : {}), + ...(receiptPartnersPatch ? {receiptPartners: receiptPartnersPatch.receiptPartners} : {}), ...codingRulesPatch, - pendingFields: {...targetPolicy.pendingFields, ...pendingFields, ...timeTrackingPendingFields}, + pendingFields: {...targetPolicy.pendingFields, ...pendingFields, ...timeTrackingPendingFields, ...receiptPartnersPendingFields}, }, }); @@ -270,7 +304,7 @@ function buildCopyPolicySettingsData( onyxMethod: Onyx.METHOD.MERGE, key: policyKey, value: { - pendingFields: {...clearedPendingFields, ...timeTrackingClearedPendingFields}, + pendingFields: {...clearedPendingFields, ...timeTrackingClearedPendingFields, ...receiptPartnersClearedPendingFields}, errors: null, }, }); diff --git a/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx b/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx index ebf77d15456b..2c957c5c67e3 100644 --- a/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx +++ b/src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx @@ -22,6 +22,7 @@ import { areAllTargetsAccountingCompatible, areAllTargetsCompatibleForAccountingPart, FEATURE_ROWS, + getReceiptPartnersCopySettingsDescription, getTimeTrackingCopySettingsDescription, isCopyPolicySettingsPartEnabledOnSource, } from '@libs/CopyPolicySettingsUtils'; @@ -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: diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 5157dda31b3b..ef435f8a4c64 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -141,7 +141,7 @@ type UberReceiptPartner = { /** * form data for uber partner */ - connectFormData: string; + connectFormData?: string; /** * auto invite for uber connection */ diff --git a/tests/unit/CopyPolicySettingsUtilsTest.ts b/tests/unit/CopyPolicySettingsUtilsTest.ts index 5ddb3408e3d4..ca8517f9acf5 100644 --- a/tests/unit/CopyPolicySettingsUtilsTest.ts +++ b/tests/unit/CopyPolicySettingsUtilsTest.ts @@ -1,4 +1,3 @@ -import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import { areAllTargetsAccountingCompatible, areAllTargetsCompatibleForAccountingPart, @@ -6,15 +5,18 @@ import { 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): Policy { const base = createRandomPolicy(0, CONST.POLICY.TYPE.CORPORATE); @@ -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'}}); @@ -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); @@ -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', () => { @@ -319,6 +334,7 @@ describe('CopyPolicySettingsUtils', () => { expect(parts).toContain('invoices'); expect(parts).toContain('travel'); expect(parts).toContain('timeTracking'); + expect(parts).toContain('receiptPartners'); }); }); });