Skip to content

Latest commit

 

History

History
182 lines (153 loc) · 9.86 KB

File metadata and controls

182 lines (153 loc) · 9.86 KB

CLAUDE.md — Agent & LLM Context for quickbooks-cli

This file provides context for AI agents, LLMs, and developers working on this codebase.

Project Overview

quickbooks-cli (binary: qb) is a Rust CLI for the QuickBooks Online Accounting API v3. It covers 36 entity types, 25+ financial reports, OAuth2 PKCE authentication, SQLite response caching, and rate limiting.

Reference implementation: This project mirrors the architecture of osodevops/xero-cli — the team's Xero CLI.

Build & Test

cargo build                     # Build
cargo test                      # Run all tests (74 tests)
cargo clippy -- -D warnings     # Lint (must pass with zero warnings)
cargo fmt --check               # Format check
cargo run -- --help             # Run CLI
cargo run -- customers list --help  # Specific command help

The project compiles with zero warnings under clippy -D warnings.

Architecture & Code Organization

src/
├── main.rs                    # Entry point (~50 lines): Cli::parse(), tracing init, GlobalArgs, dispatch()
├── lib.rs                     # pub mod declarations + VERSION const (with embedded git hash)
├── error.rs                   # QBError enum (thiserror + miette Diagnostic), exit codes 0-10
│
├── cli/                       # CLI layer — one file per entity
│   ├── mod.rs                 # Cli struct (Parser), GlobalArgs, Commands enum (43 variants), dispatch()
│   ├── common.rs              # build_client(): config → auth → rate limiter → QBClient → CachedClient
│   ├── auth.rs                # Login, Status, Refresh, Logout subcommands
│   ├── reports.rs             # qb reports run <type> with 25+ report types
│   ├── query.rs               # qb query run "<SQL>" with auto-pagination
│   ├── cache.rs               # qb cache clear/stats
│   ├── config.rs              # qb config init/show/set/path
│   ├── completions.rs         # Shell completions (bash/zsh/fish) + man pages
│   └── {entity}.rs            # 36 entity files (customers.rs, invoices.rs, etc.)
│
├── api/                       # API client layer
│   ├── client.rs              # QBClient: reqwest wrapper with retry, rate limit, bearer token,
│   │                          #   ?minorversion=75, prod/sandbox URLs, Fault error parsing,
│   │                          #   intuit_tid logging, SyncToken stale object detection
│   ├── pagination.rs          # STARTPOSITION-based pagination (QBO-specific, 1-indexed)
│   └── endpoints/             # One file per entity + shared helpers
│       ├── common.rs          # extract_entity(), extract_query_entities(), build_query()
│       ├── reports.rs         # run_report(), REPORT_TYPES constant, resolve_report_name()
│       └── {entity}.rs        # 36 entity endpoint files with Filters struct + CRUD functions
│
├── auth/                      # Authentication layer
│   ├── mod.rs                 # TokenSet struct, Environment enum, ensure_authenticated()
│   ├── pkce.rs                # OAuth2 PKCE browser flow + manual flow for headless servers
│   ├── refresh.rs             # Token refresh with rotation handling + fs2 file lock for concurrency
│   └── token_store.rs         # OS keyring storage (Apple/Windows/Linux native) with file fallback
│
├── cache/                     # SQLite caching layer
│   ├── mod.rs                 # CachedClient middleware: caches GETs, invalidates on writes
│   └── store.rs               # CacheStore: SQLite via rusqlite (bundled), TTL eviction, stats
│
├── config/                    # Configuration
│   ├── mod.rs                 # AppConfig::load(), save(), default paths
│   ├── file.rs                # ConfigFile TOML schema (5 sections, all with defaults)
│   └── profiles.rs            # Profile struct (realm_id, environment, company_name)
│
├── models/                    # Serde data models — one file per entity
│   ├── common.rs              # Shared types: Ref, MetaData, Address, PhoneNumber, EmailAddress,
│   │                          #   WebAddress, Line, LinkedTxn, QueryResponse
│   ├── report.rs              # Report model + flatten_report() + flatten_general_ledger()
│   └── {entity}.rs            # 36 entity model files (all with #[serde(flatten)] extra field)
│
├── output/                    # Output formatting
│   ├── mod.rs                 # OutputFormat enum (Table/Json/Csv/Yaml), Tabular trait, render()
│   ├── table.rs               # comfy-table rendering
│   ├── json.rs                # JSON pretty/compact
│   ├── csv.rs                 # CSV via csv crate
│   └── yaml.rs                # YAML via serde_yaml
│
└── rate_limit/                # Rate limiting
    ├── limiter.rs             # Sliding window (500/min) + Semaphore (10 concurrent)
    ├── backoff.rs             # Exponential backoff with jitter (1s initial, 60s max, 3 retries)
    └── budget.rs              # Per-minute budget tracking with auto-reset

