Skip to content

Commit 2e6e8ca

Browse files
authored
Merge pull request #40443 from teneeto/feature/39616/support-for-offline-tax-tracking
support for offline tax tracking
2 parents 6f9f7ab + ea26dc9 commit 2e6e8ca

16 files changed

+266
-175
lines changed

src/components/MoneyRequestConfirmationList.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,11 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
181181
action?: IOUAction;
182182
};
183183

184-
const getTaxAmount = (transaction: OnyxEntry<OnyxTypes.Transaction>, defaultTaxValue: string) => {
185-
const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? '';
186-
return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0);
184+
const getTaxAmount = (transaction: OnyxEntry<OnyxTypes.Transaction>, policy: OnyxEntry<OnyxTypes.Policy>) => {
185+
const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction) ?? '';
186+
187+
const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ?? '';
188+
return TransactionUtils.calculateTaxAmount(taxPercentage, transaction?.amount ?? 0);
187189
};
188190

189191
function MoneyRequestConfirmationList({
@@ -317,9 +319,10 @@ function MoneyRequestConfirmationList({
317319
isDistanceRequest ? currency : iouCurrencyCode,
318320
);
319321
const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode);
320-
const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : '';
322+
const taxRateTitle = TransactionUtils.getTaxName(policy, transaction);
321323

322324
const previousTransactionAmount = usePrevious(transaction?.amount);
325+
const previousTransactionCurrency = usePrevious(transaction?.currency);
323326

324327
const isFocused = useIsFocused();
325328
const [formError, debouncedFormError, setFormError] = useDebouncedState('');
@@ -373,15 +376,15 @@ function MoneyRequestConfirmationList({
373376

374377
// Calculate and set tax amount in transaction draft
375378
useEffect(() => {
376-
const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString();
379+
const taxAmount = getTaxAmount(transaction, policy).toString();
377380
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
378381

379-
if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) {
382+
if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount && previousTransactionCurrency === transaction?.currency) {
380383
return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true);
381384
}
382385

383386
IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true);
384-
}, [taxRates?.defaultValue, transaction, transactionID, previousTransactionAmount]);
387+
}, [policy, transaction, transactionID, previousTransactionAmount, previousTransactionCurrency]);
385388

386389
// If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again
387390
if (isEditingSplitBill && didConfirm) {

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ function MoneyRequestView({
109109
created: transactionDate,
110110
amount: transactionAmount,
111111
taxAmount: transactionTaxAmount,
112-
taxCode: transactionTaxCode,
113112
currency: transactionCurrency,
114113
comment: transactionDescription,
115114
merchant: transactionMerchant,
@@ -134,11 +133,7 @@ function MoneyRequestView({
134133
const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency);
135134

136135
const taxRatesDescription = taxRates?.name;
137-
const taxRateTitle =
138-
taxRates &&
139-
(transactionTaxCode === taxRates?.defaultExternalID
140-
? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction)
141-
: transactionTaxCode && TransactionUtils.getTaxName(taxRates?.taxes, transactionTaxCode));
136+
const taxRateTitle = TransactionUtils.getTaxName(policy, transaction);
142137

143138
// Flags for allowing or disallowing editing an expense
144139
const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID);

src/components/TaxPicker.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@ import type {OnyxEntry} from 'react-native-onyx';
44
import type {EdgeInsets} from 'react-native-safe-area-context';
55
import useLocalize from '@hooks/useLocalize';
66
import useStyleUtils from '@hooks/useStyleUtils';
7+
import * as IOUUtils from '@libs/IOUUtils';
78
import * as OptionsListUtils from '@libs/OptionsListUtils';
89
import * as TransactionUtils from '@libs/TransactionUtils';
910
import CONST from '@src/CONST';
11+
import type {IOUAction} from '@src/CONST';
1012
import ONYXKEYS from '@src/ONYXKEYS';
11-
import type {Policy} from '@src/types/onyx';
13+
import type {Policy, Transaction} from '@src/types/onyx';
1214
import SelectionList from './SelectionList';
1315
import RadioListItem from './SelectionList/RadioListItem';
1416

1517
type TaxPickerOnyxProps = {
1618
/** The policy which the user has access to and which the report is tied to */
1719
policy: OnyxEntry<Policy>;
20+
21+
/** All the data for the transaction */
22+
transaction: OnyxEntry<Transaction>;
1823
};
1924

2025
type TaxPickerProps = TaxPickerOnyxProps & {
@@ -25,6 +30,10 @@ type TaxPickerProps = TaxPickerOnyxProps & {
2530
// eslint-disable-next-line react/no-unused-prop-types
2631
policyID?: string;
2732

33+
/** ID of the transaction */
34+
// eslint-disable-next-line react/no-unused-prop-types
35+
transactionID?: string;
36+
2837
/**
2938
* Safe area insets required for reflecting the portion of the view,
3039
* that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
@@ -33,9 +42,13 @@ type TaxPickerProps = TaxPickerOnyxProps & {
3342

3443
/** Callback to fire when a tax is pressed */
3544
onSubmit: (tax: OptionsListUtils.TaxRatesOption) => void;
45+
46+
/** The action to take */
47+
// eslint-disable-next-line react/no-unused-prop-types
48+
action?: IOUAction;
3649
};
3750

38-
function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerProps) {
51+
function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit}: TaxPickerProps) {
3952
const StyleUtils = useStyleUtils();
4053
const {translate} = useLocalize();
4154
const [searchValue, setSearchValue] = useState('');
@@ -60,7 +73,10 @@ function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerPr
6073
];
6174
}, [selectedTaxRate]);
6275

