diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 3f11cd3ae642..85a75d9bf952 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -181,9 +181,11 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & action?: IOUAction; }; -const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { - const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? ''; - return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); +const getTaxAmount = (transaction: OnyxEntry, policy: OnyxEntry) => { + const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction) ?? ''; + + const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ?? ''; + return TransactionUtils.calculateTaxAmount(taxPercentage, transaction?.amount ?? 0); }; function MoneyRequestConfirmationList({ @@ -317,9 +319,10 @@ function MoneyRequestConfirmationList({ isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : ''; + const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); const previousTransactionAmount = usePrevious(transaction?.amount); + const previousTransactionCurrency = usePrevious(transaction?.currency); const isFocused = useIsFocused(); const [formError, debouncedFormError, setFormError] = useDebouncedState(''); @@ -373,15 +376,15 @@ function MoneyRequestConfirmationList({ // Calculate and set tax amount in transaction draft useEffect(() => { - const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString(); + const taxAmount = getTaxAmount(transaction, policy).toString(); const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); - if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) { + if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount && previousTransactionCurrency === transaction?.currency) { return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true); } IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true); - }, [taxRates?.defaultValue, transaction, transactionID, previousTransactionAmount]); + }, [policy, transaction, transactionID, previousTransactionAmount, previousTransactionCurrency]); // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 8f9f7e873166..361bc0b863c2 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -109,7 +109,6 @@ function MoneyRequestView({ created: transactionDate, amount: transactionAmount, taxAmount: transactionTaxAmount, - taxCode: transactionTaxCode, currency: transactionCurrency, comment: transactionDescription, merchant: transactionMerchant, @@ -134,11 +133,7 @@ function MoneyRequestView({ const formattedTaxAmount = 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)); + const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); // Flags for allowing or disallowing editing an expense const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 9553fe4451ad..3ff2bc6a9cfb 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -4,17 +4,22 @@ 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 IOUUtils from '@libs/IOUUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; +import type {IOUAction} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy} from '@src/types/onyx'; +import type {Policy, Transaction} from '@src/types/onyx'; import SelectionList from './SelectionList'; import RadioListItem from './SelectionList/RadioListItem'; type TaxPickerOnyxProps = { /** The policy which the user has access to and which the report is tied to */ policy: OnyxEntry; + + /** All the data for the transaction */ + transaction: OnyxEntry; }; type TaxPickerProps = TaxPickerOnyxProps & { @@ -25,6 +30,10 @@ type TaxPickerProps = TaxPickerOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types policyID?: string; + /** ID of the transaction */ + // eslint-disable-next-line react/no-unused-prop-types + transactionID?: 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. @@ -33,9 +42,13 @@ type TaxPickerProps = TaxPickerOnyxProps & { /** Callback to fire when a tax is pressed */ onSubmit: (tax: OptionsListUtils.TaxRatesOption) => void; + + /** The action to take */ + // eslint-disable-next-line react/no-unused-prop-types + action?: IOUAction; }; -function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerProps) { +function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit}: TaxPickerProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); @@ -60,7 +73,10 @@ function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerPr ]; }, [selectedTaxRate]); - const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Tax[], searchValue), [taxRates, searchValue, selectedOptions]); + const sections = useMemo( + () => OptionsListUtils.getTaxRatesSection(policy, selectedOptions as OptionsListUtils.Tax[], searchValue, transaction), + [searchValue, selectedOptions, policy, transaction], + ); const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(sections[0].data.length > 0, searchValue); @@ -88,4 +104,12 @@ export default withOnyx({ policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + transaction: { + key: ({transactionID, action}) => { + if (action === CONST.IOU.ACTION.CREATE || IOUUtils.isMovingTransactionFromTrackExpense(action)) { + return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}` as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`; + } + return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; + }, + }, })(TaxPicker); diff --git a/src/libs/API/parameters/CreateDistanceRequestParams.ts b/src/libs/API/parameters/CreateDistanceRequestParams.ts index b9adee4393ec..68469899af06 100644 --- a/src/libs/API/parameters/CreateDistanceRequestParams.ts +++ b/src/libs/API/parameters/CreateDistanceRequestParams.ts @@ -11,6 +11,8 @@ type CreateDistanceRequestParams = { created: string; category?: string; tag?: string; + taxCode?: string; + taxAmount?: number; billable?: boolean; transactionThreadReportID: string; createdReportActionIDForThread: string; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 12e7f297db7d..eba705c23722 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -106,7 +106,7 @@ type TaxRatesOption = { tooltipText?: string; isDisabled?: boolean; keyForList?: string; - data: Partial; + isSelected?: boolean; }; type TaxSection = { @@ -165,6 +165,8 @@ type GetOptionsConfig = { includeSelectedOptions?: boolean; includeTaxRates?: boolean; taxRates?: TaxRatesWithDefault; + policy?: OnyxEntry; + transaction?: OnyxEntry; includePolicyReportFieldOptions?: boolean; policyReportFieldOptions?: string[]; recentlyUsedPolicyReportFieldOptions?: string[]; @@ -1335,20 +1337,6 @@ function getReportFieldOptionsSection(options: string[], recentlyUsedOptions: st return reportFieldOptionsSections; } -/** - * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. - * - * @param taxRates - The original tax rates object. - * @returns The transformed tax rates object.g - */ -function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record { - const defaultTaxKey = taxRates?.defaultExternalID; - const getModifiedName = (data: TaxRate, code: string) => - `${data.name} (${data.value})${defaultTaxKey === code ? ` ${CONST.DOT_SEPARATOR} ${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; -} - /** * Sorts tax rates alphabetically by name. */ @@ -1361,24 +1349,24 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] { * Builds the options for taxRates */ function getTaxRatesOptions(taxRates: Array>): TaxRatesOption[] { - return taxRates.map((taxRate) => ({ - text: taxRate.modifiedName, - keyForList: taxRate.modifiedName, - searchText: taxRate.modifiedName, - tooltipText: taxRate.modifiedName, - isDisabled: taxRate.isDisabled, - data: taxRate, - isSelected: taxRate.isSelected, + return taxRates.map(({code, modifiedName, isDisabled, isSelected}) => ({ + code, + text: modifiedName, + keyForList: modifiedName, + searchText: modifiedName, + tooltipText: modifiedName, + isDisabled, + isSelected, })); } /** * Builds the section list for tax rates */ -function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Tax[], searchInputValue: string): TaxSection[] { +function getTaxRatesSection(policy: OnyxEntry | undefined, selectedOptions: Tax[], searchInputValue: string, transaction?: OnyxEntry): TaxSection[] { const policyRatesSections = []; - const taxes = transformedTaxRates(taxRates); + const taxes = TransactionUtils.transformedTaxRates(policy, transaction); const sortedTaxRates = sortTaxRates(taxes); const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName); @@ -1665,7 +1653,8 @@ function getOptions( includeSelectedOptions = false, transactionViolations = {}, includeTaxRates, - taxRates, + policy, + transaction, includeSelfDM = false, includePolicyReportFieldOptions = false, policyReportFieldOptions = [], @@ -1701,7 +1690,7 @@ function getOptions( } if (includeTaxRates) { - const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Tax[], searchInputValue); + const taxRatesOptions = getTaxRatesSection(policy, selectedOptions as Tax[], searchInputValue, transaction); return { recentReports: [], @@ -2431,7 +2420,6 @@ export { hasEnabledTags, formatMemberForList, formatSectionsFromSearchTerm, - transformedTaxRates, getShareLogOptions, filterOptions, createOptionList, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 51c797937b1d..fe265cebe321 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -31,7 +31,6 @@ import type { ReportMetadata, Session, Task, - TaxRate, Transaction, TransactionViolation, UserWallet, @@ -452,7 +451,7 @@ type OptionData = { isSelfDM?: boolean; reportID?: string; enabled?: boolean; - data?: Partial; + code?: string; transactionThreadReportID?: string | null; shouldShowAmountInput?: boolean; amountInputProps?: MoneyRequestAmountInputProps; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 36cbb4f1ecc5..ae30d648a6aa 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -4,7 +4,7 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Policy, RecentWaypoint, Report, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {IOURequestType} from './actions/IOU'; @@ -96,6 +96,8 @@ function buildOptimisticTransaction( existingTransactionID: string | null = null, category = '', tag = '', + taxCode = '', + taxAmount = 0, billable = false, pendingFields: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}> | undefined = undefined, reimbursable = true, @@ -126,6 +128,8 @@ function buildOptimisticTransaction( filename, category, tag, + taxCode, + taxAmount, billable, reimbursable, }; @@ -651,29 +655,69 @@ function getRateID(transaction: OnyxEntry): string | undefined { } /** - * Gets the default tax name + * Gets the tax code based on selected currency. + * Returns policy default tax rate if transaction is in policy default currency, otherwise returns foreign default tax rate */ -function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction?: Transaction) { - const defaultTaxKey = taxRates.defaultExternalID; - const defaultTaxName = - (defaultTaxKey && `${taxRates.taxes[defaultTaxKey]?.name} (${taxRates.taxes[defaultTaxKey]?.value}) ${CONST.DOT_SEPARATOR} ${Localize.translateLocal('common.default')}`) || ''; - return transaction?.taxRate?.text ?? defaultTaxName; +function getDefaultTaxCode(policy: OnyxEntry, transaction: OnyxEntry) { + const defaultExternalID = policy?.taxRates?.defaultExternalID; + const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; + return policy?.outputCurrency === getCurrency(transaction) ? defaultExternalID : foreignTaxDefault; +} + +/** + * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. + * + * @param policy - The policy which the user has access to and which the report is tied to. + * @returns The transformed tax rates object.g + */ +function transformedTaxRates(policy: OnyxEntry | undefined, transaction?: OnyxEntry): Record { + const taxRates = policy?.taxRates; + const defaultExternalID = taxRates?.defaultExternalID; + + const defaultTaxCode = () => { + if (!transaction) { + return defaultExternalID; + } + + return policy && getDefaultTaxCode(policy, transaction); + }; + + const getModifiedName = (data: TaxRate, code: string) => + `${data.name} (${data.value})${defaultTaxCode() === code ? ` ${CONST.DOT_SEPARATOR} ${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; +} + +/** + * Gets the tax value of a selected tax + */ +function getTaxValue(policy: OnyxEntry, transaction: OnyxEntry, taxCode: string) { + return Object.values(transformedTaxRates(policy, transaction)).find((taxRate) => taxRate.code === taxCode)?.value; +} + +/** + * Gets the tax name for Workspace Taxes Settings + */ +function getWorkspaceTaxesSettingsName(policy: OnyxEntry, taxCode: string) { + return Object.values(transformedTaxRates(policy)).find((taxRate) => taxRate.code === taxCode)?.modifiedName; } /** * Gets the tax name */ -function getTaxName(taxes: TaxRates, transactionTaxCode: string) { - const taxName = taxes[transactionTaxCode]?.name ?? ''; - const taxValue = taxes[transactionTaxCode]?.value ?? ''; - return transactionTaxCode && taxName && taxValue ? `${taxName} (${taxValue})` : ''; +function getTaxName(policy: OnyxEntry, transaction: OnyxEntry) { + const defaultTaxCode = getDefaultTaxCode(policy, transaction); + return Object.values(transformedTaxRates(policy, transaction)).find((taxRate) => taxRate.code === (transaction?.taxCode ?? defaultTaxCode))?.modifiedName; } export { buildOptimisticTransaction, calculateTaxAmount, + getWorkspaceTaxesSettingsName, + getDefaultTaxCode, + transformedTaxRates, + getTaxValue, getTaxName, - getDefaultTaxName, getEnabledTaxRateCount, getUpdatedTransaction, getDescription, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 34e4e300de02..024112a0888f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -36,7 +36,6 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as NextStepUtils from '@libs/NextStepUtils'; -import type {TaxRatesOption} from '@libs/OptionsListUtils'; import Permissions from '@libs/Permissions'; import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -1610,7 +1609,7 @@ function getSendInvoiceInformation( policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, ): SendInvoiceInformation { - const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', billable, comment, participants} = transaction ?? {}; + const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID ?? ''; const receiverParticipant = participants?.find((participant) => participant?.accountID); @@ -1656,6 +1655,8 @@ function getSendInvoiceInformation( undefined, category, tag, + taxCode, + taxAmount, billable, ); @@ -1753,6 +1754,8 @@ function getMoneyRequestInformation( existingTransactionID: string | undefined, category: string | undefined, tag: string | undefined, + taxCode: string | undefined, + taxAmount: number | undefined, billable: boolean | undefined, policy: OnyxEntry | undefined, policyTagList: OnyxEntry | undefined, @@ -1834,6 +1837,8 @@ function getMoneyRequestInformation( existingTransactionID, category, tag, + taxCode, + ReportUtils.isExpenseReport(iouReport) ? -(taxAmount ?? 0) : taxAmount, billable, isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, ); @@ -1964,6 +1969,8 @@ function getTrackExpenseInformation( receipt: Receipt | undefined, category: string | undefined, tag: string | undefined, + taxCode: string | undefined, + taxAmount: number | undefined, billable: boolean | undefined, policy: OnyxEntry | undefined, policyTagList: OnyxEntry | undefined, @@ -2061,6 +2068,8 @@ function getTrackExpenseInformation( existingTransactionID ?? null, category, tag, + taxCode, + taxAmount, billable, isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, false, @@ -2156,6 +2165,8 @@ function createDistanceRequest( created: string, category: string | undefined, tag: string | undefined, + taxCode: string | undefined, + taxAmount: number | undefined, amount: number, currency: string, merchant: string, @@ -2200,6 +2211,8 @@ function createDistanceRequest( undefined, category, tag, + taxCode, + taxAmount, billable, policy, policyTagList, @@ -2223,6 +2236,8 @@ function createDistanceRequest( created: currentCreated, category, tag, + taxCode, + taxAmount, billable, transactionThreadReportID, createdReportActionIDForThread, @@ -2835,17 +2850,21 @@ function updateMoneyRequestTaxAmount( API.write('UpdateMoneyRequestTaxAmount', params, onyxData); } +type UpdateMoneyRequestTaxRateParams = { + transactionID: string; + optimisticReportActionID: string; + taxCode: string; + taxAmount?: number; + policy: OnyxEntry; + policyTagList: OnyxEntry; + policyCategories: OnyxEntry; +}; + /** Updates the created tax rate of an expense */ -function updateMoneyRequestTaxRate( - transactionID: string, - optimisticReportActionID: string, - taxCode: string, - policy: OnyxEntry, - policyTagList: OnyxEntry, - policyCategories: OnyxEntry, -) { +function updateMoneyRequestTaxRate({transactionID, optimisticReportActionID, taxCode, taxAmount, policy, policyTagList, policyCategories}: UpdateMoneyRequestTaxRateParams) { const transactionChanges = { taxCode, + ...(taxAmount && {taxAmount}), }; const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, policy, policyTagList, policyCategories, true); API.write('UpdateMoneyRequestTaxRate', params, onyxData); @@ -3297,6 +3316,8 @@ function requestMoney( isMovingTransactionFromTrackExpense ? (linkedTrackedExpenseReportAction?.originalMessage as IOUMessage)?.IOUTransactionID : undefined, category, tag, + taxCode, + taxAmount, billable, policy, policyTagList, @@ -3482,6 +3503,8 @@ function trackExpense( receipt, category, tag, + taxCode, + taxAmount, billable, policy, policyTagList, @@ -3691,6 +3714,8 @@ function createSplitsAndOnyxData( undefined, category, tag, + undefined, + undefined, billable, ); @@ -3922,6 +3947,8 @@ function createSplitsAndOnyxData( undefined, category, tag, + undefined, + undefined, billable, ); @@ -4242,6 +4269,8 @@ function startSplitBill({ undefined, category, tag, + undefined, + undefined, billable, ); @@ -4631,6 +4660,8 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA undefined, updatedTransaction?.category, updatedTransaction?.tag, + undefined, + undefined, updatedTransaction?.billable, ); @@ -5017,6 +5048,7 @@ type UpdateMoneyRequestAmountAndCurrencyParams = { transactionThreadReportID: string; currency: string; amount: number; + taxAmount?: number; policy?: OnyxEntry; policyTagList?: OnyxEntry; policyCategories?: OnyxEntry; @@ -5028,6 +5060,7 @@ function updateMoneyRequestAmountAndCurrency({ transactionThreadReportID, currency, amount, + taxAmount, policy, policyTagList, policyCategories, @@ -5035,6 +5068,7 @@ function updateMoneyRequestAmountAndCurrency({ const transactionChanges = { amount, currency, + ...(taxAmount && {taxAmount}), }; const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null; @@ -6404,8 +6438,8 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On return participants; } -function setMoneyRequestTaxRate(transactionID: string, taxRate: TaxRatesOption) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {taxRate}); +function setMoneyRequestTaxRate(transactionID: string, taxCode: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {taxCode}); } function setMoneyRequestTaxAmount(transactionID: string, taxAmount: number, isDraft: boolean) { diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 6786198b1dc8..b320766edd7a 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -56,6 +56,16 @@ type IOURequestStepAmountProps = IOURequestStepAmountOnyxProps & transaction: OnyxEntry; }; +function getTaxAmount(transaction: OnyxEntry, policy: OnyxEntry, newAmount: number) { + if (!transaction?.amount) { + return; + } + const transactionTaxCode = transaction?.taxCode ?? ''; + const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction) ?? ''; + const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, transactionTaxCode ?? defaultTaxCode) ?? ''; + return CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount)); +} + function IOURequestStepAmount({ report, route: { @@ -280,7 +290,9 @@ function IOURequestStepAmount({ return; } - IOU.updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount}); + const taxAmount = getTaxAmount(transaction, policy, newAmount); + + IOU.updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount}); Navigation.dismissModal(); }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 33fe560cb7ad..fc9e237d129b 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -85,9 +85,9 @@ function IOURequestStepConfirmation({ const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; const receiptType = transaction?.receipt?.type; - const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; - const transactionTaxCode = transaction?.taxRate ? transaction.taxRate.data?.code : foreignTaxDefault; - const transactionTaxAmount = transaction?.taxAmount; + const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction); + const transactionTaxCode = (transaction?.taxCode ? transaction?.taxCode : defaultTaxCode) ?? ''; + const transactionTaxAmount = transaction?.taxAmount ?? 0; const isSharingTrackExpense = action === CONST.IOU.ACTION.SHARE; const isCategorizingTrackExpense = action === CONST.IOU.ACTION.CATEGORIZE; const isSubmittingFromTrackExpense = action === CONST.IOU.ACTION.SUBMIT; @@ -300,6 +300,8 @@ function IOURequestStepConfirmation({ transaction.created, transaction.category, transaction.tag, + transactionTaxCode, + transactionTaxAmount, transaction.amount, transaction.currency, transaction.merchant, @@ -311,7 +313,7 @@ function IOURequestStepConfirmation({ customUnitRateID, ); }, - [policy, policyCategories, policyTags, report, transaction], + [policy, policyCategories, policyTags, report, transaction, transactionTaxCode, transactionTaxAmount], ); const createTransaction = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 50b53d0734c9..0d5972cffcb9 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -283,6 +283,8 @@ function IOURequestStepDistance({ transaction?.created ?? '', '', '', + '', + 0, 0, transaction?.currency ?? 'USD', translate('iou.fieldPending'), diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index 22a00591b4fc..5a79578cc8ba 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -15,7 +15,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Policy, PolicyCategories, PolicyTagList, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; +import type {Policy, PolicyCategories, PolicyTagList, Transaction} from '@src/types/onyx'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -34,15 +34,17 @@ type IOURequestStepTaxAmountPageProps = IOURequestStepTaxAmountPageOnyxProps & transaction: OnyxEntry; }; -function getTaxAmount(transaction: OnyxEntry, taxRates: TaxRatesWithDefault | undefined, isEditing: boolean): number | undefined { +function getTaxAmount(transaction: OnyxEntry, policy: OnyxEntry, isEditing: boolean): number | undefined { if (!transaction?.amount) { return; } const transactionTaxAmount = TransactionUtils.getAmount(transaction); const transactionTaxCode = transaction?.taxCode ?? ''; - const defaultTaxValue = taxRates?.defaultValue; - const moneyRequestTaxPercentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? ''; - const editingTaxPercentage = (transactionTaxCode ? taxRates?.taxes[transactionTaxCode]?.value : moneyRequestTaxPercentage) ?? ''; + const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction) ?? ''; + const getTaxValue = (taxCode: string) => TransactionUtils.getTaxValue(policy, transaction, taxCode); + const defaultTaxValue = getTaxValue(defaultTaxCode); + const moneyRequestTaxPercentage = (transactionTaxCode ? getTaxValue(transactionTaxCode) : defaultTaxValue) ?? ''; + const editingTaxPercentage = (transactionTaxCode ? getTaxValue(transactionTaxCode) : moneyRequestTaxPercentage) ?? ''; const taxPercentage = isEditing ? editingTaxPercentage : moneyRequestTaxPercentage; return CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, transactionTaxAmount)); } @@ -65,7 +67,6 @@ function IOURequestStepTaxAmountPage({ const transactionDetails = ReportUtils.getTransactionDetails(transaction); const currency = CurrencyUtils.isValidCurrencyCode(selectedCurrency) ? selectedCurrency : transactionDetails?.currency; - const taxRates = policy?.taxRates; useFocusEffect( useCallback(() => { @@ -150,7 +151,7 @@ function IOURequestStepTaxAmountPage({ isEditing={Boolean(backTo || isEditing)} currency={currency} amount={transactionDetails?.taxAmount} - taxAmount={getTaxAmount(transaction, taxRates, Boolean(backTo || isEditing))} + taxAmount={getTaxAmount(transaction, policy, Boolean(backTo || isEditing))} ref={(e) => (textInput.current = e)} onCurrencyButtonPress={navigateToCurrencySelectionPage} onSubmitButtonPress={updateTaxAmount} diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index da3a244a2db2..9376aa65e4e6 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -5,14 +5,13 @@ import TaxPicker from '@components/TaxPicker'; import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import type {TaxRatesOption} from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {Policy, PolicyCategories, PolicyTagList, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; +import type {Policy, PolicyCategories, PolicyTagList, Transaction} from '@src/types/onyx'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -31,10 +30,11 @@ type IOURequestStepTaxRatePageProps = IOURequestStepTaxRatePageOnyxProps & transaction: OnyxEntry; }; -function getTaxAmount(taxRates: TaxRatesWithDefault, selectedTaxRate: string, amount: number): number | undefined { - const percentage = Object.values(OptionsListUtils.transformedTaxRates(taxRates)).find((taxRate) => taxRate.modifiedName?.includes(selectedTaxRate))?.value; - if (percentage) { - return TransactionUtils.calculateTaxAmount(percentage, amount); +function getTaxAmount(policy: OnyxEntry, transaction: OnyxEntry, selectedTaxCode: string, amount: number): number | undefined { + const getTaxValue = (taxCode: string) => TransactionUtils.getTaxValue(policy, transaction, taxCode); + const taxPercentage = getTaxValue(selectedTaxCode); + if (taxPercentage) { + return TransactionUtils.calculateTaxAmount(taxPercentage, amount); } } @@ -52,46 +52,49 @@ function IOURequestStepTaxRatePage({ const isEditing = action === CONST.IOU.ACTION.EDIT; const taxRates = policy?.taxRates; - const defaultExternalID = taxRates?.defaultExternalID; - const transactionDetails = ReportUtils.getTransactionDetails(transaction); - const transactionTaxCode = transactionDetails?.taxCode; const navigateBack = () => { Navigation.goBack(backTo); }; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const moneyRequestSelectedTaxRate = transaction?.taxRate?.keyForList || (taxRates && TransactionUtils.getDefaultTaxName(taxRates)); - const editingSelectedTaxRate = - taxRates && - (transactionTaxCode === defaultExternalID - ? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction) - : transactionTaxCode && TransactionUtils.getTaxName(taxRates.taxes, transactionTaxCode)); - - const updateTaxRates = (taxes: OptionsListUtils.TaxRatesOption) => { + + const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); + + const updateTaxRates = (taxes: TaxRatesOption) => { + if (!transaction || !taxes.code || !taxRates) { + Navigation.goBack(); + return; + } + + const taxAmount = getTaxAmount(policy, transaction, taxes?.code, TransactionUtils.getAmount(transaction, false, true)); + if (isEditing) { - const newTaxCode = taxes.data.code; - if (newTaxCode === undefined || newTaxCode === TransactionUtils.getTaxCode(transaction)) { + const newTaxCode = taxes.code; + if (newTaxCode === TransactionUtils.getTaxCode(transaction)) { navigateBack(); return; } - IOU.updateMoneyRequestTaxRate(transaction?.transactionID ?? '', report?.reportID ?? '', newTaxCode, policy, policyTags, policyCategories); + IOU.updateMoneyRequestTaxRate({ + transactionID: transaction?.transactionID ?? '', + optimisticReportActionID: report?.reportID ?? '', + taxCode: newTaxCode, + taxAmount: CurrencyUtils.convertToBackendAmount(taxAmount ?? 0), + policy, + policyTagList: policyTags, + policyCategories, + }); navigateBack(); return; } - if (!transaction || !taxes.text || !taxRates) { - Navigation.goBack(backTo); - return; - } - const taxAmount = getTaxAmount(taxRates, taxes.text, transaction?.amount); + if (taxAmount === undefined) { - Navigation.goBack(backTo); + navigateBack(); return; } const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(taxAmount); - IOU.setMoneyRequestTaxRate(transaction?.transactionID, taxes); + IOU.setMoneyRequestTaxRate(transaction?.transactionID, taxes?.code ?? ''); IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true); - Navigation.goBack(backTo); + navigateBack(); }; return ( @@ -102,9 +105,11 @@ function IOURequestStepTaxRatePage({ testID={IOURequestStepTaxRatePage.displayName} > ); diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx index b006732aec7b..84cee5a7a05a 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx @@ -29,15 +29,12 @@ function WorkspaceTaxesSettingsForeignCurrency({ const {translate} = useLocalize(); const styles = useThemeStyles(); - const taxRates = policy?.taxRates; - const foreignTaxDefault = taxRates?.foreignTaxDefault ?? ''; - const defaultExternalID = taxRates?.defaultExternalID ?? ''; + const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault ?? ''; - const selectedTaxRate = - foreignTaxDefault === defaultExternalID ? taxRates && TransactionUtils.getDefaultTaxName(taxRates) : TransactionUtils.getTaxName(taxRates?.taxes ?? {}, foreignTaxDefault); + const selectedTaxRate = TransactionUtils.getWorkspaceTaxesSettingsName(policy, foreignTaxDefault); const submit = (taxes: OptionsListUtils.TaxRatesOption) => { - setForeignCurrencyDefault(policyID, taxes.data.code ?? ''); + setForeignCurrencyDefault(policyID, taxes.code ?? ''); Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID)); }; diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx index e1ecfbde3a37..cdaec8e3f815 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx @@ -30,10 +30,11 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ const {translate} = useLocalize(); const styles = useThemeStyles(); - const selectedTaxRate = policy?.taxRates && TransactionUtils.getDefaultTaxName(policy?.taxRates); + const defaultExternalID = policy?.taxRates?.defaultExternalID ?? ''; + const selectedTaxRate = policy?.taxRates && TransactionUtils.getWorkspaceTaxesSettingsName(policy, defaultExternalID); const submit = (taxes: OptionsListUtils.TaxRatesOption) => { - setWorkspaceCurrencyDefault(policyID, taxes.data.code ?? ''); + setWorkspaceCurrencyDefault(policyID, taxes.code ?? ''); Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID)); }; diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 0df4e2fe124b..44932ee557e6 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; import * as OptionsListUtils from '@src/libs/OptionsListUtils'; import * as ReportUtils from '@src/libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Policy, PolicyCategories, Report, TaxRatesWithDefault} from '@src/types/onyx'; +import type {PersonalDetails, Policy, PolicyCategories, Report, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; type PersonalDetailsList = Record; @@ -2572,103 +2572,85 @@ describe('OptionsListUtils', () => { }, }, }; + const policy = { + taxRates: taxRatesWithDefault, + } as Policy; + + const transaction = { + taxCode: 'CODE1', + } as Transaction; const resultList: OptionsListUtils.CategorySection[] = [ { - title: '', - shouldShow: false, - // data sorted alphabetically by name data: [ { - // Adds 'Default' title to default tax. - // Adds value to tax name for more description. - text: 'Tax exempt 1 (0%) • Default', + code: 'CODE1', + isDisabled: undefined, + isSelected: undefined, keyForList: 'Tax exempt 1 (0%) • Default', searchText: 'Tax exempt 1 (0%) • Default', + text: 'Tax exempt 1 (0%) • Default', tooltipText: 'Tax exempt 1 (0%) • Default', - isDisabled: undefined, - isSelected: undefined, - // creates a data option. - data: { - name: 'Tax exempt 1', - code: 'CODE1', - modifiedName: 'Tax exempt 1 (0%) • Default', - value: '0%', - }, }, { - text: 'Tax option 3 (5%)', + code: 'CODE3', + isDisabled: undefined, + isSelected: undefined, keyForList: 'Tax option 3 (5%)', searchText: 'Tax option 3 (5%)', + text: 'Tax option 3 (5%)', tooltipText: 'Tax option 3 (5%)', - isDisabled: undefined, - isSelected: undefined, - data: { - name: 'Tax option 3', - code: 'CODE3', - modifiedName: 'Tax option 3 (5%)', - value: '5%', - }, }, { - text: 'Tax rate 2 (3%)', + code: 'CODE2', + isDisabled: undefined, + isSelected: undefined, keyForList: 'Tax rate 2 (3%)', searchText: 'Tax rate 2 (3%)', + text: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', - isDisabled: undefined, - isSelected: undefined, - data: { - name: 'Tax rate 2', - code: 'CODE2', - modifiedName: 'Tax rate 2 (3%)', - value: '3%', - }, }, ], + shouldShow: false, + title: '', }, ]; const searchResultList: OptionsListUtils.CategorySection[] = [ { - title: '', - shouldShow: true, - // data sorted alphabetically by name data: [ { - text: 'Tax rate 2 (3%)', + code: 'CODE2', + isDisabled: undefined, + isSelected: undefined, keyForList: 'Tax rate 2 (3%)', searchText: 'Tax rate 2 (3%)', + text: 'Tax rate 2 (3%)', tooltipText: 'Tax rate 2 (3%)', - isDisabled: undefined, - isSelected: undefined, - data: { - name: 'Tax rate 2', - code: 'CODE2', - modifiedName: 'Tax rate 2 (3%)', - value: '3%', - }, }, ], + shouldShow: true, + title: '', }, ]; const wrongSearchResultList: OptionsListUtils.CategorySection[] = [ { - title: '', - shouldShow: true, data: [], + shouldShow: true, + title: '', }, ]; - const result = OptionsListUtils.getFilteredOptions([], [], [], emptySearch, [], [], false, false, false, {}, [], false, {}, [], false, false, true, taxRatesWithDefault); + const result = OptionsListUtils.getTaxRatesSection(policy, [], emptySearch, transaction); - expect(result.taxRatesOptions).toStrictEqual(resultList); + expect(result).toStrictEqual(resultList); - const searchResult = OptionsListUtils.getFilteredOptions([], [], [], search, [], [], false, false, false, {}, [], false, {}, [], false, false, true, taxRatesWithDefault); - expect(searchResult.taxRatesOptions).toStrictEqual(searchResultList); + const searchResult = OptionsListUtils.getTaxRatesSection(policy, [], search, transaction); + expect(searchResult).toStrictEqual(searchResultList); - const wrongSearchResult = OptionsListUtils.getFilteredOptions([], [], [], wrongSearch, [], [], false, false, false, {}, [], false, {}, [], false, false, true, taxRatesWithDefault); - expect(wrongSearchResult.taxRatesOptions).toStrictEqual(wrongSearchResultList); + const wrongSearchResult = OptionsListUtils.getTaxRatesSection(policy, [], wrongSearch, transaction); + expect(wrongSearchResult).toStrictEqual(wrongSearchResultList); }); it('formatMemberForList()', () => {