Skip to content

fix(quickbooks): align the integration with Intuit's published API models - #7555

Merged
waleedlatif1 merged 21 commits into
stagingfrom
integration/quickbooks-doc-alignment
Sep 6, 2026
Merged

fix(quickbooks): align the integration with Intuit's published API models#7555
waleedlatif1 merged 21 commits into
stagingfrom
integration/quickbooks-doc-alignment

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Aligns the QuickBooks integration with Intuit's published API models across tools, block, triggers, webhook ingress, and OAuth
  • Fixes three defects that silently corrupted customer accounting records: replacing a payment's invoice allocations detached linked credit memos/expenses/checks/journal entries; a full item update round-tripped InvStartDate/QtyOnHand (which Intuit reads as an inventory adjustment on update); and an item account change rewrote every linked historical transaction (missing include=donotupdateaccountontxns)
  • Hardens the app-level webhook ingress: verifier tokens are now decrypted lazily and short-circuit on first match instead of fanning out over every connected company before the signature check, and one unmodelled event no longer 400s the whole batch (Intuit retries a 400 indefinitely and withholds later events, so that was a cross-workspace outage per app)
  • Gates the 12 internal provider operations behind the trusted-identity and input-size checks they previously bypassed, and contract-binds them
  • Stops persisting the Intuit OIDC identity token, which was projected into the credential payload of every QuickBooks tool call and read by none
  • Sales receipts and refund receipts no longer require a customer (Intuit's request models don't), zero amounts/quantities/unit prices are accepted, and documented length limits are enforced locally
  • Adds CurrencyRef and GlobalTaxCalculation to the create paths — both conditionally required, so every create previously failed on multicurrency-enabled and non-US companies
  • Adds ten missing report endpoints, the France-locale trial balance, date_macro, qzurl, and an employee filter; corrects report parameter entries that refused controls Intuit accepts
  • Adds sales receipt and bill payment void tools, plus address Line3Line5, Fault.type classification, AccountSubType validation, and the documented sparse refund-receipt update
  • Splits the dual-semantic transactionId subblock (live across 20 operations, so a by-ID read target survived an operation switch into an update) and registers saved-state migrations for it and four other retired ids

Type of Change

  • Bug fix

Testing

Tested manually. Full apps/sim suite passes (3,034 files / 42,143 tests), bun run type-check clean, all 45 audits green including docs:check, tool-metadata:check, check:api-validation, and the subblock ID stability check. Every claim was validated against Intuit's published model files before any change; 13 candidate findings were rejected with evidence rather than "fixed".

Known-unresolved, deliberately not changed

  • Paginated queries emit no ORDERBY, so paging can skip or duplicate rows. Id is sortable: false on all 21 entities these tools query and the sortable sets don't intersect, so a fix needs a per-entity ordering map and an accepted change in result order — a product decision, not a bug fix.
  • Whether BillPayment accepts Line: [] is genuinely ambiguous in Intuit's own models (keyed Line [0..n], flagged Required, described as "zero or more"). Behavior is unchanged and the ambiguity is recorded in a TSDoc note.
  • A vendor-change guard on bill payment updates is unverifiable from the published models; recommended to confirm against a sandbox realm before widening the shared full-update signature.

Behavior changes worth noting

  • The trigger's entityType output now emits the canonical entity name (Invoice) instead of the wire token (invoice). The old value could not be fed into the QuickBooks read tools it exists for, but any deployed workflow comparing against the lowercase value will stop matching.
  • A webhook app key with no configured verifier tokens now returns 401 instead of 404, removing an app-key enumeration oracle.
  • Refund receipt updates post a sparse body directly; a stale sync token now surfaces as an Intuit fault rather than a local message.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

🤖 Generated with Claude Code

https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

waleedlatif1 and others added 19 commits September 5, 2026 17:37
Three defects on the QuickBooks CloudEvents ingress:

- The pre-ack path loaded and decrypted every account connected to the
  addressed Intuit app before checking the signature. Verifier tokens are
  now produced by an async generator and consumed one at a time, stopping
  at the first match, so a legitimate delivery no longer burns the whole
  app's fan-out inside Intuit's 3-second acknowledgement budget.
