Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/components/AddUnreportedExpenseFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type AddUnreportedExpenseFooterProps = {
};

function AddUnreportedExpenseFooter({selectedIds, report, reportToConfirm, reportNextStep, policy, policyCategories, errorMessage, setErrorMessage}: AddUnreportedExpenseFooterProps) {
const {translate} = useLocalize();
const {translate, toLocaleDigit} = useLocalize();
const styles = useThemeStyles();
const {isBetaEnabled} = usePermissions();
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
Expand Down Expand Up @@ -80,6 +80,8 @@ function AddUnreportedExpenseFooter({selectedIds, report, reportToConfirm, repor
reportNextStep,
policyCategories,
allTransactions,
translate,
toLocaleDigit,
});
}
});
Expand Down
27 changes: 27 additions & 0 deletions src/libs/DistanceRequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,32 @@ function getRateForP2P(currency: string, transaction: OnyxEntry<Transaction>): M
};
}

function getRateForP2PInUnit(currency: string, transaction: OnyxEntry<Transaction>, unit: Unit): number {
const mileageRate = getRateForP2P(currency, transaction);

if (!mileageRate.rate) {
return 0;
}

// If requested unit matches the currency's native unit, return the rate directly
if (unit === mileageRate.unit) {
return mileageRate.rate;
}

// If units don't match, convert between km and miles
if (unit === 'km' && mileageRate.unit === 'mi') {
// Convert miles rate to km rate: km rate = mile rate / CONST.CUSTOM_UNITS.MILES_TO_KILOMETERS
return mileageRate.rate / CONST.CUSTOM_UNITS.MILES_TO_KILOMETERS;
}
if (unit === 'mi' && mileageRate.unit === 'km') {
// Convert km rate to miles rate: mile rate = km rate * CONST.CUSTOM_UNITS.MILES_TO_KILOMETERS
return mileageRate.rate * CONST.CUSTOM_UNITS.MILES_TO_KILOMETERS;
}

// Fallback: return rate as-is (shouldn't happen with valid units)
return mileageRate.rate;
}

