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
46 changes: 44 additions & 2 deletions src/hooks/useDeleteTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {getIOUActionForTransactions} from '@libs/actions/IOU/Duplicate';
import {getIOURequestPolicyID} from '@libs/actions/IOU/MoneyRequest';
import {initSplitExpenseItemData} from '@libs/actions/IOU/SplitExpenseItems';
import {updateSplitTransactions} from '@libs/actions/IOU/SplitTransactionUpdate';
import {deleteTrackExpense} from '@libs/actions/IOU/TrackExpense';
import initSplitExpense from '@libs/actions/SplitExpenses';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {calculateAmount as calculateIOUAmount} from '@libs/IOUUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOriginalMessage, isActionableTrackExpense, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
import {isArchivedReport, isExpenseReport, isInvoiceReport, isIOUReport, isSelfDM} from '@libs/ReportUtils';
import {getActiveGroupSearchHashes} from '@libs/SearchUIUtils';
import {
Expand All @@ -21,7 +22,7 @@ import {

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx';
import type {Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolations} from '@src/types/onyx';
import type {SplitExpense} from '@src/types/onyx/IOU';

import type {OnyxCollection} from 'react-native-onyx';
Expand Down Expand Up @@ -360,6 +361,47 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
const iouReport = isIOUReport(candidateIOUReport) || isExpenseReport(candidateIOUReport) || isInvoiceReport(candidateIOUReport) ? candidateIOUReport : undefined;
const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`];
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${action?.childReportID}`];

// A self-DM expense is a tracked expense: it has no IOU report to key the cleanup on, so it goes
// through the track-expense flow, which resolves the self-DM report actions and the whisper.
if (isSelfDM(candidateIOUReport) && isTrackExpenseAction(action)) {
// The Onyx collection can be missing the self-DM actions when the delete comes from Search, where
// they are read from the search snapshot instead, so the ones passed in fill the gaps. Actionable
// track expense whispers are matched on their own since they are built without a `reportID`.
const selfDMReportActions: ReportActions = {
...Object.fromEntries(
reportActions
.filter((chatAction) => chatAction.reportID === candidateIOUReport?.reportID || isActionableTrackExpense(chatAction))
.map((chatAction) => [chatAction.reportActionID, chatAction]),
),
...allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${candidateIOUReport?.reportID}`],
};

deleteTrackExpense({
chatReportID: candidateIOUReport?.reportID,
chatReport: candidateIOUReport,
chatReportActions: selfDMReportActions,
transactionID,
reportAction: action,
iouReport: undefined,
chatIOUReport: undefined,
transactions: duplicateTransactions,
violations: duplicateTransactionViolations,
isSingleTransactionView,
isChatReportArchived: isArchivedReport(allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${candidateIOUReport?.reportID}`]),
isChatIOUReportArchived: false,
allTransactionViolationsParam: transactionViolations,
currentUserAccountID: currentUserPersonalDetails.accountID,
currentUserEmail: currentUserPersonalDetails.email ?? '',
policy: undefined,
});
deletedTransactionIDs.push(transactionID);
if (action.childReportID) {
deletedTransactionThreadReportIDs.add(action.childReportID);
}
continue;
}

const chatIOUReportID = chatReport?.reportID;
const isChatIOUReportArchived = isArchivedReport(allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${chatIOUReportID}`]);
const iouPolicy = iouReport?.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${iouReport.policyID}`] : undefined;
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/parameters/SplitTransactionParams.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {Unit} from '@src/types/onyx/Policy';
import type {Comment, WaypointCollection} from '@src/types/onyx/Transaction';

