README.md

# ExQuickbooks

ExQuickbooks is an Elixir client for the **QuickBooks Online Accounting API**.
It keeps the public API library-oriented and explicit: callers build a client,
run OAuth flows, use resource helpers, and handle expected failures through
`{:ok, result}` / `{:error, %ExQuickbooks.Error{}}` tuples.

## Installation

Add `ex_quickbooks` to your dependencies:

```elixir
def deps do
  [
    {:ex_quickbooks, "~> 0.8.0"}
  ]
end
```

## What the library covers

- OAuth 2 authorization URL generation, code exchange, and refresh
- a validated client struct for sandbox or production QuickBooks access
- shared request builders and a Req-based HTTP pipeline
- company bootstrap helpers and generic query support
- resource modules for customers, items, invoices, payments, accounts, and vendors
- CDC helpers for incremental synchronization
- typed errors for validation, auth, rate limiting, API faults, and network failures

## Sandbox setup

1. Create an app in the [Intuit Developer portal](https://developer.intuit.com/).
2. In **Keys & OAuth**, copy your **Client ID** and **Client Secret**.
3. Add a local redirect URI such as `http://localhost:4000/auth/quickbooks/callback`.
4. Open the sandbox company provided in your developer dashboard and note its `realmId`.
5. Use the `com.intuit.quickbooks.accounting` scope during OAuth.

Useful references:

- [Intuit OAuth 2.0 docs](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0)
- [Intuit sandbox docs](https://developer.intuit.com/app/developer/qbo/docs/develop/sandboxes)
- [QuickBooks Online quick start](https://developer.intuit.com/app/developer/qbo/docs/get-started/quick-start)

## OAuth usage

Generate the authorization URL:

```elixir
{:ok, authorization_url} =
  ExQuickbooks.Auth.authorization_url(
    client_id: "client-id",
    redirect_uri: "http://localhost:4000/auth/quickbooks/callback",
    state: "csrf-token"
  )
```

Exchange the callback code for tokens:

```elixir
{:ok, token} =
  ExQuickbooks.Auth.exchange_code(
    client_id: "client-id",
    client_secret: "client-secret",
    redirect_uri: "http://localhost:4000/auth/quickbooks/callback",
    code: "authorization-code",
    realm_id: "9130357992221046"
  )
```

Refresh tokens with the latest refresh token returned by Intuit:

```elixir
{:ok, refreshed_token} =
  ExQuickbooks.Auth.refresh_tokens(
    client_id: "client-id",
    client_secret: "client-secret",
    refresh_token: token.refresh_token,
    realm_id: token.realm_id
  )
```

## Basic client creation

Construct a client once you have tokens:

```elixir
{:ok, client} =
  ExQuickbooks.new(
    client_id: "client-id",
    client_secret: "client-secret",
    redirect_uri: "http://localhost:4000/auth/quickbooks/callback",
    realm_id: token.realm_id,
    access_token: token.access_token,
    refresh_token: token.refresh_token,
    environment: :sandbox,
    minor_version: 75
  )
```

You can use `ExQuickbooks.request_path/3` to inspect the company-scoped request
path that the shared HTTP pipeline will use:

```elixir
ExQuickbooks.request_path(client, ["customer"], query: [active: true])
#=> "/v3/company/9130357992221046/customer?active=true&minorversion=75"
```

## Company bootstrap and query helpers

Confirm the client can reach the target company:

```elixir
{:ok, company_info} = ExQuickbooks.CompanyInfo.get(client)
```

Run raw QuickBooks queries with optional pagination:

```elixir
{:ok, query_response} =
  ExQuickbooks.Query.run(
    client,
    "SELECT * FROM Customer",
    start_position: 1,
    max_results: 25
  )

{:ok, {"Customer", customers}} =
  ExQuickbooks.Query.top_level_collection(query_response)
```

## Customer and invoice flows

Fetch active customers:

```elixir
{:ok, customers} =
  ExQuickbooks.Customers.list(
    client,
    where: "Active = true",
    max_results: 25
  )
```

Create and update a customer:

```elixir
{:ok, created_customer} =
  ExQuickbooks.Customers.create(client, %{
    "DisplayName" => "Acme"
  })

{:ok, updated_customer} =
  ExQuickbooks.Customers.update(client, %{
    "Id" => created_customer["Id"],
    "SyncToken" => created_customer["SyncToken"],
    "DisplayName" => "Acme Updated"
  })
```

Create and update an invoice:

```elixir
{:ok, created_invoice} =
  ExQuickbooks.Invoices.create(client, %{
    "CustomerRef" => %{"value" => created_customer["Id"]},
    "Line" => [
      %{
        "Amount" => 100,
        "DetailType" => "SalesItemLineDetail"
      }
    ]
  })

{:ok, updated_invoice} =
  ExQuickbooks.Invoices.update(client, %{
    "Id" => created_invoice["Id"],
    "SyncToken" => created_invoice["SyncToken"],
    "PrivateNote" => "Updated through ExQuickbooks"
  })
```

The same `list/2`, `get/3`, `create/3`, and `update/3` pattern is available for:

- `ExQuickbooks.Items`
- `ExQuickbooks.Payments`
- `ExQuickbooks.Accounts`
- `ExQuickbooks.Vendors`

## CDC sync helpers

Fetch grouped changes since a checkpoint:

```elixir
{:ok, customer_changes} =
  ExQuickbooks.CDC.fetch(
    client,
    [:customer],
    "2026-04-20T00:00:00Z"
  )

{:ok, item_changes} =
  ExQuickbooks.CDC.fetch(
    client,
    [:item],
    "2026-04-20T00:00:00Z"
  )

{:ok, invoice_and_payment_changes} =
  ExQuickbooks.CDC.fetch(
    client,
    [:invoice, :payment],
    "2026-04-20T00:00:00Z"
  )
```

Each entity key maps to grouped records and deleted IDs:

```elixir
%{
  "Customer" => %{
    records: [%{"Id" => "123"}],
    deleted_ids: [%{"Type" => "Customer", "Id" => "456"}]
  }
}
```

## Error handling

The library returns typed `ExQuickbooks.Error` values for expected failures:

```elixir
case ExQuickbooks.Customers.get(client, "123") do
  {:ok, customer} ->
    {:ok, customer}

  {:error, %ExQuickbooks.Error{type: :not_found}} ->
    {:error, :missing_customer}

  {:error, %ExQuickbooks.Error{type: :rate_limited, details: %{"retry_after" => retry_after}}} ->
    {:error, {:retry_later, retry_after}}

  {:error, %ExQuickbooks.Error{type: :unauthorized}} ->
    {:error, :refresh_required}

  {:error, %ExQuickbooks.Error{} = error} ->
    {:error, error}
end
```

## Versioning strategy

ExQuickbooks is still pre-`1.0`, so versioning is conservative:

- additive endpoint and helper support bumps the **minor** version (`0.x`)
- bug fixes and documentation-only updates bump the **patch** version
- breaking API changes will use the next pre-`1.0` minor version, and then move
  to normal SemVer major releases once the package reaches `1.0`