63-
const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Tax[], searchValue), [taxRates, searchValue, selectedOptions]);
76+
const sections = useMemo(
77+
() => OptionsListUtils.getTaxRatesSection(policy, selectedOptions as OptionsListUtils.Tax[], searchValue, transaction),
78+
[searchValue, selectedOptions, policy, transaction],
79+
);
6480

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

@@ -88,4 +104,12 @@ export default withOnyx<TaxPickerProps, TaxPickerOnyxProps>({
88104
policy: {
89105
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
90106
},
107+
transaction: {
108+
key: ({transactionID, action}) => {
109+
if (action === CONST.IOU.ACTION.CREATE || IOUUtils.isMovingTransactionFromTrackExpense(action)) {
110+
return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}` as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`;
111+
}
112+
return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
113+
},
114+
},
91115
})(TaxPicker);

src/libs/API/parameters/CreateDistanceRequestParams.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ type CreateDistanceRequestParams = {
1111
created: string;
1212
category?: string;
1313
tag?: string;
14+
taxCode?: string;
15+
taxAmount?: number;
1416
billable?: boolean;
1517
transactionThreadReportID: string;
1618
createdReportActionIDForThread: string;

src/libs/OptionsListUtils.ts

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ type TaxRatesOption = {
106106
tooltipText?: string;
107107
isDisabled?: boolean;
108108
keyForList?: string;
109-
data: Partial<TaxRate>;
109+
isSelected?: boolean;
110110
};
111111

112112
type TaxSection = {
@@ -165,6 +165,8 @@ type GetOptionsConfig = {
165165
includeSelectedOptions?: boolean;
166166
includeTaxRates?: boolean;
167167
taxRates?: TaxRatesWithDefault;
168+
policy?: OnyxEntry<Policy>;
169+
transaction?: OnyxEntry<Transaction>;
168170
includePolicyReportFieldOptions?: boolean;
169171
policyReportFieldOptions?: string[];
170172
recentlyUsedPolicyReportFieldOptions?: string[];
@@ -1335,20 +1337,6 @@ function getReportFieldOptionsSection(options: string[], recentlyUsedOptions: st
13351337
return reportFieldOptionsSections;
13361338
}
13371339

1338-
/**
1339-
* Transforms tax rates to a new object format - to add codes and new name with concatenated name and value.
1340-
*
1341-
* @param taxRates - The original tax rates object.
1342-
* @returns The transformed tax rates object.g
1343-
*/
1344-
function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record<string, TaxRate> {
1345-
const defaultTaxKey = taxRates?.defaultExternalID;
1346-
const getModifiedName = (data: TaxRate, code: string) =>
1347-
`${data.name} (${data.value})${defaultTaxKey === code ? ` ${CONST.DOT_SEPARATOR} ${Localize.translateLocal('common.default')}` : ''}`;
1348-
const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}]));
1349-
return taxes;
1350-
}
1351-
13521340
/**
13531341
* Sorts tax rates alphabetically by name.
13541342
*/
@@ -1361,24 +1349,24 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] {
13611349
* Builds the options for taxRates
13621350
*/
13631351
function getTaxRatesOptions(taxRates: Array<Partial<TaxRate>>): TaxRatesOption[] {
1364-
return taxRates.map((taxRate) => ({
1365-
text: taxRate.modifiedName,
1366-
keyForList: taxRate.modifiedName,
1367-
searchText: taxRate.modifiedName,
1368-
tooltipText: taxRate.modifiedName,
1369-
isDisabled: taxRate.isDisabled,
1370-
data: taxRate,
1371-
isSelected: taxRate.isSelected,
1352+
return taxRates.map(({code, modifiedName, isDisabled, isSelected}) => ({
1353+
code,
1354+
text: modifiedName,
1355+
keyForList: modifiedName,
1356+
searchText: modifiedName,
1357+
tooltipText: modifiedName,
1358+
isDisabled,
1359+
isSelected,
13721360
}));
13731361
}
13741362

13751363
/**
13761364
* Builds the section list for tax rates
13771365
*/
1378-
function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Tax[], searchInputValue: string): TaxSection[] {
1366+
function getTaxRatesSection(policy: OnyxEntry<Policy> | undefined, selectedOptions: Tax[], searchInputValue: string, transaction?: OnyxEntry<Transaction>): TaxSection[] {
13791367
const policyRatesSections = [];
13801368

1381-
const taxes = transformedTaxRates(taxRates);
1369+
const taxes = TransactionUtils.transformedTaxRates(policy, transaction);
13821370

13831371
const sortedTaxRates = sortTaxRates(taxes);
13841372
const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName);
@@ -1665,7 +1653,8 @@ function getOptions(
16651653
includeSelectedOptions = false,
16661654
transactionViolations = {},
16671655
includeTaxRates,
1668-
taxRates,
1656+
policy,
1657+
transaction,
16691658
includeSelfDM = false,
16701659
includePolicyReportFieldOptions = false,
16711660
policyReportFieldOptions = [],
@@ -1701,7 +1690,7 @@ function getOptions(
17011690
}
17021691

17031692
if (includeTaxRates) {
1704-
const taxRatesOptions = getTaxRatesSection(taxRates, selectedOptions as Tax[], searchInputValue);
1693+
const taxRatesOptions = getTaxRatesSection(policy, selectedOptions as Tax[], searchInputValue, transaction);
17051694

17061695
return {
17071696
recentReports: [],
@@ -2431,7 +2420,6 @@ export {
24312420
hasEnabledTags,
24322421
formatMemberForList,
24332422
formatSectionsFromSearchTerm,
2434-
transformedTaxRates,
24352423
getShareLogOptions,
24362424
filterOptions,
24372425
createOptionList,

src/libs/ReportUtils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import type {
3131
ReportMetadata,
3232
Session,
3333
Task,
34-
TaxRate,
3534
Transaction,
3635
TransactionViolation,
3736
UserWallet,
@@ -452,7 +451,7 @@ type OptionData = {
452451
isSelfDM?: boolean;
453452
reportID?: string;
454453
enabled?: boolean;
455-
data?: Partial<TaxRate>;
454+
code?: string;
456455
transactionThreadReportID?: string | null;
457456
shouldShowAmountInput?: boolean;
458457
amountInputProps?: MoneyRequestAmountInputProps;

src/libs/TransactionUtils.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Onyx from 'react-native-onyx';
44
import type {ValueOf} from 'type-fest';
55
import CONST from '@src/CONST';
66
import ONYXKEYS from '@src/ONYXKEYS';
7-
import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation} from '@src/types/onyx';
7+
import type {Policy, RecentWaypoint, Report, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx';
88
import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
99
import {isEmptyObject} from '@src/types/utils/EmptyObject';
1010
import type {IOURequestType} from './actions/IOU';
@@ -96,6 +96,8 @@ function buildOptimisticTransaction(
9696
existingTransactionID: string | null = null,
9797
category = '',
9898
tag = '',
99+
taxCode = '',
100+
taxAmount = 0,
99101
billable = false,
100102
pendingFields: Partial<{[K in TransactionPendingFieldsKey]: ValueOf<typeof CONST.RED_BRICK_ROAD_PENDING_ACTION>}> | undefined = undefined,
101103
reimbursable = true,
@@ -126,6 +128,8 @@ function buildOptimisticTransaction(
126128
filename,
127129
category,
128130
tag,
131+
taxCode,
132+
taxAmount,
129133
billable,
130134
reimbursable,
131135
};
@@ -651,29 +655,69 @@ function getRateID(transaction: OnyxEntry<Transaction>): string | undefined {
651655
}
652656

653657
/**
654-
* Gets the default tax name
658+
* Gets the tax code based on selected currency.
659+
* Returns policy default tax rate if transaction is in policy default currency, otherwise returns foreign default tax rate
655660
*/
656-
function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction?: Transaction) {
657-
const defaultTaxKey = taxRates.defaultExternalID;
658-
const defaultTaxName =
659-
(defaultTaxKey && `${taxRates.taxes[defaultTaxKey]?.name} (${taxRates.taxes[defaultTaxKey]?.value}) ${CONST.DOT_SEPARATOR} ${Localize.translateLocal('common.default')}`) || '';
660-
return transaction?.taxRate?.text ?? defaultTaxName;
661+
function getDefaultTaxCode(policy: OnyxEntry<Policy>, transaction: OnyxEntry<Transaction>) {
662+
const defaultExternalID = policy?.taxRates?.defaultExternalID;
663+
const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault;
664+
return policy?.outputCurrency === getCurrency(transaction) ? defaultExternalID : foreignTaxDefault;
665+
}
666+
667+
/**
668+
* Transforms tax rates to a new object format - to add codes and new name with concatenated name and value.
669+
*
670+
* @param policy - The policy which the user has access to and which the report is tied to.
671+
* @returns The transformed tax rates object.g
672+
*/
673+
function transformedTaxRates(policy: OnyxEntry<Policy> | undefined, transaction?: OnyxEntry<Transaction>): Record<string, TaxRate> {
674+
const taxRates = policy?.taxRates;
675+
const defaultExternalID = taxRates?.defaultExternalID;
676+
677+
const defaultTaxCode = () => {
678+
if (!transaction) {
679+
return defaultExternalID;
680+
}
681+
682+
return policy && getDefaultTaxCode(policy, transaction);
683+
};
684+
685+
const getModifiedName = (data: TaxRate, code: string) =>
686+
`${data.name} (${data.value})${defaultTaxCode() === code ? ` ${CONST.DOT_SEPARATOR} ${Localize.translateLocal('common.default')}` : ''}`;
687+
const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}]));
688+
return taxes;
689+
}
690+
691+
/**
692+
* Gets the tax value of a selected tax
693+
*/
694+
function getTaxValue(policy: OnyxEntry<Policy>, transaction: OnyxEntry<Transaction>, taxCode: string) {
695+
return Object.values(transformedTaxRates(policy, transaction)).find((taxRate) => taxRate.code === taxCode)?.value;
696+
}
697+
698+
/**
699+
* Gets the tax name for Workspace Taxes Settings
700+
*/
701+
function getWorkspaceTaxesSettingsName(policy: OnyxEntry<Policy>, taxCode: string) {
702+
return Object.values(transformedTaxRates(policy)).find((taxRate) => taxRate.code === taxCode)?.modifiedName;
661703
}
662704

663705
/**
664706
* Gets the tax name
665707
*/
666-
function getTaxName(taxes: TaxRates, transactionTaxCode: string) {
667-
const taxName = taxes[transactionTaxCode]?.name ?? '';
668-
const taxValue = taxes[transactionTaxCode]?.value ?? '';
669-
return transactionTaxCode && taxName && taxValue ? `${taxName} (${taxValue})` : '';
708+
function getTaxName(policy: OnyxEntry<Policy>, transaction: OnyxEntry<Transaction>) {
709+
const defaultTaxCode = getDefaultTaxCode(policy, transaction);
710+
return Object.values(transformedTaxRates(policy, transaction)).find((taxRate) => taxRate.code === (transaction?.taxCode ?? defaultTaxCode))?.modifiedName;
670711
}
671712

672713
export {
673714
buildOptimisticTransaction,
674715
calculateTaxAmount,
716+
getWorkspaceTaxesSettingsName,
717+
getDefaultTaxCode,
718+
transformedTaxRates,
719+
getTaxValue,
675720
getTaxName,
676-
getDefaultTaxName,
677721
getEnabledTaxRateCount,
678722
getUpdatedTransaction,
679723
getDescription,

0 commit comments

Comments
 (0)