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
7 changes: 6 additions & 1 deletion src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const {unit, rate} = DistanceRequestUtils.getRate({transaction, policy});
const distance = getDistanceInMeters(transactionBackup ?? transaction, unit);
const currency = transactionCurrency ?? CONST.CURRENCY.USD;
const isCustomUnitOutOfPolicy = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY);
const isCustomUnitOutOfPolicy = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY) || (isDistanceRequest && !rate);
const rateToDisplay = isCustomUnitOutOfPolicy ? translate('common.rateOutOfPolicy') : DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline);
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate);
let merchantTitle = isEmptyMerchant ? '' : transactionMerchant;
Expand Down Expand Up @@ -349,6 +349,10 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
return translate(translationPath);
}

if (isCustomUnitOutOfPolicy && field === 'customUnitRateID') {
return translate('violations.customUnitOutOfPolicy');
}

// Return violations if there are any
if (field !== 'merchant' && hasViolations(field, data, policyHasDependentTags, tagValue)) {
const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue);
Expand Down Expand Up @@ -377,6 +381,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
canEditDate,
canEditMerchant,
canEdit,
isCustomUnitOutOfPolicy,
],
);

Expand Down
6 changes: 5 additions & 1 deletion src/components/TransactionItemRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
isMerchantMissing,
isScanning,
isTransactionPendingDelete,
isUnreportedAndHasInvalidDistanceRateTransaction,
} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -178,7 +179,8 @@ function TransactionItemRow({

const merchantOrDescriptionName = useMemo(() => getMerchantNameWithFallback(transactionItem, translate, shouldUseNarrowLayout), [shouldUseNarrowLayout, transactionItem, translate]);
const missingFieldError = useMemo(() => {
const hasFieldErrors = hasMissingSmartscanFields(transactionItem);
const isCustomUnitOutOfPolicy = isUnreportedAndHasInvalidDistanceRateTransaction(transactionItem);
const hasFieldErrors = hasMissingSmartscanFields(transactionItem) || isCustomUnitOutOfPolicy;
if (hasFieldErrors) {
const amountMissing = isAmountMissing(transactionItem);
const merchantMissing = isMerchantMissing(transactionItem);
Expand All @@ -190,6 +192,8 @@ function TransactionItemRow({
error = translate('iou.missingAmount');
} else if (merchantMissing) {
error = translate('iou.missingMerchant');
} else if (isCustomUnitOutOfPolicy) {
error = translate('violations.customUnitOutOfPolicy');
}
return error;
}
Expand Down
2 changes: 1 addition & 1 deletion src/libs/DistanceRequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
};

let lastSelectedDistanceRates: OnyxEntry<LastSelectedDistanceRates> = {};
Onyx.connect({

Check warning on line 25 in src/libs/DistanceRequestUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES,
callback: (value) => {
lastSelectedDistanceRates = value;
Expand All @@ -30,7 +30,7 @@
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 33 in src/libs/DistanceRequestUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -186,7 +186,7 @@
translate: LocaleContextProps['translate'],
useShortFormUnit?: boolean,
): string {
if (!hasRoute || !rate || !unit || !distanceInMeters) {
if (!hasRoute || !unit || !distanceInMeters) {
return translate('iou.fieldPending');
}

Expand Down
7 changes: 6 additions & 1 deletion src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
getCreated as getTransactionCreatedDate,
getMerchant as getTransactionMerchant,
isPendingCardOrScanningTransaction,
isUnreportedAndHasInvalidDistanceRateTransaction,
isViolationDismissed,
} from './TransactionUtils';
import shouldShowTransactionYear from './TransactionUtils/shouldShowTransactionYear';
Expand Down Expand Up @@ -153,7 +154,7 @@
];

let currentAccountID: number | undefined;
Onyx.connect({

Check warning on line 157 in src/libs/SearchUIUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (session) => {
currentAccountID = session?.accountID;
Expand Down Expand Up @@ -680,6 +681,11 @@
if (!isTransaction && !isReportEntry(key)) {
return CONST.SEARCH.ACTION_TYPES.VIEW;
}

const transaction = isTransaction ? data[key] : undefined;
if (isUnreportedAndHasInvalidDistanceRateTransaction(transaction)) {
return CONST.SEARCH.ACTION_TYPES.REVIEW;
}
// Tracked and unreported expenses don't have a report, so we return early.
if (!report) {
return CONST.SEARCH.ACTION_TYPES.VIEW;
Expand All @@ -689,7 +695,6 @@
return CONST.SEARCH.ACTION_TYPES.PAID;
}

const transaction = isTransaction ? data[key] : undefined;
// 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.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (transaction?.errors || report?.errors) {
Expand Down
17 changes: 13 additions & 4 deletions src/libs/TransactionPreviewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
isPending,
isPerDiemRequest,
isScanning,
isUnreportedAndHasInvalidDistanceRateTransaction,
} from './TransactionUtils';

const emptyPersonalDetails: OnyxTypes.PersonalDetails = {
Expand Down Expand Up @@ -209,12 +210,14 @@ function getTransactionPreviewTextAndTranslationPaths({
RBRMessage = actionsWithErrors.length > 1 ? {translationPath: 'violations.reviewRequired'} : {text: actionsWithErrors.at(0)};
}

RBRMessage ??= {text: ''};

let previewHeaderText: TranslationPathOrText[] = [showCashOrCard];

if (isDistanceRequest(transaction)) {
previewHeaderText = [{translationPath: 'common.distance'}];

if (RBRMessage === undefined && isUnreportedAndHasInvalidDistanceRateTransaction(transaction)) {
RBRMessage = {translationPath: 'violations.customUnitOutOfPolicy'};
}
} else if (isPerDiemRequest(transaction)) {
previewHeaderText = [{translationPath: 'common.perDiem'}];
} else if (isTransactionScanning) {
Expand All @@ -223,6 +226,8 @@ function getTransactionPreviewTextAndTranslationPaths({
previewHeaderText = [{translationPath: 'iou.split'}];
}

RBRMessage ??= {text: ''};

if (!isCreatedMissing(transaction)) {
const created = getFormattedCreated(transaction);
const date = DateUtils.formatWithUTCTimeZone(created, DateUtils.doesDateBelongToAPastYear(created) ? CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT : CONST.DATE.MONTH_DAY_ABBR_FORMAT);
Expand Down Expand Up @@ -317,8 +322,12 @@ function createTransactionPreviewConditionals({

const shouldShowCategory = !!categoryForDisplay && isReportAPolicyExpenseChat;

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasAnyViolations = hasViolationsOfTypeNotice || hasWarningTypeViolation(transaction, violations, true) || hasViolation(transaction, violations, true);
const hasAnyViolations =
isUnreportedAndHasInvalidDistanceRateTransaction(transaction) ||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
hasViolationsOfTypeNotice ||
hasWarningTypeViolation(transaction, violations, true) ||
hasViolation(transaction, violations, true);
const hasErrorOrOnHold = hasFieldErrors || (!isFullySettled && !isFullyApproved && isTransactionOnHold);
const hasReportViolationsOrActionErrors = (isReportOwner(iouReport) && hasReportViolations(iouReport?.reportID)) || hasActionsWithErrors(iouReport?.reportID);
const shouldShowRBR = hasAnyViolations || hasErrorOrOnHold || hasReportViolationsOrActionErrors;
Expand Down
28 changes: 25 additions & 3 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@

let allTransactions: OnyxCollection<Transaction> = {};

Onyx.connect({

Check warning on line 89 in src/libs/TransactionUtils/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -98,7 +98,7 @@
});

let allTransactionDrafts: OnyxCollection<Transaction> = {};
Onyx.connect({

Check warning on line 101 in src/libs/TransactionUtils/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -107,7 +107,7 @@
});

let allReports: OnyxCollection<Report> = {};
Onyx.connect({

Check warning on line 110 in src/libs/TransactionUtils/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -116,7 +116,7 @@
});

let allTransactionViolations: OnyxCollection<TransactionViolations> = {};
Onyx.connect({

Check warning on line 119 in src/libs/TransactionUtils/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => (allTransactionViolations = value),
Expand All @@ -124,7 +124,7 @@

let currentUserEmail = '';
let currentUserAccountID = -1;
Onyx.connect({

Check warning on line 127 in src/libs/TransactionUtils/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (val) => {
currentUserEmail = val?.email ?? '';
Expand Down Expand Up @@ -657,6 +657,25 @@
return !!transaction?.pendingFields?.waypoints;
}

/**
* Verify that the transaction is in Self DM and that its distance rate is invalid.
*/
function isUnreportedAndHasInvalidDistanceRateTransaction(transaction: OnyxInputOrEntry<Transaction>, policyParam: OnyxEntry<Policy> = undefined) {
if (transaction && isDistanceRequest(transaction)) {
const report = getReportOrDraftReport(transaction.reportID);
// eslint-disable-next-line deprecation/deprecation
const policy = policyParam ?? getPolicy(report?.policyID);
const {rate} = DistanceRequestUtils.getRate({transaction, policy});
const isUnreported = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;

if (isUnreported && !rate) {
return true;
}
}

return false;
}

/**
* Return the merchant field from the transaction, return the modifiedMerchant if present.
*/
Expand All @@ -669,9 +688,11 @@
const mileageRate = DistanceRequestUtils.getRate({transaction, policy});
const {unit, rate} = mileageRate;
const distanceInMeters = getDistanceInMeters(transaction, unit);
return DistanceRequestUtils.getDistanceMerchant(true, distanceInMeters, unit, rate, transaction.currency, translateLocal, (digit) =>
toLocaleDigit(IntlStore.getCurrentLocale(), digit),
);
if (!isUnreportedAndHasInvalidDistanceRateTransaction(transaction, policy)) {
return DistanceRequestUtils.getDistanceMerchant(true, distanceInMeters, unit, rate, transaction.currency, translateLocal, (digit) =>
toLocaleDigit(IntlStore.getCurrentLocale(), digit),
);
}
}
return transaction?.modifiedMerchant ? transaction.modifiedMerchant : (transaction?.merchant ?? '');
}
Expand Down Expand Up @@ -1740,6 +1761,7 @@
getTransactionPendingAction,
isTransactionPendingDelete,
createUnreportedExpenseSections,
isUnreportedAndHasInvalidDistanceRateTransaction,
};

export type {TransactionChanges};
126 changes: 126 additions & 0 deletions tests/unit/TransactionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import CONST from '@src/CONST';
import IntlStore from '@src/languages/IntlStore';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Attendee} from '@src/types/onyx/IOU';
import type {CustomUnit, Rate} from '@src/types/onyx/Policy';
import type {ReportCollectionDataSet} from '@src/types/onyx/Report';
import type {TransactionCustomUnit} from '@src/types/onyx/Transaction';
import * as TransactionUtils from '../../src/libs/TransactionUtils';
import type {Policy, Transaction} from '../../src/types/onyx';
import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies';
Expand Down Expand Up @@ -69,6 +71,40 @@ const reportCollectionDataSet: ReportCollectionDataSet = {
[`${ONYXKEYS.COLLECTION.REPORT}${FAKE_APPROVED_REPORT_ID}`]: approvedReport,
[`${ONYXKEYS.COLLECTION.REPORT}${FAKE_OPEN_REPORT_SECOND_USER_ID}`]: secondUserOpenReport,
};
const defaultDistanceRatePolicyID1: Record<string, Rate> = {
customUnitRateID1: {
currency: 'USD',
customUnitRateID: 'customUnitRateID1',
enabled: true,
name: 'Default Rate',
rate: 70,
subRates: [],
},
};
const distanceRateTransactionID1: TransactionCustomUnit = {
customUnitID: 'customUnitID1',
customUnitRateID: 'customUnitRateID1',
distanceUnit: 'mi',
name: 'Distance',
};
const distanceRateTransactionID2: TransactionCustomUnit = {
customUnitID: 'customUnitID2',
customUnitRateID: 'customUnitRateID2',
distanceUnit: 'mi',
name: 'Distance',
};
const defaultCustomUnitPolicyID1: Record<string, CustomUnit> = {
customUnitID1: {
attributes: {
unit: 'mi',
},
customUnitID: 'customUnitID1',
defaultCategory: 'Car',
enabled: true,
name: 'Distance',
rates: defaultDistanceRatePolicyID1,
},
};

describe('TransactionUtils', () => {
beforeAll(() => {
Expand Down Expand Up @@ -520,4 +556,94 @@ describe('TransactionUtils', () => {
expect(result).toEqual(expected);
});
});

describe('isUnreportedAndHasInvalidDistanceRateTransaction', () => {
it('should be false when transaction is null', () => {
const fakePolicy: Policy = {
...createRandomPolicy(0),
customUnits: defaultCustomUnitPolicyID1,
};
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(null, fakePolicy);
expect(result).toBe(false);
});
it('should be false when transaction is not distance type transaction', () => {
const fakePolicy: Policy = {
...createRandomPolicy(0),
customUnits: defaultCustomUnitPolicyID1,
};
const transaction: Transaction = {
...generateTransaction(),
iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL,
};
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
expect(result).toBe(false);
});
it('should be false when transaction is reported', () => {
const fakePolicy: Policy = {
...createRandomPolicy(0),
customUnits: defaultCustomUnitPolicyID1,
};
const transaction: Transaction = {
...generateTransaction(),
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
reportID: '1',
};
const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
expect(result).toBe(false);
});
it('should be false when transaction is unreported and has valid rate', () => {
const fakePolicy: Policy = {
...createRandomPolicy(0),
customUnits: defaultCustomUnitPolicyID1,
};
const transaction: Transaction = {
...generateTransaction(),
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
reportID: '0',
comment: {
customUnit: distanceRateTransactionID1,
type: 'customUnit',
},
};

const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
expect(result).toBe(false);
});
it('should be false when transaction is unreported, has invalid rate but policy has default rate', () => {
const fakePolicy: Policy = {
...createRandomPolicy(0),
customUnits: defaultCustomUnitPolicyID1,
};
const transaction: Transaction = {
...generateTransaction(),
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
reportID: '0',
comment: {
customUnit: distanceRateTransactionID2,
type: 'customUnit',
},
};

const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
expect(result).toBe(false);
});
it('should be true when transaction is unreported, has invalid rate and policy has no default rate', () => {
const fakePolicy: Policy = {
...createRandomPolicy(0),
customUnits: {},
};
const transaction: Transaction = {
...generateTransaction(),
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
reportID: '0',
comment: {
customUnit: distanceRateTransactionID2,
type: 'customUnit',
},
};

const result = TransactionUtils.isUnreportedAndHasInvalidDistanceRateTransaction(transaction, fakePolicy);
expect(result).toBe(true);
});
});
});
Loading