Key Patterns

Entity Pattern (repeat for all 36 entities)

Each entity has exactly 3 files:

  1. Model (src/models/{entity}.rs):

    • #[derive(Debug, Clone, Serialize, Deserialize)] struct
    • All fields are Option<T> with #[serde(rename = "QBOFieldName")]
    • Uses rust_decimal::Decimal for monetary amounts
    • Last field: #[serde(flatten)] pub extra: HashMap<String, serde_json::Value>
  2. Endpoint (src/api/endpoints/{entity}.rs):

    • {Entity}Filters struct with where_clause, order_by, max_results, start_position
    • Functions: list(), list_all(), get(), create(), update(), delete()
    • Uses build_query() for QBO SQL and extract_entity()/extract_query_entities() for response parsing
    • List entities: soft delete (POST with Active=false, sparse=true)
    • Transaction entities: hard delete (POST to {entity}?operation=delete)
  3. CLI (src/cli/{entity}.rs):

    • {Entity}Commands enum with List, Get, Create, Update, Delete subcommands
    • impl Tabular for {Entity} with headers() and row() for table output
    • execute() async function: builds client, dispatches to endpoint, renders output
    • List uses --where and --order-by flags, checks global.all_pages
    • Create supports --file and inline flags with --dry-run
    • Update auto-fetches SyncToken before sending
    • Delete requires --confirm

Adding a New Entity

  1. Create src/models/{entity}.rs with serde struct
  2. Create src/api/endpoints/{entity}.rs with CRUD functions
  3. Create src/cli/{entity}.rs with Commands enum + Tabular impl + execute()
  4. Add pub mod {entity}; to src/models/mod.rs, src/api/endpoints/mod.rs, src/cli/mod.rs
  5. Add Commands variant + dispatch arm in src/cli/mod.rs

QBO API Response Formats

  • Single entity read (GET /customer/{id}): {"Customer": {...}}
  • Query/list (GET /query?query=SELECT * FROM Customer): {"QueryResponse": {"Customer": [...], "startPosition": 1, "maxResults": 100}}
  • Create/Update (POST): {"Customer": {...}} (returns entity)
  • Delete (POST ?operation=delete): minimal response
  • Errors: {"Fault": {"Error": [{"Message": "...", "Detail": "...", "code": "..."}], "type": "ValidationFault"}}
  • Reports (GET /reports/ProfitAndLoss): {"Header": {...}, "Columns": {...}, "Rows": {"Row": [...]}}

QBO-Specific Behaviors

  • SyncToken: Required for every update/delete. Changes on each mutation. Auto-fetched by the CLI.
  • Token Rotation: QBO refresh tokens change on every use. Must persist new token immediately. Protected by fs2 file lock.
  • Pagination: Uses STARTPOSITION (1-indexed) and MAXRESULTS in the query string, not page-based.
  • Minor Version: ?minorversion=75 appended to all requests.
  • Base URLs: Production quickbooks.api.intuit.com, Sandbox sandbox-quickbooks.api.intuit.com
  • Realm ID: Company identifier, part of the URL path: /v3/company/{realmId}/

Configuration Defaults

Setting Default Notes
Output format table Auto-switches to JSON when piped
Page size 100 QBO max is 1000
Minor version 75 Current QBO API version
Cache list TTL 300s 5 minutes
Cache get TTL 900s 15 minutes
Requests/min 500 QBO limit
Max concurrent 10 QBO limit
Retry max 3 With exponential backoff
Callback port 8844 For OAuth PKCE flow

Error Handling

All errors use thiserror + miette::Diagnostic with:

  • Human-readable messages
  • Error codes (qb::auth, qb::api, etc.)
  • Help text with actionable suggestions
  • Structured exit codes (0-10) for scripting

Dependencies (Key)

  • tokio — async runtime
  • reqwest (rustls-tls) — HTTP client, no OpenSSL
  • clap 4 (derive) — CLI parsing with env var support
  • serde / serde_json / toml / csv / serde_yaml — serialization
  • oauth2 — OAuth2 PKCE flow
  • keyring — OS-native credential storage
  • rusqlite (bundled) — SQLite cache, no system dependency
  • comfy-table — table output
  • thiserror + miette — error handling with diagnostics
  • rust_decimal — financial arithmetic
  • fs2 — cross-platform file locks (token refresh safety)

Testing

  • 87 unit tests covering: error types, config parsing, output formats, model deserialization, rate limiting, backoff, budget tracking, cache operations, query parsing, report flattening, URL construction, fault parsing
  • Tests use tempfile for isolated cache/token tests
  • Async tests use #[tokio::test]
  • No integration tests against live QBO API yet (use wiremock for future tests)