xero-kiwi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +2 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +89 -0
  6. data/Rakefile +89 -0
  7. data/docs/accounting/address.md +54 -0
  8. data/docs/accounting/branding-theme.md +92 -0
  9. data/docs/accounting/contact-group.md +91 -0
  10. data/docs/accounting/contact.md +166 -0
  11. data/docs/accounting/credit-note.md +97 -0
  12. data/docs/accounting/external-link.md +33 -0
  13. data/docs/accounting/invoice.md +134 -0
  14. data/docs/accounting/organisation.md +119 -0
  15. data/docs/accounting/overpayment.md +94 -0
  16. data/docs/accounting/payment-terms.md +58 -0
  17. data/docs/accounting/payment.md +99 -0
  18. data/docs/accounting/phone.md +45 -0
  19. data/docs/accounting/prepayment.md +111 -0
  20. data/docs/accounting/user.md +109 -0
  21. data/docs/client.md +174 -0
  22. data/docs/connections.md +166 -0
  23. data/docs/errors.md +271 -0
  24. data/docs/getting-started.md +138 -0
  25. data/docs/oauth.md +508 -0
  26. data/docs/retries-and-rate-limits.md +224 -0
  27. data/docs/tokens.md +339 -0
  28. data/lib/xero_kiwi/accounting/address.rb +58 -0
  29. data/lib/xero_kiwi/accounting/allocation.rb +66 -0
  30. data/lib/xero_kiwi/accounting/branding_theme.rb +76 -0
  31. data/lib/xero_kiwi/accounting/contact.rb +153 -0
  32. data/lib/xero_kiwi/accounting/contact_group.rb +57 -0
  33. data/lib/xero_kiwi/accounting/contact_person.rb +45 -0
  34. data/lib/xero_kiwi/accounting/credit_note.rb +115 -0
  35. data/lib/xero_kiwi/accounting/external_link.rb +38 -0
  36. data/lib/xero_kiwi/accounting/invoice.rb +142 -0
  37. data/lib/xero_kiwi/accounting/line_item.rb +64 -0
  38. data/lib/xero_kiwi/accounting/organisation.rb +138 -0
  39. data/lib/xero_kiwi/accounting/overpayment.rb +107 -0
  40. data/lib/xero_kiwi/accounting/payment.rb +105 -0
  41. data/lib/xero_kiwi/accounting/payment_terms.rb +77 -0
  42. data/lib/xero_kiwi/accounting/phone.rb +46 -0
  43. data/lib/xero_kiwi/accounting/prepayment.rb +109 -0
  44. data/lib/xero_kiwi/accounting/tracking_category.rb +42 -0
  45. data/lib/xero_kiwi/accounting/user.rb +80 -0
  46. data/lib/xero_kiwi/client.rb +576 -0
  47. data/lib/xero_kiwi/connection.rb +78 -0
  48. data/lib/xero_kiwi/errors.rb +34 -0
  49. data/lib/xero_kiwi/identity.rb +40 -0
  50. data/lib/xero_kiwi/oauth/id_token.rb +102 -0
  51. data/lib/xero_kiwi/oauth/pkce.rb +51 -0
  52. data/lib/xero_kiwi/oauth.rb +232 -0
  53. data/lib/xero_kiwi/token.rb +99 -0
  54. data/lib/xero_kiwi/token_refresher.rb +53 -0
  55. data/lib/xero_kiwi/version.rb +5 -0
  56. data/lib/xero_kiwi.rb +33 -0
  57. data/llms-full.txt +3351 -0
  58. data/llms.txt +56 -0
  59. data/sig/xero_kiwi.rbs +4 -0
  60. metadata +164 -0
