xero-kiwi 0.9.0 → 0.10.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 +4 -4
- data/CHANGELOG.md +86 -0
- data/README.md +42 -0
- data/Rakefile +82 -0
- data/docs/accounting/contact-group.md +1 -0
- data/docs/accounting/contact.md +1 -0
- data/docs/accounting/credit-note.md +32 -6
- data/docs/accounting/invoice.md +12 -11
- data/docs/accounting/organisation.md +1 -0
- data/docs/accounting/overpayment.md +30 -6
- data/docs/accounting/payment.md +4 -3
- data/docs/accounting/prepayment.md +36 -10
- data/docs/client.md +53 -1
- data/docs/connections.md +15 -0
- data/docs/oauth.md +98 -0
- data/lib/xero_kiwi/accounting/contact.rb +3 -2
- data/lib/xero_kiwi/accounting/contact_group.rb +1 -0
- data/lib/xero_kiwi/accounting/credit_note.rb +3 -0
- data/lib/xero_kiwi/accounting/hydrator.rb +44 -2
- data/lib/xero_kiwi/accounting/invoice.rb +2 -1
- data/lib/xero_kiwi/accounting/line_item.rb +1 -1
- data/lib/xero_kiwi/accounting/organisation.rb +2 -1
- data/lib/xero_kiwi/accounting/overpayment.rb +1 -0
- data/lib/xero_kiwi/accounting/payment.rb +1 -0
- data/lib/xero_kiwi/accounting/payment_terms.rb +2 -2
- data/lib/xero_kiwi/accounting/prepayment.rb +2 -0
- data/lib/xero_kiwi/accounting/resource.rb +23 -3
- data/lib/xero_kiwi/client.rb +18 -2
- data/lib/xero_kiwi/oauth.rb +55 -0
- data/lib/xero_kiwi/version.rb +1 -1
- data/llms-full.txt +325 -37
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 122c7e012d619aabdf473185bf0e39e1e04204940d2c6d6b96888d1b6f73b76c
|
|
4
|
+
data.tar.gz: 2bbb94621f3e417275b1ea326a2e54e50f03083c01991863e2f91a16af5945ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0266fde7e2c101e0533f9e3202ba2871f0a17c170e27ff82dd380448028f608da61f697c3ca08d2a965245125c0987f0a9ddb54ccc01e0a89f47d437fd5aa62d
|
|
7
|
+
data.tar.gz: 93e24cd65f1ebf4f7dec4a97fe17d352fb7f6d2ed95f22e6466f403f9997859d16d51569614736c9a1ae77351f94b306dd8304b8f368f465d7c040f2eebb4514
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.10.0] - 2026-09-29
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **Money attributes now return `BigDecimal` instead of `Float`.** Xero sends money as a JSON number, so Ruby parsed it into a Float and the `:decimal` type declaration passed it straight through, doing nothing. Floats cannot represent most decimal fractions, so arithmetic between two money fields drifts while each one still prints correctly:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
17228.67 + 2584.3 # => 19812.969999999998, where Xero's Total is 19812.97
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Measured against the recorded `invoices/list` response, `sub_total + total_tax == total` failed on **3 of 55 invoices** as Floats and on **none** as BigDecimals. Every individual value round-trips exactly — all 111 distinct money literals in that recording — so the failure only appears once you do arithmetic, which is what makes it quiet.
|
|
14
|
+
|
|
15
|
+
Applies to all 36 `:decimal` attributes across `Invoice`, `CreditNote`, `Prepayment`, `Overpayment`, `Payment`, `Allocation` and `LineItem`. Floats and Integers convert via `#to_d`, which uses the shortest decimal that round-trips, so BigDecimal holds the number Xero wrote rather than the binary approximation of it. Numeric strings are parsed with `BigDecimal()` rather than `String#to_d`, because `to_d` answers `0.0` for unparseable input and a silent zero in a money field is the failure this gem has shipped twice. Unparseable input reads as `nil`, as it does for `:date`.
|
|
16
|
+
|
|
17
|
+
Upgrading: comparison is not a concern — `BigDecimal("19812.97")` equals the Float `19812.97`, and a `decimal`/`numeric` column takes it unchanged. A consumer verified that last point on real data, passing five money fields straight into decimal columns with no conversion and getting identical rows.
|
|
18
|
+
|
|
19
|
+
Two things do change. `to_s` gives `"0.1981297e5"` rather than `"19812.97"` (use `to_s("F")`), and `to_json` gives that same scientific-notation **string** where a Float gave a JSON number — a visible change in shape if you write a **typed** money attribute into a `jsonb` column. **`raw` is unaffected**: it holds Xero's parsed payload untouched, so money in there is still a Float and still serialises as a JSON number. `#inspect` renders decimals in plain form, so debugging output is unaffected.
|
|
20
|
+
|
|
21
|
+
**Check for `.to_f` on money you persist.** It silently undoes this change, and a `decimal(19,4)` column holds more precision than a Float can carry — `BigDecimal("1234567890123.4567").to_f` round-trips to `1234567890123.456`. Out of reach for GBP or ZAR at ordinary invoice sizes, not for IDR or VND. A `.to_f` left over from an XML-era client, where the value arrived as a String, is where to look.
|
|
22
|
+
|
|
23
|
+
- **`""` now reads as `nil` on `:decimal` attributes too.** 0.8.0 normalised empty strings on `:string`, `:enum` and `:guid` and explicitly left `:decimal` alone as wanting its own decision. This is that decision. The alternative was `BigDecimal("")` raising, or a silent zero — the same shape as both allocation regressions.
|
|
24
|
+
|
|
25
|
+
- **Removed `Invoice#sales_tax_calculation_type_code`.** It read the payload key `"SalesTaxCalculationTypeCode"`, which appears in none of this gem's recorded responses, nowhere in Xero's 911K OpenAPI spec, and in none of a consumer's 368 recorded interactions across both the XML and JSON eras. Xero has no invoice-level sales-tax-calculation field under any name; its US sales tax fields are on the line item. The reader has returned `nil` since the first commit.
|
|
26
|
+
|
|
27
|
+
Removing it cannot break working code, because there has never been a value to depend on. It is listed as Breaking because the method disappears: a caller reading it goes from a silent `nil` to a `NoMethodError`, which is the same trade the empty-string change made in 0.8.0. If Xero ever does send the key, `retain_raw: true` exposes it through `invoice.raw`.
|
|
28
|
+
|
|
29
|
+
Recording why it went, so nobody re-adds it from the same plausible-sounding guess: it was never verified against a payload or a schema, and it was documented as "US auto sales tax calculation type", which made a permanent `nil` read as an answer about the organisation rather than a gap in the gem.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`invoice.credit_notes`, `invoice.prepayments` and `invoice.overpayments` returned objects with the applied amount missing.** Xero nests **allocation stubs** under an invoice, not whole documents: a stub carries `AppliedAmount`, the amount applied to *that* invoice. Nothing modelled that key, so the nearest-looking reader was `total` — the document's own total, and a different number on **19 of the 24 stubs** in the recorded response. On one, `total` was `10983.65` where `applied_amount` was `857.35`.
|
|
34
|
+
|
|
35
|
+
All three resources now model `applied_amount`. It is `nil` on a document fetched in its own right, where Xero sends no such key, and populated on every stub in the recording. The only route to it before was `invoice.raw["CreditNotes"]` with `retain_raw: true`, since nested objects carry no `#raw`.
|
|
36
|
+
|
|
37
|
+
Found by the coverage task below rather than by comparison or by reading Xero's docs — the key is in no doc table, because the docs describe the XML representation and this shape only exists in JSON.
|
|
38
|
+
|
|
39
|
+
- **`LineItem#account_id` was reading a key Xero does not send.** It mapped to `"AccountId"`; Xero sends `"AccountID"`, so the attribute was `nil` on every line item ever returned. The string `AccountId` appears **zero times** in Xero's 911K OpenAPI spec, and a consumer's recorded JSON response carries `AccountID` on all 18 of its populated line items.
|
|
40
|
+
|
|
41
|
+
No recording in this repo could have caught it: list endpoints omit line items, so `LineItem` has never had a real payload here. It was found by comparing the classes against Xero's published spec, which is now `rake xero:schema`.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`XeroKiwi::OAuth#client_credentials_token`** — Xero's non-tenanted `client_credentials` grant, authenticating the application rather than a user.
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
token = oauth.client_credentials_token(scopes: "app.connections")
|
|
49
|
+
client = XeroKiwi::Client.new(access_token: token.access_token)
|
|
50
|
+
client.connections
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
It is for the calls that belong to the app rather than to a connection — `connections` and `delete_connection`, both already non-tenanted — in the case where **no user token is available**: tearing down an organisation, or reassigning which user owns a sync. Without it, a consumer managing connections has to hand-roll the same POST, which is what prompted this: one had ported everything off its legacy client except this single grant, and was keeping the old client alive to serve it.
|
|
54
|
+
|
|
55
|
+
`redirect_uri` is not required. `scopes:` takes a String or an Array, joined the way `authorization_url` joins them, and is omitted from the request entirely when nil so Xero assigns the app's own scopes.
|
|
56
|
+
|
|
57
|
+
**The token cannot be refreshed.** Xero issues none for this grant, so `token.refreshable?` is false and a `Client` holding one raises `AuthenticationError` on expiry rather than attempting a renewal that cannot succeed — `Client#can_refresh?` already required a refresh token, so no change was needed there. Renewal is calling this again, which costs one request and needs no stored state.
|
|
58
|
+
|
|
59
|
+
Failures raise `XeroKiwi::OAuth::ClientCredentialsError`, which subclasses `AuthenticationError` so existing rescues still catch it.
|
|
60
|
+
|
|
61
|
+
**`Client#connections` takes an optional `tenant_id:`**, which that grant requires. A user token identifies a user, so Xero can answer "which tenants can you see" unaided; an app token identifies nobody and answers `400 Xero-User-Id and/or Xero-Tenant-Id header must be supplied.` Measured by a consumer against live Xero — same token, same request, one header apart. It takes a String or a `XeroKiwi::Connection`; omitting it sends no header, which is what a user token wants and is unchanged. `delete_connection` needs no tenant header and takes none.
|
|
62
|
+
|
|
63
|
+
So "app tokens are untenanted" holds at the *token* level and not at the request level. The first version of these docs showed `client.connections` on a client-credentials token, which is the call that 400s.
|
|
64
|
+
|
|
65
|
+
- `Accounting::Organisation` now models `tax_number_name`, which names what the organisation's locale calls its tax number (`"VAT Number"` on the recorded tenant). Present and populated in the recording, previously reachable only through `raw`.
|
|
66
|
+
|
|
67
|
+
- Fields Xero's spec documents and the recordings confirm on every record, all previously reachable only through `raw`: `Invoice#is_discounted`, `Invoice#has_errors`, `CreditNote#has_errors`, `CreditNote#invoice_addresses`, `Contact#has_validation_errors`, `Payment#has_validation_errors`, `ContactGroup#has_validation_errors`, and `Prepayment#branding_theme_id`.
|
|
68
|
+
|
|
69
|
+
- **`attribute` accepts several candidate keys**, as `xero: %w[TrackingCategoryOption TrackingOptionName]`. The first one present in the payload wins. It is for a field with no recorded payload to settle which key Xero sends: a wrong single key reads `nil` forever and is indistinguishable from a field the tenant never fills, which is how two regressions shipped. `Contact#tracking_option_name` uses it — Xero's spec calls that field `TrackingCategoryOption`, kiwi called it `TrackingOptionName`, and neither spelling appears in any recording because no tenant to hand has a contact-level tracking default.
|
|
70
|
+
|
|
71
|
+
- **`rake xero:schema`** compares the resource classes against Xero's published OpenAPI spec, cached for a day. It is the other half of `xero:coverage`: a recording shows what one tenant populated, the spec shows what an endpoint can return at all. Each has caught what the other missed — the spec found the `AccountID` bug that no payload here could, and the recordings hold `User#GlobalUserID`, which the spec omits entirely and whose absence emptied a consumer's memberships table.
|
|
72
|
+
|
|
73
|
+
It reports rather than fails, for the same reason `xero:coverage` does, and because the spec is not authoritative on its own: it documents `CreditNote#DueDate`, which Xero sends on none of the 18 recorded credit notes, and omits the four `TrackingOption` booleans that a live capture proved real. Treat a difference as a question; a payload settles it where one exists.
|
|
74
|
+
|
|
75
|
+
- **`rake xero:coverage`** compares every recorded response against the resource classes that model it, and reports keys Xero sends that nothing reads, attributes nil in every recording, declared types that disagree with what arrived, and classes no recording exercises. Every silent bug this gem has shipped would have appeared in one of those four lists, with the disproving payload already committed. It reports rather than fails: gating it would need an allowlist of legitimately-absent keys, and an allowlist becomes a list nobody reads.
|
|
76
|
+
|
|
77
|
+
It also names the blind spot, and distinguishes *no cassette here* from *unverified*, which are not the same thing. `LineItem`, `Tracking`, `TrackingCategory` and `TrackingOption` have no cassette in this repo but have each been checked against a real response elsewhere — the task now says so, and says which. `Tracking` was the last class resting on Xero's spec alone, and a live `GET /Invoices/{id}` on a line item carrying two tracking categories settled it: four keys sent, four modelled, none nil. Only `ExternalLink` is genuinely unverified — no payload anywhere has carried a populated instance.
|
|
78
|
+
|
|
79
|
+
The walk also descends into attributes that hydrate through a custom lambda, which it previously skipped. `PaymentTerms` and `PaymentTerm` had read as exercised by nothing while a recorded contact carried both, and the type check immediately showed `PaymentTerm#day` arriving as an Integer against a `:string` declaration. Those attributes now declare `of:` purely so the audit can find the class; `Hydrator` ignores it when `hydrate:` is set.
|
|
80
|
+
|
|
81
|
+
### Documentation
|
|
82
|
+
|
|
83
|
+
- Money attribute types were documented three different ways for the same field — `String` on Overpayment and Prepayment, `Numeric` on CreditNote and Payment, `String/Numeric` on Invoice — and three examples showed `total # => "100.00"`, a quoted string it never was. All 32 rows now say `BigDecimal`, generated from the attribute declarations so they cannot drift apart again.
|
|
84
|
+
|
|
85
|
+
- Each of Credit Note, Prepayment and Overpayment gained a **Nested under an invoice** section covering the stub shape, with the figures taken from the recorded response rather than invented.
|
|
86
|
+
|
|
87
|
+
- `docs/client.md` documents money as a fourth thing to check when migrating from an XML client, alongside the existing nesting, key-name and type axes.
|
|
88
|
+
|
|
3
89
|
## [0.9.0] - 2026-09-29
|
|
4
90
|
|
|
5
91
|
### Added
|
data/README.md
CHANGED
|
@@ -117,6 +117,48 @@ default `redis://127.0.0.1:6379/15`.
|
|
|
117
117
|
docker compose down # when you're done
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
### Checking a resource against real payloads
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
bundle exec rake xero:coverage
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Compares every recorded response in `spec/fixtures/vcr_cassettes` against
|
|
127
|
+
the resource classes that model it, and reports four things: keys Xero sends
|
|
128
|
+
that nothing reads, attributes that were nil in every recording, declared
|
|
129
|
+
types that disagree with what arrived, and classes no recording exercises at
|
|
130
|
+
all.
|
|
131
|
+
|
|
132
|
+
**Run it whenever you add or change a resource.** Every silent bug this gem
|
|
133
|
+
has shipped would have appeared in one of those four lists, and in each case
|
|
134
|
+
the disproving payload was already committed — the recordings were never the
|
|
135
|
+
gap, reading them was.
|
|
136
|
+
|
|
137
|
+
It reports rather than fails, and deliberately. Gating it would need an
|
|
138
|
+
allowlist of the legitimately-absent keys, and an allowlist becomes a list
|
|
139
|
+
nobody reads — which is the failure this is meant to prevent. Read the
|
|
140
|
+
output instead.
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
bundle exec rake xero:schema
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The other half: compares the resource classes against **Xero's published
|
|
147
|
+
OpenAPI spec**, downloaded and cached for a day. A recording shows only what
|
|
148
|
+
one tenant happened to populate; the spec lists what an endpoint can return
|
|
149
|
+
at all.
|
|
150
|
+
|
|
151
|
+
Run both. Neither is sufficient, and each has caught what the other missed:
|
|
152
|
+
|
|
153
|
+
| | Found by |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `LineItem` read `AccountId`, Xero sends `AccountID` | **spec** — no recording here carries a line item |
|
|
156
|
+
| `User#global_user_id` | **recordings** — absent from the spec entirely |
|
|
157
|
+
| `CreditNote#due_date` would be a gap | **neither** — the spec documents it, Xero sends it on none |
|
|
158
|
+
|
|
159
|
+
Treat a difference as a question rather than a defect. A payload settles it
|
|
160
|
+
where one exists.
|
|
161
|
+
|
|
120
162
|
### Releasing
|
|
121
163
|
|
|
122
164
|
**Merge first, then bump.** `rake release` tags whatever is checked out, so a
|
data/Rakefile
CHANGED
|
@@ -10,6 +10,88 @@ require "rubocop/rake_task"
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
12
12
|
require_relative "tasks/llms"
|
|
13
|
+
require_relative "tasks/coverage"
|
|
14
|
+
require_relative "tasks/schema"
|
|
15
|
+
|
|
16
|
+
namespace :xero do
|
|
17
|
+
desc "Compare every recorded Xero response against the resources that model it"
|
|
18
|
+
task :coverage do
|
|
19
|
+
$LOAD_PATH.unshift File.expand_path("lib", __dir__)
|
|
20
|
+
require "xero_kiwi"
|
|
21
|
+
|
|
22
|
+
report = Coverage.survey
|
|
23
|
+
|
|
24
|
+
puts "Keys Xero sends that nothing models"
|
|
25
|
+
puts "-" * 70
|
|
26
|
+
Coverage.unmodelled(report).sort_by { |klass, _| klass.name }.each do |klass, rows|
|
|
27
|
+
puts "\n #{Coverage.short(klass)} (#{report[:seen][klass]} recorded)"
|
|
28
|
+
rows.each do |key, populated, total|
|
|
29
|
+
state = populated.positive? ? "populated #{populated}/#{total}" : "always empty"
|
|
30
|
+
puts format(" %-30<key>s %<state>s", key: key, state: state)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts "\n\nAttributes nil in every recording"
|
|
35
|
+
puts "-" * 70
|
|
36
|
+
puts " A wrong `xero:` key is indistinguishable from a field this tenant never fills."
|
|
37
|
+
Coverage.unpopulated(report).sort_by { |klass, _| klass.name }.each do |klass, names|
|
|
38
|
+
puts "\n #{Coverage.short(klass)} (#{report[:seen][klass]} recorded)"
|
|
39
|
+
names.each do |name|
|
|
40
|
+
puts format(" %-30<name>s -> %<key>s", name: name, key: klass.attributes[name][:xero].inspect)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
puts "\n\nDeclared type against what Xero sends"
|
|
45
|
+
puts "-" * 70
|
|
46
|
+
Coverage.mistyped(report).sort_by { |klass, _| klass.name }.each do |klass, rows|
|
|
47
|
+
puts "\n #{Coverage.short(klass)}"
|
|
48
|
+
rows.each do |name, type, counts|
|
|
49
|
+
actual = counts.map { |cls, n| "#{cls} x#{n}" }.join(", ")
|
|
50
|
+
puts format(" %-26<name>s declared %-9<type>s got %<actual>s",
|
|
51
|
+
name: name, type: type.inspect, actual: actual)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
puts "\n\nClasses no cassette here exercises"
|
|
56
|
+
puts "-" * 70
|
|
57
|
+
Coverage.unexercised(report).sort_by(&:name).each do |klass|
|
|
58
|
+
short = Coverage.short(klass)
|
|
59
|
+
note = Coverage::CONFIRMED_ELSEWHERE[short]
|
|
60
|
+
puts note ? " #{short} — confirmed elsewhere: #{note}" : " #{short} — unverified"
|
|
61
|
+
end
|
|
62
|
+
puts
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc "Compare the resource classes against Xero's published OpenAPI spec"
|
|
66
|
+
task :schema do
|
|
67
|
+
$LOAD_PATH.unshift File.expand_path("lib", __dir__)
|
|
68
|
+
require "xero_kiwi"
|
|
69
|
+
|
|
70
|
+
puts "Fields Xero documents that we do not read"
|
|
71
|
+
puts "-" * 70
|
|
72
|
+
Schema.unmodelled.each do |klass, keys|
|
|
73
|
+
puts "\n #{Coverage.short(klass)}"
|
|
74
|
+
keys.each { |key| puts " #{key}" }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts "\n\nKeys we read that Xero does not document"
|
|
78
|
+
puts "-" * 70
|
|
79
|
+
puts " Either the spec omits the field or we invented the key. Only a"
|
|
80
|
+
puts " payload can say which — check `rake xero:coverage` for one."
|
|
81
|
+
Schema.undocumented.each do |klass, keys|
|
|
82
|
+
puts "\n #{Coverage.short(klass)}"
|
|
83
|
+
keys.each { |key| puts " #{key}" }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
unmatched = Schema.unmatched
|
|
87
|
+
unless unmatched.empty?
|
|
88
|
+
puts "\n\nClasses with no matching schema in the spec"
|
|
89
|
+
puts "-" * 70
|
|
90
|
+
unmatched.each { |klass| puts " #{Coverage.short(klass)}" }
|
|
91
|
+
end
|
|
92
|
+
puts
|
|
93
|
+
end
|
|
94
|
+
end
|
|
13
95
|
|
|
14
96
|
namespace :llms do
|
|
15
97
|
desc "Regenerate llms.txt and llms-full.txt from README and docs/"
|
|
@@ -50,6 +50,7 @@ fields Xero returns:
|
|
|
50
50
|
| `contact_group_id` | `String` | The unique Xero identifier for the group. |
|
|
51
51
|
| `name` | `String` | The display name (e.g. "VIP Customers"). |
|
|
52
52
|
| `status` | `String` | `"ACTIVE"` (only active groups are returned by Xero). |
|
|
53
|
+
| `has_validation_errors` | `Boolean` | Whether Xero flagged validation errors on the group. |
|
|
53
54
|
| `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
|
|
|
55
56
|
## Predicates
|
data/docs/accounting/contact.md
CHANGED
|
@@ -87,6 +87,7 @@ are always returned on list and single-contact responses:
|
|
|
87
87
|
| `batch_payments` | `Hash` | Batch payment details (raw). |
|
|
88
88
|
| `discount` | `Float` | Default discount rate. |
|
|
89
89
|
| `balances` | `Hash` | Outstanding and overdue AR/AP balances (raw). |
|
|
90
|
+
| `has_validation_errors` | `Boolean` | Whether Xero flagged validation errors on the contact. |
|
|
90
91
|
| `has_attachments` | `Boolean` | Whether the contact has attachments. |
|
|
91
92
|
|
|
92
93
|
## The ContactPerson object
|
|
@@ -50,22 +50,48 @@ fields Xero returns:
|
|
|
50
50
|
| `status` | `String` | e.g. `"DRAFT"`, `"SUBMITTED"`, `"AUTHORISED"`, `"PAID"`, `"VOIDED"`. |
|
|
51
51
|
| `line_amount_types` | `String` | `"Inclusive"`, `"Exclusive"`, or `"NoTax"`. |
|
|
52
52
|
| `line_items` | `Array<XeroKiwi::Accounting::LineItem>` | The line items. See [Prepayments — LineItem](prepayment.md#the-lineitem-object). |
|
|
53
|
-
| `sub_total` | `
|
|
54
|
-
| `total_tax` | `
|
|
55
|
-
| `total` | `
|
|
56
|
-
| `
|
|
53
|
+
| `sub_total` | `BigDecimal` | The subtotal excluding taxes. |
|
|
54
|
+
| `total_tax` | `BigDecimal` | The total tax amount. |
|
|
55
|
+
| `total` | `BigDecimal` | The total (subtotal + total tax). |
|
|
56
|
+
| `applied_amount` | `BigDecimal` | Only set when this credit note is nested under an invoice, where it is the amount applied to *that* invoice — not the credit note total. `nil` on a credit note fetched in its own right. See [Nested under an invoice](#nested-under-an-invoice). |
|
|
57
|
+
| `cis_deduction` | `BigDecimal` | CIS deduction (UK Construction Industry Scheme only). |
|
|
57
58
|
| `updated_date_utc` | `Time` | When the credit note was last modified, parsed as UTC. |
|
|
58
59
|
| `currency_code` | `String` | The currency code (e.g. `"NZD"`). |
|
|
59
|
-
| `currency_rate` | `
|
|
60
|
+
| `currency_rate` | `BigDecimal` | The currency rate (1.0 for base currency). |
|
|
60
61
|
| `fully_paid_on_date` | `Time` | When the credit note was fully allocated, parsed as UTC. |
|
|
61
62
|
| `reference` | `String` | Additional reference number (ACCRECCREDIT only). |
|
|
62
63
|
| `sent_to_contact` | `Boolean` | Whether the credit note has been sent to the contact. |
|
|
63
|
-
| `remaining_credit` | `
|
|
64
|
+
| `remaining_credit` | `BigDecimal` | The remaining credit balance. |
|
|
64
65
|
| `allocations` | `Array<XeroKiwi::Accounting::Allocation>` | Allocations to invoices. Each has `allocation_id`, the allocated value (as both `amount` and `applied_amount` — see below), `date`, `is_deleted`, and an `invoice` reference. |
|
|
65
66
|
| `payments` | `Array<XeroKiwi::Accounting::Payment>` | Payment records (references), as on prepayments and overpayments. See [Payments](payment.md). |
|
|
66
67
|
| `branding_theme_id` | `String` | The branding theme ID applied to the credit note. |
|
|
68
|
+
| `has_errors` | `Boolean` | Whether Xero flagged validation errors on the credit note. |
|
|
69
|
+
| `invoice_addresses` | `Array<Hash>` | Invoice addresses (US auto sales tax only). Empty array when absent. |
|
|
67
70
|
| `has_attachments` | `Boolean` | Whether the credit note has attachments. |
|
|
68
71
|
|
|
72
|
+
## Nested under an invoice
|
|
73
|
+
|
|
74
|
+
`invoice.credit_notes` does not return whole credit notes. Xero nests an
|
|
75
|
+
**allocation stub** there — a partial credit note describing how much of it
|
|
76
|
+
was applied to *that* invoice:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
invoice = client.invoices(tenant_id).first
|
|
80
|
+
stub = invoice.credit_notes.find { |s| s.applied_amount != s.total }
|
|
81
|
+
|
|
82
|
+
stub.applied_amount # => BigDecimal("857.35") applied to this invoice
|
|
83
|
+
stub.total # => BigDecimal("10983.65") the credit note's own total
|
|
84
|
+
stub.credit_note_id # => "4bbdbaaf-…" fetch it in full with this
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`applied_amount` and `total` answer different questions, and they differ on
|
|
88
|
+
8 of the 11 stubs in this gem's recorded response. Reaching for `total`
|
|
89
|
+
to ask "how much was applied to this invoice" overstates it.
|
|
90
|
+
|
|
91
|
+
A stub carries only `credit_note_id`, `applied_amount`, `total` and `date`.
|
|
92
|
+
Everything else is `nil` or empty, including `line_items` — fetch the
|
|
93
|
+
credit note by its ID when you need the rest of it.
|
|
94
|
+
|
|
69
95
|
## Predicates
|
|
70
96
|
|
|
71
97
|
```ruby
|
data/docs/accounting/invoice.md
CHANGED
|
@@ -31,7 +31,7 @@ items. Fetch an individual invoice for full details including line items.
|
|
|
31
31
|
|
|
32
32
|
```ruby
|
|
33
33
|
inv = client.invoice(tenant_id, "243216c5-369e-4056-ac67-05388f86dc81")
|
|
34
|
-
inv.total # => "2025.00"
|
|
34
|
+
inv.total # => BigDecimal("2025.00")
|
|
35
35
|
inv.invoice_number # => "OIT00546"
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -54,31 +54,32 @@ cover both the list and single-invoice responses:
|
|
|
54
54
|
| `status` | `String` | e.g. `"DRAFT"`, `"SUBMITTED"`, `"AUTHORISED"`, `"PAID"`, `"VOIDED"`, `"DELETED"`. |
|
|
55
55
|
| `line_amount_types` | `String` | `"Inclusive"`, `"Exclusive"`, or `"NoTax"`. |
|
|
56
56
|
| `line_items` | `Array<XeroKiwi::Accounting::LineItem>` | The line items (empty on list, populated on single). See [Prepayments — LineItem](prepayment.md#the-lineitem-object). |
|
|
57
|
-
| `sub_total` | `
|
|
58
|
-
| `total_tax` | `
|
|
59
|
-
| `total` | `
|
|
60
|
-
| `total_discount` | `
|
|
57
|
+
| `sub_total` | `BigDecimal` | The subtotal excluding taxes. |
|
|
58
|
+
| `total_tax` | `BigDecimal` | The total tax amount. |
|
|
59
|
+
| `total` | `BigDecimal` | The total (subtotal + total tax). |
|
|
60
|
+
| `total_discount` | `BigDecimal` | Total discounts on line items. |
|
|
61
61
|
| `updated_date_utc` | `Time` | When the invoice was last modified, parsed as UTC. |
|
|
62
62
|
| `currency_code` | `String` | The currency code (e.g. `"NZD"`). |
|
|
63
|
-
| `currency_rate` | `
|
|
63
|
+
| `currency_rate` | `BigDecimal` | The currency rate (1.0 for base currency). |
|
|
64
64
|
| `reference` | `String` | Additional reference number (ACCREC only). |
|
|
65
65
|
| `branding_theme_id` | `String` | The branding theme ID. |
|
|
66
66
|
| `url` | `String` | URL link to a source document. |
|
|
67
67
|
| `sent_to_contact` | `Boolean` | Whether the invoice displays as "sent" in Xero. |
|
|
68
68
|
| `expected_payment_date` | `Time` | Expected payment date (ACCREC only). |
|
|
69
69
|
| `planned_payment_date` | `Time` | Planned payment date (ACCPAY only). |
|
|
70
|
+
| `is_discounted` | `Boolean` | Whether any line item carries a discount. |
|
|
71
|
+
| `has_errors` | `Boolean` | Whether Xero flagged validation errors on the invoice. |
|
|
70
72
|
| `has_attachments` | `Boolean` | Whether the invoice has attachments. |
|
|
71
73
|
| `repeating_invoice_id` | `String` | The repeating invoice template ID, if applicable. |
|
|
72
74
|
| `payments` | `Array<XeroKiwi::Accounting::Payment>` | Payment records (references). See [Payments](payment.md). |
|
|
73
75
|
| `credit_notes` | `Array<Hash>` | Credit notes applied (raw hashes). |
|
|
74
76
|
| `prepayments` | `Array<Hash>` | Prepayments applied (raw hashes). |
|
|
75
77
|
| `overpayments` | `Array<Hash>` | Overpayments applied (raw hashes). |
|
|
76
|
-
| `amount_due` | `
|
|
77
|
-
| `amount_paid` | `
|
|
78
|
-
| `amount_credited` | `
|
|
79
|
-
| `cis_deduction` | `
|
|
78
|
+
| `amount_due` | `BigDecimal` | Amount remaining to be paid. |
|
|
79
|
+
| `amount_paid` | `BigDecimal` | Sum of payments received. |
|
|
80
|
+
| `amount_credited` | `BigDecimal` | Sum of credit notes, overpayments, and prepayments applied. |
|
|
81
|
+
| `cis_deduction` | `BigDecimal` | CIS deduction (UK Construction Industry Scheme only). |
|
|
80
82
|
| `fully_paid_on_date` | `Time` | When the invoice was fully paid, parsed as UTC. |
|
|
81
|
-
| `sales_tax_calculation_type_code` | `String` | US auto sales tax calculation type. |
|
|
82
83
|
| `invoice_addresses` | `Array<Hash>` | Invoice addresses (US auto sales tax only). |
|
|
83
84
|
|
|
84
85
|
## Predicates
|
|
@@ -44,6 +44,7 @@ Xero returns:
|
|
|
44
44
|
| `registration_number` | `String` | The company registration number. |
|
|
45
45
|
| `employer_identification_number` | `String` | The EIN (US organisations). |
|
|
46
46
|
| `tax_number` | `String` | The tax/VAT number. |
|
|
47
|
+
| `tax_number_name` | `String` | What the organisation's locale calls that number, e.g. `"VAT Number"`. |
|
|
47
48
|
| `financial_year_end_day` | `Integer` | Day of month the financial year ends (1–31). |
|
|
48
49
|
| `financial_year_end_month` | `Integer` | Month the financial year ends (1–12). |
|
|
49
50
|
| `sales_tax_basis` | `String` | e.g. `"Payments"`, `"CASH"`, `"ACCRUALS"`. |
|
|
@@ -29,7 +29,7 @@ overpayments = client.overpayments(connection)
|
|
|
29
29
|
|
|
30
30
|
```ruby
|
|
31
31
|
op = client.overpayment(tenant_id, "aea95d78-ea48-456b-9b08-6bc012600072")
|
|
32
|
-
op.total # => "100.00"
|
|
32
|
+
op.total # => BigDecimal("100.00")
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
`client.overpayment` hits `GET /api.xro/2.0/Overpayments/{OverpaymentID}` and
|
|
@@ -50,18 +50,42 @@ fields Xero returns:
|
|
|
50
50
|
| `status` | `String` | e.g. `"AUTHORISED"`, `"PAID"`, `"VOIDED"`. |
|
|
51
51
|
| `line_amount_types` | `String` | `"Inclusive"`, `"Exclusive"`, or `"NoTax"`. |
|
|
52
52
|
| `line_items` | `Array<XeroKiwi::Accounting::LineItem>` | The line items. See [Prepayments — LineItem](prepayment.md#the-lineitem-object). |
|
|
53
|
-
| `sub_total` | `
|
|
54
|
-
| `total_tax` | `
|
|
55
|
-
| `total` | `
|
|
53
|
+
| `sub_total` | `BigDecimal` | The subtotal excluding taxes. |
|
|
54
|
+
| `total_tax` | `BigDecimal` | The total tax amount. |
|
|
55
|
+
| `total` | `BigDecimal` | The total (subtotal + total tax). |
|
|
56
|
+
| `applied_amount` | `BigDecimal` | Only set when this overpayment is nested under an invoice, where it is the amount applied to *that* invoice — not the overpayment total. `nil` on an overpayment fetched in its own right. See [Nested under an invoice](#nested-under-an-invoice). |
|
|
56
57
|
| `updated_date_utc` | `Time` | When the overpayment was last modified, parsed as UTC. |
|
|
57
58
|
| `currency_code` | `String` | The currency code (e.g. `"NZD"`). |
|
|
58
|
-
| `currency_rate` | `
|
|
59
|
-
| `remaining_credit` | `
|
|
59
|
+
| `currency_rate` | `BigDecimal` | The currency rate (1.0 for base currency). |
|
|
60
|
+
| `remaining_credit` | `BigDecimal` | The remaining credit balance. |
|
|
60
61
|
| `allocations` | `Array<XeroKiwi::Accounting::Allocation>` | Allocations to invoices. Each has `allocation_id`, the allocated value (as both `amount` and `applied_amount` — see below), `date`, `is_deleted`, and an `invoice` reference. |
|
|
61
62
|
| `payments` | `Array<XeroKiwi::Accounting::Payment>` | Payment records (references). See [Payments](payment.md). |
|
|
62
63
|
| `has_attachments` | `Boolean` | Whether the overpayment has attachments. |
|
|
63
64
|
| `reference` | `String` | Reference for the overpayment. |
|
|
64
65
|
|
|
66
|
+
## Nested under an invoice
|
|
67
|
+
|
|
68
|
+
`invoice.overpayments` does not return whole overpayments. Xero nests an
|
|
69
|
+
**allocation stub** there — a partial overpayment describing how much of it
|
|
70
|
+
was applied to *that* invoice:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
invoice = client.invoices(tenant_id).first
|
|
74
|
+
stub = invoice.overpayments.find { |s| s.applied_amount != s.total }
|
|
75
|
+
|
|
76
|
+
stub.applied_amount # => BigDecimal("1366.2") applied to this invoice
|
|
77
|
+
stub.total # => BigDecimal("1367.0") the overpayment's own total
|
|
78
|
+
stub.overpayment_id # => "c678f3f6-…" fetch it in full with this
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`applied_amount` and `total` answer different questions, and they differ on
|
|
82
|
+
4 of the 4 stubs in this gem's recorded response. Reaching for `total`
|
|
83
|
+
to ask "how much was applied to this invoice" overstates it.
|
|
84
|
+
|
|
85
|
+
A stub carries only `overpayment_id`, `applied_amount`, `total` and `date`.
|
|
86
|
+
Everything else is `nil` or empty, including `line_items` — fetch the
|
|
87
|
+
overpayment by its ID when you need the rest of it.
|
|
88
|
+
|
|
65
89
|
## Predicates
|
|
66
90
|
|
|
67
91
|
```ruby
|
data/docs/accounting/payment.md
CHANGED
|
@@ -44,10 +44,11 @@ fields Xero returns:
|
|
|
44
44
|
|-----------|------|------------|
|
|
45
45
|
| `payment_id` | `String` | The unique Xero identifier. |
|
|
46
46
|
| `date` | `Time` | The date the payment was made, parsed as UTC. |
|
|
47
|
-
| `currency_rate` | `
|
|
48
|
-
| `amount` | `
|
|
49
|
-
| `bank_amount` | `
|
|
47
|
+
| `currency_rate` | `BigDecimal` | Exchange rate (1.0 for base currency). |
|
|
48
|
+
| `amount` | `BigDecimal` | The payment amount in the invoice's currency. |
|
|
49
|
+
| `bank_amount` | `BigDecimal` | The payment amount in the account's currency. |
|
|
50
50
|
| `reference` | `String` | An optional description for the payment. |
|
|
51
|
+
| `has_validation_errors` | `Boolean` | Whether Xero flagged validation errors on the payment. |
|
|
51
52
|
| `is_reconciled` | `Boolean` | Whether the payment has been reconciled. |
|
|
52
53
|
| `status` | `String` | e.g. `"AUTHORISED"`, `"DELETED"`. |
|
|
53
54
|
| `payment_type` | `String` | e.g. `"ACCRECPAYMENT"`, `"ACCPAYPAYMENT"`. See Xero docs for all types. |
|
|
@@ -29,7 +29,7 @@ prepayments = client.prepayments(connection)
|
|
|
29
29
|
|
|
30
30
|
```ruby
|
|
31
31
|
prepayment = client.prepayment(tenant_id, "aea95d78-ea48-456b-9b08-6bc012600072")
|
|
32
|
-
prepayment.total # => "100.00"
|
|
32
|
+
prepayment.total # => BigDecimal("100.00")
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
`client.prepayment` hits `GET /api.xro/2.0/Prepayments/{PrepaymentID}` and
|
|
@@ -50,17 +50,19 @@ fields Xero returns:
|
|
|
50
50
|
| `status` | `String` | e.g. `"AUTHORISED"`, `"PAID"`, `"VOIDED"`. |
|
|
51
51
|
| `line_amount_types` | `String` | `"Inclusive"`, `"Exclusive"`, or `"NoTax"`. |
|
|
52
52
|
| `line_items` | `Array<XeroKiwi::Accounting::LineItem>` | The line items. See [LineItem](#the-lineitem-object). |
|
|
53
|
-
| `sub_total` | `
|
|
54
|
-
| `total_tax` | `
|
|
55
|
-
| `total` | `
|
|
53
|
+
| `sub_total` | `BigDecimal` | The subtotal excluding taxes. |
|
|
54
|
+
| `total_tax` | `BigDecimal` | The total tax amount. |
|
|
55
|
+
| `total` | `BigDecimal` | The total (subtotal + total tax). |
|
|
56
|
+
| `applied_amount` | `BigDecimal` | Only set when this prepayment is nested under an invoice, where it is the amount applied to *that* invoice — not the prepayment total. `nil` on a prepayment fetched in its own right. See [Nested under an invoice](#nested-under-an-invoice). |
|
|
56
57
|
| `updated_date_utc` | `Time` | When the prepayment was last modified, parsed as UTC. |
|
|
57
58
|
| `currency_code` | `String` | The currency code (e.g. `"NZD"`). |
|
|
58
|
-
| `currency_rate` | `
|
|
59
|
+
| `currency_rate` | `BigDecimal` | The currency rate (1.0 for base currency). |
|
|
59
60
|
| `invoice_number` | `String` | The invoice number (for receive prepayments only). |
|
|
60
61
|
| `reference` | `String` | The prepayment reference, as on credit notes and overpayments. Nil when Xero sends it empty. |
|
|
61
|
-
| `remaining_credit` | `
|
|
62
|
+
| `remaining_credit` | `BigDecimal` | The remaining credit balance. |
|
|
62
63
|
| `allocations` | `Array<XeroKiwi::Accounting::Allocation>` | Allocations to invoices. Each has `allocation_id`, the allocated value (as both `amount` and `applied_amount` — see below), `date`, `is_deleted`, and an `invoice` reference. |
|
|
63
64
|
| `payments` | `Array<XeroKiwi::Accounting::Payment>` | Payment records (references). See [Payments](payment.md). |
|
|
65
|
+
| `branding_theme_id` | `String` | The branding theme applied, as on credit notes and invoices. |
|
|
64
66
|
| `has_attachments` | `Boolean` | Whether the prepayment has attachments. |
|
|
65
67
|
| `fully_paid_on_date` | `Time` | When the prepayment was fully allocated, parsed as UTC. |
|
|
66
68
|
|
|
@@ -72,14 +74,38 @@ documents (prepayments, invoices, etc.):
|
|
|
72
74
|
| Attribute | Type | What it is |
|
|
73
75
|
|-----------|------|------------|
|
|
74
76
|
| `description` | `String` | Line item description. |
|
|
75
|
-
| `quantity` | `
|
|
76
|
-
| `unit_amount` | `
|
|
77
|
+
| `quantity` | `BigDecimal` | Quantity. |
|
|
78
|
+
| `unit_amount` | `BigDecimal` | Unit amount. |
|
|
77
79
|
| `account_code` | `String` | The account code. |
|
|
80
|
+
| `account_id` | `String` | The account ID. Xero sends this as `AccountID`; kiwi read `AccountId` until 0.10.0 and so returned `nil`. |
|
|
78
81
|
| `tax_type` | `String` | The tax type override. |
|
|
79
|
-
| `tax_amount` | `
|
|
80
|
-
| `line_amount` | `
|
|
82
|
+
| `tax_amount` | `BigDecimal` | The calculated tax amount. |
|
|
83
|
+
| `line_amount` | `BigDecimal` | The line total. |
|
|
81
84
|
| `tracking` | `Array<Hash>` | Tracking categories (raw, max 2 per line). |
|
|
82
85
|
|
|
86
|
+
## Nested under an invoice
|
|
87
|
+
|
|
88
|
+
`invoice.prepayments` does not return whole prepayments. Xero nests an
|
|
89
|
+
**allocation stub** there — a partial prepayment describing how much of it
|
|
90
|
+
was applied to *that* invoice:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
invoice = client.invoices(tenant_id).first
|
|
94
|
+
stub = invoice.prepayments.find { |s| s.applied_amount != s.total }
|
|
95
|
+
|
|
96
|
+
stub.applied_amount # => BigDecimal("999.99") applied to this invoice
|
|
97
|
+
stub.total # => BigDecimal("1784.87") the prepayment's own total
|
|
98
|
+
stub.prepayment_id # => "4a27d582-…" fetch it in full with this
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`applied_amount` and `total` answer different questions, and they differ on
|
|
102
|
+
7 of the 9 stubs in this gem's recorded response. Reaching for `total`
|
|
103
|
+
to ask "how much was applied to this invoice" overstates it.
|
|
104
|
+
|
|
105
|
+
A stub carries only `prepayment_id`, `applied_amount`, `total` and `date`.
|
|
106
|
+
Everything else is `nil` or empty, including `line_items` — fetch the
|
|
107
|
+
prepayment by its ID when you need the rest of it.
|
|
108
|
+
|
|
83
109
|
## Predicates
|
|
84
110
|
|
|
85
111
|
```ruby
|
data/docs/client.md
CHANGED
|
@@ -215,6 +215,58 @@ This is the third axis on which the two representations differ, after
|
|
|
215
215
|
nesting and key names. Treat "it's the same field, so it's the same value"
|
|
216
216
|
as the assumption to check rather than the one to rely on.
|
|
217
217
|
|
|
218
|
+
**Money is a `BigDecimal`, not a String and not a Float.** An XML client
|
|
219
|
+
hands you `"19812.97"`; Xero's JSON sends the number `19812.97`, which Ruby
|
|
220
|
+
parses as a Float. Kiwi converts every money field to `BigDecimal` on the
|
|
221
|
+
way in, because Floats cannot represent most decimal fractions and so
|
|
222
|
+
arithmetic between two of them drifts:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
17228.67 + 2584.3 # => 19812.969999999998 ← Float
|
|
226
|
+
invoice.sub_total + invoice.total_tax == invoice.total # => true
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
That identity failed on 3 of the 55 invoices in this gem's recorded
|
|
230
|
+
response as Floats, and on none of them as BigDecimals. Each individual
|
|
231
|
+
value was correct in both — it is only arithmetic between fields that goes
|
|
232
|
+
wrong, which is what makes it quiet.
|
|
233
|
+
|
|
234
|
+
For migration this mostly helps. Comparison is not a concern —
|
|
235
|
+
`BigDecimal("19812.97")` equals the Float `19812.97`, equals
|
|
236
|
+
`BigDecimal("19812.97")`, and `BigDecimal("100")` equals `100` — and a
|
|
237
|
+
`decimal`/`numeric` column takes it unchanged. Two things do change:
|
|
238
|
+
|
|
239
|
+
- **`to_s` gives `"0.1981297e5"`**, not `"19812.97"`. Use `to_s("F")` for a
|
|
240
|
+
plain decimal string.
|
|
241
|
+
- **`to_json` gives the string `"0.1981297e5"`**, where a Float gave the
|
|
242
|
+
number `19812.97`. Writing a **typed** money attribute into a `jsonb`
|
|
243
|
+
column or an API response changes the stored shape — from a JSON number
|
|
244
|
+
to a JSON string in scientific notation. Call `to_s("F")` or `to_f` on
|
|
245
|
+
the way in, depending on which you want.
|
|
246
|
+
|
|
247
|
+
**`raw` is not affected.** It holds Xero's parsed payload untouched, so a
|
|
248
|
+
money value in there is still a Float and still serialises as a JSON
|
|
249
|
+
number. A consumer whose `jsonb` columns are fed from `raw` sees no
|
|
250
|
+
change at all — that separation is the whole point of `raw`.
|
|
251
|
+
|
|
252
|
+
`is_a?(Float)` is also now false, though `is_a?(Numeric)` still holds.
|
|
253
|
+
|
|
254
|
+
**Check for `.to_f` on money you persist.** Converting back to Float
|
|
255
|
+
immediately undoes this change, and does so silently: a `decimal(19,4)`
|
|
256
|
+
column holds more precision than a Float can carry, so the round trip loses
|
|
257
|
+
digits rather than raising.
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
BigDecimal("1234567890123.4567").round(4) # => 1234567890123.4567
|
|
261
|
+
BigDecimal("1234567890123.4567").to_f.to_d.round(4) # => 1234567890123.456
|
|
262
|
+
BigDecimal("999999999999999.9999").to_f.to_d.round(4) # => 1000000000000000.0
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Out of reach for currencies like GBP or ZAR at ordinary invoice sizes, not
|
|
266
|
+
for IDR or VND. A `.to_f` left over from an XML-era client — where the value
|
|
267
|
+
arrived as a String and had to be converted — is the likely place to find
|
|
268
|
+
one.
|
|
269
|
+
|
|
218
270
|
One piece of luck worth knowing about: the two piles tend to fail
|
|
219
271
|
differently. A `dig` that misses returns nil and writes a blank record
|
|
220
272
|
quietly. Code that assumed a Hash, such as `Array#to_h` on what is now a
|
|
@@ -226,7 +278,7 @@ review time for the silent ones.
|
|
|
226
278
|
|
|
227
279
|
| Method | Returns | Purpose |
|
|
228
280
|
|--------|---------|---------|
|
|
229
|
-
| `client.connections` | `Array<XeroKiwi::Connection>` | Fetch the tenants this token is authorised against. See [Connections](connections.md). |
|
|
281
|
+
| `client.connections(tenant_id: nil)` | `Array<XeroKiwi::Connection>` | Fetch the tenants this token is authorised against. `tenant_id:` is required on a client-credentials token and unused on a user token. See [Connections](connections.md). |
|
|
230
282
|
| `client.contacts(tenant_id_or_connection)` | `Array<XeroKiwi::Accounting::Contact>` | Fetch the contacts for a tenant. See [Contacts](accounting/contact.md). |
|
|
231
283
|
| `client.contact(tenant_id_or_connection, contact_id)` | `XeroKiwi::Accounting::Contact` | Fetch a single contact by ID. See [Contacts](accounting/contact.md). |
|
|
232
284
|
| `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). |
|
data/docs/connections.md
CHANGED
|
@@ -26,6 +26,21 @@ if the user authorised your app but didn't pick any tenants (rare, but
|
|
|
26
26
|
possible). The endpoint doesn't take any filtering parameters — Xero returns
|
|
27
27
|
everything the token has access to.
|
|
28
28
|
|
|
29
|
+
### On a client-credentials token, pass `tenant_id:`
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
client.connections(tenant_id: org_tenant_id)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Everything above assumes a **user** token, where no argument is needed. An
|
|
36
|
+
app token from the [`client_credentials` grant](oauth.md#the-client-credentials-grant-no-user)
|
|
37
|
+
identifies no user, so Xero can't work out whose connections to list and
|
|
38
|
+
answers `400 Xero-User-Id and/or Xero-Tenant-Id header must be supplied.`
|
|
39
|
+
|
|
40
|
+
`tenant_id:` takes a String or a `XeroKiwi::Connection`, and sets the
|
|
41
|
+
`Xero-Tenant-Id` header. Omitting it sends no header, which is what a user
|
|
42
|
+
token wants. `delete_connection` needs no tenant header on either.
|
|
43
|
+
|
|
29
44
|
## The Connection object
|
|
30
45
|
|
|
31
46
|
Each `XeroKiwi::Connection` is an immutable value object exposing the fields Xero
|