- One unmodelled element rejected the entire delivery with 400. Intuit
  retries a 400 indefinitely and withholds later events until one is
  acknowledged, so a single bad payload stopped webhooks for every Sim
  workspace on that Intuit app. The array shape is still bounded, but
  elements are parsed individually, unparseable ones are dropped with a
  warning, and the delivery is acknowledged with 200.
- formatInput emitted the lowercase wire token ("invoice") as entityType.
  It now resolves the trigger definition from the parsed entity and emits
  the canonical QuickBooks name ("Invoice") the read tools expect;
  eventType still carries the raw wire string.

Also count an unroutable company id as ignored rather than failed, so a
permanently impossible event no longer retries three times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
… transaction history

Two of these silently rewrite a customer's accounting records.

An Item full update echoes the record read back from QuickBooks. Intuit
documents InvStartDate asymmetrically: "For read operations, the date returned
in this field is always the originally provided inventory start date. For
update operations, the date supplied is interpreted as the inventory adjust
date, is stored as such in the underlying data model, and is reflected in the
QuickBooks Online UI." QtyOnHand is re-asserted the same way. Both are
"Required for Inventory type items", so neither can simply be dropped from the
body — refuse the update instead, matching create_item's existing
Service/NonInventory restriction. Intuit also documents inactivation as "Not
valid for Category item types", so an Active change on a Category is refused
too.

The Item update also posted to a bare endpoint. Intuit: "Add the query
parameter, include=donotupdateaccountontxns, to the endpoint to supress
updating the income or expense account on any existing transactions associated
with this Item object." Without it, changing an item's account rewrote every
historical transaction linked to it. The parameter is documented on the Item
update alone, so it is not applied to any other entity.

Also aligns the shared plumbing with the documented model:

- PhysicalAddress documents Line1-Line5; the write map carried only Line1/Line2,
  so an address Sim had just read could not be written back.
- Fault.type (ValidationFault / SystemFault / AuthenticationFault /
  AuthorizationFault) was discarded, hiding the classification that separates a
  bad payload from a dead token. Matched by prefix because Intuit's pages
  disagree between "ValidationFault" and type="Validation".
- The query string used the form-encoded "+" for spaces; Intuit's own example
  percent-encodes them.
- MAXRESULTS was capped at 100 against a documented maximum of 1,000.
- Validates documented constraints locally: DisplayName <=500, Item.Name <=100
  with no tabs, new lines, or colons, and an email address Intuit can store.
- update_item's activeStatus and update_employee's displayName now carry the
  Category and Payroll caveats Intuit documents.

Adds the missing query-builder coverage and a full-update test that would catch
feeding the sanitized record into the merge, which would null every vendor's
TaxIdentifier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
… the provider ops

The QuickBooks internal handler returned for all twelve provider create/update
tool ids above both the operation input cap and the trusted-identity check, so
those gates only ever ran for the three file tool ids. Hoist both above the
switch, matching the Asana handler.

The same twelve operations had no boundary schema — executeToolOperationImplementation
only checks the input is a non-array object before casting to the operation's
param type. Author a contract per operation and route them through
executeInternalJsonToolOperation, the canonical in-process path. The two file
operations now parse through their contracts as well, which were previously
declared but unreferenced.

Also pass the transfer signal, not the caller's, when reading a failed
transaction-PDF response body, so a stalled Intuit error body stays bounded by
the 60s transfer deadline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…the webhook batch ceiling

The OIDC identity JWT is only meaningful at connection time, where profile.accountId
is already derived from it. Persisting it projected the token into the credential
payload of every QuickBooks tool call, none of which read it. Gating it in
token-resolution instead would break Shopify, which reads params.idToken as a shop
domain fallback.

Also exports QUICKBOOKS_WEBHOOK_MAX_EVENTS from the contract so the route no longer
carries a second copy of the batch ceiling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…nt links

buildPaymentLines returned only invoice LinkedTxn entries, and the
unapplyOmittedInvoices path sent that array as the payment's entire Line
collection. Intuit documents Payment Line.LinkedTxn.TxnType as one of
Expense, Check, CreditCardCredit, JournalEntry, CreditMemo or Invoice, and
an update as "send all the Lines that need to be present MINUS the lines
that need to be removed" — so replacing invoice allocations silently
detached every applied credit memo, expense, check and journal entry.
Non-invoice lines are now carried forward first, in the order QuickBooks
returned them, and counted against the payment total.

