Skip to content
Merged
2 changes: 0 additions & 2 deletions config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,6 @@
"../../src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/Search/SearchPage.tsx" "react-hooks/refs" 31
"../../src/pages/Search/SearchPage.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/TransactionDuplicate/Confirmation.tsx" "react-hooks/immutability" 2
"../../src/pages/TransactionDuplicate/Confirmation.tsx" "react-hooks/preserve-manual-memoization" 1
"../../src/pages/TransactionDuplicate/Confirmation.tsx" "react-hooks/refs" 12
"../../src/pages/TransactionDuplicate/Review.tsx" "react-hooks/preserve-manual-memoization" 1
"../../src/pages/TransactionMerge/MergeFieldReview.tsx" "no-restricted-syntax" 1
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/MergeDuplicatesParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type MergeDuplicatesParams = {
billable: boolean;
reimbursable: boolean;
tag: string;
taxCode?: string;
receiptID: number;
reportID: string | undefined;
reportActionID?: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/ResolveDuplicatesParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ResolveDuplicatesParams = {
billable: boolean;
reimbursable: boolean;
tag: string;
taxCode?: string;

/** The reportActionID of the dismissed violation action in the kept transaction thread report */
dismissedViolationReportActionID: string;
Expand Down
5 changes: 4 additions & 1 deletion src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2551,14 +2551,17 @@ function getTransactionID(report?: OnyxEntry<Report>): string | undefined {
}

function buildNewTransactionAfterReviewingDuplicates(reviewDuplicateTransaction: OnyxEntry<ReviewDuplicates>, duplicatedTransaction: OnyxEntry<Transaction>): Partial<Transaction> {
const {duplicates, ...restReviewDuplicateTransaction} = reviewDuplicateTransaction ?? {};
const {duplicates, taxAmount, ...restReviewDuplicateTransaction} = reviewDuplicateTransaction ?? {};
const hasUpdatedTaxCode = reviewDuplicateTransaction?.taxCode !== undefined && reviewDuplicateTransaction?.taxCode !== duplicatedTransaction?.taxCode;

return {
...duplicatedTransaction,
...restReviewDuplicateTransaction,
modifiedMerchant: reviewDuplicateTransaction?.merchant,
merchant: reviewDuplicateTransaction?.merchant,
comment: {...reviewDuplicateTransaction?.comment, comment: reviewDuplicateTransaction?.description},
// If the taxCode changes, apply the reviewed tax amount and clear stale taxName/taxValue so MoneyRequestView derives them fresh from the policy.
...(hasUpdatedTaxCode && {taxAmount, taxName: undefined, taxValue: undefined}),
};
}

Expand Down
148 changes: 103 additions & 45 deletions src/libs/actions/IOU/Duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,40 +71,105 @@ function getIOUActionForTransactions(transactionIDList: Array<string | undefined
);
}

type MergeDuplicatesFuncParams = MergeDuplicatesParams & {currentUserLogin: string; currentUserAccountID: number};

/** Merge several transactions into one by updating the fields of the one we want to keep and deleting the rest */
function mergeDuplicates({transactionThreadReportID: optimisticTransactionThreadReportID, currentUserLogin, currentUserAccountID, ...params}: MergeDuplicatesFuncParams) {
const allParams: MergeDuplicatesParams = {...params};
const allTransactions = getAllTransactions();
const allTransactionViolations = getAllTransactionViolations();
const allReports = getAllReports();
const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];
type DuplicateTransactionParams = {
transactionID: string | undefined;
originalSelectedTransaction: OnyxEntry<OnyxTypes.Transaction>;
billable: boolean;
comment: string;
category: string;
created: string;
currency: string;
merchant: string;
reimbursable: boolean;
tag: string;
taxCode?: string;
taxAmount?: number;
taxValue?: string;
};

const optimisticTransactionData: OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION> = {
function buildOptimisticTransactionData({
transactionID,
originalSelectedTransaction,
billable,
comment,
category,
created,
currency,
merchant,
reimbursable,
tag,
taxCode,
taxAmount,
taxValue,
}: DuplicateTransactionParams): OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION> {
return {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
...originalSelectedTransaction,
billable: params.billable,
billable,
comment: {
comment: params.comment,
comment,
},
category: params.category,
created: params.created,
currency: params.currency,
modifiedMerchant: params.merchant,
reimbursable: params.reimbursable,
tag: params.tag,
category,
created,
currency,
modifiedMerchant: merchant,
reimbursable,
tag,
taxCode: taxCode ?? originalSelectedTransaction?.taxCode,
taxAmount: taxAmount ?? originalSelectedTransaction?.taxAmount,
taxValue: taxValue ?? originalSelectedTransaction?.taxValue,
// Clear `taxName` to stay consistent with the server response,
// and avoid retaining an outdated value that doesn't match the new `taxCode`.
taxName: taxCode ? null : originalSelectedTransaction?.taxName,
},
};
}

const failureTransactionData: OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION> = {
function buildFailureTransactionData(transactionID: string | undefined, originalSelectedTransaction: OnyxEntry<OnyxTypes.Transaction>): OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION> {
return {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
value: originalSelectedTransaction as OnyxTypes.Transaction,
};
}

type MergeDuplicatesFuncParams = MergeDuplicatesParams & {currentUserLogin: string; currentUserAccountID: number; taxAmount?: number; taxValue?: string};

/** Merge several transactions into one by updating the fields of the one we want to keep and deleting the rest */
function mergeDuplicates({
transactionThreadReportID: optimisticTransactionThreadReportID,
currentUserLogin,
currentUserAccountID,
taxAmount,
taxValue,
...params
}: MergeDuplicatesFuncParams) {
const allParams: MergeDuplicatesParams = {...params};
const allTransactions = getAllTransactions();
const allTransactionViolations = getAllTransactionViolations();
const allReports = getAllReports();
const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];

const optimisticTransactionData = buildOptimisticTransactionData({
transactionID: params.transactionID,
originalSelectedTransaction,
billable: params.billable,
comment: params.comment,
category: params.category,
created: params.created,
currency: params.currency,
merchant: params.merchant,
reimbursable: params.reimbursable,
tag: params.tag,
taxCode: params.taxCode,
taxAmount,
taxValue,
});

const failureTransactionData = buildFailureTransactionData(params.transactionID, originalSelectedTransaction);

const optimisticTransactionDuplicatesData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION>> = params.transactionIDList.map((id) => ({
onyxMethod: Onyx.METHOD.SET,
Expand Down Expand Up @@ -344,7 +409,7 @@ function mergeDuplicates({transactionThreadReportID: optimisticTransactionThread
}

/** Instead of merging the duplicates, it updates the transaction we want to keep and puts the others on hold without deleting them */
function resolveDuplicates(params: MergeDuplicatesParams) {
function resolveDuplicates({taxAmount, taxValue, ...params}: MergeDuplicatesParams & {taxAmount?: number; taxValue?: string}) {
if (!params.transactionID) {
return;
}
Expand All @@ -354,30 +419,23 @@ function resolveDuplicates(params: MergeDuplicatesParams) {

const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];

const optimisticTransactionData: OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION> = {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`,
value: {
...originalSelectedTransaction,
billable: params.billable,
comment: {
comment: params.comment,
},
category: params.category,
created: params.created,
currency: params.currency,
modifiedMerchant: params.merchant,
reimbursable: params.reimbursable,
tag: params.tag,
},
};
const optimisticTransactionData = buildOptimisticTransactionData({
transactionID: params.transactionID,
originalSelectedTransaction,
billable: params.billable,
comment: params.comment,
category: params.category,
created: params.created,
currency: params.currency,
merchant: params.merchant,
reimbursable: params.reimbursable,
tag: params.tag,
taxCode: params.taxCode,
taxAmount,
taxValue,
});

const failureTransactionData: OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION> = {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`,
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
value: originalSelectedTransaction as OnyxTypes.Transaction,
};
const failureTransactionData = buildFailureTransactionData(params.transactionID, originalSelectedTransaction);

const optimisticTransactionViolations: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS>> = [...params.transactionIDList, params.transactionID].map((id) => {
const violations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? [];
Expand Down
36 changes: 28 additions & 8 deletions src/pages/TransactionDuplicate/Confirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function Confirmation() {
const [reviewDuplicatesReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reviewDuplicates?.reportID}`);
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(reviewDuplicatesReport?.policyID)}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`);
const [duplicatedTransactionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(reviewDuplicatesReport?.policyID)}`);
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(reviewDuplicatesReport?.policyID)}`);
const compareResult = TransactionUtils.compareDuplicateTransactionFields(policyTags ?? {}, transaction, allDuplicates, reviewDuplicatesReport, undefined, policy, policyCategories);
const {goBack} = useReviewDuplicatesNavigation(Object.keys(compareResult.change ?? {}), 'confirmation', route.params.threadReportID, route.params.backTo);
Expand All @@ -73,25 +74,44 @@ function Confirmation() {
() => TransactionUtils.buildMergeDuplicatesParams(reviewDuplicates, duplicates ?? [], newTransaction),
[duplicates, reviewDuplicates, newTransaction],
);
const reviewDuplicatesTaxCode = reviewDuplicates?.taxCode;
const reviewDuplicatesTaxAmount = reviewDuplicates?.taxAmount;
const duplicatedTransactionTaxCode = duplicatedTransaction?.taxCode;
const taxRates = duplicatedTransactionPolicy?.taxRates?.taxes;
const taxData = useMemo(() => {
const taxCode = reviewDuplicatesTaxCode ?? '';
const taxRate = taxCode ? taxRates?.[taxCode] : undefined;
// Preserve taxAmount and taxValue if taxCode is deleted or remains unchanged compared to duplicatedTransaction?.taxCode.
if (!taxRate || (taxCode && duplicatedTransactionTaxCode === taxCode) || reviewDuplicatesTaxAmount === undefined) {
return;
}

return {
taxAmount: -reviewDuplicatesTaxAmount,
taxValue: taxRate?.value,
taxCode,
};
}, [reviewDuplicatesTaxCode, reviewDuplicatesTaxAmount, taxRates, duplicatedTransactionTaxCode]);
const isReportOwner = iouReport?.ownerAccountID === currentUserPersonalDetails?.accountID;
const currentUserAccountID = currentUserPersonalDetails.accountID;
const currentUserLogin = currentUserPersonalDetails?.login;
const childReportID = reportAction?.childReportID;

const handleMergeDuplicates = useCallback(() => {
const transactionThreadReportID = reportAction?.childReportID ?? generateReportID();
if (!reportAction?.childReportID) {
transactionsMergeParams.transactionThreadReportID = transactionThreadReportID;
}
mergeDuplicates({...transactionsMergeParams, currentUserAccountID: currentUserPersonalDetails.accountID, currentUserLogin: currentUserPersonalDetails?.login ?? ''});
const transactionThreadReportID = childReportID ?? generateReportID();
const mergeParams = !childReportID ? {...transactionsMergeParams, transactionThreadReportID} : transactionsMergeParams;
mergeDuplicates({...mergeParams, ...taxData, currentUserAccountID, currentUserLogin: currentUserLogin ?? ''});
if (isSuperWideRHPDisplayed) {
Navigation.dismissToSuperWideRHP();
return;
}
Navigation.dismissModal();
}, [reportAction?.childReportID, transactionsMergeParams, currentUserPersonalDetails.accountID, currentUserPersonalDetails?.login, isSuperWideRHPDisplayed]);
}, [childReportID, transactionsMergeParams, taxData, currentUserAccountID, currentUserLogin, isSuperWideRHPDisplayed]);

const handleResolveDuplicates = useCallback(() => {
resolveDuplicates(transactionsMergeParams);
resolveDuplicates({...transactionsMergeParams, ...taxData});
Navigation.dismissToSuperWideRHP();
}, [transactionsMergeParams]);
}, [transactionsMergeParams, taxData]);

const contextMenuStateValue = useMemo(
() => ({
Expand Down
5 changes: 3 additions & 2 deletions src/pages/TransactionDuplicate/ReviewTaxCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ function ReviewTaxRate() {
const {translate} = useLocalize();
const {getCurrencyDecimals} = useCurrencyListActions();
const [reviewDuplicates] = useOnyx(ONYXKEYS.REVIEW_DUPLICATES);
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reviewDuplicates?.reportID ?? route.params.threadReportID}`);
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reviewDuplicates?.reportID}`);
const policy = usePolicy(report?.policyID);
const transactionID = getTransactionID(report);
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`);
const transactionID = getTransactionID(transactionThreadReport);
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`);
const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
const allDuplicateIDs = useMemo(
Expand Down
2 changes: 2 additions & 0 deletions tests/actions/IOUTest/DuplicateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ describe('actions/Duplicate', () => {
billable: true,
reimbursable: false,
tag: 'UpdatedProject',
taxCode: '',
receiptID: 123,
reportID,
};
Expand Down Expand Up @@ -533,6 +534,7 @@ describe('actions/Duplicate', () => {
billable: true,
reimbursable: false,
tag: 'UpdatedProject',
taxCode: '',
receiptID: 123,
reportID,
};
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/TransactionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2822,6 +2822,39 @@ describe('TransactionUtils', () => {
});
});

describe('buildNewTransactionAfterReviewingDuplicates', () => {
it('preserves the kept transaction tax amount when the selected tax code matches the existing tax code', () => {
const duplicatedTransaction = generateTransaction({
transactionID: 'transaction1',
reportID: 'report1',
taxCode: 'id_TAX_RATE_1',
taxAmount: -500,
taxValue: '5%',
});

const reviewDuplicates = {
duplicates: [],
transactionID: 'transaction1',
reportID: 'report1',
merchant: 'Updated Merchant',
category: 'Travel',
tag: 'Project',
taxCode: 'id_TAX_RATE_1',
taxAmount: 900,
description: 'Updated comment',
comment: duplicatedTransaction.comment ?? {},
reimbursable: false,
billable: true,
};

const updatedTransaction = TransactionUtils.buildNewTransactionAfterReviewingDuplicates(reviewDuplicates, duplicatedTransaction);

expect(updatedTransaction.taxCode).toBe('id_TAX_RATE_1');
expect(updatedTransaction.taxAmount).toBe(-500);
expect(updatedTransaction.taxValue).toBe('5%');
});
});

describe('getTagArrayFromName', () => {
it('splits simple tag by colon', () => {
expect(TransactionUtils.getTagArrayFromName('tag1:tag2:tag3')).toEqual(['tag1', 'tag2', 'tag3']);
Expand Down
Loading