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
6 changes: 5 additions & 1 deletion src/libs/ReportPreviewActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
isReportApproved,
isSettled,
} from './ReportUtils';
import {hasSubmissionBlockingViolations, isPending, isScanning} from './TransactionUtils';
import {hasSmartScanFailedWithMissingFields, hasSubmissionBlockingViolations, isPending, isScanning} from './TransactionUtils';

function canSubmit(
report: Report,
Expand All @@ -47,6 +47,10 @@ function canSubmit(

const isAnyReceiptBeingScanned = transactions?.some((transaction) => isScanning(transaction));

if (hasSmartScanFailedWithMissingFields(transactions ?? [], report)) {
return false;
}

if (transactions?.some((transaction) => hasSubmissionBlockingViolations(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) {
return false;
}
Expand Down
5 changes: 5 additions & 0 deletions src/libs/ReportPrimaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
allHavePendingRTERViolation,
getTransactionViolations,
hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils,
hasSmartScanFailedWithMissingFields,
hasSubmissionBlockingViolations,
isDuplicate,
isOnHold as isOnHoldTransactionUtils,
Expand Down Expand Up @@ -121,6 +122,10 @@ function isSubmitAction(
return false;
}

if (hasSmartScanFailedWithMissingFields(reportTransactions ?? [], report)) {
return false;
}

if (violations && currentUserEmail && currentUserAccountID !== undefined) {
if (reportTransactions.some((transaction) => hasSubmissionBlockingViolations(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) {
return false;
Expand Down
5 changes: 5 additions & 0 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
allHavePendingRTERViolation,
getOriginalTransactionWithSplitInfo,
hasReceipt as hasReceiptTransactionUtils,
hasSmartScanFailedWithMissingFields,
hasSubmissionBlockingViolations,
isDuplicate,
isManagedCardTransaction as isManagedCardTransactionTransactionUtils,
Expand Down Expand Up @@ -210,6 +211,10 @@ function isSubmitAction({
return false;
}

if (hasSmartScanFailedWithMissingFields(reportTransactions ?? [], report)) {
return false;
}

if (violations && currentUserLogin && currentUserAccountID !== undefined) {
if (reportTransactions.some((transaction) => hasSubmissionBlockingViolations(transaction, violations, currentUserLogin, currentUserAccountID, report, policy))) {
return false;
Expand Down
10 changes: 10 additions & 0 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2760,6 +2760,15 @@ function shouldReuseInitialTransaction(
return !isMultiScanEnabled || (transactions.length === 1 && (!initialTransaction.receipt?.source || initialTransaction.receipt?.isTestReceipt === true));
}

/**
* Check if the transaction has a smartscan failed with missing fields before violation is written
*/
function hasSmartScanFailedWithMissingFields(transactions: Transaction[], report: OnyxEntry<Report>): boolean {
return transactions.some(
(transaction) => isScanRequest(transaction) && transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCAN_FAILED && hasMissingSmartscanFields(transaction, report),
);
}

export {
buildOptimisticTransaction,
calculateTaxAmount,
Expand Down Expand Up @@ -2897,6 +2906,7 @@ export {
isTimeRequest,
getExpenseTypeTranslationKey,
isDistanceTypeRequest,
hasSmartScanFailedWithMissingFields,
};

export type {TransactionChanges};
2 changes: 2 additions & 0 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
getWaypoints,
hasAnyTransactionWithoutRTERViolation,
hasDuplicateTransactions,
hasSmartScanFailedWithMissingFields,
hasSubmissionBlockingViolations,
isCustomUnitRateIDForP2P,
isDistanceRequest as isDistanceRequestTransactionUtils,
Expand Down Expand Up @@ -811,7 +812,7 @@
};

let allPersonalDetails: OnyxTypes.PersonalDetailsList = {};
Onyx.connect({

Check warning on line 815 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand Down Expand Up @@ -907,7 +908,7 @@
};

let allTransactions: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 911 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -921,7 +922,7 @@
});

let allTransactionDrafts: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 925 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -930,7 +931,7 @@
});

let allTransactionViolations: NonNullable<OnyxCollection<OnyxTypes.TransactionViolations>> = {};
Onyx.connect({

Check warning on line 934 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -944,7 +945,7 @@
});

let allPolicyTags: OnyxCollection<OnyxTypes.PolicyTagLists> = {};
Onyx.connect({

Check warning on line 948 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY_TAGS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -957,7 +958,7 @@
});

let allReports: OnyxCollection<OnyxTypes.Report>;
Onyx.connect({

Check warning on line 961 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -966,7 +967,7 @@
});

let allReportNameValuePairs: OnyxCollection<OnyxTypes.ReportNameValuePairs>;
Onyx.connect({

Check warning on line 970 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -976,7 +977,7 @@

let userAccountID = -1;
let currentUserEmail = '';
Onyx.connect({

Check warning on line 980 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserEmail = value?.email ?? '';
Expand All @@ -985,7 +986,7 @@
});

let deprecatedCurrentUserPersonalDetails: OnyxEntry<OnyxTypes.PersonalDetails>;
Onyx.connect({

Check warning on line 989 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
deprecatedCurrentUserPersonalDetails = value?.[userAccountID] ?? undefined;
Expand All @@ -993,7 +994,7 @@
});

let allReportActions: OnyxCollection<OnyxTypes.ReportActions>;
Onyx.connect({

Check warning on line 997 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand Down Expand Up @@ -10550,6 +10551,7 @@
hasTransactionWithoutRTERViolation &&
!isReportArchived &&
!hasAnySubmissionBlockingViolations &&
!hasSmartScanFailedWithMissingFields(transactions, report) &&
transactions.length > 0
);
}
Expand Down
45 changes: 45 additions & 0 deletions tests/actions/ReportPreviewActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,51 @@ describe('getReportPreviewAction', () => {
).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW);
});

it('canSubmit should return false for expense preview when smartscan failed with missing fields (before violation is written)', async () => {
const TRANSACTION_ID = 'TRANSACTION_ID';
const report: Report = {
...createRandomReport(REPORT_ID, undefined),
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
isWaitingOnBankAccount: false,
};

const policy = createRandomPolicy(0);
policy.autoReportingFrequency = CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE;
policy.type = CONST.POLICY.TYPE.CORPORATE;
if (policy.harvesting) {
policy.harvesting.enabled = false;
}
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);

const transaction = {
transactionID: TRANSACTION_ID,
reportID: `${REPORT_ID}`,
iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN,
receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED},
merchant: '',
} as unknown as Transaction;

const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.parentReportID));

await waitForBatchedUpdatesWithAct();

expect(
getReportPreviewAction({
isReportArchived: isReportArchived.current,
currentUserAccountID: CURRENT_USER_ACCOUNT_ID,
currentUserLogin: CURRENT_USER_EMAIL,
report,
policy,
transactions: [transaction],
bankAccountList: {},
reportMetadata: undefined,
}),
).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW);
});

describe('canApprove', () => {
it('should return true for report being processed', async () => {
const report = {
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/IOUUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,39 @@ describe('canSubmitReport', () => {
const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID));
expect(canSubmitReport(report, policy, [], undefined, isReportArchived.current, '', currentUserAccountID)).toBe(false);
});

it('returns false when SmartScan failed with missing fields before violation is written', async () => {
await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID});
const policy: Policy = {
...createRandomPolicy(8),
ownerAccountID: currentUserAccountID,
areRulesEnabled: true,
preventSelfApproval: false,
autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE,
harvesting: {enabled: false},
};
const report: Report = {
...createRandomReport(8, undefined),
type: CONST.REPORT.TYPE.EXPENSE,
managerID: currentUserAccountID,
ownerAccountID: currentUserAccountID,
policyID: policy.id,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
};

const transaction: Transaction = {
...createRandomTransaction(1),
reportID: report.reportID,
iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN,
receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED},
merchant: 'Coffee',
created: '',
amount: 100,
};

expect(canSubmitReport(report, policy, [transaction], undefined, false, '', currentUserAccountID)).toBe(false);
});
});

describe('Check valid amount for IOU/Expense request', () => {
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/ReportPrimaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,42 @@ describe('getPrimaryAction', () => {
).toBe('');
});

it('should not return SUBMIT when smartscan failed with missing fields before violation is written', async () => {
const report = {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
} as unknown as Report;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
const policy = {
autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE,
};
const TRANSACTION_ID = 'TRANSACTION_ID';
const transaction = {
transactionID: TRANSACTION_ID,
reportID: `${REPORT_ID}`,
iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN,
receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED},
merchant: '',
} as unknown as Transaction;

expect(
getReportPrimaryAction({
currentUserLogin: CURRENT_USER_EMAIL,
currentUserAccountID: CURRENT_USER_ACCOUNT_ID,
report,
chatReport,
reportTransactions: [transaction],
violations: {},
bankAccountList: {},
policy: policy as Policy,
isChatReportArchived: false,
}),
).toBe('');
});

it('should return an empty string for invoice report when the chat report is archived', async () => {
// Given the invoice data
const {policy, convertedInvoiceChat: invoiceChatReport}: InvoiceTestData = InvoiceData;
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/ReportSecondaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,46 @@ describe('getSecondaryAction', () => {
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT)).toBe(false);
});

it('should not include SUBMIT option when smartscan failed with missing fields before violation is written', async () => {
const report = {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: EMPLOYEE_ACCOUNT_ID,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
total: 10,
} as unknown as Report;
const policy = {
autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT,
harvesting: {
enabled: true,
},
} as unknown as Policy;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);

const TRANSACTION_ID = 'TRANSACTION_ID';
const transaction = {
transactionID: TRANSACTION_ID,
reportID: `${REPORT_ID}`,
iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN,
receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED},
merchant: '',
} as unknown as Transaction;

const result = getSecondaryReportActions({
currentUserLogin: EMPLOYEE_EMAIL,
currentUserAccountID: EMPLOYEE_ACCOUNT_ID,
report,
chatReport,
reportTransactions: [transaction],
originalTransaction: {} as Transaction,
violations: {},
bankAccountList: {},
policy,
});
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT)).toBe(false);
});

it('includes APPROVE option for approver and report with duplicates', async () => {
const report = {
reportID: REPORT_ID,
Expand Down
Loading