Also aligned with Intuit's documented model:
- salesreceiptrequest requires only Line, refundreceiptrequest only
  DepositToAccountRef and Line; neither lists CustomerRef, so customerId is
  optional on both receipt paths and CustomerRef is emitted only when given.
- Line.Amount, SalesItemLineDetail.Qty and UnitPrice carry no positivity or
  non-zero constraint, so zero is accepted (finite/2-decimal/safe-range
  checks unchanged).
- Enforce the documented maximum lengths locally: DocNumber 21,
  MemoRef.value 1000, Line.Description 4000.
- Add void_sales_receipt, the documented
  salesreceipt?operation=update&include=void sparse void.
- read_sales_transactions maxResults description now says 1–1000, matching
  validateQuickBooksPagination.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…model

Validated against Intuit's live machine-readable model files
(EntityJsonObject_v1.json, CodesModelsJsonObjects_v2.json).

- CurrencyRef is "Conditionally required" on all seven purchasing and
  accounting create models ("This must be defined if multicurrency is
  enabled for the company") and was never written, so every create failed
  on a multicurrency company. Creates now accept an ISO 4217 currencyCode.
- GlobalTaxCalculation is "Conditionally required" on Bill, VendorCredit,
  PurchaseOrder, JournalEntry, and Deposit, and Optional on Purchase
  ("Not applicable to US companies; required for non-US companies"). Those
  six creates now accept it; JournalEntry accepts only the two values
  Intuit documents for it. BillPayment has no such property and is left alone.
- Add void_bill_payment, the documented
  POST /billpayment?operation=update&include=void operation.
- itembasedexpenselinedetail.Required is [] and ItemRef is Optional, so an
  item line no longer requires itemId.
- The Deposit sparse update leaves "missing elements untouched", so
  depositAccountId is no longer forced on every update.
- BillPayment DocNumber and APAccountRef are Optional writable fields and
  were unreachable; PurchaseOrder DueDate was emitted by the body builder
  but had no parameter.
- Correct the maxResults range in the two read tools to 1-1000, matching
  validateQuickBooksPagination.
- Record the unresolved BillPayment "Line [0..n]" / requiredFlag Required
  contradiction as a TSDoc note; behavior unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…tomerId types

The sparse guard's TSDoc claimed Intuit documents `sparse` as required to void
any object. That is false for Invoice, whose void request model is
`deleterequest` (Id + SyncToken, no sparse); only the `include=void` form
carries it. The code was already correct; the comment would have led an editor
to 'fix' void_invoice.ts into breaking it.

Also narrows QuickBooksCreateSalesReceiptParams.customerId to optional so the
type matches the required:false declaration, per salesreceiptrequest.Required
listing only Line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…atalog

Every capability flag is now transcribed from that report's own `*query` model
in Intuit's machine-readable report catalog, which is the source the developer
docs render their parameter tables from.

Reports:
- ap_aging_detail now advertises accountingMethod; `agedpayabledetailquery`
  documents `accounting_method`, so Sim was throwing on a request Intuit accepts.
- ap_aging_summary now accepts customerId; `agedpayablesquery` documents `customer`.
- Adds trial_balance_fr. Intuit documents one report with two endpoints —
  TrialBalanceFR for FR-locale companies, TrialBalance otherwise.
- Adds the ten remaining documented report endpoints: AccountList,
  CustomerBalanceDetail, CustomerIncome, GeneralLedger, InventoryValuationDetail,
  InventoryValuationSummary, ClassSales, DepartmentSales, TaxSummary,
  VendorBalanceDetail. Each one's flags come from its own query model.
- Exposes `date_macro` and `qzurl`, both documented per-report query params.
  `qzurl` is what populates the quick-zoom `href` links the row outputs already
  declare, and `date_macro` is mutually exclusive with an explicit date range.
- Exposes the `employee` filter that only `profitandlossdetailquery` documents,
  and adds the `Employee` report-header echo Intuit's `reportheader` model lists.

Attachments:
- parseQuickBooksAttachableResponse takes an operation label. Reading an
  attachment by id reported failures as "attachment upload failed".
- A dotless file name no longer reports itself as its own extension, so an
  unattachable `backup` is refused as extensionless rather than as "the backup
  file type".
- Records why `.jpg` is canonicalized to image/jpeg: QuickBooks normalizes
  content type on ingest and `attachablerequest` has no ContentType property.

Replaces the file_operations tool-wiring tests, which asserted only that
`operation.input` is an identity projection, with coverage of the extension
allowlist, MIME canonicalization, file-name sanitization, Attachable metadata,
and fault labelling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…fund receipt update

Intuit's BillPaymentCheck.BankAccountRef requires "Account.AccountType set to
Bank and Account.AccountSubType set to Checking", and
BillPaymentCreditCard.CCAccountRef requires "AccountType set to Credit Card and
AccountSubType set to CreditCard". The create-bill-payment guard only compared
AccountType, so a Savings or Line-of-Credit account passed the local check and
failed at Intuit.

Intuit documents RefundReceipt::UPDATE "Sparse update a refund receipt", which
"only elements specified in the request are updated. Missing elements are left
untouched." The operation used the read-merge-write full update instead, adding
a round trip and a read/write race. Invoice, Estimate and SalesReceipt already
post their sparse bodies directly; RefundReceipt now matches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
The update path now posts a sparse body directly instead of read-merge-write,
per RefundReceipt::UPDATE 'Sparse update a refund receipt'. The LLM-facing tool
description still claimed a full update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…nges

Registers the two Wave 2 void tools, feeds the create parameters that had no
UI, and corrects the block-level and report-metadata defects, all against
Intuit's published request/response and report query models.

Registration
- Export and register quickbooks_void_sales_receipt and
  quickbooks_void_bill_payment; both were reachable from no surface.
- Mirror the void_customer_payment sites: operation option, canvas sentence,
  transaction ID / sync token / confirm conditions, tools.access, and params.

Parameters that existed on tools but had no UI
- currencyCode on every create whose request model marks CurrencyRef
  conditionally required under multicurrency.
- globalTaxCalculation on the same set minus bill payment, which
  billpaymentresponse does not carry; JournalEntry narrows to the two values
  its model documents.
- dueDate on create and update purchase order, apAccountId and documentNumber
  on create bill payment.
- Report date macro, quick-zoom links, and the employee filter; the quick-zoom
  string is coerced in tools.config.params, never in tools.config.tool.
- Report dropdown now offers all 26 documented reports.

Block defects
- Sales receipt and refund receipt no longer require a customer; Intuit's
  salesreceiptrequest and refundreceiptrequest do not list CustomerRef.
- Pagination accepts the documented ceiling of 1000, not 100.
- The attachment file name no longer means two opposite things: the upload
  override is scoped to Add Attachment in File mode like its siblings, and the
  saved-file name gets its own field.
- Item account fields explain the locale rule instead of a bare placeholder,
  and the inconsistent advanced/basic pairs are aligned.

Report metadata
- summarize_column_by collapses to the single twelve-value list every one of
  the fourteen documenting models shares, Employees included.
- appaid, arpaid, and group_by are gated per report: the customer and vendor
  balance models and inventoryvaluationdetailquery document them too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
…he missing subblock migrations

`transactionId` was one control across the three by-ID reads and all fourteen
updates and voids, and `tools.config.params` republished that single stored
value as the read target, `paymentId`, `billId`, `purchaseOrderId`,
`journalEntryId` and the rest. Because subblock values are keyed by ID and are
never cleared when the operation changes, a bill ID read under Read Purchasing
Transactions survived a switch to Update Purchase Order and addressed the wrong
entity while the block still validated. The read path moves to
`readTransactionId` and `transactionId` keeps the mutations.

Registers the operation-scoped migration for that rename plus the four fields an
earlier change orphaned without one: the three `summarize_column_by` subsets
that collapsed into `reportSummarizeBy`, and the download-side
`attachmentFileName` that moved to `downloadAttachmentFileName`.

`syncToken` is left alone: it is live only across updates and voids, never
across the read/mutate boundary, and carries one value space. A stale token
after an operation switch is rejected by QuickBooks rather than silently
targeting the wrong entity.

Regenerates the integration docs, tool metadata, and integration catalog, which
were already stale on this branch from the Wave 1-3 tool description changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 6, 2026 9:34am UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Aligns the QuickBooks integration with Intuit’s published API models and strengthens its contracts, webhook handling, OAuth credential storage, accounting mutations, reports, and workflow migrations.

  • Preserves linked accounting data during updates and avoids unintended inventory or historical-transaction adjustments.
  • Adds missing create, update, void, report, tax, currency, and filtering capabilities.
  • Hardens webhook verification and tolerates individually unmodelled events without rejecting an entire delivery.
  • Extends contract/tool parameter-parity coverage to all JSON and file operations.
  • Migrates retired or dual-purpose workflow subblock identifiers to their replacements.

Confidence Score: 5/5

The PR appears safe to merge, with no outstanding correctness, security, or repository-rule violations identified.

Both previous findings are resolved: the omitted bill-payment and purchase-order contract fields remain declared, and the parity regression test now covers all three file operations using schema introspection compatible with the repository’s Zod version. No new actionable issue was found in the changes since the previous review.

Important Files Changed

Filename Overview
apps/sim/app/api/webhooks/quickbooks/[appKey]/route.ts Streams verifier tokens for signature checking and acknowledges valid deliveries while dropping only unmodelled events.
apps/sim/lib/webhooks/quickbooks-credentials.ts Lazily resolves and decrypts QuickBooks webhook verifier tokens by application key.
apps/sim/lib/api/contracts/tools/quickbooks.ts Expands and bounds QuickBooks operation contracts to match the exposed tool parameters.
apps/sim/tools/quickbooks/accounting_utils.ts Updates shared accounting request construction to preserve native records and support corrected Intuit fields.
apps/sim/blocks/blocks/quickbooks.ts Aligns block inputs, conditions, operation metadata, report controls, and transaction identifiers with the updated tools.
apps/sim/lib/workflows/migrations/subblock-migrations.ts Registers migrations for retired and split QuickBooks subblock identifiers.
apps/sim/lib/internal/quickbooks/contract-param-parity.test.ts Extends contract/tool parameter-parity coverage from twelve JSON operations to all three file operations.

Sequence Diagram

sequenceDiagram
  participant Intuit
  participant Route as QuickBooks webhook route
  participant Credentials as Verifier token stream
  participant Queue as Ingress queue
  participant Worker as Webhook ingress worker
  participant Workflows
  Intuit->>Route: Signed event batch
  Route->>Credentials: Stream verifier tokens
  Credentials-->>Route: Tokens until signature matches
  Route->>Route: Parse and retain modelled events
  alt At least one modelled event
    Route->>Queue: Enqueue authenticated batch
    Queue->>Worker: Execute ingress job
    Worker->>Worker: Build company routing key
    Worker->>Workflows: Dispatch to matching targets
  else All events are unmodelled
    Route-->>Intuit: 200 acknowledgment
  end
  Route-->>Intuit: 200 acknowledgment
Loading

Reviews (3): Last reviewed commit: "test(quickbooks): extend contract parity..." | Re-trigger Greptile

Comment thread apps/sim/lib/api/contracts/tools/quickbooks.ts
…he contracts dropped

A contract body is a Zod object, so any key it does not declare is stripped
before the provider operation runs - silently, with no validation error. The
contracts were authored before currencyCode/apAccountId/documentNumber were
added to Create Bill Payment and dueDate to Update Purchase Order, so those
params were dead: the block forwarded them and they never reached Intuit.

Adds a parity test across all twelve contract-bound operations so a param added
to a tool without its contract fails instead of silently disappearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/internal/quickbooks/contract-param-parity.test.ts
The download body is a discriminated union and the add-attachment body carries a
superRefine, so neither exposes a flat shape - but their declared keys are still
introspectable, so all three file tools are now held to the same parity rule as
the twelve JSON operations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@waleedlatif1
waleedlatif1 merged commit 0049514 into staging Sep 6, 2026
31 checks passed
@waleedlatif1
waleedlatif1 deleted the integration/quickbooks-doc-alignment branch September 6, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant