diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 0c3868312c418..ef03a5d4d717b 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -103,7 +103,7 @@ const getNewSelection = (oldSelection: Selection, prevLength: number, newLength: return {start: cursorPosition, end: cursorPosition}; }; -const defaultOnFormatAmount = (amount: number) => CurrencyUtils.convertToFrontendAmountAsString(amount); +const defaultOnFormatAmount = (amount: number, currency?: string) => CurrencyUtils.convertToFrontendAmountAsString(amount, currency ?? CONST.CURRENCY.USD); function MoneyRequestAmountInput( { diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 7e6682492eb28..26f9a365528bb 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -329,10 +329,10 @@ function MoneyRequestConfirmationList({ taxCode = transaction?.taxCode ?? TransactionUtils.getDefaultTaxCode(policy, transaction) ?? ''; } const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? ''; - const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount); + const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, currency); const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString())); IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', taxAmountInSmallestCurrencyUnits); - }, [policy, shouldShowTax, previousTransactionAmount, previousTransactionCurrency, transaction, isDistanceRequest, customUnitRateID]); + }, [policy, shouldShowTax, previousTransactionAmount, previousTransactionCurrency, transaction, isDistanceRequest, customUnitRateID, currency]); // 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/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 7b54fbf0bed7a..be7ce9aca8b5f 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -87,8 +87,9 @@ function convertToBackendAmount(amountAsFloat: number): number { * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsInteger(amountAsInt: number): number { - return Math.trunc(amountAsInt) / 100.0; +function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string = CONST.CURRENCY.USD): number { + const decimals = getCurrencyDecimals(currency); + return Number((Math.trunc(amountAsInt) / 100.0).toFixed(decimals)); } /** @@ -96,11 +97,12 @@ function convertToFrontendAmountAsInteger(amountAsInt: number): number { * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsString(amountAsInt: number | null | undefined): string { +function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD): string { if (amountAsInt === null || amountAsInt === undefined) { return ''; } - return convertToFrontendAmountAsInteger(amountAsInt).toFixed(2); + const decimals = getCurrencyDecimals(currency); + return convertToFrontendAmountAsInteger(amountAsInt, currency).toFixed(decimals); } /** @@ -111,7 +113,7 @@ function convertToFrontendAmountAsString(amountAsInt: number | null | undefined) * @param currency - IOU currency */ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string { - const convertedAmount = convertToFrontendAmountAsInteger(amountInCents); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency); /** * Fallback currency to USD if it empty string or undefined */ @@ -137,7 +139,7 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR * @param currency - IOU currency */ function convertToShortDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string { - const convertedAmount = convertToFrontendAmountAsInteger(amountInCents); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency); return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, { style: 'currency', @@ -168,7 +170,7 @@ function convertAmountToDisplayString(amount = 0, currency: string = CONST.CURRE * Acts the same as `convertAmountToDisplayString` but the result string does not contain currency */ function convertToDisplayStringWithoutCurrency(amountInCents: number, currency: string = CONST.CURRENCY.USD) { - const convertedAmount = convertToFrontendAmountAsInteger(amountInCents); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency); return NumberFormatUtils.formatToParts(BaseLocaleListener.getPreferredLocale(), convertedAmount, { style: 'currency', currency, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b28e5b7829655..9f2c727b94ba1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -9,6 +9,7 @@ import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {IOURequestType} from './actions/IOU'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; +import {getCurrencyDecimals} from './CurrencyUtils'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; @@ -714,9 +715,11 @@ function hasWarningTypeViolation(transactionID: string, transactionViolations: O /** * Calculates tax amount from the given expense amount and tax percentage */ -function calculateTaxAmount(percentage: string, amount: number) { +function calculateTaxAmount(percentage: string, amount: number, currency: string) { const divisor = Number(percentage.slice(0, -1)) / 100 + 1; - return Math.round(amount - amount / divisor) / 100; + const taxAmount = (amount - amount / divisor) / 100; + const decimals = getCurrencyDecimals(currency); + return parseFloat(taxAmount.toFixed(decimals)); } /** diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index ff061e7382c69..4db7a13171cb1 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -70,8 +70,8 @@ type MoneyRequestAmountFormProps = { }; const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01; -const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean) => - isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmountAsInteger(Math.abs(taxAmount)); +const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean, currency: string) => + isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmountAsInteger(Math.abs(taxAmount), currency); const AMOUNT_VIEW_ID = 'amountView'; const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView'; @@ -148,14 +148,17 @@ function MoneyRequestAmountForm( }); }, [isFocused, wasFocused]); - const initializeAmount = useCallback((newAmount: number) => { - const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount) : ''; - moneyRequestAmountInput.current?.changeAmount(frontendAmount); - moneyRequestAmountInput.current?.changeSelection({ - start: frontendAmount.length, - end: frontendAmount.length, - }); - }, []); + const initializeAmount = useCallback( + (newAmount: number) => { + const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount, currency) : ''; + moneyRequestAmountInput.current?.changeAmount(frontendAmount); + moneyRequestAmountInput.current?.changeSelection({ + start: frontendAmount.length, + end: frontendAmount.length, + }); + }, + [currency], + ); useEffect(() => { if (!currency || typeof amount !== 'number') { @@ -218,7 +221,7 @@ function MoneyRequestAmountForm( return; } - if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { + if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm, currency)) { setFormError(translate('iou.error.invalidTaxAmount', {amount: formattedTaxAmount})); return; } diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 04c8e772844b5..a64af40a32a5b 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -294,7 +294,7 @@ function IOURequestStepAmount({ const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, currentTransaction, currency) ?? ''; const taxCode = (currency !== transactionCurrency ? defaultTaxCode : transactionTaxCode) ?? defaultTaxCode; const taxPercentage = TransactionUtils.getTaxValue(policy, currentTransaction, taxCode) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount)); + const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount, currency ?? CONST.CURRENCY.USD)); if (isSplitBill) { IOU.setDraftSplitTransaction(transactionID, {amount: newAmount, currency, taxCode, taxAmount}); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 9399a2a65d9f7..4f70d3e4fee9b 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -81,7 +81,7 @@ function IOURequestStepDistanceRate({ const taxRateExternalID = policyCustomUnitRate?.attributes?.taxRateExternalID ?? '-1'; const taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, TransactionUtils.getDistance(transaction)); const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxRateExternalID) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount)); + const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, rates[customUnitRateID].currency ?? CONST.CURRENCY.USD)); IOU.setMoneyRequestTaxAmount(transactionID, taxAmount); IOU.setMoneyRequestTaxRate(transactionID, taxRateExternalID); } diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index e67f837089385..4b3b5cc13eb16 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -50,7 +50,7 @@ function getTaxAmount(transaction: OnyxEntry, policy: OnyxEntry, transaction: OnyxEntry TransactionUtils.getTaxValue(policy, transaction, taxCode); const taxPercentage = getTaxValue(selectedTaxCode); if (taxPercentage) { - return TransactionUtils.calculateTaxAmount(taxPercentage, amount); + return TransactionUtils.calculateTaxAmount(taxPercentage, amount, getCurrency(transaction)); } } diff --git a/tests/unit/CurrencyUtilsTest.ts b/tests/unit/CurrencyUtilsTest.ts index 87b7c7ee4569c..5322faff763f3 100644 --- a/tests/unit/CurrencyUtilsTest.ts +++ b/tests/unit/CurrencyUtilsTest.ts @@ -107,27 +107,40 @@ describe('CurrencyUtils', () => { describe('convertToFrontendAmountAsInteger', () => { test.each([ - [2500, 25], - [2550, 25.5], - [25, 0.25], - [2500, 25], - [2500.5, 25], // The backend should never send a decimal .5 value - ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount)).toBe(expectedResult); + [2500, 25, 'USD'], + [2550, 25.5, 'USD'], + [25, 0.25, 'USD'], + [2500, 25, 'USD'], + [2500.5, 25, 'USD'], // The backend should never send a decimal .5 value + [2500, 25, 'VND'], + [2550, 26, 'VND'], + [25, 0, 'VND'], + [2586, 26, 'VND'], + [2500.5, 25, 'VND'], // The backend should never send a decimal .5 value + ])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult, currency) => { + expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount, currency)).toBe(expectedResult); }); }); describe('convertToFrontendAmountAsString', () => { test.each([ - [2500, '25.00'], - [2550, '25.50'], - [25, '0.25'], - [2500.5, '25.00'], - [null, ''], - [undefined, ''], - [0, '0.00'], - ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult) => { - expect(CurrencyUtils.convertToFrontendAmountAsString(input)).toBe(expectedResult); + [2500, '25.00', 'USD'], + [2550, '25.50', 'USD'], + [25, '0.25', 'USD'], + [2500.5, '25.00', 'USD'], + [null, '', 'USD'], + [undefined, '', 'USD'], + [0, '0.00', 'USD'], + [2500, '25', 'VND'], + [2550, '26', 'VND'], + [25, '0', 'VND'], + [2500.5, '25', 'VND'], + [null, '', 'VND'], + [undefined, '', 'VND'], + [0, '0', 'VND'], + [2586, '26', 'VND'], + ])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult, currency) => { + expect(CurrencyUtils.convertToFrontendAmountAsString(input, currency ?? CONST.CURRENCY.USD)).toBe(expectedResult); }); });