From b2f4fabeecf9369f405bff2d5875d121ef386528 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 20 May 2026 11:50:59 -0700 Subject: [PATCH 1/2] Fix export action after failed QBO auto-export MarkExportFailed updates errorFields.export in real time but not hasExportError. Treat active export errorFields as a failure signal so the Export to QuickBooks Online action appears without reopening. Co-authored-by: Cursor --- src/libs/ReportPreviewActionUtils.ts | 5 ++-- src/libs/ReportUtils.ts | 13 +++++----- tests/unit/ReportUtilsTest.ts | 36 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts index 35e30fda030f..048182205482 100644 --- a/src/libs/ReportPreviewActionUtils.ts +++ b/src/libs/ReportPreviewActionUtils.ts @@ -9,6 +9,7 @@ import { getMoneyRequestSpendBreakdown, getParentReport, getReportTransactions, + hasExportError as hasExportErrorUtil, hasOnlyNonReimbursableTransactions, isClosedReport, isCurrentUserSubmitter, @@ -122,7 +123,7 @@ function canPay( const isReimbursed = isSettled(report); const isExported = report.isExportedToIntegration ?? false; - const hasExportError = report?.hasExportError ?? false; + const hasExportError = hasExportErrorUtil(undefined, report); const didExportFail = !isExported && hasExportError; if ( @@ -177,7 +178,7 @@ function canExport(report: Report, currentUserLogin: string, policy?: Policy) { return false; } - const hasExportError = report.hasExportError ?? false; + const hasExportError = hasExportErrorUtil(undefined, report); if (syncEnabled && !hasExportError) { return false; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2043736d9990..1f7d5db9944a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12568,7 +12568,6 @@ function getIntegrationNameFromExportMessage(reportActions: OnyxEntry | ReportAction[], report?: OnyxEntry): boolean { - // If report object is provided and has the property, use it directly if (report?.isExportedToIntegration !== undefined) { return report.isExportedToIntegration; } @@ -12616,9 +12615,13 @@ function isExported(reportActions: OnyxEntry | ReportAction[], re } function hasExportError(reportActions: OnyxEntry | ReportAction[], report?: OnyxEntry) { - // If report object is provided and has the property, use it directly - if (report?.hasExportError !== undefined) { - return report.hasExportError; + if (report?.hasExportError) { + return true; + } + + const exportErrors = report?.errorFields?.export; + if (exportErrors && Object.values(exportErrors).some((error) => error != null)) { + return true; } // Fallback to checking actions for backward compatibility @@ -12926,7 +12929,6 @@ function canRejectReportAction(currentUserLogin: string, report: Report): boolea } function hasReportBeenReopened(report: OnyxEntry, reportActions?: OnyxEntry | ReportAction[]): boolean { - // If report object is provided and has the property, use it directly if (report?.hasReportBeenReopened !== undefined) { return report.hasReportBeenReopened; } @@ -12941,7 +12943,6 @@ function hasReportBeenReopened(report: OnyxEntry, reportActions?: OnyxEn } function hasReportBeenRetracted(report: OnyxEntry, reportActions?: OnyxEntry | ReportAction[]): boolean { - // If report object is provided and has the property, use it directly if (report?.hasReportBeenRetracted !== undefined) { return report.hasReportBeenRetracted; } diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 70f1c9f4133c..4835171f5cd3 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -118,6 +118,7 @@ import { getWorkspaceNameUpdatedMessage, hasActionWithErrorsForTransaction, hasEmptyReportsForPolicy, + hasExportError, hasReceiptError, hasSmartscanError, hasVisibleReportFieldViolations, @@ -17619,4 +17620,39 @@ describe('ReportUtils', () => { expect(createdActionForThread).toBeNull(); }); }); + + describe('hasExportError', () => { + it('returns true when report.hasExportError is true', () => { + const report = {hasExportError: true} as Report; + expect(hasExportError([], report)).toBe(true); + }); + + it('returns true when errorFields.export is populated but hasExportError is false', () => { + const report = { + hasExportError: false, + errorFields: {export: {'1708946640843000': 'export failed'}}, + } as Report; + expect(hasExportError([], report)).toBe(true); + }); + + it('returns false when errorFields.export is cleared', () => { + const report = { + hasExportError: false, + errorFields: {export: null}, + } as Report; + expect(hasExportError([], report)).toBe(false); + }); + + it('returns true when an integration message action exists in reportActions', () => { + const report = {hasExportError: false} as Report; + const reportActions = [ + { + actionName: CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE, + reportActionID: '1', + created: '2024-01-01', + }, + ] as ReportAction[]; + expect(hasExportError(reportActions, report)).toBe(true); + }); + }); }); From 2b03a443cbc66c5a230b07d35c6693b8a83d89af Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 21 May 2026 09:56:04 -0700 Subject: [PATCH 2/2] Fix TypeScript errors in hasExportError tests Use as unknown as Report for fixtures with errorFields to avoid TS2352 overlap errors. Co-authored-by: Cursor --- tests/unit/ReportUtilsTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 4835171f5cd3..950cecc2acb9 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -17631,7 +17631,7 @@ describe('ReportUtils', () => { const report = { hasExportError: false, errorFields: {export: {'1708946640843000': 'export failed'}}, - } as Report; + } as unknown as Report; expect(hasExportError([], report)).toBe(true); }); @@ -17639,7 +17639,7 @@ describe('ReportUtils', () => { const report = { hasExportError: false, errorFields: {export: null}, - } as Report; + } as unknown as Report; expect(hasExportError([], report)).toBe(false); });