OUT-3742: tighten QBO Fault handling + bind SQL projections to row schemas - #251
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes two related bugs:
Confidence Score: 4/5Safe to merge with one fix: QBFaultSchema strict array check silently drops faults whose Error field is null or a non-array object. The core fault-handling path now silently swallows a QBO Fault response whenever Fault.Error is null or arrives as a non-array shape — safeParse returns false and assertNotQBFault returns without throwing. The old schema (z.unknown().optional()) always succeeded and always threw. Replacing .optional().default([]) with .catch([]) closes the gap. src/type/dto/intuitAPI.dto.ts — the QBFaultSchema.Fault.Error definition. Important Files Changed
Sequence DiagramsequenceDiagram
participant QBO as QBO API
participant IA as IntuitAPI
participant WEH as withErrorHandler
participant Log as qb_sync_logs
QBO->>IA: "Fault { Error: [{code: 6000, ...}] }"
IA->>IA: "assertNotQBFault()<br/>QBFaultSchema.safeParse()<br/>errors[0].code to 6000"
IA->>WEH: throw APIError(6000, msg, errors)
WEH->>WEH: "getMessageAndCodeFromError()<br/>code = 6000, source = intuit"
WEH->>WEH: "getCategory(code=6000) to ACCOUNT"
WEH->>WEH: "httpStatusOut = clamp(6000) to 400"
WEH->>Log: "FAILED, category=ACCOUNT"
WEH-->>QBO: "HTTP 400 { error, errors }"
Reviews (4): Last reviewed commit: "chore(OUT-3742): switch Fault.code to tr..." | Re-trigger Greptile |
94e4704 to
aad2d0d
Compare
QBO returns Fault.Error as a typed array (`[{ code, Message, Detail, element }]`).
The old code only read `code` from the non-array branch, so APIError.status always
fell back to 400 and the real fault code (e.g. 6240) survived only on errors[].
Tightens QBFaultSchema, simplifies assertNotQBFault to use errors[0]?.code with
a finite-check fallback, and clamps non-HTTP statuses in withErrorHandler before
NextResponse.json. Also guards getMessageAndCodeFromError against an empty
errors[] and surfaces a clearer failure when a non-voided invoice delete is
requested.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces 8 hand-written SELECT projections in src/utils/intuitAPI.ts with 4 typed constants (QB_INVOICE_COLUMNS / QB_ITEM_COLUMNS / QB_CUSTOMER_COLUMNS / QB_ACCOUNT_COLUMNS), each `as const satisfies ReadonlyArray<keyof <RowType>>`. A typo or stale field name fails tsc; a regression test asserts every schema-required key stays in the constant so a drop can't ZodError at runtime on response parse. Out of scope: resolveUniqueCustomerName (DisplayName-only queries against Vendor/Employee that don't have shared row schemas) and _getCompanyInfo (SELECT * with a single-field schema — no drift risk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`z.coerce.number()` runs `Number(value)` then validates the result; Zod's number validator rejects NaN. So a Fault payload with a non-numeric `code` (e.g. "abc") would fail QBFaultSchema.safeParse and assertNotQBFault would silently return — the fault is dropped and the caller never throws. The prior comment claimed assertNotQBFault treats NaN as "no usable code", but the NaN never reached it. Wrap the code field with .catch(NaN) so a non-numeric value yields NaN instead of a parse failure. assertNotQBFault's existing Number.isFinite check handles NaN and falls back to BAD_REQUEST. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old comment claimed the schema "coerces" and assertNotQBFault treats NaN as no-code. Pre-.catch(NaN), Zod's number validator rejected NaN before it ever reached assertNotQBFault. Reflect what's actually true now: missing code short-circuits via .optional(); non-numeric code yields NaN via .catch(NaN); either way Number.isFinite fails and BAD_REQUEST wins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…est titles - Schema: replace .coerce.number().catch(NaN).optional() with a transform that returns number-or-undefined. Output type is `number | undefined`, no NaN, no parse failure on malformed codes. - assertNotQBFault: simplify the code check now that the schema can't yield NaN. - Trim verbose comments across this branch's diff (clamp comment, schema comments, isQBODuplicateDocNumberError JSDoc, test-file comments) and refresh the stale handleInvoiceDeleted comment. - Rewrite test it() titles in intuitAPI.responses.test.ts and intuitAPI.queryColumns.test.ts as plain English instead of programmer notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1365d4a to
b5c3e58
Compare
Problem
assertNotQBFaultonly readcodeon the non-array branch, soAPIError.statuswas always 400 for real QBO faults.Shipped
QBFaultSchema; simplifiedassertNotQBFaultto readerrors[0]?.code. Clamp non-HTTP statuses inwithErrorHandler.QB_INVOICE_COLUMNS,QB_ITEM_COLUMNS,QB_CUSTOMER_COLUMNS,QB_ACCOUNT_COLUMNS) bind each SELECT to its row schema viasatisfies. Regression test guards required-key supersets.handleInvoiceDeletedthrows on non-voided delete;getMessageAndCodeFromErrorguards an emptyerrors[].Code sites
src/type/dto/intuitAPI.dto.tssrc/utils/intuitAPI.tssrc/app/api/core/utils/withErrorHandler.tssrc/app/api/quickbooks/invoice/invoice.service.tssrc/utils/error.tstest/unit/utils/intuitAPI.{responses,queryColumns}.test.tsBehavior change
qb_sync_logs.failed_record_categoryfor live 6000/6190 faults now resolves toACCOUNT(wasQB_API_ERROR) becausegetCategoryreads the real status. HTTP response still 400.