OUT-3528: notify IUs when QBO sync fails with manual-fix errors#238
OUT-3528: notify IUs when QBO sync fails with manual-fix errors#238SandipBajracharya wants to merge 9 commits intopreviewfrom
Conversation
Persists the QBO Fault.Error.code on each sync log row so failures can be routed to a notification action and filtered in the sync history CSV by error type. Nullable varchar(50) — zero-downtime migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getMessageAndCodeFromError now returns the Intuit Fault.Error.code (e.g. 6140) instead of the HTTP status when the error originates from QBO; renames IntuitErrorType.Code → code to match the actual payload casing. Each FAILED-path createQBSyncLog / updateQBSyncLog call site (webhook, payment, sync resync) now passes the resulting errorCode so it lands in qb_sync_logs.error_code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maps 11 manual-fix QBO error codes (6140, 6240, 6210, 6540, 610, 2500, 6190, 6000, 5010, 620, 2390) to NotificationActions and provides per-action in-product + email copy via a single NotificationCopy registry. Bodies interpolate "(during X, ref Y)" from log entityType/eventType + identifier so IUs can pinpoint the failing record. Two appended advisories (MANUAL_EDIT_NOTE for recoverable failures, FINAL_FAILURE_NOTE for 5010 invoice). 5010 + 6240 branch on entityType (separate QBO namespaces); 6000/6190 keep cause-honest framing without manual-edit advisory. IU_RECIPIENT_ACTIONS in NotificationService is derived from the registry keys so a new action can't be added without an opt-in. Sentry capture added for dispatch failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fier
SyncLogService schedules an IU notification on every transition
INTO LogStatus.FAILED (PENDING/SUCCESS → FAILED). Retries that keep
a row FAILED do not re-page; partial updates that don't set status
short-circuit before any DB lookup. Dispatch runs in afterIfAvailable
so the request scope (and any enclosing transaction) commits first;
notifier failures are captured to Sentry and never undo the sync log
write.
5010 stale-object on products and customers is suppressed because
those flows pre-fetch SyncToken via update{Product,Customer}SyncToken
and recover automatically on the next 3-hour cron tick. The invoice
flow uses the cached qbSyncToken directly with no equivalent refresh,
so 5010 on invoices remains user-actionable and surfaces.
Sync history CSV now exposes the error_code column so IUs can
filter past failures by QBO code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
notification.helper.test: AUTH_RECONNECT regression guard, per-action non-empty body assertions, 6240 entityType branching (item-only copy must not leak Customer/Vendor/Employee), 5010 invoice "failure is final" copy. syncErrorNotifier.test: registry lookups (known + unknown codes, empty/null), entity-key fallback ladder, status-not-FAILED skip, unknown-code skip, 5010 suppression on product/customer/payment (parameterized) vs dispatch on invoice, getPortalConnection-null senderId fallback to ''. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Both unit test files had hand-maintained action lists that could silently miss new entries. Replaces them with derivations: - syncErrorNotifier.test: it.each over Object.entries(UserActionableErrorCodes) - notification.helper.test: it.each over Object.values(NotificationActions) (no manual filter — AUTH_RECONNECT is asserted explicitly elsewhere and runs through the smoke loop too) A new code in the registry or a new enum value now picks up smoke coverage automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bd276df to
938d9c8
Compare
Greptile SummaryThis PR surfaces QBO sync failures to workspace IUs via in-product and email notifications, routing 11 user-actionable QBO fault codes through a new
Confidence Score: 3/5Two P1 issues should be resolved before merge: potential notification flooding for workspace-level errors and unverified IntuitErrorType field casing that could silently break all error-code-based notifications. Two independent P1 findings: (1) the per-entity dispatch model for 6190/6210 could flood IUs during systemic outages, and (2) the Code→code rename in IntuitErrorType is unvalidated against live QBO payloads — if wrong, all error codes silently become NaN and no notifications fire. Either alone would cap at 4; both together with the TOCTOU race lower it to 3. src/utils/error.ts (casing correctness), src/app/api/quickbooks/syncLog/syncErrorNotifier.ts (flood/dedup for systemic codes), src/app/api/quickbooks/syncLog/syncLog.service.ts (TOCTOU on priorStatus read) Important Files Changed
Sequence DiagramsequenceDiagram
participant WH as WebhookService / SyncService / PaymentService
participant SLS as SyncLogService
participant AIA as afterIfAvailable
participant SEN as SyncErrorNotifier
participant NS as NotificationService
participant CP as CopilotAPI
WH->>SLS: updateQBSyncLog({status: FAILED, errorCode: "6140"}, conditions, priorStatus)
SLS->>SLS: findFirst(priorStatus) if unknown
SLS->>SLS: db.update(QBSyncLog)
SLS->>SLS: scheduleFailureNotification(log) if PENDING→FAILED
SLS->>AIA: defer async callback
AIA-->>SEN: new SyncErrorNotifier(user).notify(log)
SEN->>SEN: getActionForErrorCode("6140") → QB_DUPLICATE_DOC_NUMBER
SEN->>SEN: suppress if 5010 + entityType != invoice
SEN->>SEN: buildContext(log)
SEN->>SEN: getPortalConnection(workspaceId)
SEN->>NS: sendNotificationToIU(senderId, action, context)
NS->>CP: getInternalUsers()
CP-->>NS: IU list
NS->>CP: createNotification(IU, inProduct, email)
|
| export type IntuitErrorType = { | ||
| Message: string | ||
| Detail: string | ||
| Code: string | ||
| Element?: string | ||
| code: string | ||
| element?: string |
There was a problem hiding this comment.
IntuitErrorType casing change may silently miss the fault code
The PR renames Code → code and Element → element on IntuitErrorType. If the actual QBO JSON Fault payload delivers these fields with their original casing (Code, Element), then (error.errors?.[0] as IntuitErrorType).code evaluates to undefined, Number(undefined) produces NaN, and NaN.toString() is stored as "NaN" in error_code — matching nothing in UserActionableErrorCodes and producing a silently broken notification path.
Since Message and Detail remain capitalised (matching QBO's documented schema), it is worth verifying with a real sandbox fault payload that QBO uses lowercase code / element before merging.
There was a problem hiding this comment.
Error payload from intuit has code and element in lowercase.
"Fault":
{
"Error": [
{
"Message": "<message>",
"Detail": "<detail>",
"code": "<code>",
"element": ""
}],
"type": "ValidationFault"
},
"time": "2017-09-29T09:17:31.892-07:00"
}```
| return await copilot.getInternalUsers() | ||
| default: | ||
| return null | ||
| if (IU_RECIPIENT_ACTIONS.has(action)) { |
There was a problem hiding this comment.
@SandipBajracharya I don't understand this change. Previously it'd have returned null for NotificationActions.QB_DUPLICATE_DOC_NUMBER , NotificationActions.QB_DUPLICATE_NAME, etc. Is this intentional?
There was a problem hiding this comment.
@priosshrsth we had not implemented NotificationActions.QB_DUPLICATE_DOC_NUMBER or any other actions before. This is recent change.
Greptile flagged that buildEntityReference unconditionally interpolated both action and id even when describeAction returned '' (entityType or eventType missing), producing "(during , ref INV-001)". Today's qb_sync_logs schema prevents this on the real path (entityType + eventType are NOT NULL), but the helper is callable from anywhere and a future caller passing partial context would surface broken copy. Conditional segment-building drops a missing dimension instead of emitting an empty placeholder; if both are absent the parenthetical is omitted entirely. Three regression tests added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Copilot payment.succeeded webhook triggers a Fee (Purchase entity) creation in QuickBooks — calling it "payment completion" in IU notifications is misleading. Special-case the (payment, succeeded) pair in describeAction so the reference clause reads "during invoice fees creation, ref PUR-…" instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QBO 6430 ("Invalid account type used") fires when a transaction
references an account whose AccountType doesn't match what QuickBooks
expects — e.g. a Purchase line citing a non-Expense account, or an
Item citing a non-Income account. Most plausible cause for us is an
account configured under settings (expense/income) being changed to
a different type in QuickBooks, or the wrong account picked at
setup.
Body explains the cause and points the IU at both remediation paths
(fix the account type in QuickBooks or change the selection in this
app's settings). Auto-coverage from the parameterized helper +
notifier tests picks up the new entry without manual additions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
When QBO returns an error caused by a manual change in QuickBooks that our app can't auto-resolve (e.g. 6140 duplicate doc number, 6210 closed accounting period, 6540 deposited transaction locked), surface an in-product + email notification to the workspace's IUs via the existing Copilot notification path. Until now these failures landed silently in
qb_sync_logswith no IU-facing surfacing.updateProductSyncToken/updateCustomerSyncTokenon the next 3-hour cron tick). 5010 on invoices remains user-actionable since the invoice flow uses cachedqbSyncTokendirectly with no equivalent refresh.error_codecolumn so IUs can filter past failures by QBO code.Notification copy
A supervisor-facing recap of every body string is in
docs/OUT-3528-notification-recap.md(local-only —docs/is gitignored). Awaiting product / supervisor sign-off on copy before merge.How it works
qb_sync_logs.error_code varchar(50). Zero-downtime migration.getMessageAndCodeFromErrorreturns the QBOFault.Error.code(e.g.6140) instead of the HTTP status when the error originates from QBO. Each FAILED-path call site (webhook, payment, sync resync) threads the code intocreateQBSyncLog/updateQBSyncLog.UserActionableErrorCodes: Record<string, NotificationActions>insrc/constant/intuitErrorCode.ts.NotificationCopyregistry insrc/app/api/notification/notification.helper.tsholds title + in-product body + email subject/header/body per action.IU_RECIPIENT_ACTIONSis derived from the registry keys.SyncErrorNotifieris fired fromSyncLogService.scheduleFailureNotificationviaafterIfAvailableso the request scope (and any enclosing transaction) commits first; notifier failures are captured to Sentry and never undo the sync log write.Commits
feat(OUT-3528): add error_code column to qb_sync_logsfeat(OUT-3528): plumb QBO error codes into sync log writesfeat(OUT-3528): IU notifications for user-actionable QBO errorsfeat(OUT-3528): dispatch sync-failure notifications via SyncErrorNotifiertest(OUT-3528): unit coverage for notifier and helperTest plan
yarn tsc --noEmitcleanyarn lint:checkno new errors / warnings on touched filesyarn vitest run --project unit— 94/94 passingvitest run --project integration) — needs Docker fortestcontainers/postgresql, deferred to CI / a Docker-capable envReviewer notes
entityType: items live in a separate QBO namespace from Customer/Vendor/Employee, so the item-flow copy doesn't claim cross-namespace collision.IU_RECIPIENT_ACTIONSset.🤖 Generated with Claude Code