Skip to content

Commit

Permalink
Merge pull request #40443 from teneeto/feature/39616/support-for-offl…
Browse files Browse the repository at this point in the history
…ine-tax-tracking

support for offline tax tracking
  • Loading branch information
MonilBhavsar authored May 13, 2024
2 parents 6f9f7ab + ea26dc9 commit 2e6e8ca
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 175 deletions.
17 changes: 10 additions & 7 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,11 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
action?: IOUAction;
};

const getTaxAmount = (transaction: OnyxEntry<OnyxTypes.Transaction>, defaultTaxValue: string) => {
const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? '';
return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0);
const getTaxAmount = (transaction: OnyxEntry<OnyxTypes.Transaction>, policy: OnyxEntry<OnyxTypes.Policy>) => {
const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction) ?? '';

const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ?? '';
return TransactionUtils.calculateTaxAmount(taxPercentage, transaction?.amount ?? 0);
};

function MoneyRequestConfirmationList({
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 1 addition & 6 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ function MoneyRequestView({
created: transactionDate,
amount: transactionAmount,
taxAmount: transactionTaxAmount,
taxCode: transactionTaxCode,
currency: transactionCurrency,
comment: transactionDescription,
merchant: transactionMerchant,
Expand All @@ -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);
Expand Down
30 changes: 27 additions & 3 deletions src/components/TaxPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Policy>;

/** All the data for the transaction */
transaction: OnyxEntry<Transaction>;
};

type TaxPickerProps = TaxPickerOnyxProps & {
Expand All @@ -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.
Expand All @@ -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('');
Expand All @@ -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);

Expand Down Expand Up @@ -88,4 +104,12 @@ export default withOnyx<TaxPickerProps, TaxPickerOnyxProps>({
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);
2 changes: 2 additions & 0 deletions src/libs/API/parameters/CreateDistanceRequestParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type CreateDistanceRequestParams = {
created: string;
category?: string;
tag?: string;
taxCode?: string;
taxAmount?: number;
billable?: boolean;
transactionThreadReportID: string;
createdReportActionIDForThread: string;
Expand Down
44 changes: 16 additions & 28 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ type TaxRatesOption = {
tooltipText?: string;
isDisabled?: boolean;
keyForList?: string;
data: Partial<TaxRate>;
isSelected?: boolean;
};

type TaxSection = {
Expand Down Expand Up @@ -165,6 +165,8 @@ type GetOptionsConfig = {
includeSelectedOptions?: boolean;
includeTaxRates?: boolean;
taxRates?: TaxRatesWithDefault;
policy?: OnyxEntry<Policy>;
transaction?: OnyxEntry<Transaction>;
includePolicyReportFieldOptions?: boolean;
policyReportFieldOptions?: string[];
recentlyUsedPolicyReportFieldOptions?: string[];
Expand Down Expand Up @@ -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<string, TaxRate> {
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.
*/
Expand All @@ -1361,24 +1349,24 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] {
* Builds the options for taxRates
*/
function getTaxRatesOptions(taxRates: Array<Partial<TaxRate>>): 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<Policy> | undefined, selectedOptions: Tax[], searchInputValue: string, transaction?: OnyxEntry<Transaction>): TaxSection[] {
const policyRatesSections = [];

const taxes = transformedTaxRates(taxRates);
const taxes = TransactionUtils.transformedTaxRates(policy, transaction);

const sortedTaxRates = sortTaxRates(taxes);
const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.modifiedName);
Expand Down Expand Up @@ -1665,7 +1653,8 @@ function getOptions(
includeSelectedOptions = false,
transactionViolations = {},
includeTaxRates,
taxRates,
policy,
transaction,
includeSelfDM = false,
includePolicyReportFieldOptions = false,
policyReportFieldOptions = [],
Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -2431,7 +2420,6 @@ export {
hasEnabledTags,
formatMemberForList,
formatSectionsFromSearchTerm,
transformedTaxRates,
getShareLogOptions,
filterOptions,
createOptionList,
Expand Down
3 changes: 1 addition & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import type {
ReportMetadata,
Session,
Task,
TaxRate,
Transaction,
TransactionViolation,
UserWallet,
Expand Down Expand Up @@ -452,7 +451,7 @@ type OptionData = {
isSelfDM?: boolean;
reportID?: string;
enabled?: boolean;
data?: Partial<TaxRate>;
code?: string;
transactionThreadReportID?: string | null;
shouldShowAmountInput?: boolean;
amountInputProps?: MoneyRequestAmountInputProps;
Expand Down
68 changes: 56 additions & 12 deletions src/libs/TransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -96,6 +96,8 @@ function buildOptimisticTransaction(
existingTransactionID: string | null = null,
category = '',
tag = '',
taxCode = '',
taxAmount = 0,
billable = false,
pendingFields: Partial<{[K in TransactionPendingFieldsKey]: ValueOf<typeof CONST.RED_BRICK_ROAD_PENDING_ACTION>}> | undefined = undefined,
reimbursable = true,
Expand Down Expand Up @@ -126,6 +128,8 @@ function buildOptimisticTransaction(
filename,
category,
tag,
taxCode,
taxAmount,
billable,
reimbursable,
};
Expand Down Expand Up @@ -651,29 +655,69 @@ function getRateID(transaction: OnyxEntry<Transaction>): 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<Policy>, transaction: OnyxEntry<Transaction>) {
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<Policy> | undefined, transaction?: OnyxEntry<Transaction>): Record<string, TaxRate> {
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<Policy>, transaction: OnyxEntry<Transaction>, 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<Policy>, 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<Policy>, transaction: OnyxEntry<Transaction>) {
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,
Expand Down
Loading

0 comments on commit 2e6e8ca

Please sign in to comment.