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.
- checksums.yaml +7 -0
- data/.env.example +2 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +89 -0
- data/Rakefile +89 -0
- data/docs/accounting/address.md +54 -0
- data/docs/accounting/branding-theme.md +92 -0
- data/docs/accounting/contact-group.md +91 -0
- data/docs/accounting/contact.md +166 -0
- data/docs/accounting/credit-note.md +97 -0
- data/docs/accounting/external-link.md +33 -0
- data/docs/accounting/invoice.md +134 -0
- data/docs/accounting/organisation.md +119 -0
- data/docs/accounting/overpayment.md +94 -0
- data/docs/accounting/payment-terms.md +58 -0
- data/docs/accounting/payment.md +99 -0
- data/docs/accounting/phone.md +45 -0
- data/docs/accounting/prepayment.md +111 -0
- data/docs/accounting/user.md +109 -0
- data/docs/client.md +174 -0
- data/docs/connections.md +166 -0
- data/docs/errors.md +271 -0
- data/docs/getting-started.md +138 -0
- data/docs/oauth.md +508 -0
- data/docs/retries-and-rate-limits.md +224 -0
- data/docs/tokens.md +339 -0
- data/lib/xero_kiwi/accounting/address.rb +58 -0
- data/lib/xero_kiwi/accounting/allocation.rb +66 -0
- data/lib/xero_kiwi/accounting/branding_theme.rb +76 -0
- data/lib/xero_kiwi/accounting/contact.rb +153 -0
- data/lib/xero_kiwi/accounting/contact_group.rb +57 -0
- data/lib/xero_kiwi/accounting/contact_person.rb +45 -0
- data/lib/xero_kiwi/accounting/credit_note.rb +115 -0
- data/lib/xero_kiwi/accounting/external_link.rb +38 -0
- data/lib/xero_kiwi/accounting/invoice.rb +142 -0
- data/lib/xero_kiwi/accounting/line_item.rb +64 -0
- data/lib/xero_kiwi/accounting/organisation.rb +138 -0
- data/lib/xero_kiwi/accounting/overpayment.rb +107 -0
- data/lib/xero_kiwi/accounting/payment.rb +105 -0
- data/lib/xero_kiwi/accounting/payment_terms.rb +77 -0
- data/lib/xero_kiwi/accounting/phone.rb +46 -0
- data/lib/xero_kiwi/accounting/prepayment.rb +109 -0
- data/lib/xero_kiwi/accounting/tracking_category.rb +42 -0
- data/lib/xero_kiwi/accounting/user.rb +80 -0
- data/lib/xero_kiwi/client.rb +576 -0
- data/lib/xero_kiwi/connection.rb +78 -0
- data/lib/xero_kiwi/errors.rb +34 -0
- data/lib/xero_kiwi/identity.rb +40 -0
- data/lib/xero_kiwi/oauth/id_token.rb +102 -0
- data/lib/xero_kiwi/oauth/pkce.rb +51 -0
- data/lib/xero_kiwi/oauth.rb +232 -0
- data/lib/xero_kiwi/token.rb +99 -0
- data/lib/xero_kiwi/token_refresher.rb +53 -0
- data/lib/xero_kiwi/version.rb +5 -0
- data/lib/xero_kiwi.rb +33 -0
- data/llms-full.txt +3351 -0
- data/llms.txt +56 -0
- data/sig/xero_kiwi.rbs +4 -0
- 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.
|
data/docs/connections.md
ADDED
|
@@ -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.
|