Skip to content

Commit

Permalink
Merge pull request #42571 from kubabutkiewicz/feat/dupe-detection-con…
Browse files Browse the repository at this point in the history
…firmation

Feat/dupe detection confirmation
  • Loading branch information
pecanoro authored Jul 19, 2024
2 parents 9e811c2 + a352bfb commit 47d3301
Show file tree
Hide file tree
Showing 27 changed files with 405 additions and 98 deletions.
9 changes: 6 additions & 3 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,8 +951,8 @@ const ROUTES = {
getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tax-code` as const,
},
TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: {
route: 'r/:threadReportID/duplicates/confirm',
getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const,
route: 'r/:threadReportID/duplicates/review/description',
getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/description` as const,
},
TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE: {
route: 'r/:threadReportID/duplicates/review/reimbursable',
Expand All @@ -962,7 +962,10 @@ const ROUTES = {
route: 'r/:threadReportID/duplicates/review/billable',
getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/billable` as const,
},

TRANSACTION_DUPLICATE_CONFIRMATION_PAGE: {
route: 'r/:threadReportID/duplicates/confirm',
getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const,
},
POLICY_ACCOUNTING_XERO_IMPORT: {
route: 'settings/workspaces/:policyID/accounting/xero/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ const SCREENS = {
TAX_CODE: 'Transaction_Duplicate_Tax_Code',
REIMBURSABLE: 'Transaction_Duplicate_Reimburable',
BILLABLE: 'Transaction_Duplicate_Billable',
CONFIRMATION: 'Transaction_Duplicate_Confirmation',
},

IOU_SEND: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ function MoneyRequestPreviewContent({
} else if ('reimbursable' in comparisonResult.change) {
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(route.params?.threadReportID));
} else {
// Navigation to confirm screen will be done in seperate PR
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(route.params?.threadReportID));
}
};

Expand Down
108 changes: 65 additions & 43 deletions src/components/ReportActionItem/MoneyRequestView.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/hooks/useReviewDuplicatesNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function useReviewDuplicatesNavigation(stepNames: string[], currentScreenName: S
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(threadReportID));
break;
default:
// Navigation to confirm screen will be done in seperate PR
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(threadReportID));
break;
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3968,6 +3968,8 @@ export default {
categoryToKeep: 'Choose which category to keep',
isTransactionBillable: 'Choose if transaction is billable',
keepThisOne: 'Keep this one',
confirmDetails: `Confirm the details you're keeping`,
confirmDuplicatesInfo: `The duplicate requests you don't keep will be held for the member to delete`,
hold: 'Hold',
},
violationDismissal: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4487,6 +4487,8 @@ export default {
categoryToKeep: 'Elige qué categoría quieres conservar',
isTransactionBillable: 'Elige si la transacción es facturable',
keepThisOne: 'Mantener éste',
confirmDetails: 'Confirma los detalles que conservas',
confirmDuplicatesInfo: 'Los duplicados que no conserves se guardarán para que el usuario los elimine',
hold: 'Bloqueado',
},
violationDismissal: {
Expand Down
17 changes: 17 additions & 0 deletions src/libs/API/parameters/TransactionMergeParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type TransactionMergeParams = {
transactionID: string;
transactionIDList: string[];
created: string;
merchant: string;
amount: number;
currency: string;
category: string;
comment: string;
billable: boolean;
reimbursable: boolean;
tag: string;
receiptID: number;
reportID: string;
};

export default TransactionMergeParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export type {default as SearchParams} from './Search';
export type {default as SendInvoiceParams} from './SendInvoiceParams';
export type {default as PayInvoiceParams} from './PayInvoiceParams';
export type {default as MarkAsCashParams} from './MarkAsCashParams';
export type {default as TransactionMergeParams} from './TransactionMergeParams';
export type {default as UpdateSubscriptionTypeParams} from './UpdateSubscriptionTypeParams';
export type {default as SignUpUserParams} from './SignUpUserParams';
export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscriptionAutoRenewParams';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ const WRITE_COMMANDS = {
SEND_INVOICE: 'SendInvoice',
PAY_INVOICE: 'PayInvoice',
MARK_AS_CASH: 'MarkAsCash',
TRANSACTION_MERGE: 'Transaction_Merge',
UPDATE_SUBSCRIPTION_TYPE: 'UpdateSubscriptionType',
SIGN_UP_USER: 'SignUpUser',
UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew',
Expand Down Expand Up @@ -549,6 +550,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams;
[WRITE_COMMANDS.PAY_INVOICE]: Parameters.PayInvoiceParams;
[WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams;
[WRITE_COMMANDS.TRANSACTION_MERGE]: Parameters.TransactionMergeParams;
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_TYPE]: Parameters.UpdateSubscriptionTypeParams;
[WRITE_COMMANDS.SIGN_UP_USER]: Parameters.SignUpUserParams;
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ const TransactionDuplicateStackNavigator = createModalStackNavigator<Transaction
[SCREENS.TRANSACTION_DUPLICATE.TAX_CODE]: () => require<ReactComponentModule>('../../../../pages/TransactionDuplicate/ReviewTaxCode').default,
[SCREENS.TRANSACTION_DUPLICATE.BILLABLE]: () => require<ReactComponentModule>('../../../../pages/TransactionDuplicate/ReviewBillable').default,
[SCREENS.TRANSACTION_DUPLICATE.REIMBURSABLE]: () => require<ReactComponentModule>('../../../../pages/TransactionDuplicate/ReviewReimbursable').default,
[SCREENS.TRANSACTION_DUPLICATE.CONFIRMATION]: () => require<ReactComponentModule>('../../../../pages/TransactionDuplicate/Confirmation').default,
});

