Skip to content
Draft
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
3 changes: 3 additions & 0 deletions src/libs/API/parameters/SplitTransactionParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type SplitTransactionSplitParam = {
modifiedExpenseReportActionID?: string;
reimbursable?: boolean;
billable?: boolean;
taxCode?: string;
taxAmount?: number;
taxValue?: string;
reportID?: string;
quantity?: number;
customUnitRateID?: string;
Expand Down
18 changes: 18 additions & 0 deletions src/libs/actions/IOU/SplitExpenseItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ function initSplitExpenseItemData(
reportID: reportID ?? transaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID),
reimbursable: transactionDetails?.reimbursable,
billable: transactionDetails?.billable,
taxCode: transactionDetails?.taxCode,
taxAmount: transactionDetails?.taxAmount,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not copy the full tax amount to every split

When a taxable expense is split into multiple new split expenses and the user saves without opening each tax row, every SplitExpense initialized here carries the original transaction's full taxAmount; the new updateSplitTransactions payload then sends that value for each split. For example, splitting an expense with $10 tax into two lines sends $10 tax on both child transactions, corrupting tax totals unless the user manually edits each one. This should be prorated/recomputed for the split amount or omitted until explicitly set.

Useful? React with 👍 / 👎.

taxValue: transactionDetails?.taxValue,
customUnit: customUnit ?? transaction?.comment?.customUnit ?? undefined,
waypoints: transaction?.comment?.waypoints ?? undefined,
odometerStart: transaction?.comment?.odometerStart ?? undefined,
Expand Down Expand Up @@ -230,6 +233,11 @@ function initDraftSplitExpenseDataForEdit(draftTransaction: OnyxEntry<OnyxTypes.
reportID,
created: splitTransactionData?.created ?? '',
category: splitTransactionData?.category ?? '',
reimbursable: splitTransactionData?.reimbursable,
billable: splitTransactionData?.billable,
taxCode: splitTransactionData?.taxCode,
taxAmount: splitTransactionData?.taxAmount,
taxValue: splitTransactionData?.taxValue,
customUnit: splitTransactionData?.customUnit,
waypoints: splitTransactionData?.waypoints ?? undefined,
odometerStart: splitTransactionData?.odometerStart ?? undefined,
Expand Down Expand Up @@ -581,6 +589,11 @@ function updateSplitExpenseField(
odometerStart: splitExpenseDraftTransaction?.comment?.odometerStart ?? undefined,
odometerEnd: splitExpenseDraftTransaction?.comment?.odometerEnd ?? undefined,
amount: splitExpenseDraftTransaction?.amount ?? 0,
reimbursable: transactionDetails?.reimbursable,
billable: transactionDetails?.billable,
taxCode: transactionDetails?.taxCode,
taxAmount: transactionDetails?.taxAmount,
taxValue: transactionDetails?.taxValue,
routes: splitExpenseDraftTransaction?.routes ?? undefined,
merchant: splitExpenseDraftTransaction?.modifiedMerchant ? splitExpenseDraftTransaction.modifiedMerchant : (splitExpenseDraftTransaction?.merchant ?? ''),
};
Expand Down Expand Up @@ -718,6 +731,10 @@ function clearSplitTransactionDraftErrors(transactionID: string | undefined) {
});
}

function updateSplitExpenseDraftField(fields: Partial<OnyxTypes.Transaction>) {
Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, fields);
}