/**
* Calculates the expense amount based on distance, unit, and rate.
*
Expand Down Expand Up @@ -427,6 +453,7 @@ export default {
getDistanceForDisplay,
getRoundedDistanceInUnits,
getRateForP2P,
getRateForP2PInUnit,
getCustomUnitRateID,
convertToDistanceInMeters,
getTaxableAmount,
Expand Down
9 changes: 6 additions & 3 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
};

let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
Onyx.connect({

Check warning on line 135 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.POLICY_TAGS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -153,10 +153,12 @@
return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {};
}

function hasCustomUnit(transaction: OnyxEntry<Transaction> | Partial<Transaction>): boolean {
return transaction?.comment?.type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && !!transaction?.comment?.customUnit;
}

function hasDistanceCustomUnit(transaction: OnyxEntry<Transaction> | Partial<Transaction>): boolean {
const type = transaction?.comment?.type;
const customUnitName = transaction?.comment?.customUnit?.name;
return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE;
return transaction?.comment?.type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && transaction?.comment?.customUnit?.name === CONST.CUSTOM_UNITS.NAME_DISTANCE;
}

function isDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
Expand Down Expand Up @@ -2759,6 +2761,7 @@
didReceiptScanSucceed,
getValidWaypoints,
getValidDuplicateTransactionIDs,
hasCustomUnit,
isDistanceRequest,
isMapDistanceRequest,
isGPSDistanceRequest,
Expand Down
76 changes: 65 additions & 11 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import lodashClone from 'lodash/clone';
import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import * as API from '@libs/API';
import type {
ChangeTransactionsReportParams,
Expand All @@ -13,7 +14,9 @@
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as CollectionUtils from '@libs/CollectionUtils';
import {getCurrencySymbol} from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {buildNextStepNew, buildOptimisticNextStep} from '@libs/NextStepUtils';
import * as NumberUtils from '@libs/NumberUtils';
import {rand64} from '@libs/NumberUtils';
Expand All @@ -33,7 +36,17 @@
hasViolations as hasViolationsReportUtils,
shouldEnableNegative,
} from '@libs/ReportUtils';
import {isManagedCardTransaction, isOnHold, shouldClearConvertedAmount, waypointHasValidAddress} from '@libs/TransactionUtils';
import {
getCurrency,
getDistanceInMeters,
hasCustomUnit,
isDistanceRequest,
isManagedCardTransaction,
isManualDistanceRequest,
isOnHold,
shouldClearConvertedAmount,
waypointHasValidAddress,
} from '@libs/TransactionUtils';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -53,12 +66,12 @@
import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage';
import type {OnyxData} from '@src/types/onyx/Request';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type {Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type {Comment, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type TransactionState from '@src/types/utils/TransactionStateType';
import {getPolicyTags} from './IOU/index';

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

Check warning on line 74 in src/libs/actions/Transaction.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 @@ -67,7 +80,7 @@
});

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

Check warning on line 83 in src/libs/actions/Transaction.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 @@ -79,7 +92,7 @@
});

const allTransactionViolation: OnyxCollection<TransactionViolation[]> = {};
Onyx.connect({

Check warning on line 95 in src/libs/actions/Transaction.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,
callback: (transactionViolation, key) => {
if (!key || !transactionViolation) {
Expand All @@ -91,7 +104,7 @@
});

let allTransactionViolations: TransactionViolations = [];
Onyx.connect({

Check warning on line 107 in src/libs/actions/Transaction.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,
callback: (val) => (allTransactionViolations = val ?? []),
});
Expand Down Expand Up @@ -762,6 +775,8 @@
reportNextStep?: OnyxEntry<ReportNextStepDeprecated>;
policyCategories?: OnyxEntry<PolicyCategories>;
allTransactions: OnyxCollection<Transaction>;
translate: LocaleContextProps['translate'];
toLocaleDigit: LocaleContextProps['toLocaleDigit'];
};

function changeTransactionsReport({
Expand All @@ -774,6 +789,8 @@
reportNextStep,
policyCategories,
allTransactions,
translate,
toLocaleDigit,
}: ChangeTransactionsReportProps) {
const reportID = newReport?.reportID ?? CONST.REPORT.UNREPORTED_REPORT_ID;

Expand Down Expand Up @@ -951,19 +968,56 @@
created: oldIOUAction?.created ?? DateUtils.getDBTime(),
};

let comment: NullishDeep<Comment> | undefined;
let modifiedAmount: number | undefined;
let modifiedMerchant: string | undefined;
if (isUnreported) {
// If the transaction is on hold, we need to unhold it because unreported transactions (on selfDM) should never remain on hold.
comment = {
hold: null,
};

// If the transaction has a custom unit then update it to `_FAKE_P2P_ID_` so it's no longer tied to the policy's rate which would cause the "Rate out of policy" violation to appear.
if (hasCustomUnit(transaction)) {
comment.customUnit = {
customUnitID: CONST.CUSTOM_UNITS.FAKE_P2P_ID,
customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID,
};

// For distance requests we also need to set the defaultP2PRate and update the amount and merchant
if (isDistanceRequest(transaction)) {
const currency = getCurrency(transaction);
const unit = transaction?.comment?.customUnit?.distanceUnit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES;
const distanceInMeters = getDistanceInMeters(transaction, unit);
const defaultP2PRate = DistanceRequestUtils.getRateForP2PInUnit(currency, transaction, unit);
comment.customUnit.defaultP2PRate = defaultP2PRate;
modifiedAmount = -DistanceRequestUtils.getDistanceRequestAmount(distanceInMeters, unit, defaultP2PRate);
modifiedMerchant = DistanceRequestUtils.getDistanceMerchant(
true,
distanceInMeters,
unit,
defaultP2PRate,
currency,
translate,
toLocaleDigit,
getCurrencySymbol,
isManualDistanceRequest(transaction),
);
}
}
}

// 1. Optimistically change the reportID on the passed transactions
// Only set pendingAction for transactions that need convertedAmount recalculation
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
value: {
reportID,
comment,
modifiedAmount,
modifiedMerchant,
...(shouldClearAmount && {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(isUnreported && {
comment: {
hold: null,
},
}),
...(shouldClearAmount && {convertedAmount: null}),
...(oldIOUAction ? {linkedTrackedExpenseReportAction: newIOUAction} : {}),
},
Expand All @@ -983,16 +1037,16 @@
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
value: {
reportID: transaction.reportID,
comment: transaction.comment,
modifiedAmount: transaction.modifiedAmount,
modifiedMerchant: transaction.modifiedMerchant,
...(shouldClearAmount && {pendingAction: transaction.pendingAction ?? null}),
comment: {
hold: transaction.comment?.hold,
},
...(shouldClearAmount && {convertedAmount: transaction.convertedAmount}),
},
});

// Optimistically clear all violations for the transaction when moving to self DM report
if (reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
if (isUnreported) {
const duplicateViolation = currentTransactionViolations?.[transaction.transactionID]?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION);
const duplicateTransactionIDs = duplicateViolation?.data?.duplicates;
if (duplicateTransactionIDs) {
Expand Down
6 changes: 5 additions & 1 deletion src/pages/NewReportWorkspaceSelectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag
const {selectedTransactions, selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
const styles = useThemeStyles();
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const {translate, localeCompare} = useLocalize();
const {translate, localeCompare, toLocaleDigit} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP, {canBeMissing: true});
const isRHPOnReportInSearch = isRHPOnSearchMoneyRequestReportPage();
Expand Down Expand Up @@ -141,6 +141,8 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag
reportNextStep,
policyCategories: undefined,
allTransactions,
translate,
toLocaleDigit,
});

// eslint-disable-next-line rulesdir/no-default-id-values
Expand Down Expand Up @@ -175,6 +177,8 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag
activePolicyID,
clearSelectedTransactions,
betas,
translate,
toLocaleDigit,
],
);

Expand Down
8 changes: 8 additions & 0 deletions src/pages/Search/SearchTransactionsChangeReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {useSearchContext} from '@components/Search/SearchContext';
import type {ListItem} from '@components/SelectionListWithSections/types';
import useConditionalCreateEmptyReportConfirmation from '@hooks/useConditionalCreateEmptyReportConfirmation';
import useHasPerDiemTransactions from '@hooks/useHasPerDiemTransactions';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
Expand All @@ -27,6 +28,7 @@ type TransactionGroupListItem = ListItem & {
};

function SearchTransactionsChangeReport() {
const {translate, toLocaleDigit} = useLocalize();
const {selectedTransactions, clearSelectedTransactions} = useSearchContext();
const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]);
const transactions = useMemo(
Expand Down Expand Up @@ -111,6 +113,8 @@ function SearchTransactionsChangeReport() {
reportNextStep,
policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyForMovingExpensesID}`],
allTransactions: transactions,
translate,
toLocaleDigit,
});
clearSelectedTransactions();
});
Expand Down Expand Up @@ -153,6 +157,8 @@ function SearchTransactionsChangeReport() {
reportNextStep,
policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${item.policyID}`],
allTransactions: transactions,
translate,
toLocaleDigit,
});
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
Expand All @@ -172,6 +178,8 @@ function SearchTransactionsChangeReport() {
accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
email: session?.email ?? '',
allTransactions: transactions,
translate,
toLocaleDigit,
});
clearSelectedTransactions();
Navigation.goBack();
Expand Down
6 changes: 6 additions & 0 deletions src/pages/iou/request/step/IOURequestEditReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {useSearchContext} from '@components/Search/SearchContext';
import type {ListItem} from '@components/SelectionListWithSections/types';
import useConditionalCreateEmptyReportConfirmation from '@hooks/useConditionalCreateEmptyReportConfirmation';
import useHasPerDiemTransactions from '@hooks/useHasPerDiemTransactions';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
Expand Down Expand Up @@ -34,6 +35,7 @@ type IOURequestEditReportProps = WithWritableReportOrNotFoundProps<typeof SCREEN
function IOURequestEditReport({route}: IOURequestEditReportProps) {
const {backTo, reportID, action, shouldTurnOffSelectionMode} = route.params;

const {translate, toLocaleDigit} = useLocalize();
const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
const [allReports] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}`, {canBeMissing: false});
const [selectedReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false});
Expand Down Expand Up @@ -77,6 +79,8 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
reportNextStep,
policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${item.policyID}`],
allTransactions,
translate,
toLocaleDigit,
});
turnOffMobileSelectionMode();
clearSelectedTransactions(true);
Expand All @@ -95,6 +99,8 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
email: session?.email ?? '',
allTransactions,
translate,
toLocaleDigit,
});
if (shouldTurnOffSelectionMode) {
turnOffMobileSelectionMode();
Expand Down
6 changes: 6 additions & 0 deletions src/pages/iou/request/step/IOURequestStepReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider';
import {useSearchContext} from '@components/Search/SearchContext';
import type {ListItem} from '@components/SelectionListWithSections/types';
import useConditionalCreateEmptyReportConfirmation from '@hooks/useConditionalCreateEmptyReportConfirmation';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useOptimisticDraftTransactions from '@hooks/useOptimisticDraftTransactions';
import usePermissions from '@hooks/usePermissions';
Expand Down Expand Up @@ -44,6 +45,7 @@ const getIOUActionsSelector = (actions: OnyxEntry<ReportActions>): ReportAction[

function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) {
const {backTo, action, iouType, transactionID, reportID: reportIDFromRoute, reportActionID} = route.params;
const {translate, toLocaleDigit} = useLocalize();
const [allReports] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}`, {canBeMissing: false});
const isUnreported = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const transactionReport = Object.values(allReports ?? {}).find((report) => report?.reportID === transaction?.reportID);
Expand Down Expand Up @@ -168,6 +170,8 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) {
reportNextStep: undefined,
policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${item.policyID}`],
allTransactions,
translate,
toLocaleDigit,
});
removeTransaction(transaction.transactionID);
}
Expand Down Expand Up @@ -210,6 +214,8 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) {
accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
email: session?.email ?? '',
allTransactions,
translate,
toLocaleDigit,
});
removeTransaction(transaction.transactionID);
});
Expand Down
4 changes: 3 additions & 1 deletion tests/actions/IOUTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ import createRandomTransaction from '../utils/collections/transaction';
import getOnyxValue from '../utils/getOnyxValue';
import PusherHelper from '../utils/PusherHelper';
import type {MockFetch} from '../utils/TestHelper';
import {getGlobalFetchMock, getOnyxData, localeCompare, setPersonalDetails, signInWithTestUser, translateLocal} from '../utils/TestHelper';
import {getGlobalFetchMock, getOnyxData, localeCompare, setPersonalDetails, signInWithTestUser, toLocaleDigit, translateLocal} from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
import waitForNetworkPromises from '../utils/waitForNetworkPromises';
Expand Down Expand Up @@ -11621,6 +11621,8 @@ describe('actions/IOU', () => {
email: CARLOS_EMAIL,
newReport: result.current.report,
allTransactions,
translate: translateLocal,
toLocaleDigit,
});

let updatedTransaction: OnyxEntry<Transaction>;
Expand Down
Loading
Loading