const SearchReportModalStackNavigator = createModalStackNavigator<SearchReportParamList>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import NoDropZone from '@components/DragAndDrop/NoDropZone';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {abandonReviewDuplicateTransactions} from '@libs/actions/Transaction';
import {isSafari} from '@libs/Browser';
import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
Expand All @@ -19,7 +20,7 @@ type RightModalNavigatorProps = StackScreenProps<AuthScreensParamList, typeof NA

const Stack = createStackNavigator<RightModalNavigatorParamList>();

function RightModalNavigator({navigation}: RightModalNavigatorProps) {
function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
const styles = useThemeStyles();
const styleUtils = useStyleUtils();
const {isSmallScreenWidth} = useWindowDimensions();
Expand Down Expand Up @@ -51,6 +52,19 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) {
<View style={styles.RHPNavigatorContainer(isSmallScreenWidth)}>
<Stack.Navigator
screenOptions={screenOptions}
screenListeners={{
blur: () => {
if (
// @ts-expect-error There is something wrong with a types here and it's don't see the params list
navigation.getState().routes.find((routes) => routes.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR)?.params?.screen ===
SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE ||
route.params?.screen !== SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE
) {
return;
}
abandonReviewDuplicateTransactions();
},
}}
id={NAVIGATORS.RIGHT_MODAL_NAVIGATOR}
>
<Stack.Screen
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,10 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
path: ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.route,
exact: true,
},
[SCREENS.TRANSACTION_DUPLICATE.CONFIRMATION]: {
path: ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.route,
exact: true,
},
},
},
[SCREENS.RIGHT_MODAL.SPLIT_DETAILS]: {
Expand Down
51 changes: 49 additions & 2 deletions src/libs/TransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, ReviewDuplicates, T
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';
import type {TransactionMergeParams} from './API/parameters';
import {isCorporateCard, isExpensifyCard} from './CardUtils';
import {getCurrencyDecimals} from './CurrencyUtils';
import DateUtils from './DateUtils';
Expand Down Expand Up @@ -857,7 +858,7 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia
const change: Record<string, any[]> = {};

const fieldsToCompare: FieldsToCompare = {
merchant: ['merchant', 'modifiedMerchant'],
merchant: ['modifiedMerchant', 'merchant'],
category: ['category'],
tag: ['tag'],
description: ['comment'],
Expand All @@ -866,7 +867,20 @@ function compareDuplicateTransactionFields(transactionID: string): {keep: Partia
reimbursable: ['reimbursable'],
};

const getDifferentValues = (items: Array<OnyxEntry<Transaction>>, keys: Array<keyof Transaction>) => [...new Set(items.map((item) => keys.map((key) => item?.[key])).flat())];
const getDifferentValues = (items: Array<OnyxEntry<Transaction>>, keys: Array<keyof Transaction>) => [
...new Set(
items
.map((item) => {
// Prioritize modifiedMerchant over merchant
if (keys.includes('modifiedMerchant' as keyof Transaction) && keys.includes('merchant' as keyof Transaction)) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return item?.modifiedMerchant || item?.merchant;
}
return keys.map((key) => item?.[key]);
})
.flat(),
),
];

for (const fieldName in fieldsToCompare) {
if (Object.prototype.hasOwnProperty.call(fieldsToCompare, fieldName)) {
Expand Down Expand Up @@ -913,6 +927,37 @@ function getTransactionID(threadReportID: string): string {
return IOUTransactionID;
}

function buildNewTransactionAfterReviewingDuplicates(reviewDuplicateTransaction: OnyxEntry<ReviewDuplicates>): Partial<Transaction> {
const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${reviewDuplicateTransaction?.transactionID}`] ?? undefined;
const {duplicates, ...restReviewDuplicateTransaction} = reviewDuplicateTransaction ?? {};

return {
...originalTransaction,
...restReviewDuplicateTransaction,
modifiedMerchant: reviewDuplicateTransaction?.merchant,
merchant: reviewDuplicateTransaction?.merchant,
comment: {comment: reviewDuplicateTransaction?.description},
};
}

function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry<ReviewDuplicates>, originalTransaction: Partial<Transaction>): TransactionMergeParams {
return {
amount: -getAmount(originalTransaction as OnyxEntry<Transaction>, false),
reportID: originalTransaction?.reportID ?? '',
receiptID: originalTransaction?.receipt?.receiptID ?? 0,
currency: getCurrency(originalTransaction as OnyxEntry<Transaction>),
created: getFormattedCreated(originalTransaction as OnyxEntry<Transaction>),
transactionID: reviewDuplicates?.transactionID ?? '',
transactionIDList: reviewDuplicates?.duplicates ?? [],
billable: reviewDuplicates?.billable ?? false,
reimbursable: reviewDuplicates?.reimbursable ?? false,
category: reviewDuplicates?.category ?? '',
tag: reviewDuplicates?.tag ?? '',
merchant: reviewDuplicates?.merchant ?? '',
comment: reviewDuplicates?.description ?? '',
};
}

export {
buildOptimisticTransaction,
calculateTaxAmount,
Expand Down Expand Up @@ -983,6 +1028,8 @@ export {
getTransaction,
compareDuplicateTransactionFields,
getTransactionID,
buildNewTransactionAfterReviewingDuplicates,
buildTransactionsMergeParams,
getReimbursable,
};

Expand Down
71 changes: 71 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
StartSplitBillParams,
SubmitReportParams,
TrackExpenseParams,
TransactionMergeParams,
UnapproveExpenseReportParams,
UpdateMoneyRequestParams,
} from '@libs/API/parameters';
Expand Down Expand Up @@ -7351,6 +7352,75 @@ function getIOURequestPolicyID(transaction: OnyxEntry<OnyxTypes.Transaction>, re
return workspaceSender?.policyID ?? report?.policyID ?? '-1';
}

/** Merge several transactions into one by updating the fields of the one we want to keep and deleting the rest */
function mergeDuplicates(params: TransactionMergeParams) {
const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];

const optimisticTransactionData: OnyxUpdate = {
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 failureTransactionData: OnyxUpdate = {
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 optimisticTransactionDuplicatesData: OnyxUpdate[] = params.transactionIDList.map((id) => ({
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${id}`,
value: null,
}));

const failureTransactionDuplicatesData: OnyxUpdate[] = params.transactionIDList.map((id) => ({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${id}`,
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
value: allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`] as OnyxTypes.Transaction,
}));

