Skip to content

Commit b52c6b5

Browse files
committed
Restore non-default distance rates in optimistic duplicate clone
PR #89448 dropped non-default distance rates from the optimistic clone to avoid stale duplicates after the server's Pusher response merged. Online this is fine — the server response repopulates the rates. But offline the response never arrives, leaving the duplicated workspace with only the default rate visible (deploy blocker #89865). Restore the non-default rates with their source IDs so they're visible in the duplicate's distance rates page offline. To prevent the optimistic-vs-server duplicate that the original PR was avoiding, null the source rate IDs in successData — when the API succeeds, the server's Pusher event populates the full rate set with fresh server IDs and the stale optimistic IDs are gone.
1 parent 326f70e commit b52c6b5

3 files changed

Lines changed: 44 additions & 6 deletions

File tree

src/libs/PolicyUtils.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,16 +303,27 @@ function cloneCustomUnitWithNewIDs(unit: CustomUnit, newCustomUnitID: string, ne
303303
// The server-side DUPLICATE_POLICY assigns newDefaultRateID to the source's default rate.
304304
// Mirror getDefaultMileageRate's selection (enabled rates, sorted by index with
305305
// CONST.DEFAULT_NUMBER_ID for missing indexes) so the optimistic clone aligns with the
306-
// rate the expense flow will later treat as default. Other source rates get fresh server
307-
// IDs, so we drop them from the optimistic state to avoid stale duplicates.
306+
// rate the expense flow will later treat as default. Non-default rates keep their source
307+
// IDs so they remain visible offline; successData clears them after the API succeeds so
308+
// the server response can repopulate with fresh server IDs without leaving duplicates.
308309
const defaultRate = Object.values(unit.rates)
309310
.filter((rate) => rate.enabled !== false)
310311
.sort((a, b) => (a.index ?? CONST.DEFAULT_NUMBER_ID) - (b.index ?? CONST.DEFAULT_NUMBER_ID))
311312
.at(0);
313+
const rates: Record<string, Rate> = {};
314+
for (const rate of Object.values(unit.rates)) {
315+
if (rate.customUnitRateID === defaultRate?.customUnitRateID) {
316+
continue;
317+
}
318+
rates[rate.customUnitRateID] = rate;
319+
}
320+
if (defaultRate) {
321+
rates[newDefaultRateID] = {...defaultRate, customUnitRateID: newDefaultRateID};
322+
}
312323
return {
313324
...unit,
314325
customUnitID: newCustomUnitID,
315-
rates: defaultRate ? {[newDefaultRateID]: {...defaultRate, customUnitRateID: newDefaultRateID}} : {},
326+
rates,
316327
};
317328
}
318329

src/libs/actions/Policy/Policy.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3236,6 +3236,22 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp
32363236
const {customUnitID: distanceCustomUnitID, customUnitRateID} = buildOptimisticDistanceRateCustomUnits(outputCurrency);
32373237
const perDiemCustomUnitID = generateCustomUnitID();
32383238

3239+
// Source non-default distance rate IDs — kept in optimistic data so they're visible offline,
3240+
// then nulled in successData so the server's Pusher response can repopulate with fresh server
3241+
// IDs without leaving optimistic-vs-server duplicates.
3242+
const sourceDistanceUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
3243+
const sourceDistanceDefaultRate = sourceDistanceUnit
3244+
? Object.values(sourceDistanceUnit.rates)
3245+
.filter((rate) => rate.enabled !== false)
3246+
.sort((a, b) => (a.index ?? CONST.DEFAULT_NUMBER_ID) - (b.index ?? CONST.DEFAULT_NUMBER_ID))
3247+
.at(0)
3248+
: undefined;
3249+
const sourceNonDefaultDistanceRateIDs = sourceDistanceUnit
3250+
? Object.values(sourceDistanceUnit.rates)
3251+
.filter((rate) => rate.customUnitRateID !== sourceDistanceDefaultRate?.customUnitRateID)
3252+
.map((rate) => rate.customUnitRateID)
3253+
: [];
3254+
32393255
const optimisticAnnounceChat = ReportUtils.buildOptimisticAnnounceChat(targetPolicyID, [...policyMemberAccountIDs], currentUserAccountID);
32403256
const announceRoomChat = optimisticAnnounceChat.announceChatData;
32413257

@@ -3351,6 +3367,13 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp
33513367
type: null,
33523368
areReportFieldsEnabled: null,
33533369
},
3370+
...(sourceNonDefaultDistanceRateIDs.length > 0 && {
3371+
customUnits: {
3372+
[distanceCustomUnitID]: {
3373+
rates: Object.fromEntries(sourceNonDefaultDistanceRateIDs.map((id) => [id, null])),
3374+
},
3375+
},
3376+
}),
33543377
},
33553378
},
33563379
{

tests/unit/PolicyUtilsTest.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ describe('PolicyUtils', () => {
352352
).toBeUndefined();
353353
});
354354

355-
it('clones the source default rate (lowest enabled index) under the API-known customUnitRateID', () => {
355+
it('rebinds the default rate to the API-known customUnitRateID and keeps non-default rates with their source IDs', () => {
356356
const distanceUnitWithMultipleRates = {
357357
customUnitID: 'srcDist',
358358
name: CONST.CUSTOM_UNITS.NAME_DISTANCE,
@@ -377,13 +377,14 @@ describe('PolicyUtils', () => {
377377
...distanceUnitWithMultipleRates,
378378
customUnitID: 'newDist',
379379
rates: {
380+
rateB: {customUnitRateID: 'rateB', name: 'New Rate 1', rate: 100, currency: 'USD', enabled: true, index: 1, attributes: {taxRateExternalID: 'tax_other'}},
380381
newRate: {customUnitRateID: 'newRate', name: 'Default Rate', rate: 70, currency: 'USD', enabled: true, index: 0, attributes: {taxRateExternalID: 'tax_default'}},
381382
},
382383
},
383384
});
384385
});
385386

386-
it('drops all rates when no enabled rate exists', () => {
387+
it('keeps source rates with their source IDs when no enabled rate exists', () => {
387388
const distanceUnitAllDisabled = {
388389
customUnitID: 'srcDist',
389390
name: CONST.CUSTOM_UNITS.NAME_DISTANCE,
@@ -402,7 +403,9 @@ describe('PolicyUtils', () => {
402403
perDiemCustomUnitID: 'newPerDiem',
403404
customUnitRateID: 'newRate',
404405
});
405-
expect(result?.newDist.rates).toEqual({});
406+
expect(result?.newDist.rates).toEqual({
407+
rateA: {customUnitRateID: 'rateA', name: 'Disabled', rate: 50, currency: 'USD', enabled: false, index: 0},
408+
});
406409
});
407410

408411
it('treats missing index as 0 when picking the default rate', () => {
@@ -426,6 +429,7 @@ describe('PolicyUtils', () => {
426429
customUnitRateID: 'newRate',
427430
});
428431
expect(result?.newDist.rates).toEqual({
432+
rateB: {customUnitRateID: 'rateB', name: 'Indexed Rate', rate: 100, currency: 'USD', enabled: true, index: 1},
429433
newRate: {customUnitRateID: 'newRate', name: 'No-Index Rate', rate: 70, currency: 'USD', enabled: true},
430434
});
431435
});

0 commit comments

Comments
 (0)