export {
updateSplitExpenseDistanceFromAmount,
initSplitExpenseItemData,
Expand All @@ -731,4 +748,5 @@ export {
updateSplitExpenseField,
updateSplitExpenseAmountField,
clearSplitTransactionDraftErrors,
updateSplitExpenseDraftField,
};
3 changes: 3 additions & 0 deletions src/libs/actions/IOU/SplitTransactionUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ function updateSplitTransactions({
},
reimbursable: split?.reimbursable,
billable: split?.billable,
taxCode: split?.taxCode,
taxAmount: split?.taxAmount,
taxValue: split?.taxValue,
Comment on lines +261 to +263

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep optimistic split taxes in sync with the payload

These per-split tax fields are now sent to the API, but the optimistic transaction built later in this same function still uses originalTransactionDetails.taxCode/taxAmount/taxValue instead of the current splitExpense values. When a user changes a split's tax rate or tax amount and saves, offline mode and the immediate post-save UI/search state continue showing the original tax values until the server response arrives, which makes the new editor appear not to have saved the tax change locally.

Useful? React with 👍 / 👎.

quantity: split.customUnit?.quantity ?? undefined,
customUnitRateID: split.customUnit?.customUnitRateID,
odometerStart: split.odometerStart,
Expand Down
86 changes: 82 additions & 4 deletions src/pages/iou/SplitExpenseEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import {useSearchResultsContext} from '@components/Search/SearchContext';
import Switch from '@components/Switch';
import Text from '@components/Text';
import useAllTransactions from '@hooks/useAllTransactions';
import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
Expand All @@ -24,24 +26,25 @@
import useSplitEffectivePolicy from '@hooks/useSplitEffectivePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import type {ViolationField} from '@hooks/useViolations';
import {initDraftSplitExpenseDataForEdit, removeSplitExpenseField, updateSplitExpenseField} from '@libs/actions/IOU/SplitExpenseItems';
import {initDraftSplitExpenseDataForEdit, removeSplitExpenseField, updateSplitExpenseDraftField, updateSplitExpenseField} from '@libs/actions/IOU/SplitExpenseItems';
import {openPolicyCategoriesPage} from '@libs/actions/Policy/Category';
import {openPolicyTagsPage} from '@libs/actions/Policy/Tag';
import {getDecodedLeafCategoryName, isCategoryDescriptionRequired, isCategoryMissing} from '@libs/CategoryUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SplitExpenseParamList} from '@libs/Navigation/types';
import {hasEnabledOptions} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {getDistanceRateCustomUnitRate, getTagLists, hasAnyPaidPolicy, isGroupPolicyByType} from '@libs/PolicyUtils';
import {getDistanceRateCustomUnitRate, getTagLists, hasAnyPaidPolicy, isGroupPolicyByType, isTaxTrackingEnabled} from '@libs/PolicyUtils';
import {getReportName} from '@libs/ReportNameUtils';
import {isSplitAction} from '@libs/ReportSecondaryActionUtils';
import type {TransactionDetails} from '@libs/ReportUtils';
import {getParsedComment, getReportOrDraftReport, getTransactionDetails, isSelfDM} from '@libs/ReportUtils';
import {getTagVisibility, hasEnabledTags} from '@libs/TagsOptionsListUtils';
import {getDistanceInMeters, getRateID, getTag, getTagForDisplay, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest} from '@libs/TransactionUtils';
import {getDistanceInMeters, getRateID, getTag, getTagForDisplay, getTaxName, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand All @@ -63,7 +66,7 @@
const [originalTransactionDraft] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${splitExpenseDraftTransaction?.comment?.originalTransactionID}`, undefined, [
splitExpenseDraftTransaction?.comment?.originalTransactionID,
]);

console.log('>>>>>>>>>>>>>>>>>>', splitExpenseDraftTransaction);

Check failure on line 69 in src/pages/iou/SplitExpenseEditPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unexpected console statement. Only these console methods are allowed: debug, error

Check failure on line 69 in src/pages/iou/SplitExpenseEditPage.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

Unexpected console statement. Only these console methods are allowed: debug, error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove transaction debug logging

This console.log runs on every render of the split details page and prints the full draft transaction, including merchant, amount, tax, report, and participant-related data, into production browser/native logs. Since this page is opened for real expenses, it leaks sensitive expense details and adds noisy render-time logging; remove the debug statement before shipping.

Useful? React with 👍 / 👎.

const splitExpenseDraftTransactionDetails = useMemo<Partial<TransactionDetails>>(() => getTransactionDetails(splitExpenseDraftTransaction) ?? {}, [splitExpenseDraftTransaction]);
const allTransactions = useAllTransactions();

Expand Down Expand Up @@ -164,6 +167,13 @@

const previousTagsVisibility = usePrevious(tagVisibility.map((v) => v.shouldShow)) ?? [];

const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat || isExpenseUnreported, effectivePolicy, isDistanceRequest(splitExpenseDraftTransaction), false, false);
const taxRatesDescription = effectivePolicy?.taxRates?.name;
const taxRateTitle = getTaxName(effectivePolicy, splitExpenseDraftTransaction);

const shouldShowBillable = (isPolicyExpenseChat || isExpenseUnreported) && (!!splitExpenseDraftTransactionDetails?.billable || isBillableEnabledOnPolicy(effectivePolicy));
const shouldShowReimbursable = (isPolicyExpenseChat || (isExpenseUnreported && !!effectivePolicy)) && effectivePolicy?.disabledFields?.reimbursable !== true;

const isDistance = isDistanceRequest(splitExpenseDraftTransaction);
const isManualDistance = isManualDistanceRequest(splitExpenseDraftTransaction);
const isOdometerDistance = isOdometerDistanceRequest(splitExpenseDraftTransaction);
Expand Down Expand Up @@ -424,6 +434,74 @@
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
/>
{shouldShowTax && (
<MenuItemWithTopDescription
shouldShowRightIcon
key={translate('common.tax')}
description={taxRatesDescription ?? translate('common.tax')}
title={taxRateTitle}
numberOfLinesTitle={2}
onPress={() => {
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(
CONST.IOU.ACTION.EDIT,
CONST.IOU.TYPE.SPLIT,
CONST.IOU.OPTIMISTIC_TRANSACTION_ID,
reportID,
Navigation.getActiveRoute(),
),
);
}}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
/>
)}
{shouldShowTax && (
<MenuItemWithTopDescription
shouldShowRightIcon
key={translate('iou.taxAmount')}
description={translate('iou.taxAmount')}
title={convertToDisplayString(Math.abs(splitExpenseDraftTransaction?.taxAmount ?? 0), currency)}
numberOfLinesTitle={2}
onPress={() => {
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(
CONST.IOU.ACTION.EDIT,
CONST.IOU.TYPE.SPLIT,
CONST.IOU.OPTIMISTIC_TRANSACTION_ID,
reportID,
Navigation.getActiveRoute(),
),
);
}}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
/>
)}
{shouldShowReimbursable && (
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.mh5]}>
<Text>{translate('common.reimbursable')}</Text>
<Switch
accessibilityLabel={translate('common.reimbursable')}
isOn={splitExpenseDraftTransaction?.reimbursable ?? true}
onToggle={(value) => {
updateSplitExpenseDraftField({reimbursable: value});
}}
/>
</View>
)}
{shouldShowBillable && (
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.mh5]}>
<Text>{translate('common.billable')}</Text>
<Switch
accessibilityLabel={translate('common.billable')}
isOn={splitExpenseDraftTransaction?.billable ?? false}
onToggle={(value) => {
updateSplitExpenseDraftField({billable: value});
}}
/>
</View>
)}
<MenuItemWithTopDescription
key={translate('common.report')}
description={translate('common.report')}
Expand Down
9 changes: 9 additions & 0 deletions src/types/onyx/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ type SplitExpense = {
/** Whether the split expense is billable */
billable?: boolean;

/** Tax code applied to this split */
taxCode?: string;

/** Tax amount for this split (in cents) */
taxAmount?: number;

/** Tax percentage value as a string (e.g. "20%") */
taxValue?: string;

/** Custom unit data for distance requests */
customUnit?: TransactionCustomUnit;

Expand Down
Loading