const optimisticTransactionViolations: OnyxUpdate[] = [...params.transactionIDList, params.transactionID].map((id) => {
const violations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? [];
return {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`,
value: violations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION),
};
});

const failureTransactionViolations: OnyxUpdate[] = [...params.transactionIDList, params.transactionID].map((id) => {
const violations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? [];
return {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`,
value: violations,
};
});

const optimisticData: OnyxUpdate[] = [];
const failureData: OnyxUpdate[] = [];

optimisticData.push(optimisticTransactionData, ...optimisticTransactionDuplicatesData, ...optimisticTransactionViolations);
failureData.push(failureTransactionData, ...failureTransactionDuplicatesData, ...failureTransactionViolations);

API.write(WRITE_COMMANDS.TRANSACTION_MERGE, params, {optimisticData, failureData});
}

export {
adjustRemainingSplitShares,
approveMoneyRequest,
Expand Down Expand Up @@ -7418,5 +7488,6 @@ export {
updateMoneyRequestTag,
updateMoneyRequestTaxAmount,
updateMoneyRequestTaxRate,
mergeDuplicates,
};
export type {GPSPoint as GpsPoint, IOURequestType};
5 changes: 5 additions & 0 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ function setReviewDuplicatesKey(values: Partial<ReviewDuplicates>) {
});
}

function abandonReviewDuplicateTransactions() {
Onyx.set(ONYXKEYS.REVIEW_DUPLICATES, null);
}

function clearError(transactionID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null, errorFields: {route: null}});
}
Expand Down Expand Up @@ -470,5 +474,6 @@ export {
markAsCash,
dismissDuplicateTransactionViolation,
setReviewDuplicatesKey,
abandonReviewDuplicateTransactions,
openDraftDistanceExpense,
};
Loading

0 comments on commit 47d3301

Please sign in to comment.