Skip to content

Commit 3566ef6

Browse files
authored
Merge pull request Expensify#65301 from dmkt9/fix/63806
Fix/63806 - After moving distance expense to Self DM, "Distance" field is shown as "Pending"
2 parents d1bbd41 + 4e48e88 commit 3566ef6

7 files changed

Lines changed: 182 additions & 11 deletions

File tree

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
254254
const {unit, rate} = DistanceRequestUtils.getRate({transaction, policy});
255255
const distance = getDistanceInMeters(transactionBackup ?? transaction, unit);
256256
const currency = transactionCurrency ?? CONST.CURRENCY.USD;
257-
const isCustomUnitOutOfPolicy = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY);
257+
const isCustomUnitOutOfPolicy = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY) || (isDistanceRequest && !rate);
258258
const rateToDisplay = isCustomUnitOutOfPolicy ? translate('common.rateOutOfPolicy') : DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline);
259259
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate);
260260
let merchantTitle = isEmptyMerchant ? '' : transactionMerchant;
@@ -349,6 +349,10 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
349349
return translate(translationPath);
350350
}
351351

352+
if (isCustomUnitOutOfPolicy && field === 'customUnitRateID') {
353+
return translate('violations.customUnitOutOfPolicy');
354+
}
355+
352356
// Return violations if there are any
353357
if (field !== 'merchant' && hasViolations(field, data, policyHasDependentTags, tagValue)) {
354358
const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue);
@@ -377,6 +381,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
377381
canEditDate,
378382
canEditMerchant,
379383
canEdit,
384+
isCustomUnitOutOfPolicy,
380385
],
381386
);
382387

