Skip to content

Commit

Permalink
Merge pull request #33927 from teneeto/feat/31672/add-edit-fields-for…
Browse files Browse the repository at this point in the history
…-tax-tracking

feat: add edit fields for tax tracking
  • Loading branch information
MonilBhavsar committed Apr 2, 2024
2 parents 5986481 + 3188357 commit ee5ab95
Show file tree
Hide file tree
Showing 25 changed files with 526 additions and 162 deletions.
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,8 @@ const CONST = {
RECEIPT: 'receipt',
DISTANCE: 'distance',
TAG: 'tag',
TAX_RATE: 'taxRate',
TAX_AMOUNT: 'taxAmount',
},
FOOTER: {
EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`,
Expand Down
29 changes: 24 additions & 5 deletions src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
Expand All @@ -21,6 +22,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import {isTaxPolicyEnabled} from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportUtils from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
Expand Down Expand Up @@ -204,6 +206,11 @@ const defaultProps = {
isPolicyExpenseChat: false,
};

const getTaxAmount = (transaction, defaultTaxValue) => {
const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || '';
return TransactionUtils.calculateTaxAmount(percentage, transaction.amount);
};

function MoneyTemporaryForRefactorRequestConfirmationList({
bankAccountRoute,
canModifyParticipants,
Expand Down Expand Up @@ -277,7 +284,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);

// A flag for showing tax rate
const shouldShowTax = isPolicyExpenseChat && policy && lodashGet(policy, 'tax.trackingEnabled', policy.isTaxTrackingEnabled);
const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy);

// A flag for showing the billable field
const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true);
Expand All @@ -292,9 +299,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
);
const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode);

const defaultTaxKey = taxRates.defaultExternalID;
const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || '';
const taxRateTitle = (transaction.taxRate && transaction.taxRate.text) || defaultTaxName;
const taxRateTitle = TransactionUtils.getDefaultTaxName(taxRates, transaction);

const previousTransactionAmount = usePrevious(transaction.amount);

const isFocused = useIsFocused();
const [formError, setFormError] = useState('');
Expand Down Expand Up @@ -362,6 +369,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency);
}, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]);

// Calculate and set tax amount in transaction draft
useEffect(() => {
const taxAmount = getTaxAmount(transaction, taxRates.defaultValue);
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));

if (transaction.taxAmount && previousTransactionAmount === transaction.amount) {
return IOU.setMoneyRequestTaxAmount(transaction.transactionID, transaction.taxAmount, true);
}

IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true);
}, [taxRates.defaultValue, transaction, previousTransactionAmount]);

/**
* Returns the participants with amount
* @param {Array} participants
Expand Down Expand Up @@ -855,7 +874,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
key={`${taxRates.name}${formattedTaxAmount}`}
shouldShowRightIcon={!isReadOnly}
title={formattedTaxAmount}
description={taxRates.name}
description={translate('iou.taxAmount')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
Expand Down
40 changes: 40 additions & 0 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as CardUtils from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import {isTaxPolicyEnabled} from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
Expand Down Expand Up @@ -100,6 +101,8 @@ function MoneyRequestView({
const {
created: transactionDate,
amount: transactionAmount,
taxAmount: transactionTaxAmount,
taxCode: transactionTaxCode,
currency: transactionCurrency,
comment: transactionDescription,
merchant: transactionMerchant,
Expand All @@ -119,6 +122,15 @@ function MoneyRequestView({
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
const cardProgramName = isCardTransaction && transactionCardID !== undefined ? CardUtils.getCardDescription(transactionCardID) : '';
const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
const taxRates = policy?.taxRates;
const formattedTaxAmount = transactionTaxAmount ? CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency) : '';

const taxRatesDescription = taxRates?.name;
const taxRateTitle =
taxRates &&
(transactionTaxCode === taxRates?.defaultExternalID
? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction)
: transactionTaxCode && TransactionUtils.getTaxName(taxRates?.taxes, transactionTaxCode));

// Flags for allowing or disallowing editing a money request
const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID);
Expand Down Expand Up @@ -147,6 +159,9 @@ function MoneyRequestView({
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true));

// A flag for showing tax rate
const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy) && transactionTaxCode && transactionTaxAmount;

const {getViolationsForField} = useViolations(transactionViolations ?? []);
const hasViolations = useCallback(
(field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0,
Expand Down Expand Up @@ -423,6 +438,31 @@ function MoneyRequestView({
/>
</OfflineWithFeedback>
)}
{shouldShowTax && (
<OfflineWithFeedback pendingAction={getPendingFieldAction('taxCode')}>
<MenuItemWithTopDescription
title={taxRateTitle ?? ''}
description={taxRatesDescription}
interactive={canEdit}
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_RATE))}
/>
</OfflineWithFeedback>
)}

{shouldShowTax && (
<OfflineWithFeedback pendingAction={getPendingFieldAction('taxAmount')}>
<MenuItemWithTopDescription
title={formattedTaxAmount ? formattedTaxAmount.toString() : ''}
description={translate('iou.taxAmount')}
interactive={canEdit}
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT))}
/>
</OfflineWithFeedback>
)}
{shouldShowBillable && (
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
<View>
Expand Down
53 changes: 32 additions & 21 deletions src/components/TaxPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import React, {useCallback, useMemo, useState} from 'react';
import React, {useMemo, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import type {TaxRatesWithDefault} from '@src/types/onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import SelectionList from './SelectionList';
import RadioListItem from './SelectionList/RadioListItem';
import type {ListItem} from './SelectionList/types';

type TaxPickerProps = {
/** Collection of tax rates attached to a policy */
taxRates?: TaxRatesWithDefault;
type TaxPickerOnyxProps = {
/** The policy which the user has access to and which the report is tied to */
policy: OnyxEntry<Policy>;
};

type TaxPickerProps = TaxPickerOnyxProps & {
/** The selected tax rate of an expense */
selectedTaxRate?: string;

/** ID of the policy */
// eslint-disable-next-line react/no-unused-prop-types
policyID?: string;

/**
* Safe area insets required for reflecting the portion of the view,
* that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
Expand All @@ -27,55 +36,57 @@ type TaxPickerProps = {
onSubmit: (tax: ListItem) => void;
};

function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPickerProps) {
function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');

const taxRates = policy?.taxRates;
const taxRatesCount = TransactionUtils.getEnabledTaxRateCount(taxRates?.taxes ?? {});
const isTaxRatesCountBelowThreshold = taxRatesCount < CONST.TAX_RATES_LIST_THRESHOLD;

const shouldShowTextInput = !isTaxRatesCountBelowThreshold;

const getTaxName = useCallback((key: string) => taxRates?.taxes[key]?.name, [taxRates?.taxes]);

const selectedOptions = useMemo(() => {
if (!selectedTaxRate) {
return [];
}

return [
{
name: getTaxName(selectedTaxRate),
name: selectedTaxRate,
enabled: true,
accountID: null,
},
];
}, [selectedTaxRate, getTaxName]);
}, [selectedTaxRate]);

const sections = useMemo(
() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue, selectedTaxRate),
[taxRates, searchValue, selectedOptions, selectedTaxRate],
);
const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue), [taxRates, searchValue, selectedOptions]);

const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(sections[0].data.length > 0, searchValue);

const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]);

return (
<SelectionList
ListItem={RadioListItem}
onSelectRow={onSubmit}
initiallyFocusedOptionKey={selectedTaxRate}
sections={sections}
containerStyle={{paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}}
textInputLabel={shouldShowTextInput ? translate('common.search') : undefined}
isRowMultilineSupported
headerMessage={headerMessage}
textInputValue={searchValue}
textInputLabel={shouldShowTextInput ? translate('common.search') : undefined}
onChangeText={setSearchValue}
onSelectRow={onSubmit}
ListItem={RadioListItem}
initiallyFocusedOptionKey={selectedOptionKey ?? undefined}
isRowMultilineSupported
containerStyle={{paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}}
/>
);
}

TaxPicker.displayName = 'TaxPicker';

export default TaxPicker;
export default withOnyx<TaxPickerProps, TaxPickerOnyxProps>({
policy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
},
})(TaxPicker);
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ const WRITE_COMMANDS = {
UPDATE_MONEY_REQUEST_BILLABLE: 'UpdateMoneyRequestBillable',
UPDATE_MONEY_REQUEST_MERCHANT: 'UpdateMoneyRequestMerchant',
UPDATE_MONEY_REQUEST_TAG: 'UpdateMoneyRequestTag',
UPDATE_MONEY_REQUEST_TAX_AMOUNT: 'UpdateMoneyRequestTaxAmount',
UPDATE_MONEY_REQUEST_TAX_RATE: 'UpdateMoneyRequestTaxRate',
UPDATE_MONEY_REQUEST_DISTANCE: 'UpdateMoneyRequestDistance',
UPDATE_MONEY_REQUEST_CATEGORY: 'UpdateMoneyRequestCategory',
UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription',
Expand Down Expand Up @@ -329,6 +331,8 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_AMOUNT]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_RATE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION]: Parameters.UpdateMoneyRequestParams;
Expand Down
32 changes: 32 additions & 0 deletions src/libs/ModifiedExpenseMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ function buildMessageFragmentForValue(
}
}

/**
* Get the absolute value for a tax amount.
*/
function getTaxAmountAbsValue(taxAmount: number): number {
// IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value
return Math.abs(taxAmount ?? 0);
}

/**
* Get the message line for a modified expense.
*/
Expand Down Expand Up @@ -116,6 +124,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
'currency' in reportActionOriginalMessage;

const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage;

if (hasModifiedAmount) {
const oldCurrency = reportActionOriginalMessage?.oldCurrency ?? '';
const oldAmountValue = reportActionOriginalMessage?.oldAmount ?? 0;
Expand Down Expand Up @@ -216,6 +225,29 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
});
}

const hasModifiedTaxAmount = reportActionOriginalMessage && 'oldTaxAmount' in reportActionOriginalMessage && 'taxAmount' in reportActionOriginalMessage;
if (hasModifiedTaxAmount) {
const currency = reportActionOriginalMessage?.currency;

const taxAmount = CurrencyUtils.convertToDisplayString(getTaxAmountAbsValue(reportActionOriginalMessage?.taxAmount ?? 0), currency);
const oldTaxAmountValue = getTaxAmountAbsValue(reportActionOriginalMessage?.oldTaxAmount ?? 0);
const oldTaxAmount = oldTaxAmountValue > 0 ? CurrencyUtils.convertToDisplayString(oldTaxAmountValue, currency) : '';
buildMessageFragmentForValue(taxAmount, oldTaxAmount, Localize.translateLocal('iou.taxAmount'), false, setFragments, removalFragments, changeFragments);
}

const hasModifiedTaxRate = reportActionOriginalMessage && 'oldTaxRate' in reportActionOriginalMessage && 'taxRate' in reportActionOriginalMessage;
if (hasModifiedTaxRate) {
buildMessageFragmentForValue(
reportActionOriginalMessage?.taxRate ?? '',
reportActionOriginalMessage?.oldTaxRate ?? '',
Localize.translateLocal('iou.taxRate'),
false,
setFragments,
removalFragments,
changeFragments,
);
}

const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage;
if (hasModifiedBillable) {
buildMessageFragmentForValue(
Expand Down
10 changes: 5 additions & 5 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1191,8 +1191,8 @@ function hasEnabledTags(policyTagList: Array<PolicyTagList[keyof PolicyTagList]>
* @param taxRates - The original tax rates object.
* @returns The transformed tax rates object.g
*/
function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined, defaultKey?: string): Record<string, TaxRate> {
const defaultTaxKey = defaultKey ?? taxRates?.defaultExternalID;
function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record<string, TaxRate> {
const defaultTaxKey = taxRates?.defaultExternalID;
const getModifiedName = (data: TaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`;
const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}]));
return taxes;
Expand All @@ -1212,7 +1212,7 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] {
function getTaxRatesOptions(taxRates: Array<Partial<TaxRate>>): Option[] {
return taxRates.map((taxRate) => ({
text: taxRate.modifiedName,
keyForList: taxRate.code,
keyForList: taxRate.modifiedName,
searchText: taxRate.modifiedName,
tooltipText: taxRate.modifiedName,
isDisabled: taxRate.isDisabled,
Expand All @@ -1223,10 +1223,10 @@ function getTaxRatesOptions(taxRates: Array<Partial<TaxRate>>): Option[] {
/**
* Builds the section list for tax rates
*/
function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string, defaultTaxKey?: string): CategorySection[] {
function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): CategorySection[] {
const policyRatesSections = [];

const taxes = transformedTaxRates(taxRates, defaultTaxKey);
const taxes = transformedTaxRates(taxRates);

const sortedTaxRates = sortTaxRates(taxes);
const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled);
Expand Down
Loading

0 comments on commit ee5ab95

Please sign in to comment.