Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsCon
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {hasHeldExpenses as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {approveMoneyRequest} from '@userActions/IOU/ReportWorkflow';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -26,10 +27,11 @@ function useConfirmApproval(reportID: string | undefined, startApprovedAnimation
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);

const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? '');
const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
const isAnyTransactionOnHold = hasHeldExpensesReportUtils(Object.values(reportTransactions));

const confirmApproval = () => {
if (isDelegateAccessRestricted) {
Expand Down
10 changes: 6 additions & 4 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,11 +611,9 @@ function dismissDuplicateTransactionViolation({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`,
value: {
...transaction,
comment: {
...transaction?.comment,
dismissedViolations: {
duplicatedTransaction: {
[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: {
[dismissedPersonalDetails.login ?? '']: getUnixTime(new Date()),
},
},
Expand All @@ -635,7 +633,11 @@ function dismissDuplicateTransactionViolation({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`,
value: {
...transaction,
comment: {
dismissedViolations: {
[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: transaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION] ?? null,
},
},
},
}));

Expand Down
81 changes: 81 additions & 0 deletions tests/unit/TransactionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,87 @@ describe('Transaction', () => {
expect(Object.keys(reportActions ?? {}).length).toBe(0);
});

it('should preserve an existing hold when optimistic dismissal is built from a stale transaction snapshot', async () => {
const transactionID = 'dismissTxnHold';
const threadReportID = 'threadDismissHold';
const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const;
const testerEmail = 'tester@example.com';
const mockViolations: TransactionViolation[] = [{name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: 'warning'}];

mockFetch.pause();

await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations);

const transactionInOnyx = generateTransaction({
transactionID,
reportID: FAKE_OLD_REPORT_ID,
comment: {
hold: 'holdReportActionID',
dismissedViolations: {
[CONST.VIOLATIONS.SMARTSCAN_FAILED]: {
owner: 123,
},
},
},
});
await Onyx.merge(transactionKey, transactionInOnyx);

const staleTransaction = {
...transactionInOnyx,
comment: {
dismissedViolations: {
[CONST.VIOLATIONS.SMARTSCAN_FAILED]: {
owner: 123,
},
},
hold: undefined,
},
} as Transaction;

const iouAction = {
reportActionID: rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
childReportID: threadReportID,
actorAccountID: CURRENT_USER_ID,
created: DateUtils.getDBTime(),
originalMessage: {
IOUReportID: FAKE_OLD_REPORT_ID,
IOUTransactionID: transactionID,
amount: transactionInOnyx.amount,
currency: transactionInOnyx.currency,
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
},
};
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[iouAction.reportActionID]: iouAction});
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`, {});

mockFetch.fail();

dismissDuplicateTransactionViolation({
transactionIDs: [transactionID],
dismissedPersonalDetails: {login: testerEmail, accountID: CURRENT_USER_ID},
expenseReport: newReport,
policy: undefined,
isASAPSubmitBetaEnabled: false,
allTransactions: {[transactionKey]: staleTransaction},
});
await waitForBatchedUpdates();

const optimisticTransaction = await getOnyxValue(transactionKey);
const duplicateDismissals = optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION];
expect(optimisticTransaction?.comment?.hold).toBe('holdReportActionID');
expect(optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123});
expect(duplicateDismissals?.[testerEmail]).toEqual(expect.any(Number));

await mockFetch.resume();
await waitForBatchedUpdates();

const revertedTransaction = await getOnyxValue(transactionKey);
expect(revertedTransaction?.comment?.hold).toBe('holdReportActionID');
expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123});
expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]).toBeUndefined();
});

it('should not modify Onyx data when tag list does not exist at given index (empty violations array)', async () => {
const transactionID = 'dismissTxn3';
const threadReportID = 'threadDismiss3';
Expand Down
Loading