src/components/TransactionItemRow/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
isMerchantMissing,
3131
isScanning,
3232
isTransactionPendingDelete,
33+
isUnreportedAndHasInvalidDistanceRateTransaction,
3334
} from '@libs/TransactionUtils';
3435
import variables from '@styles/variables';
3536
import CONST from '@src/CONST';
@@ -178,7 +179,8 @@ function TransactionItemRow({
178179

179180
const merchantOrDescriptionName = useMemo(() => getMerchantNameWithFallback(transactionItem, translate, shouldUseNarrowLayout), [shouldUseNarrowLayout, transactionItem, translate]);
180181
const missingFieldError = useMemo(() => {
181-
const hasFieldErrors = hasMissingSmartscanFields(transactionItem);
182+
const isCustomUnitOutOfPolicy = isUnreportedAndHasInvalidDistanceRateTransaction(transactionItem);
183+
const hasFieldErrors = hasMissingSmartscanFields(transactionItem) || isCustomUnitOutOfPolicy;
182184
if (hasFieldErrors) {
183185
const amountMissing = isAmountMissing(transactionItem);
184186
const merchantMissing = isMerchantMissing(transactionItem);
@@ -190,6 +192,8 @@ function TransactionItemRow({
190192
error = translate('iou.missingAmount');
191193
} else if (merchantMissing) {
192194
error = translate('iou.missingMerchant');
195+
} else if (isCustomUnitOutOfPolicy) {
196+
error = translate('violations.customUnitOutOfPolicy');
193197
}
194198
return error;
195199
}

src/libs/DistanceRequestUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ function getDistanceForDisplay(
186186
translate: LocaleContextProps['translate'],
187187
useShortFormUnit?: boolean,
188188
): string {
189-
if (!hasRoute || !rate || !unit || !distanceInMeters) {
189+
if (!hasRoute || !unit || !distanceInMeters) {
190190
return translate('iou.fieldPending');
191191
}
192192

src/libs/SearchUIUtils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
getCreated as getTransactionCreatedDate,
8585
getMerchant as getTransactionMerchant,
8686
isPendingCardOrScanningTransaction,
87+
isUnreportedAndHasInvalidDistanceRateTransaction,
8788
isViolationDismissed,
8889
} from './TransactionUtils';
8990
import shouldShowTransactionYear from './TransactionUtils/shouldShowTransactionYear';
@@ -681,6 +682,11 @@ function getAction(data: OnyxTypes.SearchResults['data'], allViolations: OnyxCol
681682
if (!isTransaction && !isReportEntry(key)) {
682683
return CONST.SEARCH.ACTION_TYPES.VIEW;
683684
}
685+
686+
const transaction = isTransaction ? data[key] : undefined;
687+
if (isUnreportedAndHasInvalidDistanceRateTransaction(transaction)) {
688+
return CONST.SEARCH.ACTION_TYPES.REVIEW;
689+
}
684690
// Tracked and unreported expenses don't have a report, so we return early.
685691
if (!report) {
686692
return CONST.SEARCH.ACTION_TYPES.VIEW;
@@ -690,7 +696,6 @@ function getAction(data: OnyxTypes.SearchResults['data'], allViolations: OnyxCol
690696
return CONST.SEARCH.ACTION_TYPES.PAID;
691697
}
692698

693-
const transaction = isTransaction ? data[key] : undefined;
694699
// We need to check both options for a falsy value since the transaction might not have an error but the report associated with it might. We return early if there are any errors for performance reasons, so we don't need to compute any other possible actions.
695700
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
696701
if (transaction?.errors || report?.errors) {

src/libs/TransactionPreviewUtils.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
isPending,
3737
isPerDiemRequest,
3838
isScanning,
39+
isUnreportedAndHasInvalidDistanceRateTransaction,
3940
} from './TransactionUtils';
4041

4142
const emptyPersonalDetails: OnyxTypes.PersonalDetails = {
@@ -210,12 +211,14 @@ function getTransactionPreviewTextAndTranslationPaths({
210211
RBRMessage = actionsWithErrors.length > 1 ? {translationPath: 'violations.reviewRequired'} : {text: actionsWithErrors.at(0)};
211212
}
212213

213-
RBRMessage ??= {text: ''};
214-
215214
let previewHeaderText: TranslationPathOrText[] = [showCashOrCard];
216215

217216
if (isDistanceRequest(transaction)) {
218217
previewHeaderText = [{translationPath: 'common.distance'}];
218+
219+
if (RBRMessage === undefined && isUnreportedAndHasInvalidDistanceRateTransaction(transaction)) {
220+
RBRMessage = {translationPath: 'violations.customUnitOutOfPolicy'};
221+
}
219222
} else if (isPerDiemRequest(transaction)) {
220223
previewHeaderText = [{translationPath: 'common.perDiem'}];
221224
} else if (isTransactionScanning) {
@@ -224,6 +227,8 @@ function getTransactionPreviewTextAndTranslationPaths({
224227
previewHeaderText = [{translationPath: 'iou.split'}];
225228
}
226229

230+
RBRMessage ??= {text: ''};
231+
227232
if (!isCreatedMissing(transaction)) {
228233
const created = getFormattedCreated(transaction);
229234
const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT);
@@ -318,8 +323,12 @@ function createTransactionPreviewConditionals({
318323

319324
const shouldShowCategory = !!categoryForDisplay && isReportAPolicyExpenseChat;
320325

321-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
322-
const hasAnyViolations = hasViolationsOfTypeNotice || hasWarningTypeViolation(transaction, violations, true) || hasViolation(transaction, violations, true);
326+
const hasAnyViolations =
327+
isUnreportedAndHasInvalidDistanceRateTransaction(transaction) ||
328+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
329+
hasViolationsOfTypeNotice ||
330+
hasWarningTypeViolation(transaction, violations, true) ||
331+
hasViolation(transaction, violations, true);
323332
const hasErrorOrOnHold = hasFieldErrors || (!isFullySettled && !isFullyApproved && isTransactionOnHold);
324333
const hasReportViolationsOrActionErrors = (isReportOwner(iouReport) && hasReportViolations(iouReport?.reportID)) || hasActionsWithErrors(iouReport?.reportID);
325334
const shouldShowRBR = hasAnyViolations || hasErrorOrOnHold || hasReportViolationsOrActionErrors;

src/libs/TransactionUtils/index.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,25 @@ function isFetchingWaypointsFromServer(transaction: OnyxInputOrEntry<Transaction
657657
return !!transaction?.pendingFields?.waypoints;
658658
}
659659

660+
/**
661+
* Verify that the transaction is in Self DM and that its distance rate is invalid.
662+
*/
663+
function isUnreportedAndHasInvalidDistanceRateTransaction(transaction: OnyxInputOrEntry<Transaction>, policyParam: OnyxEntry<Policy> = undefined) {
664+
if (transaction && isDistanceRequest(transaction)) {
665+
const report = getReportOrDraftReport(transaction.reportID);
666+
// eslint-disable-next-line deprecation/deprecation
667+
const policy = policyParam ?? getPolicy(report?.policyID);
668+
const {rate} = DistanceRequestUtils.getRate({transaction, policy});
669+
const isUnreported = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
670+
671+
if (isUnreported && !rate) {
672+
return true;
673+
}
674+
}
675+
676+
return false;
677+
}
678+
660679
/**
661680
* Return the merchant field from the transaction, return the modifiedMerchant if present.
662681
*/
@@ -669,9 +688,11 @@ function getMerchant(transaction: OnyxInputOrEntry<Transaction>, policyParam: On
669688
const mileageRate = DistanceRequestUtils.getRate({transaction, policy});
670689
const {unit, rate} = mileageRate;
671690
const distanceInMeters = getDistanceInMeters(transaction, unit);
672-
return DistanceRequestUtils.getDistanceMerchant(true, distanceInMeters, unit, rate, transaction.currency, translateLocal, (digit) =>
673-
toLocaleDigit(IntlStore.getCurrentLocale(), digit),
674-
);
691+
if (!isUnreportedAndHasInvalidDistanceRateTransaction(transaction, policy)) {
692+
return DistanceRequestUtils.getDistanceMerchant(true, distanceInMeters, unit, rate, transaction.currency, translateLocal, (digit) =>
693+
toLocaleDigit(IntlStore.getCurrentLocale(), digit),
694+
);
695+
}
675696
}
676697
return transaction?.modifiedMerchant ? transaction.modifiedMerchant : (transaction?.merchant ?? '');
677698
}
@@ -1740,6 +1761,7 @@ export {
17401761
getTransactionPendingAction,
17411762
isTransactionPendingDelete,
17421763
createUnreportedExpenseSections,
1764+
isUnreportedAndHasInvalidDistanceRateTransaction,
17431765
};
17441766

17451767
export type {TransactionChanges};

tests/unit/TransactionUtilsTest.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import CONST from '@src/CONST';
44
import IntlStore from '@src/languages/IntlStore';
55
import ONYXKEYS from '@src/ONYXKEYS';
66
import type {Attendee} from '@src/types/onyx/IOU';
7+
import type {CustomUnit, Rate} from '@src/types/onyx/Policy';
78
import type {ReportCollectionDataSet} from '@src/types/onyx/Report';
9+
import type {TransactionCustomUnit} from '@src/types/onyx/Transaction';
810
import * as TransactionUtils from '../../src/libs/TransactionUtils';
911
import type {Policy, Transaction} from '../../src/types/onyx';
1012
import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies';
@@ -69,6 +71,40 @@ const reportCollectionDataSet: ReportCollectionDataSet = {
6971
[`${ONYXKEYS.COLLECTION.REPORT}${FAKE_APPROVED_REPORT_ID}`]: approvedReport,
7072
[`${ONYXKEYS.COLLECTION.REPORT}${FAKE_OPEN_REPORT_SECOND_USER_ID}`]: secondUserOpenReport,
7173
};
74+
const defaultDistanceRatePolicyID1: Record<string, Rate> = {
75+
customUnitRateID1: {
76+
currency: 'USD',
77+
customUnitRateID: 'customUnitRateID1',
78+
enabled: true,
79+
name: 'Default Rate',
80+
rate: 70,
81+
subRates: [],
82+
},
83+
};
84+
const distanceRateTransactionID1: TransactionCustomUnit = {
85+
customUnitID: 'customUnitID1',
86+
customUnitRateID: 'customUnitRateID1',
87+
distanceUnit: 'mi',
88+
name: 'Distance',
89+
};
90+
const distanceRateTransactionID2: TransactionCustomUnit = {
91+
customUnitID: 'customUnitID2',
92+
customUnitRateID: 'customUnitRateID2',
93+
distanceUnit: 'mi',
94+
name: 'Distance',
95+
};
96+
const defaultCustomUnitPolicyID1: Record<string, CustomUnit> = {
97+
customUnitID1: {
98+
attributes: {
99+
unit: 'mi',
100+
},
101+
customUnitID: 'customUnitID1',
102+
defaultCategory: 'Car',
103+
enabled: true,
104+
name: 'Distance',
105+
rates: defaultDistanceRatePolicyID1,
106+
},
107+
};
72108

73109
describe('TransactionUtils', () => {
74110
beforeAll(() => {
@@ -520,4 +556,94 @@ describe('TransactionUtils', () => {
520556
expect(result).toEqual(expected);
521557
});
522558
});
559+
560+
describe('isUnreportedAndHasInvalidDistanceRateTransaction', () => {
561+
it('should be false when transaction is null', () => {
562+
const fakePolicy: Policy = {
563+
...createRandomPolicy(0),
564+
customUnits: defaultCustomUnitPolicyID1,
565+
};
566+
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(null, fakePolicy);
567+
expect(result).toBe(false);
568+
});
569+
it('should be false when transaction is not distance type transaction', () => {
570+
const fakePolicy: Policy = {
571+
...createRandomPolicy(0),
572+
customUnits: defaultCustomUnitPolicyID1,
573+
};
574+
const transaction: Transaction = {
575+
...generateTransaction(),
576+
iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL,
577+
};
578+
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
579+
expect(result).toBe(false);
580+
});
581+
it('should be false when transaction is reported', () => {
582+
const fakePolicy: Policy = {
583+
...createRandomPolicy(0),
584+
customUnits: defaultCustomUnitPolicyID1,
585+
};
586+
const transaction: Transaction = {
587+
...generateTransaction(),
588+
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
589+
reportID: '1',
590+
};
591+
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
592+
expect(result).toBe(false);
593+
});
594+
it('should be false when transaction is unreported and has valid rate', () => {
595+
const fakePolicy: Policy = {
596+
...createRandomPolicy(0),
597+
customUnits: defaultCustomUnitPolicyID1,
598+
};
599+
const transaction: Transaction = {
600+
...generateTransaction(),
601+
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
602+
reportID: '0',
603+
comment: {
604+
customUnit: distanceRateTransactionID1,
605+
type: 'customUnit',
606+
},
607+
};
608+
609+
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
610+
expect(result).toBe(false);
611+
});
612+
it('should be false when transaction is unreported, has invalid rate but policy has default rate', () => {
613+
const fakePolicy: Policy = {
614+
...createRandomPolicy(0),
615+
customUnits: defaultCustomUnitPolicyID1,
616+
};
617+
const transaction: Transaction = {
618+
...generateTransaction(),
619+
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
620+
reportID: '0',
621+
comment: {
622+
customUnit: distanceRateTransactionID2,
623+
type: 'customUnit',
624+
},
625+
};
626+
627+
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
628+
expect(result).toBe(false);
629+
});
630+
it('should be true when transaction is unreported, has invalid rate and policy has no default rate', () => {
631+
const fakePolicy: Policy = {
632+
...createRandomPolicy(0),
633+
customUnits: {},
634+
};
635+
const transaction: Transaction = {
636+
...generateTransaction(),
637+
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
638+
reportID: '0',
639+
comment: {
640+
customUnit: distanceRateTransactionID2,
641+
type: 'customUnit',
642+
},
643+
};
644+
645+
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
646+
expect(result).toBe(true);
647+
});
648+
});
523649
});

0 commit comments

Comments
 (0)