@@ -0,0 +1,111 @@
1
+ # Prepayments
2
+
3
+ A Xero **prepayment** is a payment received or made in advance of an invoice.
4
+ Prepayments are created via the BankTransactions endpoint and refunded via the
5
+ Payments endpoint. This resource lets you retrieve prepayments and their
6
+ allocations. You need a `tenant_id` from a [connection](../connections.md)
7
+ before you can fetch prepayments.
8
+
9
+ > See: [Xero docs — Prepayments](https://developer.xero.com/documentation/api/accounting/prepayments)
10
+
11
+ ## Listing prepayments
12
+
13
+ ```ruby
14
+ client = XeroKiwi::Client.new(access_token: "ya29...")
15
+
16
+ # Pass a tenant ID string…
17
+ prepayments = client.prepayments("70784a63-d24b-46a9-a4db-0e70a274b056")
18
+
19
+ # …or a XeroKiwi::Connection (its tenant_id is used automatically).
20
+ connection = client.connections.first
21
+ prepayments = client.prepayments(connection)
22
+ ```
23
+
24
+ `client.prepayments` hits `GET /api.xro/2.0/Prepayments` with the
25
+ `Xero-Tenant-Id` header set to the tenant you specify. It returns an
26
+ `Array<XeroKiwi::Accounting::Prepayment>`.
27
+
28
+ ## Fetching a single prepayment
29
+
30
+ ```ruby
31
+ prepayment = client.prepayment(tenant_id, "aea95d78-ea48-456b-9b08-6bc012600072")
32
+ prepayment.total # => "100.00"
33
+ ```
34
+
35
+ `client.prepayment` hits `GET /api.xro/2.0/Prepayments/{PrepaymentID}` and
36
+ returns a single `XeroKiwi::Accounting::Prepayment`, or `nil` if the response is
37
+ empty.
38
+
39
+ ## The Prepayment object
40
+
41
+ Each `XeroKiwi::Accounting::Prepayment` is an immutable value object exposing the
42
+ fields Xero returns:
43
+
44
+ | Attribute | Type | What it is |
45
+ |-----------|------|------------|
46
+ | `prepayment_id` | `String` | The unique Xero identifier. |
47
+ | `type` | `String` | `"RECEIVE-PREPAYMENT"` or `"SPEND-PREPAYMENT"`. |
48
+ | `contact` | `XeroKiwi::Accounting::Contact` | The contact (reference — use `contact.reference?` to check). See [Contacts](contact.md). |
49
+ | `date` | `Time` | The date the prepayment was created, parsed as UTC. |
50
+ | `status` | `String` | e.g. `"AUTHORISED"`, `"PAID"`, `"VOIDED"`. |
51
+ | `line_amount_types` | `String` | `"Inclusive"`, `"Exclusive"`, or `"NoTax"`. |
52
+ | `line_items` | `Array<XeroKiwi::Accounting::LineItem>` | The line items. See [LineItem](#the-lineitem-object). |
53
+ | `sub_total` | `String` | The subtotal excluding taxes. |
54
+ | `total_tax` | `String` | The total tax amount. |
55
+ | `total` | `String` | The total (subtotal + total tax). |
56
+ | `updated_date_utc` | `Time` | When the prepayment was last modified, parsed as UTC. |
57
+ | `currency_code` | `String` | The currency code (e.g. `"NZD"`). |
58
+ | `currency_rate` | `String` | The currency rate (1.0 for base currency). |
59
+ | `invoice_number` | `String` | The invoice number (for receive prepayments only). |
60
+ | `remaining_credit` | `String` | The remaining credit balance. |
61
+ | `allocations` | `Array<XeroKiwi::Accounting::Allocation>` | Allocations to invoices. Each allocation has an `invoice` reference. |
62
+ | `payments` | `Array<XeroKiwi::Accounting::Payment>` | Payment records (references). See [Payments](payment.md). |
63
+ | `has_attachments` | `Boolean` | Whether the prepayment has attachments. |
64
+ | `fully_paid_on_date` | `Time` | When the prepayment was fully allocated, parsed as UTC. |
65
+
66
+ ## The LineItem object
67
+
68
+ Each `XeroKiwi::Accounting::LineItem` is an immutable value object shared across
69
+ documents (prepayments, invoices, etc.):
70
+
71
+ | Attribute | Type | What it is |
72
+ |-----------|------|------------|
73
+ | `description` | `String` | Line item description. |
74
+ | `quantity` | `Float` | Quantity. |
75
+ | `unit_amount` | `Float` | Unit amount. |
76
+ | `account_code` | `String` | The account code. |
77
+ | `tax_type` | `String` | The tax type override. |
78
+ | `tax_amount` | `Float` | The calculated tax amount. |
79
+ | `line_amount` | `Float` | The line total. |
80
+ | `tracking` | `Array<Hash>` | Tracking categories (raw, max 2 per line). |
81
+
82
+ ## Predicates
83
+
84
+ ```ruby
85
+ prepayment.receive? # type == "RECEIVE-PREPAYMENT"
86
+ prepayment.spend? # type == "SPEND-PREPAYMENT"
87
+ ```
88
+
89
+ ## Equality and hashing
90
+
91
+ Two prepayments are `==` if they share the same `prepayment_id`. `#hash` is
92
+ consistent with `==`, so prepayments work as hash keys and in sets.
93
+
94
+ ## Error behaviour
95
+
96
+ | HTTP status | Exception | What it usually means |
97
+ |-------------|-----------|------------------------|
98
+ | 200 | (none — returns prepayments) | Success |
99
+ | 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
100
+ | 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
101
+ | 404 | `XeroKiwi::ClientError` | The prepayment ID doesn't exist |
102
+
103
+ ## Common patterns
104
+
105
+ ### Listing prepayments with remaining credit
106
+
107
+ ```ruby
108
+ prepayments = client.prepayments(tenant_id)
109
+ with_credit = prepayments.select { |p| p.remaining_credit.to_f > 0 }
110
+ with_credit.each { |p| puts "#{p.prepayment_id}: #{p.remaining_credit} remaining" }
111
+ ```
@@ -0,0 +1,109 @@
1
+ # Users
2
+
3
+ Xero **users** are the people who have access to a Xero organisation. The Users
4
+ endpoint is read-only — you can list and fetch users, but not create or modify
5
+ them through the API. You need a `tenant_id` from a
6
+ [connection](../connections.md) before you can fetch users.
7
+
8
+ > See: [Xero docs — Users](https://developer.xero.com/documentation/api/accounting/users)
9
+
10
+ ## Listing users
11
+
12
+ ```ruby
13
+ client = XeroKiwi::Client.new(access_token: "ya29...")
14
+
15
+ # Pass a tenant ID string…
16
+ users = client.users("70784a63-d24b-46a9-a4db-0e70a274b056")
17
+
18
+ # …or a XeroKiwi::Connection (its tenant_id is used automatically).
19
+ connection = client.connections.first
20
+ users = client.users(connection)
21
+ ```
22
+
23
+ `client.users` hits `GET /api.xro/2.0/Users` with the `Xero-Tenant-Id` header
24
+ set to the tenant you specify. It returns an `Array<XeroKiwi::Accounting::User>`.
25
+
26
+ ## Fetching a single user
27
+
28
+ ```ruby
29
+ user = client.user(tenant_id, "7cf47fe2-c3dd-4c6b-9895-7ba767ba529c")
30
+ user.email_address # => "john.smith@mail.com"
31
+ ```
32
+
33
+ `client.user` hits `GET /api.xro/2.0/Users/{UserID}` and returns a single
34
+ `XeroKiwi::Accounting::User`, or `nil` if the response is empty.
35
+
36
+ ## The User object
37
+
38
+ Each `XeroKiwi::Accounting::User` is an immutable value object exposing the fields
39
+ Xero returns:
40
+
41
+ | Attribute | Type | What it is |
42
+ |-----------|------|------------|
43
+ | `user_id` | `String` | The unique Xero identifier for the user. |
44
+ | `email_address` | `String` | The user's email address. |
45
+ | `first_name` | `String` | The user's first name. |
46
+ | `last_name` | `String` | The user's last name. |
47
+ | `updated_date_utc` | `Time` | When the user was last modified, parsed as UTC. |
48
+ | `is_subscriber` | `Boolean` | Whether the user is the subscriber (billing owner). |
49
+ | `organisation_role` | `String` | The user's role — see [Organisation roles](#organisation-roles) below. |
50
+
51
+ ## Predicates
52
+
53
+ ```ruby
54
+ user.subscriber? # is_subscriber == true
55
+ ```
56
+
57
+ ## Organisation roles
58
+
59
+ | Role | Description |
60
+ |------|-------------|
61
+ | `READONLY` | Read-only access |
62
+ | `INVOICEONLY` | Invoice-only access |
63
+ | `STANDARD` | Standard user |
64
+ | `FINANCIALADVISER` | Financial adviser role |
65
+ | `MANAGEDCLIENT` | Managed client (Partner Edition only) |
66
+ | `CASHBOOKCLIENT` | Cashbook client (Partner Edition only) |
67
+ | `ADMIN` | Full admin access |
68
+
69
+ ## Equality and hashing
70
+
71
+ Two users are `==` if they share the same `user_id`. `#hash` is consistent with
72
+ `==`, so users work as hash keys and in sets.
73
+
74
+ ## Date parsing
75
+
76
+ The `updated_date_utc` field uses Xero's .NET JSON timestamp format
77
+ (`/Date(1516230549137+0000)/`). XeroKiwi parses both .NET JSON and ISO 8601
78
+ formats transparently — the attribute is always a UTC `Time` object.
79
+
80
+ ## Error behaviour
81
+
82
+ | HTTP status | Exception | What it usually means |
83
+ |-------------|-----------|------------------------|
84
+ | 200 | (none — returns users) | Success |
85
+ | 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
86
+ | 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
87
+ | 404 | `XeroKiwi::ClientError` | The user ID doesn't exist in this organisation |
88
+
89
+ ## Common patterns
90
+
91
+ ### Listing all users for each tenant
92
+
93
+ ```ruby
94
+ client = XeroKiwi::Client.new(access_token: "ya29...")
95
+
96
+ client.connections.each do |conn|
97
+ users = client.users(conn)
98
+ puts "#{conn.tenant_name}: #{users.size} users"
99
+ users.each { |u| puts " #{u.first_name} #{u.last_name} (#{u.organisation_role})" }
100
+ end
101
+ ```
102
+
103
+ ### Finding the subscriber
104
+
105
+ ```ruby
106
+ users = client.users(tenant_id)
107
+ subscriber = users.find(&:subscriber?)
108
+ puts "Billing owner: #{subscriber.email_address}" if subscriber
109
+ ```
data/docs/client.md ADDED
@@ -0,0 +1,174 @@
1
+ # XeroKiwi::Client
2
+
3
+ `XeroKiwi::Client` is the entry point for talking to Xero's accounting API. You
4
+ construct one with credentials and call resource methods on it. The client
5
+ holds the OAuth token state, knows how to refresh it, and translates HTTP
6
+ errors into XeroKiwi exceptions.
7
+
8
+ ## Constructing a client
9
+
10
+ Two common shapes:
11
+
12
+ ```ruby
13
+ # Simple — access token only, no refresh capability.
14
+ client = XeroKiwi::Client.new(access_token: "ya29...")
15
+
16
+ # Full — refresh-capable, with persistence callback.
17
+ client = XeroKiwi::Client.new(
18
+ access_token: credential.access_token,
19
+ refresh_token: credential.refresh_token,
20
+ expires_at: credential.expires_at,
21
+ client_id: ENV.fetch("XERO_CLIENT_ID"),
22
+ client_secret: ENV.fetch("XERO_CLIENT_SECRET"),
23
+ on_token_refresh: ->(token) { credential.update!(token.to_h) }
24
+ )
25
+ ```
26
+
27
+ The simple form is fine for one-off scripts and quick experiments. For
28
+ anything long-running, use the full form so the client can refresh tokens for
29
+ you. See [Tokens](tokens.md) for the full refresh story.
30
+
31
+ ## Constructor options
32
+
33
+ | Option | Type | Required | Default | Purpose |
34
+ |--------|------|----------|---------|---------|
35
+ | `access_token:` | `String` | Yes | — | The OAuth2 bearer token used on every API call. |
36
+ | `refresh_token:` | `String` | No | `nil` | The refresh token. Required if you want the client to refresh expired access tokens. |
37
+ | `expires_at:` | `Time` | No | `nil` | When the access token expires. Used by the proactive refresh check. If `nil`, the client falls back to reactive refresh on 401. |
38
+ | `client_id:` | `String` | No | `nil` | Your Xero app's client ID. Required for refresh. |
39
+ | `client_secret:` | `String` | No | `nil` | Your Xero app's client secret. Required for refresh. |
40
+ | `on_token_refresh:` | `Proc` / lambda | No | `nil` | Called with the new `XeroKiwi::Token` whenever a refresh happens. Use this to persist the rotated token back to storage. |
41
+ | `adapter:` | `Symbol` / Faraday adapter | No | `Faraday.default_adapter` | The Faraday adapter to use. Override to swap in `:net_http_persistent`, `:typhoeus`, or a test adapter. |
42
+ | `user_agent:` | `String` | No | `"XeroKiwi/<version>"` | Sent as the `User-Agent` header on every request. |
43
+ | `retry_options:` | `Hash` | No | See [retries and rate limits](retries-and-rate-limits.md) | Overrides for the `faraday-retry` configuration. Merged into the defaults. |
44
+
45
+ ## What the client gives you
46
+
47
+ | Method | Returns | Purpose |
48
+ |--------|---------|---------|
49
+ | `client.connections` | `Array<XeroKiwi::Connection>` | Fetch the tenants this token is authorised against. See [Connections](connections.md). |
50
+ | `client.contacts(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::Contact>` | Fetch the contacts for a tenant. See [Contacts](accounting/contact.md). |
51
+ | `client.contact(tenant_id_or_connection, contact_id)` | `XeroKiwi::Accounting::Contact` | Fetch a single contact by ID. See [Contacts](accounting/contact.md). |
52
+ | `client.contact_groups(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::ContactGroup>` | Fetch the contact groups for a tenant. See [Contact Groups](accounting/contact-group.md). |
53
+ | `client.contact_group(tenant_id_or_connection, contact_group_id)` | `XeroKiwi::Accounting::ContactGroup` | Fetch a single contact group by ID. See [Contact Groups](accounting/contact-group.md). |
54
+ | `client.organisation(tenant_id_or_connection)` | `XeroKiwi::Accounting::Organisation` | Fetch the organisation for a tenant. See [Organisation](accounting/organisation.md). |
55
+ | `client.users(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::User>` | Fetch the users for a tenant. See [Users](accounting/user.md). |
56
+ | `client.user(tenant_id_or_connection, user_id)` | `XeroKiwi::Accounting::User` | Fetch a single user by ID. See [Users](accounting/user.md). |
57
+ | `client.credit_notes(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::CreditNote>` | Fetch the credit notes for a tenant. See [Credit Notes](accounting/credit-note.md). |
58
+ | `client.credit_note(tenant_id_or_connection, credit_note_id)` | `XeroKiwi::Accounting::CreditNote` | Fetch a single credit note by ID. See [Credit Notes](accounting/credit-note.md). |
59
+ | `client.invoices(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::Invoice>` | Fetch the invoices for a tenant. See [Invoices](accounting/invoice.md). |
60
+ | `client.invoice(tenant_id_or_connection, invoice_id)` | `XeroKiwi::Accounting::Invoice` | Fetch a single invoice by ID. See [Invoices](accounting/invoice.md). |
61
+ | `client.online_invoice_url(tenant_id_or_connection, invoice_id)` | `String` | Fetch the online invoice URL for a sales invoice. See [Invoices](accounting/invoice.md). |
62
+ | `client.payments(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::Payment>` | Fetch the payments for a tenant. See [Payments](accounting/payment.md). |
63
+ | `client.payment(tenant_id_or_connection, payment_id)` | `XeroKiwi::Accounting::Payment` | Fetch a single payment by ID. See [Payments](accounting/payment.md). |
64
+ | `client.overpayments(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::Overpayment>` | Fetch the overpayments for a tenant. See [Overpayments](accounting/overpayment.md). |
65
+ | `client.overpayment(tenant_id_or_connection, overpayment_id)` | `XeroKiwi::Accounting::Overpayment` | Fetch a single overpayment by ID. See [Overpayments](accounting/overpayment.md). |
66
+ | `client.prepayments(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::Prepayment>` | Fetch the prepayments for a tenant. See [Prepayments](accounting/prepayment.md). |
67
+ | `client.prepayment(tenant_id_or_connection, prepayment_id)` | `XeroKiwi::Accounting::Prepayment` | Fetch a single prepayment by ID. See [Prepayments](accounting/prepayment.md). |
68
+ | `client.branding_themes(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::BrandingTheme>` | Fetch the branding themes for a tenant. See [Branding Themes](accounting/branding-theme.md). |
69
+ | `client.branding_theme(tenant_id_or_connection, branding_theme_id)` | `XeroKiwi::Accounting::BrandingTheme` | Fetch a single branding theme by ID. See [Branding Themes](accounting/branding-theme.md). |
70
+ | `client.delete_connection(id_or_connection)` | `true` | Disconnect a tenant. See [Connections](connections.md). |
71
+ | `client.token` | `XeroKiwi::Token` | The current in-memory token. Inspect expiry, refreshability, etc. |
72
+ | `client.token.expired?` | `Boolean` | True if `expires_at` is in the past. |
73
+ | `client.token.expiring_soon?(within: 60)` | `Boolean` | True if `expires_at` falls inside the window. |
74
+ | `client.token.refreshable?` | `Boolean` | True if the token has a refresh token attached. |
75
+ | `client.can_refresh?` | `Boolean` | True if the client was constructed with both refresh credentials AND the current token has a `refresh_token`. |
76
+ | `client.refresh_token!` | `XeroKiwi::Token` | Force a refresh now. Returns the new token. Raises `XeroKiwi::TokenRefreshError` if there's no refresh capability. |
77
+ | `client.revoke_token!` | `true` | Revoke the current refresh token at Xero. Use for logout / "disconnect Xero" flows. See [Tokens](tokens.md). |
78
+
79
+ ## The request lifecycle
80
+
81
+ Every API call goes through `with_authenticated_request`, which wraps the
82
+ actual HTTP call with two layers of token-freshness handling:
83
+
84
+ 1. **Proactive refresh.** Before the request fires, if the token is expiring
85
+ within the default window (60 seconds) AND the client has refresh
86
+ capability, the client refreshes the token first. This covers the common
87
+ case of "the token I loaded from the database is about to expire."
88
+ 2. **The actual HTTP call** — including all the retry behaviour described
89
+ in [retries and rate limits](retries-and-rate-limits.md).
90
+ 3. **Reactive refresh on 401.** If the request returns a 401 anyway (token
91
+ was revoked early, our clock is wrong, etc.), the client refreshes once
92
+ and retries the request. A `retried` flag prevents an infinite loop —
93
+ the second 401 raises `XeroKiwi::AuthenticationError`.
94
+
95
+ If you constructed the client without refresh credentials, both layers are
96
+ skipped: a 401 raises immediately and you handle it in your own code.
97
+
98
+ ## Custom adapters
99
+
100
+ XeroKiwi uses Faraday under the hood, so you can swap the HTTP adapter for
101
+ testing or for connection pooling:
102
+
103
+ ```ruby
104
+ client = XeroKiwi::Client.new(
105
+ access_token: "...",
106
+ adapter: :net_http_persistent
107
+ )
108
+ ```
109
+
110
+ For tests:
111
+
112
+ ```ruby
113
+ require "faraday"
114
+
115
+ client = XeroKiwi::Client.new(
116
+ access_token: "...",
117
+ adapter: [:test, Faraday::Adapter::Test::Stubs.new] # or use webmock
118
+ )
119
+ ```
120
+
121
+ The adapter is also passed through to the internal `TokenRefresher`, so a
122
+ test adapter swallows refresh requests too.
123
+
124
+ ## Customising the retry policy
125
+
126
+ `retry_options:` is merged into XeroKiwi's defaults, so you only need to specify
127
+ overrides:
128
+
129
+ ```ruby
130
+ client = XeroKiwi::Client.new(
131
+ access_token: "...",
132
+ retry_options: {
133
+ max: 8, # try up to 8 retries (default: 4)
134
+ interval: 1.0 # initial wait of 1 second (default: 0.5)
135
+ }
136
+ )
137
+ ```
138
+
139
+ See [retries and rate limits](retries-and-rate-limits.md) for the full
140
+ configuration reference and which keys you can override.
141
+
142
+ ## Thread safety
143
+
144
+ A single client can safely be shared across threads. The internals are
145
+ thread-safe in two specific ways:
146
+
147
+ - **Token refresh** is protected by a `Mutex` with a double-check pattern. If
148
+ two threads both notice the token is expiring at the same time, only one
149
+ will actually call Xero's refresh endpoint; the other waits, then sees the
150
+ fresh token and proceeds.
151
+ - **Faraday connections** are reused across threads (Faraday's adapters are
152
+ designed for this).
153
+
154
+ There's one caveat: **manual `refresh_token!` calls don't double-check.** If
155
+ you call `client.refresh_token!` from two threads simultaneously, both will
156
+ hit Xero, and the second will fail because the refresh token rotated. The
157
+ automatic path (`ensure_fresh_token!` inside `with_authenticated_request`)
158
+ deduplicates correctly.
159
+
160
+ If you're sharing a client across multiple processes (e.g. a Sidekiq pool
161
+ spread across machines), the in-process mutex doesn't help you. See
162
+ [Tokens](tokens.md#multi-process-refresh) for the multi-process gotcha and
163
+ how to handle it.
164
+
165
+ ## What the client deliberately does NOT do
166
+
167
+ - **Persist anything.** The client never writes to your database, session,
168
+ or filesystem. The `on_token_refresh` callback is your hook for that.
169
+ - **Manage OAuth state.** The client doesn't know about CSRF state or PKCE
170
+ verifiers. Use [`XeroKiwi::OAuth`](oauth.md) for the auth-code flow.
171
+ - **Validate scopes.** If your token doesn't have the right scope for an
172
+ endpoint, you'll get a 403 from Xero. The client surfaces it as a
173
+ `XeroKiwi::ClientError`; it's the caller's job to know what scopes they
174
+ asked for.
@@ -0,0 +1,166 @@
1
+ # Connections
2
+
3
+ In Xero's terminology, a **connection** is one tenant (organisation or
4
+ practice) that an access token has been authorised against. A single user can
5
+ authorise your app against multiple tenants in one OAuth flow, and each one
6
+ becomes a separate connection.
7
+
8
+ This is the first thing you'll typically call after exchanging an OAuth code
9
+ for tokens — you need the `tenant_id` from a connection before you can hit
10
+ any of the actual accounting endpoints.
11
+
12
+ > See: [Xero docs — managing connections](https://developer.xero.com/documentation/best-practices/managing-connections/connections)
13
+
14
+ ## Listing connections
15
+
16
+ ```ruby
17
+ client = XeroKiwi::Client.new(access_token: "ya29...")
18
+
19
+ client.connections.each do |connection|
20
+ puts "#{connection.tenant_name} — #{connection.tenant_id}"
21
+ end
22
+ ```
23
+
24
+ `client.connections` returns an `Array<XeroKiwi::Connection>`. The array is empty
25
+ if the user authorised your app but didn't pick any tenants (rare, but
26
+ possible). The endpoint doesn't take any filtering parameters — Xero returns
27
+ everything the token has access to.
28
+
29
+ ## The Connection object
30
+
31
+ Each `XeroKiwi::Connection` is an immutable value object exposing the fields Xero
32
+ returns:
33
+
34
+ | Attribute | Type | What it is |
35
+ |-----------|------|------------|
36
+ | `id` | `String` | The **connection** UUID. This is what you pass to `delete_connection` to disconnect. |
37
+ | `tenant_id` | `String` | The **tenant** UUID. This is what you put in the `Xero-Tenant-Id` header on every accounting API call. |
38
+ | `tenant_type` | `String` | Either `"ORGANISATION"` or `"PRACTICE"`. |
39
+ | `tenant_name` | `String` | The display name (e.g. "Maple Florists Ltd"). |
40
+ | `auth_event_id` | `String` | The OAuth event UUID. Useful for correlating with Xero's audit logs. |
41
+ | `created_date_utc` | `Time` | When the connection was first established, parsed as UTC. |
42
+ | `updated_date_utc` | `Time` | When the connection was last updated, parsed as UTC. |
43
+
44
+ ### `id` vs `tenant_id` — important
45
+
46
+ These are **different UUIDs**. Get them mixed up and your API calls will
47
+ mysteriously 401.
48
+
49
+ - `connection.id` is the unique identifier of the *connection record itself*,
50
+ used for deletion.
51
+ - `connection.tenant_id` is the unique identifier of the *tenant* (the Xero
52
+ organisation), used in the `Xero-Tenant-Id` header on every accounting
53
+ request.
54
+
55
+ ## Predicates
56
+
57
+ ```ruby
58
+ connection.organisation? # tenant_type == "ORGANISATION"
59
+ connection.practice? # tenant_type == "PRACTICE"
60
+ ```
61
+
62
+ ## Equality and hashing
63
+
64
+ Two connections are `==` if they have the same `id`, even if other attributes
65
+ differ. This is convenient for set operations:
66
+
67
+ ```ruby
68
+ old_connections - new_connections # diff by id
69
+ ```
70
+
71
+ `#hash` is consistent with `==`, so connections work as hash keys.
72
+
73
+ ## Date parsing
74
+
75
+ Xero serialises dates in C# DateTime format and frequently omits the timezone
76
+ marker on values that are documented as UTC (e.g. `"2019-07-09T23:40:30.1833130"`).
77
+ XeroKiwi force-appends a `Z` before parsing so you always get a UTC `Time` back —
78
+ without this, `Time.parse` would silently fall back to local time and you'd
79
+ get the wrong instant.
80
+
81
+ If you want the raw string Xero sent, the connection's `to_h` only exposes
82
+ the parsed `Time` objects. Reach into the response yourself if you need the
83
+ original strings.
84
+
85
+ ## Disconnecting a tenant
86
+
87
+ ```ruby
88
+ # By id
89
+ client.delete_connection("e1eede29-f875-4a5d-8470-17f6a29a88b1")
90
+
91
+ # Or by passing the Connection object — its `id` is used
92
+ connection = client.connections.first
93
+ client.delete_connection(connection)
94
+ ```
95
+
96
+ `delete_connection` returns `true` on success and raises on failure. Both
97
+ forms hit `DELETE /connections/:id`.
98
+
99
+ After a successful delete:
100
+
101
+ - The named tenant is detached from the access token.
102
+ - **Other tenants** authorised by the same token are *not* affected — only
103
+ this one connection is gone.
104
+ - Calls that include the deleted `tenant_id` in their `Xero-Tenant-Id` header
105
+ will start failing with 401.
106
+
107
+ ### Error behaviour
108
+
109
+ | HTTP status | Exception | What it usually means |
110
+ |-------------|-----------|------------------------|
111
+ | 204 No Content | (none — returns `true`) | Success |
112
+ | 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
113
+ | 403 | `XeroKiwi::ClientError` | The token doesn't have permission to disconnect this tenant |
114
+ | 404 | `XeroKiwi::ClientError` | The connection ID doesn't exist (or was already deleted) |
115
+
116
+ A 404 is usually idempotent-friendly — if you're trying to make sure a tenant
117
+ is disconnected, catching `XeroKiwi::ClientError` and ignoring 404s is reasonable:
118
+
119
+ ```ruby
120
+ begin
121
+ client.delete_connection(id)
122
+ rescue XeroKiwi::ClientError => e
123
+ raise unless e.status == 404
124
+ end
125
+ ```
126
+
127
+ ## Common patterns
128
+
129
+ ### Find a tenant by name
130
+
131
+ ```ruby
132
+ target = client.connections.find { |c| c.tenant_name == "Maple Florists Ltd" }
133
+ target&.tenant_id # use this in subsequent API calls
134
+ ```
135
+
136
+ ### Disconnect everything
137
+
138
+ For "remove Xero from my app" flows where the user wants a full disconnect,
139
+ combine deletion with token revocation:
140
+
141
+ ```ruby
142
+ client.connections.each { |c| client.delete_connection(c) }
143
+ client.revoke_token!
144
+ credential.destroy!
145
+ ```
146
+
147
+ The order matters — once you `revoke_token!`, the access token is dead and
148
+ subsequent `delete_connection` calls will 401. So delete first, then revoke.
149
+
150
+ (In practice you can skip the per-connection deletes entirely if you're going
151
+ to revoke the token anyway — revoking the refresh token invalidates the
152
+ access token, which detaches all connections at once. The per-connection
153
+ delete is for when you want to disconnect *some* tenants and keep others.)
154
+
155
+ ### Iterating without holding the array
156
+
157
+ Connections are usually a small list (1-10 entries) so loading them all is
158
+ cheap. But if you only need one:
159
+
160
+ ```ruby
161
+ client.connections.lazy.find { |c| c.tenant_id == target_tenant_id }
162
+ ```
163
+
164
+ The lazy enumerator doesn't help here — `client.connections` already does
165
+ the full HTTP fetch — but it's idiomatic and avoids materialising more than
166
+ you need into local variables.