Skip to content

feat: add pdf command to save transactions as QuickBooks-rendered PDFs - #15

Open
robschaerer wants to merge 2 commits into
voska:mainfrom
robschaerer:pdf-command
Open

robschaerer wants to merge 2 commits into
voska:mainfrom
robschaerer:pdf-command

Conversation

@robschaerer

@robschaerer robschaerer commented Sep 25, 2026 •

Copy link
Copy Markdown

What

Adds qbo pdf <entity> <id> [-o path], which saves a transaction as the PDF the QuickBooks API renders for it.

qbo pdf invoice 123 -o invoice-123.pdf

Note: the API rendering is not always identical to what the QuickBooks web UI prints or emails. On a real invoice, the API combined three lines that share a description into one summed line, while the PDF downloaded from the UI lists them separately. The same bytes come back at every minorversion, so this is QuickBooks' renderer, not a request option. The docs say so.

Why

The /v3/company/{realm}/{entity}/{id}/pdf endpoint is already reachable through qbo get invoice "123/pdf", but every request the client makes sends Accept: application/json, and QuickBooks answers 500 System Failure Error: No match for accept header. So there was no way to get a PDF out of the CLI.

How

  • api.Client.PDF sends Accept: application/pdf through the same OAuth client as every other call, so it uses the existing token refresh and error mapping (mapHTTPError).
  • Limited to the entities QuickBooks renders: invoice, estimate, salesreceipt, creditmemo, purchaseorder. Others are rejected with exit 2.
  • The id must be numeric, because it is interpolated into the URL path and a document number passed by mistake would silently fetch a different transaction.
  • A 200 whose body does not start with %PDF is refused rather than saved under a .pdf name.
  • --dry-run prints the request and returns before any network call.
  • Output follows the existing download command: hint on stderr, {entity, id, path, bytes} on stdout.
  • Docs: skills/qbo/SKILL.md and references/COMMANDS.md.

Testing

  • go test ./... passes. New tests cover the Accept header and path, rejection of a non-PDF 200 body, HTTP error mapping, entity support, input validation, and dry-run without network.
  • Checked against a production company: qbo pdf invoice <id> returned a 45 KB single-page PDF with the correct header, customer, lines and total (lines with identical descriptions combined, as noted above).

🤖 Generated with Claude Code

https://claude.ai/code/session_019jpxEQMLnxu4s6zSfHq8xR

robschaerer and others added 2 commits September 25, 2026 16:34
`qbo pdf <entity> <id> [-o path]` fetches GET /{entity}/{id}/pdf with
Accept: application/pdf. The endpoint is reachable through `qbo get`,
but every request there sends Accept: application/json and QuickBooks
answers 500 "No match for accept header".

Supported for invoice, estimate, salesreceipt, creditmemo and
purchaseorder. The id must be numeric (it is interpolated into the
path), and a 200 whose body is not a PDF is refused rather than saved.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019jpxEQMLnxu4s6zSfHq8xR
Compared against a PDF downloaded from the QuickBooks web UI: the API
rendering combines invoice lines that share a description into one
summed line, where the UI lists them separately. The API returns the
same bytes at every minorversion, so it is the renderer, not a request
option. Say so instead of claiming it is the customer's document.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019jpxEQMLnxu4s6zSfHq8xR
@voska

voska commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Thank you for this, and sorry for the wait. qbo pdf fills a real gap: the generic get path can't reach the PDF endpoint because of the Accept header, and a thin wrapper over the API is what this project is for. The direction is accepted.

I read the whole diff. It looks well considered: it goes through the existing OAuth client and error mapping, validates the entity and numeric Id before building the URL, caps the response size, refuses a 200 that isn't a %PDF body, returns before creating a client on --dry-run, and mirrors download for output and overwrite behavior. The tests cover the matching cases, and the note about QuickBooks rendering lines differently from the web UI is useful and belongs in the docs. I haven't run the code.

What's needed before it can be merged:

  1. Rebase onto current main. GitHub now reports the branch as conflicting (the base has moved since you opened it). The conflict is most likely in internal/cmd/root.go or the skill docs. Please keep the change as it is and just resolve it. A new push means I'll re-read the updated diff.
  2. CI. The latest workflow run recorded for your head was in action_required state (GitHub holds first-time contributors' runs for approval). I can't approve it. It should run after your rebase push or when a repo maintainer approves it, and a green run on the final head is needed before a merge.
  3. Owner review. The PR edits skills/qbo/SKILL.md, which agents load as instructions, so the project owner has to sign off on that file before anything merges. I can't promise when.

Nothing else is blocking from my side. Thanks again for the careful work and for documenting the behavior you saw.

— OSS Maintainer (AI agent for @voska)

This branch has not been deployed

No deployments
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.

2 participants