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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8461f3c23bcec591acd4a6bb875ff08f4bacf67573ae6e1957ad78a268d4c7bb
|
|
4
|
+
data.tar.gz: 8167dae0e2922d57099b177a3ee182f3728c28dfc45f6fd0a50ae61a50ea0feb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1a55d9c5c9022a6fbf7fcc777d8084775d54e464d87008dfa876f234f792f5ec45a427ea55dfef0adb01ee8f1101ef49999206d64e6b8cec63f0a97fec3564dd
|
|
7
|
+
data.tar.gz: 95b28e21648463606521aff37c8413e7f6a73fa9f731d30098ef757cb480beaa2f1c3da3f588b1b00ca8cf00784dcd5ec9ca2c7853d129b35ed82ddf6bececaa
|
data/.env.example
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Douglas Greyling
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Xero Kiwi
|
|
2
|
+
|
|
3
|
+
A Ruby wrapper for the [Xero](https://www.xero.com) Accounting API. XeroKiwi handles
|
|
4
|
+
the unglamorous parts of integrating with Xero — OAuth2, token refresh, rate
|
|
5
|
+
limiting, retries — so the rest of your code can focus on the actual business
|
|
6
|
+
problem.
|
|
7
|
+
|
|
8
|
+
## What's in the box
|
|
9
|
+
|
|
10
|
+
- **Full OAuth2 authorization-code flow** with PKCE support, CSRF state helpers,
|
|
11
|
+
and OIDC ID token verification against Xero's JWKS.
|
|
12
|
+
- **Automatic token refresh** with proactive (before expiry) and reactive
|
|
13
|
+
(on-401) handling, a callback hook for persisting rotated tokens, and a
|
|
14
|
+
mutex to dedupe concurrent refreshes from multiple threads.
|
|
15
|
+
- **Rate-limit-aware retries** that honour Xero's `Retry-After` header on 429s
|
|
16
|
+
and back off on transient 5xxs, built on `faraday-retry`.
|
|
17
|
+
- **A discoverable client surface** with explicit error classes for every
|
|
18
|
+
failure mode (authentication, rate limit, code exchange, ID token verification,
|
|
19
|
+
CSRF mismatch).
|
|
20
|
+
- **Connection management**: list and disconnect tenants, with token revocation
|
|
21
|
+
for "disconnect Xero from my app" flows.
|
|
22
|
+
- **Accounting resources**: fetch contacts, organisations, users, branding
|
|
23
|
+
themes, and nested objects like addresses, phones, external links, and payment
|
|
24
|
+
terms — all wrapped in proper value objects.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Add this line to your application's Gemfile:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem "xero-kiwi"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then run `bundle install`.
|
|
35
|
+
|
|
36
|
+
XeroKiwi requires Ruby 3.4.1 or newer.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require "xero_kiwi"
|
|
42
|
+
|
|
43
|
+
# Once you've completed the OAuth flow and have an access token:
|
|
44
|
+
client = XeroKiwi::Client.new(access_token: "ya29...")
|
|
45
|
+
client.connections.each do |connection|
|
|
46
|
+
puts "#{connection.tenant_name} (#{connection.tenant_type})"
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For the full OAuth flow, refresh handling, and everything else, see the docs
|
|
51
|
+
below.
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
| Doc | What it covers |
|
|
56
|
+
|-----|----------------|
|
|
57
|
+
| [Getting started](docs/getting-started.md) | Installation, the mental model, your first end-to-end request |
|
|
58
|
+
| [Client](docs/client.md) | `XeroKiwi::Client` — every constructor option, request lifecycle, configuration |
|
|
59
|
+
| [Connections](docs/connections.md) | Listing tenants, the `XeroKiwi::Connection` resource, disconnecting tenants |
|
|
60
|
+
| [Contacts](docs/accounting/contact.md) | Listing and fetching contacts, the `XeroKiwi::Accounting::Contact` resource, nested ContactPerson |
|
|
61
|
+
| [Contact Groups](docs/accounting/contact-group.md) | Listing and fetching contact groups, the `XeroKiwi::Accounting::ContactGroup` resource |
|
|
62
|
+
| [Organisation](docs/accounting/organisation.md) | Fetching an organisation, the `XeroKiwi::Accounting::Organisation` resource, nested objects |
|
|
63
|
+
| [Users](docs/accounting/user.md) | Listing and fetching users, the `XeroKiwi::Accounting::User` resource, organisation roles |
|
|
64
|
+
| [Credit Notes](docs/accounting/credit-note.md) | Listing and fetching credit notes, the `XeroKiwi::Accounting::CreditNote` resource |
|
|
65
|
+
| [Invoices](docs/accounting/invoice.md) | Listing and fetching invoices/bills, the `XeroKiwi::Accounting::Invoice` resource |
|
|
66
|
+
| [Payments](docs/accounting/payment.md) | Listing and fetching payments, the `XeroKiwi::Accounting::Payment` resource |
|
|
67
|
+
| [Overpayments](docs/accounting/overpayment.md) | Listing and fetching overpayments, the `XeroKiwi::Accounting::Overpayment` resource |
|
|
68
|
+
| [Prepayments](docs/accounting/prepayment.md) | Listing and fetching prepayments, the `XeroKiwi::Accounting::Prepayment` resource, LineItem |
|
|
69
|
+
| [Branding Themes](docs/accounting/branding-theme.md) | Listing and fetching branding themes, the `XeroKiwi::Accounting::BrandingTheme` resource |
|
|
70
|
+
| [Tokens](docs/tokens.md) | The `XeroKiwi::Token` value object, automatic refresh, revocation, persistence callbacks |
|
|
71
|
+
| [OAuth](docs/oauth.md) | Authorization URL building, code exchange, PKCE, ID token verification, full Rails-style example |
|
|
72
|
+
| [Errors](docs/errors.md) | The error hierarchy, what to catch and when |
|
|
73
|
+
| [Retries and rate limits](docs/retries-and-rate-limits.md) | How XeroKiwi handles 429s and transient failures, customising the retry policy |
|
|
74
|
+
|
|
75
|
+
## Status
|
|
76
|
+
|
|
77
|
+
XeroKiwi is in early development. The API surface for the features documented above
|
|
78
|
+
is stable, but expect new resource methods to be added over time. Breaking
|
|
79
|
+
changes will be called out in the [changelog](CHANGELOG.md).
|
|
80
|
+
|
|
81
|
+
## Contributing
|
|
82
|
+
|
|
83
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
84
|
+
https://github.com/douglasgreyling/xero-kiwi.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
The gem is available as open source under the terms of the
|
|
89
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
# Files included in llms-full.txt, in reading order. Keep README first, then
|
|
13
|
+
# the docs in the same order the README's table of contents lists them.
|
|
14
|
+
LLMS_SOURCE_FILES = %w[
|
|
15
|
+
README.md
|
|
16
|
+
docs/getting-started.md
|
|
17
|
+
docs/client.md
|
|
18
|
+
docs/oauth.md
|
|
19
|
+
docs/tokens.md
|
|
20
|
+
docs/connections.md
|
|
21
|
+
docs/accounting/contact.md
|
|
22
|
+
docs/accounting/contact-group.md
|
|
23
|
+
docs/accounting/organisation.md
|
|
24
|
+
docs/accounting/user.md
|
|
25
|
+
docs/accounting/credit-note.md
|
|
26
|
+
docs/accounting/invoice.md
|
|
27
|
+
docs/accounting/payment.md
|
|
28
|
+
docs/accounting/overpayment.md
|
|
29
|
+
docs/accounting/prepayment.md
|
|
30
|
+
docs/accounting/branding-theme.md
|
|
31
|
+
docs/accounting/address.md
|
|
32
|
+
docs/accounting/phone.md
|
|
33
|
+
docs/accounting/external-link.md
|
|
34
|
+
docs/accounting/payment-terms.md
|
|
35
|
+
docs/errors.md
|
|
36
|
+
docs/retries-and-rate-limits.md
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
LLMS_FULL_PATH = "llms-full.txt"
|
|
40
|
+
|
|
41
|
+
# Builds the llms-full.txt content from LLMS_SOURCE_FILES. Pure function:
|
|
42
|
+
# returns a String, doesn't touch the filesystem. Both `llms:build` and
|
|
43
|
+
# `llms:check` use this so the two tasks can never disagree about the
|
|
44
|
+
# expected output.
|
|
45
|
+
def build_llms_full
|
|
46
|
+
out = +""
|
|
47
|
+
out << "# XeroKiwi — full documentation\n\n"
|
|
48
|
+
out << "This file is the complete documentation for the XeroKiwi gem (a Ruby wrapper for the Xero Accounting API), assembled into a single document for LLM consumption. It contains the README and every doc in the docs/ folder, in reading order.\n\n"
|
|
49
|
+
out << "For the curated index version, see llms.txt in the same directory.\n\n"
|
|
50
|
+
out << "Source: https://github.com/douglasgreyling/xero-kiwi\n\n"
|
|
51
|
+
LLMS_SOURCE_FILES.each { |path| append_file_block(out, path) }
|
|
52
|
+
out
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def append_file_block(out, path)
|
|
56
|
+
separator = "=" * 80
|
|
57
|
+
out << "\n" << separator << "\n"
|
|
58
|
+
out << "FILE: #{path}\n"
|
|
59
|
+
out << separator << "\n\n"
|
|
60
|
+
out << File.read(path) << "\n"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
namespace :llms do
|
|
64
|
+
desc "Regenerate llms-full.txt from README and docs/"
|
|
65
|
+
task :build do
|
|
66
|
+
File.write(LLMS_FULL_PATH, build_llms_full)
|
|
67
|
+
puts "Wrote #{LLMS_FULL_PATH} (#{File.size(LLMS_FULL_PATH)} bytes, #{LLMS_SOURCE_FILES.size} source files)"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
desc "Verify llms-full.txt is up to date with README and docs/"
|
|
71
|
+
task :check do
|
|
72
|
+
expected = build_llms_full
|
|
73
|
+
actual = File.exist?(LLMS_FULL_PATH) ? File.read(LLMS_FULL_PATH) : ""
|
|
74
|
+
|
|
75
|
+
if expected == actual
|
|
76
|
+
puts "✓ #{LLMS_FULL_PATH} is up to date"
|
|
77
|
+
else
|
|
78
|
+
warn "✗ #{LLMS_FULL_PATH} is out of date with README/docs."
|
|
79
|
+
warn " Run `bundle exec rake llms:build` and commit the result."
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Top-level convenience alias.
|
|
86
|
+
desc "Regenerate llms-full.txt (alias for llms:build)"
|
|
87
|
+
task llms: "llms:build"
|
|
88
|
+
|
|
89
|
+
task default: %i[spec rubocop llms:check]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# XeroKiwi::Accounting::Address
|
|
2
|
+
|
|
3
|
+
A Xero address. Used by [Organisation](organisation.md), and in future by
|
|
4
|
+
Contact and other resources.
|
|
5
|
+
|
|
6
|
+
> See: [Xero docs — Address types](https://developer.xero.com/documentation/api/accounting/types#addresses)
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
org.addresses.each do |address|
|
|
12
|
+
puts "#{address.address_type}: #{address.address_line_1}, #{address.city}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
street = org.addresses.find(&:street?)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Attributes
|
|
19
|
+
|
|
20
|
+
| Attribute | Type | Notes |
|
|
21
|
+
|-----------|------|-------|
|
|
22
|
+
| `address_type` | `String` | `"POBOX"`, `"STREET"`, or `"DELIVERY"` |
|
|
23
|
+
| `address_line_1` | `String` | Max 500 characters |
|
|
24
|
+
| `address_line_2` | `String` | Max 500 characters |
|
|
25
|
+
| `address_line_3` | `String` | Max 500 characters |
|
|
26
|
+
| `address_line_4` | `String` | Max 500 characters |
|
|
27
|
+
| `city` | `String` | Max 255 characters |
|
|
28
|
+
| `region` | `String` | Max 255 characters |
|
|
29
|
+
| `postal_code` | `String` | Max 50 characters |
|
|
30
|
+
| `country` | `String` | Max 50 characters, letters only |
|
|
31
|
+
| `attention_to` | `String` | Max 255 characters |
|
|
32
|
+
|
|
33
|
+
## Predicates
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
address.street? # address_type == "STREET"
|
|
37
|
+
address.pobox? # address_type == "POBOX"
|
|
38
|
+
address.delivery? # address_type == "DELIVERY"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Note: `DELIVERY` is read-only via Xero's GET endpoint (if set) and is not
|
|
42
|
+
valid for Contacts — only for Organisations.
|
|
43
|
+
|
|
44
|
+
## Equality
|
|
45
|
+
|
|
46
|
+
Two addresses are `==` if all their attributes match. `#hash` is consistent
|
|
47
|
+
with `==`.
|
|
48
|
+
|
|
49
|
+
## Serialisation
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
address.to_h
|
|
53
|
+
# => { address_type: "STREET", address_line_1: "123 Main St", ... }
|
|
54
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Branding Themes
|
|
2
|
+
|
|
3
|
+
A Xero **branding theme** controls the look and feel of invoices, quotes, and
|
|
4
|
+
other documents — logo, colours, layout. Every organisation has at least one
|
|
5
|
+
(the default "Standard" theme). You need a `tenant_id` from a
|
|
6
|
+
[connection](../connections.md) before you can fetch branding themes.
|
|
7
|
+
|
|
8
|
+
> See: [Xero docs — Branding Themes](https://developer.xero.com/documentation/api/accounting/brandingthemes)
|
|
9
|
+
|
|
10
|
+
## Listing branding themes
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
client = XeroKiwi::Client.new(access_token: "ya29...")
|
|
14
|
+
|
|
15
|
+
# Pass a tenant ID string…
|
|
16
|
+
themes = client.branding_themes("70784a63-d24b-46a9-a4db-0e70a274b056")
|
|
17
|
+
|
|
18
|
+
# …or a XeroKiwi::Connection (its tenant_id is used automatically).
|
|
19
|
+
connection = client.connections.first
|
|
20
|
+
themes = client.branding_themes(connection)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`client.branding_themes` hits `GET /api.xro/2.0/BrandingThemes` with the
|
|
24
|
+
`Xero-Tenant-Id` header set to the tenant you specify. It returns an
|
|
25
|
+
`Array<XeroKiwi::Accounting::BrandingTheme>`.
|
|
26
|
+
|
|
27
|
+
## Fetching a single branding theme
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
theme = client.branding_theme(tenant_id, "dfe23d27-a3a6-4ef3-a5ca-b9e02b142dde")
|
|
31
|
+
theme.name # => "Special Projects"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`client.branding_theme` hits `GET /api.xro/2.0/BrandingThemes/{BrandingThemeID}`
|
|
35
|
+
and returns a single `XeroKiwi::Accounting::BrandingTheme`, or `nil` if the
|
|
36
|
+
response is empty.
|
|
37
|
+
|
|
38
|
+
## The BrandingTheme object
|
|
39
|
+
|
|
40
|
+
Each `XeroKiwi::Accounting::BrandingTheme` is an immutable value object exposing the
|
|
41
|
+
fields Xero returns:
|
|
42
|
+
|
|
43
|
+
| Attribute | Type | What it is |
|
|
44
|
+
|-----------|------|------------|
|
|
45
|
+
| `branding_theme_id` | `String` | The unique Xero identifier for the branding theme. |
|
|
46
|
+
| `name` | `String` | The display name (e.g. "Standard", "Special Projects"). |
|
|
47
|
+
| `logo_url` | `String` | The URL of the logo image used on the theme. May be `nil` if no custom logo is set. |
|
|
48
|
+
| `type` | `String` | The document type the theme applies to (always `"INVOICE"`). |
|
|
49
|
+
| `sort_order` | `Integer` | Ranked order of the theme. The default theme has a value of `0`. |
|
|
50
|
+
| `created_date_utc` | `Time` | When the theme was created, parsed as UTC. |
|
|
51
|
+
|
|
52
|
+
## Equality and hashing
|
|
53
|
+
|
|
54
|
+
Two branding themes are `==` if they share the same `branding_theme_id`.
|
|
55
|
+
`#hash` is consistent with `==`, so branding themes work as hash keys and in
|
|
56
|
+
sets.
|
|
57
|
+
|
|
58
|
+
## Date parsing
|
|
59
|
+
|
|
60
|
+
The `created_date_utc` field uses Xero's .NET JSON timestamp format
|
|
61
|
+
(`/Date(946684800000+0000)/`). XeroKiwi parses both .NET JSON and ISO 8601
|
|
62
|
+
formats transparently — the attribute is always a UTC `Time` object.
|
|
63
|
+
|
|
64
|
+
## Error behaviour
|
|
65
|
+
|
|
66
|
+
| HTTP status | Exception | What it usually means |
|
|
67
|
+
|-------------|-----------|------------------------|
|
|
68
|
+
| 200 | (none — returns themes) | Success |
|
|
69
|
+
| 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
|
|
70
|
+
| 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
|
|
71
|
+
| 404 | `XeroKiwi::ClientError` | The branding theme ID doesn't exist in this organisation |
|
|
72
|
+
|
|
73
|
+
## Common patterns
|
|
74
|
+
|
|
75
|
+
### Listing all themes for a tenant
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
client = XeroKiwi::Client.new(access_token: "ya29...")
|
|
79
|
+
|
|
80
|
+
themes = client.branding_themes(tenant_id)
|
|
81
|
+
themes.each do |theme|
|
|
82
|
+
puts "#{theme.name} (sort: #{theme.sort_order})"
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Finding the default theme
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
themes = client.branding_themes(tenant_id)
|
|
90
|
+
default = themes.find { |t| t.sort_order == 0 }
|
|
91
|
+
puts "Default theme: #{default.name}"
|
|
92
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Contact Groups
|
|
2
|
+
|
|
3
|
+
A Xero **contact group** is a named collection of contacts — useful for
|
|
4
|
+
organising customers or suppliers into categories like "VIP Customers" or
|
|
5
|
+
"Preferred Suppliers". You need a `tenant_id` from a
|
|
6
|
+
[connection](../connections.md) before you can fetch contact groups.
|
|
7
|
+
|
|
8
|
+
> See: [Xero docs — Contact Groups](https://developer.xero.com/documentation/api/accounting/contactgroups)
|
|
9
|
+
|
|
10
|
+
## Listing contact groups
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
client = XeroKiwi::Client.new(access_token: "ya29...")
|
|
14
|
+
|
|
15
|
+
# Pass a tenant ID string…
|
|
16
|
+
groups = client.contact_groups("70784a63-d24b-46a9-a4db-0e70a274b056")
|
|
17
|
+
|
|
18
|
+
# …or a XeroKiwi::Connection (its tenant_id is used automatically).
|
|
19
|
+
connection = client.connections.first
|
|
20
|
+
groups = client.contact_groups(connection)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`client.contact_groups` hits `GET /api.xro/2.0/ContactGroups` with the
|
|
24
|
+
`Xero-Tenant-Id` header set to the tenant you specify. It returns an
|
|
25
|
+
`Array<XeroKiwi::Accounting::ContactGroup>`. Only groups with status `ACTIVE` are
|
|
26
|
+
returned by Xero.
|
|
27
|
+
|
|
28
|
+
## Fetching a single contact group
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
group = client.contact_group(tenant_id, "97bbd0e6-ab4d-4117-9304-d90dd4779199")
|
|
32
|
+
group.name # => "VIP Customers"
|
|
33
|
+
group.contacts # => [{"ContactID" => "...", "Name" => "Boom FM"}, ...]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`client.contact_group` hits `GET /api.xro/2.0/ContactGroups/{ContactGroupID}`
|
|
37
|
+
and returns a single `XeroKiwi::Accounting::ContactGroup`, or `nil` if the
|
|
38
|
+
response is empty.
|
|
39
|
+
|
|
40
|
+
The single-group response includes a `contacts` array with the `ContactID` and
|
|
41
|
+
`Name` of each contact in the group. The list response does not include this.
|
|
42
|
+
|
|
43
|
+
## The ContactGroup object
|
|
44
|
+
|
|
45
|
+
Each `XeroKiwi::Accounting::ContactGroup` is an immutable value object exposing the
|
|
46
|
+
fields Xero returns:
|
|
47
|
+
|
|
48
|
+
| Attribute | Type | What it is |
|
|
49
|
+
|-----------|------|------------|
|
|
50
|
+
| `contact_group_id` | `String` | The unique Xero identifier for the group. |
|
|
51
|
+
| `name` | `String` | The display name (e.g. "VIP Customers"). |
|
|
52
|
+
| `status` | `String` | `"ACTIVE"` (only active groups are returned by Xero). |
|
|
53
|
+
| `contacts` | `Array<XeroKiwi::Accounting::Contact>` | The contacts in the group (references — each has `reference?` returning `true`). Only present when fetching a single group. See [Contacts](contact.md). |
|
|
54
|
+
|
|
55
|
+
## Predicates
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
group.active? # status == "ACTIVE"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Equality and hashing
|
|
62
|
+
|
|
63
|
+
Two contact groups are `==` if they share the same `contact_group_id`. `#hash`
|
|
64
|
+
is consistent with `==`, so contact groups work as hash keys and in sets.
|
|
65
|
+
|
|
66
|
+
## Error behaviour
|
|
67
|
+
|
|
68
|
+
| HTTP status | Exception | What it usually means |
|
|
69
|
+
|-------------|-----------|------------------------|
|
|
70
|
+
| 200 | (none — returns groups) | Success |
|
|
71
|
+
| 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
|
|
72
|
+
| 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
|
|
73
|
+
| 404 | `XeroKiwi::ClientError` | The contact group ID doesn't exist |
|
|
74
|
+
|
|
75
|
+
## Common patterns
|
|
76
|
+
|
|
77
|
+
### Listing all contact groups
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
groups = client.contact_groups(tenant_id)
|
|
81
|
+
groups.each { |g| puts g.name }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Fetching members of a group
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
group = client.contact_group(tenant_id, "97bbd0e6-ab4d-4117-9304-d90dd4779199")
|
|
88
|
+
group.contacts.each do |c|
|
|
89
|
+
puts "#{c.name} (#{c.contact_id})"
|
|
90
|
+
end
|
|
91
|
+
```
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Contacts
|
|
2
|
+
|
|
3
|
+
A Xero **contact** is a person or organisation you do business with — customers,
|
|
4
|
+
suppliers, or both. Contacts carry addresses, phone numbers, payment terms, and
|
|
5
|
+
metadata like tax type defaults. You need a `tenant_id` from a
|
|
6
|
+
[connection](../connections.md) before you can fetch contacts.
|
|
7
|
+
|
|
8
|
+
> See: [Xero docs — Contacts](https://developer.xero.com/documentation/api/accounting/contacts)
|
|
9
|
+
|
|
10
|
+
## Listing contacts
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
client = XeroKiwi::Client.new(access_token: "ya29...")
|
|
14
|
+
|
|
15
|
+
# Pass a tenant ID string…
|
|
16
|
+
contacts = client.contacts("70784a63-d24b-46a9-a4db-0e70a274b056")
|
|
17
|
+
|
|
18
|
+
# …or a XeroKiwi::Connection (its tenant_id is used automatically).
|
|
19
|
+
connection = client.connections.first
|
|
20
|
+
contacts = client.contacts(connection)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`client.contacts` hits `GET /api.xro/2.0/Contacts` with the `Xero-Tenant-Id`
|
|
24
|
+
header set to the tenant you specify. It returns an
|
|
25
|
+
`Array<XeroKiwi::Accounting::Contact>`.
|
|
26
|
+
|
|
27
|
+
## Fetching a single contact
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
contact = client.contact(tenant_id, "bd2270c3-8706-4c11-9cfb-000b551c3f51")
|
|
31
|
+
contact.name # => "ABC Limited"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`client.contact` hits `GET /api.xro/2.0/Contacts/{ContactID}` and returns a
|
|
35
|
+
single `XeroKiwi::Accounting::Contact`, or `nil` if the response is empty.
|
|
36
|
+
|
|
37
|
+
Fetching a single contact returns additional fields that are not included in the
|
|
38
|
+
list response (e.g. `contact_persons`, `payment_terms`, `website`).
|
|
39
|
+
|
|
40
|
+
## The Contact object
|
|
41
|
+
|
|
42
|
+
Each `XeroKiwi::Accounting::Contact` is an immutable value object. The fields below
|
|
43
|
+
are always returned on list and single-contact responses:
|
|
44
|
+
|
|
45
|
+
| Attribute | Type | What it is |
|
|
46
|
+
|-----------|------|------------|
|
|
47
|
+
| `contact_id` | `String` | The unique Xero identifier for the contact. |
|
|
48
|
+
| `contact_number` | `String` | External system identifier (read-only in Xero UI). |
|
|
49
|
+
| `account_number` | `String` | A user-defined account number. |
|
|
50
|
+
| `contact_status` | `String` | `"ACTIVE"` or `"ARCHIVED"`. |
|
|
51
|
+
| `name` | `String` | Full name of the contact or organisation. |
|
|
52
|
+
| `first_name` | `String` | First name of the contact person. |
|
|
53
|
+
| `last_name` | `String` | Last name of the contact person. |
|
|
54
|
+
| `email_address` | `String` | Email address of the contact person. |
|
|
55
|
+
| `bank_account_details` | `String` | Bank account number. |
|
|
56
|
+
| `company_number` | `String` | Company registration number (max 50 chars). |
|
|
57
|
+
| `tax_number` | `String` | ABN / GST / VAT / Tax ID Number. |
|
|
58
|
+
| `tax_number_type` | `String` | Regional type of tax number (e.g. `"ABN"`). |
|
|
59
|
+
| `accounts_receivable_tax_type` | `String` | Default AR invoice tax type. |
|
|
60
|
+
| `accounts_payable_tax_type` | `String` | Default AP invoice tax type. |
|
|
61
|
+
| `addresses` | `Array<XeroKiwi::Accounting::Address>` | The contact's addresses. See [Address](address.md). |
|
|
62
|
+
| `phones` | `Array<XeroKiwi::Accounting::Phone>` | The contact's phone numbers. See [Phone](phone.md). |
|
|
63
|
+
| `is_supplier` | `Boolean` | Whether the contact has any AP invoices. |
|
|
64
|
+
| `is_customer` | `Boolean` | Whether the contact has any AR invoices. |
|
|
65
|
+
| `default_currency` | `String` | Default currency code (e.g. `"NZD"`). |
|
|
66
|
+
| `updated_date_utc` | `Time` | When the contact was last modified, parsed as UTC. |
|
|
67
|
+
|
|
68
|
+
### Additional fields (single contact / paginated responses only)
|
|
69
|
+
|
|
70
|
+
| Attribute | Type | What it is |
|
|
71
|
+
|-----------|------|------------|
|
|
72
|
+
| `contact_persons` | `Array<XeroKiwi::Accounting::ContactPerson>` | Up to 5 contact people. See [ContactPerson](#the-contactperson-object). |
|
|
73
|
+
| `xero_network_key` | `String` | Xero network key for the contact. |
|
|
74
|
+
| `merged_to_contact_id` | `String` | ID of the destination contact if merged. |
|
|
75
|
+
| `sales_default_account_code` | `String` | Default sales account code. |
|
|
76
|
+
| `purchases_default_account_code` | `String` | Default purchases account code. |
|
|
77
|
+
| `sales_tracking_categories` | `Array<Hash>` | Default sales tracking categories (raw). |
|
|
78
|
+
| `purchases_tracking_categories` | `Array<Hash>` | Default purchases tracking categories (raw). |
|
|
79
|
+
| `sales_default_line_amount_type` | `String` | `"INCLUSIVE"`, `"EXCLUSIVE"`, or `"NONE"`. |
|
|
80
|
+
| `purchases_default_line_amount_type` | `String` | `"INCLUSIVE"`, `"EXCLUSIVE"`, or `"NONE"`. |
|
|
81
|
+
| `tracking_category_name` | `String` | Tracking category name. |
|
|
82
|
+
| `tracking_option_name` | `String` | Tracking option name. |
|
|
83
|
+
| `payment_terms` | `XeroKiwi::Accounting::PaymentTerms` | Default payment terms. See [PaymentTerms](payment-terms.md). |
|
|
84
|
+
| `contact_groups` | `Array<Hash>` | Contact groups the contact belongs to (raw). |
|
|
85
|
+
| `website` | `String` | Website URL. |
|
|
86
|
+
| `branding_theme` | `Hash` | Default branding theme (raw). |
|
|
87
|
+
| `batch_payments` | `Hash` | Batch payment details (raw). |
|
|
88
|
+
| `discount` | `Float` | Default discount rate. |
|
|
89
|
+
| `balances` | `Hash` | Outstanding and overdue AR/AP balances (raw). |
|
|
90
|
+
| `has_attachments` | `Boolean` | Whether the contact has attachments. |
|
|
91
|
+
|
|
92
|
+
## The ContactPerson object
|
|
93
|
+
|
|
94
|
+
Each `XeroKiwi::Accounting::ContactPerson` is an immutable value object:
|
|
95
|
+
|
|
96
|
+
| Attribute | Type | What it is |
|
|
97
|
+
|-----------|------|------------|
|
|
98
|
+
| `first_name` | `String` | First name. |
|
|
99
|
+
| `last_name` | `String` | Last name. |
|
|
100
|
+
| `email_address` | `String` | Email address. |
|
|
101
|
+
| `include_in_emails` | `Boolean` | Whether to include on invoice emails. |
|
|
102
|
+
|
|
103
|
+
With a predicate: `contact_person.include_in_emails?`
|
|
104
|
+
|
|
105
|
+
## Predicates
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
contact.reference? # true if this is a lightweight reference from another resource
|
|
109
|
+
contact.supplier? # is_supplier == true
|
|
110
|
+
contact.customer? # is_customer == true
|
|
111
|
+
contact.active? # contact_status == "ACTIVE"
|
|
112
|
+
contact.archived? # contact_status == "ARCHIVED"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Contact references
|
|
116
|
+
|
|
117
|
+
When a contact appears nested inside another resource (e.g. an Invoice, Prepayment,
|
|
118
|
+
or CreditNote), it is wrapped as a `XeroKiwi::Accounting::Contact` with `reference?`
|
|
119
|
+
returning `true`. Reference contacts typically carry only a subset of fields (like
|
|
120
|
+
`contact_id` and `name`) — other fields will be `nil`.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
invoice = client.invoice(tenant_id, invoice_id)
|
|
124
|
+
invoice.contact.name # => "City Agency"
|
|
125
|
+
invoice.contact.reference? # => true
|
|
126
|
+
|
|
127
|
+
# Fetch the full contact if you need all fields:
|
|
128
|
+
full_contact = client.contact(tenant_id, invoice.contact.contact_id)
|
|
129
|
+
full_contact.reference? # => false
|
|
130
|
+
full_contact.email_address # => "a.dutchess@abclimited.com"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Equality and hashing
|
|
134
|
+
|
|
135
|
+
Two contacts are `==` if they share the same `contact_id`. `#hash` is consistent
|
|
136
|
+
with `==`, so contacts work as hash keys and in sets.
|
|
137
|
+
|
|
138
|
+
## Error behaviour
|
|
139
|
+
|
|
140
|
+
| HTTP status | Exception | What it usually means |
|
|
141
|
+
|-------------|-----------|------------------------|
|
|
142
|
+
| 200 | (none — returns contacts) | Success |
|
|
143
|
+
| 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
|
|
144
|
+
| 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
|
|
145
|
+
| 404 | `XeroKiwi::ClientError` | The contact ID doesn't exist in this organisation |
|
|
146
|
+
|
|
147
|
+
## Common patterns
|
|
148
|
+
|
|
149
|
+
### Listing all customers
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
contacts = client.contacts(tenant_id)
|
|
153
|
+
customers = contacts.select(&:customer?)
|
|
154
|
+
customers.each { |c| puts "#{c.name} (#{c.default_currency})" }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Fetching a contact with full details
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# The list response omits some fields. Fetch individually for full details.
|
|
161
|
+
contact = client.contact(tenant_id, "bd2270c3-8706-4c11-9cfb-000b551c3f51")
|
|
162
|
+
puts contact.website
|
|
163
|
+
contact.contact_persons.each do |person|
|
|
164
|
+
puts " #{person.first_name} #{person.last_name} — #{person.email_address}"
|
|
165
|
+
end
|
|
166
|
+
```
|