type SplitTransactionSplitParam = {
Expand All @@ -17,6 +18,7 @@ type SplitTransactionSplitParam = {
reportID?: string;
quantity?: number;
customUnitRateID?: string;
distanceUnit?: Unit;
odometerStart?: number;
odometerEnd?: number;
waypoints?: WaypointCollection;
Expand Down
11 changes: 9 additions & 2 deletions src/libs/TransactionPreviewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ import {isCategoryMissing} from './CategoryUtils';
import DateUtils from './DateUtils';
import createDynamicRoute from './Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import {hasDynamicExternalWorkflow, isGroupPolicy as isGroupPolicyUtil} from './PolicyUtils';
import {getMostRecentActiveDEWSubmitFailedAction, getOriginalMessage, isDynamicExternalWorkflowSubmitFailedAction, isMessageDeleted, isMoneyRequestAction} from './ReportActionsUtils';
import {
getMostRecentActiveDEWSubmitFailedAction,
getOriginalMessage,
isDeletedAction,
isDynamicExternalWorkflowSubmitFailedAction,
isMessageDeleted,
isMoneyRequestAction,
} from './ReportActionsUtils';
import {hasActionWithErrorsForTransaction, hasReceiptError, isExpenseReport, isReportApproved, isSettled} from './ReportUtils';
import StringUtils from './StringUtils';
import {
Expand Down Expand Up @@ -418,7 +425,7 @@ function createTransactionPreviewConditionals({
const isFullySettled = isMoneyRequestSettled && !isSettlementOrApprovalPartial;
const isFullyApproved = isApproved && !isSettlementOrApprovalPartial;

const shouldShowSkeleton = isEmptyObject(transaction) && !isMessageDeleted(action) && action?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
const shouldShowSkeleton = isEmptyObject(transaction) && !isMessageDeleted(action) && !isDeletedAction(action) && action?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
const shouldShowTag = !!tag && isReportAPolicyExpenseChat;

const categoryForDisplay = isCategoryMissing(category) ? '' : category;
Expand Down
83 changes: 58 additions & 25 deletions src/libs/actions/IOU/SplitExpenseItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function updateSplitExpenseDistanceFromAmount(
const customUnit: TransactionCustomUnit = {
...existingCustomUnit,
quantity,
distanceUnit: unit,
};

const merchant = getDistanceMerchantFromDistance(distanceInUnits, unit, rate, transactionCurrency ?? mileageRate?.currency ?? CONST.CURRENCY.USD, getCurrencySymbol);
Expand Down Expand Up @@ -152,6 +153,39 @@ function resolveSplitMileageRate({
return baseMileageRate;
}

/**
* Resolve the rate and unit a split item is calculated with: its own selected rate when that rate still
* resolves (it can live on another workspace), and the rate of the expense being split otherwise.
*
* The unit stays the one the expense is stored with — a workspace switching between miles and kilometers
* doesn't re-express expenses that already exist, so the splits follow the same unit as their expense.
*/
function resolveSplitItemRate({
customUnit,
fallbackMileageRate,
policy,
policies,
}: {
customUnit: TransactionCustomUnit | undefined;
fallbackMileageRate: ReturnType<typeof DistanceRequestUtils.getRate>;
policy: OnyxEntry<OnyxTypes.Policy>;
policies?: OnyxCollection<OnyxTypes.Policy>;
}): {rate: number | undefined; unit: Unit | undefined} {
const unit = customUnit?.distanceUnit ?? fallbackMileageRate.unit;
const customUnitRateID = customUnit?.customUnitRateID;
if (!customUnitRateID || customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID) {
return {rate: fallbackMileageRate.rate, unit};
}

const selectedRate =
DistanceRequestUtils.getRateByCustomUnitRateID({policy, customUnitRateID}) ?? DistanceRequestUtils.getEnabledRateByCustomUnitRateIDFromAnyPolicy(customUnitRateID, policies);
if (!selectedRate?.rate || selectedRate.rate <= 0 || selectedRate.enabled === false) {
return {rate: fallbackMileageRate.rate, unit};
}

return {rate: selectedRate.rate, unit};
}

function resolveSplitItemReportID({
childTransaction,
allReports,
Expand Down Expand Up @@ -306,6 +340,7 @@ function addSplitExpenseField(
isSelfDMSplit: boolean,
personalPolicyOutputCurrency: string | undefined,
getCurrencySymbol: CurrencyListActionsContextType['getCurrencySymbol'],
policies?: OnyxCollection<OnyxTypes.Policy>,
) {
if (!transaction || !draftTransaction) {
return;
Expand All @@ -327,11 +362,20 @@ function addSplitExpenseField(
: undefined;

const mileageRate = resolveSplitMileageRate({transaction, policy, isSelfDMSplit, personalPolicyOutputCurrency});
const {unit, rate} = mileageRate;
const {unit, rate} = resolveSplitItemRate({customUnit, fallbackMileageRate: mileageRate, policy, policies});

if (rate && rate > 0 && customUnit) {
// For amount = 0, distance = 0, but we still calculate merchant format
const {merchant: calculatedMerchant} = updateSplitExpenseDistanceFromAmount(0, rate, unit, customUnit, mileageRate, getCurrencySymbol, transaction.currency);
const {customUnit: updatedCustomUnit, merchant: calculatedMerchant} = updateSplitExpenseDistanceFromAmount(
0,
rate,
unit,
customUnit,
mileageRate,
getCurrencySymbol,
transaction.currency,
);
customUnit = updatedCustomUnit;
merchant = calculatedMerchant;
}
}
Expand Down Expand Up @@ -390,6 +434,7 @@ function evenlyDistributeSplitExpenseAmounts(
isSelfDMSplit: boolean,
personalPolicyOutputCurrency: string | undefined,
getCurrencySymbol: CurrencyListActionsContextType['getCurrencySymbol'],
policies?: OnyxCollection<OnyxTypes.Policy>,
) {
if (!draftTransaction) {
return;
Expand All @@ -409,15 +454,14 @@ function evenlyDistributeSplitExpenseAmounts(

const isDistanceRequest = transaction && isDistanceRequestTransactionUtils(transaction);

// Floor-allocation with full remainder added to the last split so the last is always the largest
// Floor-allocation with the full remainder added to the first split, the way the amounts are allocated when
// the splits are created, so distributing them evenly doesn't move the remainder from one split to another
const splitCount = splitExpenses.length;
const lastIndex = splitCount - 1;

const mileageRate = resolveSplitMileageRate({transaction, policy, isSelfDMSplit, personalPolicyOutputCurrency});
const {unit, rate} = mileageRate;

const updatedSplitExpenses = splitExpenses.map((splitExpense, index) => {
const amount = calculateIOUAmount(splitCount - 1, total, currency, index === lastIndex, true);
const amount = calculateIOUAmount(splitCount - 1, total, currency, index === 0, true);
let updatedSplitExpense: SplitExpense = {
...splitExpense,
amount,
Expand All @@ -427,6 +471,7 @@ function evenlyDistributeSplitExpenseAmounts(

// Update distance for distance transactions based on new amount and rate
if (isDistanceRequest && transaction && splitExpense.customUnit && amount !== 0) {
const {unit, rate} = resolveSplitItemRate({customUnit: splitExpense.customUnit, fallbackMileageRate: mileageRate, policy, policies});
if (rate && rate > 0) {
const {customUnit: updatedCustomUnit, merchant} = updateSplitExpenseDistanceFromAmount(
amount,
Expand Down Expand Up @@ -476,6 +521,7 @@ function resetSplitExpensesByDateRange(
isSelfDMSplit: boolean,
personalPolicyOutputCurrency: string | undefined,
getCurrencySymbol: CurrencyListActionsContextType['getCurrencySymbol'],
policies?: OnyxCollection<OnyxTypes.Policy>,
) {
if (!transaction || !draftTransaction || !startDate || !endDate) {
return;
Expand All @@ -494,12 +540,10 @@ function resetSplitExpensesByDateRange(
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);

const mileageRate = resolveSplitMileageRate({transaction, policy, isSelfDMSplit, personalPolicyOutputCurrency});
const {unit, rate} = mileageRate;

// Create split expenses for each date with proportional amounts
const lastIndex = dates.length - 1;
// Create split expenses for each date with proportional amounts, the remainder going to the first one
const newSplitExpenses: SplitExpense[] = dates.map((date, index) => {
const amount = calculateIOUAmount(lastIndex, total, currency, index === lastIndex, true);
const amount = calculateIOUAmount(dates.length - 1, total, currency, index === 0, true);
let splitExpense = initSplitExpenseItemData(transaction, transactionReport, {
amount,
transactionID: rand64(),
Expand All @@ -509,6 +553,7 @@ function resetSplitExpensesByDateRange(

// Update distance for distance transactions based on new amount and rate
if (isDistanceRequest && splitExpense.customUnit && amount !== 0) {
const {unit, rate} = resolveSplitItemRate({customUnit: splitExpense.customUnit, fallbackMileageRate: mileageRate, policy, policies});
if (rate && rate > 0) {
const {customUnit: updatedCustomUnit, merchant} = updateSplitExpenseDistanceFromAmount(
amount,
Expand Down Expand Up @@ -583,6 +628,7 @@ function updateSplitExpenseField(
isSelfDMSplit: boolean,
personalPolicyOutputCurrency: string | undefined,
getCurrencySymbol: CurrencyListActionsContextType['getCurrencySymbol'],
policies?: OnyxCollection<OnyxTypes.Policy>,
) {
if (!splitExpenseDraftTransaction || !splitExpenseTransactionID || !originalTransactionDraft) {
return;
Expand Down Expand Up @@ -626,7 +672,7 @@ function updateSplitExpenseField(
// Recalculate amount for distance transactions when rate or distance changes
if (isDistanceRequest && originalTransaction) {
const mileageRate = resolveSplitMileageRate({transaction: splitExpenseDraftTransaction, policy, isSelfDMSplit, personalPolicyOutputCurrency});
const {unit, rate} = mileageRate;
const {unit, rate} = resolveSplitItemRate({customUnit: splitExpenseDraftTransaction?.comment?.customUnit, fallbackMileageRate: mileageRate, policy, policies});

if (rate && rate > 0) {
// Get distance from routes or customUnit.quantity (same logic as in initSplitExpense)
Expand Down Expand Up @@ -702,20 +748,7 @@ function updateSplitExpenseAmountField(
// Update distance for distance transactions based on new amount and rate
if (isDistanceRequest && originalTransaction && splitExpense.customUnit) {
const mileageRate = resolveSplitMileageRate({transaction: originalTransaction, policy, isSelfDMSplit, personalPolicyOutputCurrency});
const splitRateID = splitExpense.customUnit?.customUnitRateID ?? String(CONST.DEFAULT_NUMBER_ID);

// `policy` is undefined for a self-DM split on the personal rate, so also resolve the split's
// picked rate across all policies, so the selection isn't lost.
const splitSelectedRate =
DistanceRequestUtils.getRateByCustomUnitRateID({policy, customUnitRateID: splitRateID}) ??
DistanceRequestUtils.getEnabledRateByCustomUnitRateIDFromAnyPolicy(splitRateID, policies);
const isSplitP2PRate = splitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID;

// Prefer the split's own selected rate when it's a real enabled rate; otherwise fall back to
// the original-transaction rate (covers the P2P and deleted-rate cases).
const useSplitSelectedRate = !isSplitP2PRate && !!splitSelectedRate?.rate && splitSelectedRate.rate > 0 && splitSelectedRate.enabled !== false;
const rate = useSplitSelectedRate ? (splitSelectedRate?.rate ?? 0) : mileageRate.rate;
const unit = useSplitSelectedRate ? (splitSelectedRate?.unit ?? mileageRate.unit) : mileageRate.unit;
const {unit, rate} = resolveSplitItemRate({customUnit: splitExpense.customUnit, fallbackMileageRate: mileageRate, policy, policies});

if (rate && rate > 0) {
const {customUnit: updatedCustomUnit, merchant} = updateSplitExpenseDistanceFromAmount(
Expand Down
2 changes: 2 additions & 0 deletions src/libs/actions/IOU/SplitTransactionUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ function updateSplitTransactions({
billable: split?.billable,
quantity: split.customUnit?.quantity ?? undefined,
customUnitRateID: split.customUnit?.customUnitRateID,
distanceUnit: split.customUnit?.distanceUnit,
odometerStart: split.odometerStart,
odometerEnd: split.odometerEnd,
waypoints: split.waypoints,
Expand Down Expand Up @@ -802,6 +803,7 @@ function updateSplitTransactions({
const oldTransactionChanges = {
...existing,
quantity: splitTransaction.comment?.customUnit?.quantity ?? existing?.distance,
distanceUnit: splitTransaction.comment?.customUnit?.distanceUnit,
} as TransactionChanges;

if (currentSplit) {
Expand Down
4 changes: 4 additions & 0 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,10 @@ function getChangeTransactionsReportOnyxData({
IOUTransactionID: null,
},
errors: undefined,
// The expense gets its own action on the new report, so this one is retired: clear its pending
// state too, since `shouldReportActionBeVisible` keeps a pending action on screen.
// `failureData` below restores the whole action.
pendingAction: null,
Comment thread
ZhenjaHorbach marked this conversation as resolved.
},
...(trackExpenseActionableWhisper ? {[trackExpenseActionableWhisper.reportActionID]: null} : {}),
},
Expand Down
2 changes: 2 additions & 0 deletions src/pages/iou/SplitExpenseCreateDateRagePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function SplitExpenseCreateDateRagePage({route}: SplitExpenseCreateDateRagePageP
const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`);
const allTransactions = useAllTransactions();
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);

const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`];
const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`];
Expand All @@ -73,6 +74,7 @@ function SplitExpenseCreateDateRagePage({route}: SplitExpenseCreateDateRagePageP
isSelfDM(currentReport) || isSelfDM(parentReport),
personalPolicy?.outputCurrency,
getCurrencySymbol,
allPolicies,
);
Navigation.goBack(backTo);
};
Expand Down
1 change: 1 addition & 0 deletions src/pages/iou/SplitExpenseEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ function SplitExpenseEditPage({route}: SplitExpensePageProps) {
isSelfDMSplit,
personalPolicy?.outputCurrency,
getCurrencySymbol,
allPolicies,
);
Navigation.goBack(backTo);
}}
Expand Down
Loading
Loading