xero-kiwi 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 775f2c27c6c923aa8ab8c2cbb525d83ab7add7c174315e555428ccd45e520836
4
- data.tar.gz: 5d5572e18a023da88435a091636094eae2800442348eaf6e9c19c25b1127e1fd
3
+ metadata.gz: 4deb1de10e85de555f19546d2d2243135b2f95fff462e45a2cac72e8622a9e31
4
+ data.tar.gz: 46dfc6f3e6eeeda92546e95b3161aea9802fb401e823d0f2ca67d91bab3555d7
5
5
  SHA512:
6
- metadata.gz: f2f8890ad95b9c4c037a4a7d7470b3b32581730960552f6a21c436f3a9209bbf5af3a9ee78467c230c805d5d86de129f4510021098cc4dc2b2c5100e8bc2862c
7
- data.tar.gz: 7fd1302cfac5d9e5b2e5a3b0c15a720d9ad08b02d550a65c70e4fb900fa7461ad7be9d3d37c4325aa2d6b2072ac35a6ef5a21b003b63e2cf2561ddf3f4df1e8f
6
+ metadata.gz: 781f5c1cfde039f7746984185d218d2fa9e07ceb66cb660d7227291f7e45210f97f590a97cb0d24dfa7c265843c98a37b3d4fd857b41d6cd6ab7e53d1db8d646
7
+ data.tar.gz: 727152993b2424d6513c44947db8b75cd4b1a8734b000e390644c59154ba72804cd48cf16b36e0df299df250b1b6a5eebeace82aba0319adc924a30125f5bdc6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.1] - 2026-04-17
4
+
5
+ ### Changed
6
+
7
+ - Internal refactor of the accounting resource classes. Each resource now declares its fields through a shared `attribute` DSL (`lib/xero_kiwi/accounting/resource.rb`) rather than an `ATTRIBUTES` constant + hand-written `initialize`. Hydration logic (including the `/Date(ms)/` and ISO 8601 parsing previously duplicated across nine files) lives in a single `XeroKiwi::Accounting::Hydrator` module. The mixin also provides default `==` / `eql?` / `hash` (via an `identity :xxx_id` declaration for resources with a server-side primary key, structural `to_h`-based otherwise) and an ActiveRecord-style `inspect` that shows every attribute inline — nested objects collapse to a one-line reference and collections to a `[N items]` summary. No public API changes — constructor signatures and return types are preserved.
8
+
3
9
  ## [0.2.0] - 2026-04-15
4
10
 
5
11
  ### Added
data/Rakefile CHANGED
@@ -57,7 +57,7 @@ def append_file_block(out, path)
57
57
  out << "\n" << separator << "\n"
58
58
  out << "FILE: #{path}\n"
59
59
  out << separator << "\n\n"
60
- out << File.read(path) << "\n"
60
+ out << File.read(path, encoding: "UTF-8") << "\n"
61
61
  end
62
62
 
63
63
  namespace :llms do
@@ -0,0 +1,301 @@
1
+ # Attribute DSL Refactor
2
+
3
+ ## Context
4
+
5
+ Every accounting resource class in kiwi repeats the same pattern: an
6
+ `ATTRIBUTES` name-map constant, an `attr_reader(*ATTRIBUTES.keys)` call, and a
7
+ hand-written `initialize` that duplicates hydration logic across nine files.
8
+ `parse_time` alone is copy-pasted into every resource. Nested-object and
9
+ collection hydration (`Contact.new(attrs, reference: true)`,
10
+ `(attrs["LineItems"] || []).map { … }`) follows an identical shape in every
11
+ class but is written by hand each time.
12
+
13
+ This is cheap today but will bite us soon. The upcoming query work
14
+ (filtering/sorting/pagination) needs field-type metadata per attribute — adding
15
+ a second `FIELDS` constant next to `ATTRIBUTES` would double the duplication.
16
+ Writer support down the line will need serialisation metadata too. Both extend
17
+ cleanly from a single attribute declaration.
18
+
19
+ This plan extracts a small `attribute` DSL and migrates every accounting
20
+ resource to it. No behaviour change, no public API change — constructors keep
21
+ the same signature, existing specs keep passing. It's pure preparation for the
22
+ 0.3.0 querying work (tracked separately) and for writer support later.
23
+
24
+ ## Decisions locked in
25
+
26
+ - **Reader-only.** No serialisation/writer concerns in this refactor. DSL
27
+ naming (`attribute`, not `reader` or `field`) leaves room for it later.
28
+ - **No query metadata yet.** The `query: true` flag and `query_fields` map
29
+ come with the querying plan — not now. One concern at a time.
30
+ - **Public API unchanged.** `Invoice.new(hash, reference: …)` keeps working
31
+ identically. `to_h`, `attr_reader`s, `==`, `hash`, `inspect`, resource-specific
32
+ helper methods (`accounts_receivable?`, etc.) all stay.
33
+ - **Every accounting class migrates** — including nested value types
34
+ (`Address`, `Phone`, `LineItem`, `ContactPerson`, `ExternalLink`,
35
+ `PaymentTerm`, any others). Partial migration would leave two patterns
36
+ coexisting, which is worse than either alone.
37
+ - **Version: flagged, not decided.** Internal refactor with no public API
38
+ change — defensibly a patch (`0.2.1`). Could also bundle under `0.3.0` since
39
+ that's the version the querying work will ship under and this is its
40
+ foundation. Confirm before bumping.
41
+
42
+ ## Design
43
+
44
+ ### 1. `XeroKiwi::Accounting::Resource` mixin
45
+
46
+ ```ruby
47
+ # lib/xero_kiwi/accounting/resource.rb
48
+ module XeroKiwi
49
+ module Accounting
50
+ module Resource
51
+ def self.included(base) = base.extend(ClassMethods)
52
+
53
+ module ClassMethods
54
+ def payload_key(key) = @payload_key = key
55
+
56
+ def attribute(name, xero:, type: :string, of: nil, hydrate: nil)
57
+ attributes[name] = { xero: xero, type: type, of: of, hydrate: hydrate }
58
+ attr_reader name
59
+ end
60
+
61
+ def attributes = (@attributes ||= {})
62
+
63
+ def from_response(payload)
64
+ return [] if payload.nil?
65
+
66
+ items = payload[@payload_key]
67
+ return [] if items.nil?
68
+
69
+ items.map { |attrs| new(attrs) }
70
+ end
71
+ end
72
+
73
+ def initialize(attrs, reference: false)
74
+ attrs = attrs.transform_keys(&:to_s)
75
+ @is_reference = reference
76
+
77
+ self.class.attributes.each do |name, spec|
78
+ value = Hydrator.call(attrs[spec[:xero]], spec)
79
+ instance_variable_set("@#{name}", value)
80
+ end
81
+ end
82
+
83
+ def reference? = @is_reference
84
+
85
+ def to_h
86
+ self.class.attributes.keys.to_h { |k| [k, public_send(k)] }
87
+ end
88
+ end
89
+ end
90
+ end
91
+ ```
92
+
93
+ ### 2. `XeroKiwi::Accounting::Hydrator`
94
+
95
+ Shared hydration dispatch and the single home for `parse_time`.
96
+
97
+ ```ruby
98
+ # lib/xero_kiwi/accounting/hydrator.rb
99
+ module XeroKiwi
100
+ module Accounting
101
+ module Hydrator
102
+ module_function
103
+
104
+ def call(raw, spec)
105
+ return spec[:hydrate].call(raw) if spec[:hydrate]
106
+ return [] if spec[:type] == :collection && raw.nil?
107
+ return nil if raw.nil?
108
+
109
+ case spec[:type]
110
+ when :string, :enum, :guid, :bool, :decimal
111
+ raw
112
+ when :date
113
+ parse_time(raw)
114
+ when :object
115
+ spec[:of].new(raw, reference: true)
116
+ when :collection
117
+ raw.map { |item| spec[:of].new(item) }
118
+ else
119
+ raise ArgumentError, "unknown attribute type: #{spec[:type]}"
120
+ end
121
+ end
122
+
123
+ def parse_time(value)
124
+ return nil if value.nil?
125
+
126
+ str = value.to_s.strip
127
+ return nil if str.empty?
128
+
129
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
130
+ Time.at(match[1].to_i / 1000.0).utc
131
+ else
132
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
133
+ Time.iso8601(str)
134
+ end
135
+ rescue ArgumentError
136
+ nil
137
+ end
138
+ end
139
+ end
140
+ end
141
+ ```
142
+
143
+ ### 3. Supported attribute types
144
+
145
+ | Type | Hydrates to | Notes |
146
+ |---------------|-------------------------------------------------|--------------------------------------------|
147
+ | `:string` | value as-is | default |
148
+ | `:enum` | value as-is | semantic marker for future validation |
149
+ | `:guid` | value as-is | semantic marker for future query typing |
150
+ | `:bool` | value as-is | |
151
+ | `:decimal` | value as-is | Xero returns these as numbers or strings |
152
+ | `:date` | `Time` via `Hydrator.parse_time` | handles `/Date(ms)/` and ISO8601 |
153
+ | `:object` | `spec[:of].new(raw, reference: true)` | requires `of:` |
154
+ | `:collection` | `raw.map { spec[:of].new(item) }`, `nil` → `[]` | requires `of:` |
155
+
156
+ Escape hatch: `hydrate: ->(raw) { … }` for one-off fields the built-in types
157
+ can't express. Runs before dispatch.
158
+
159
+ ### 4. Before / after (example)
160
+
161
+ Invoice before — 40+ lines of `initialize`, `parse_time` method, `ATTRIBUTES`
162
+ constant duplicated:
163
+
164
+ ```ruby
165
+ ATTRIBUTES = { invoice_id: "InvoiceID", … }.freeze
166
+ attr_reader(*ATTRIBUTES.keys)
167
+
168
+ def initialize(attrs, reference: false)
169
+ attrs = attrs.transform_keys(&:to_s)
170
+ @is_reference = reference
171
+ @invoice_id = attrs["InvoiceID"]
172
+ @invoice_number = attrs["InvoiceNumber"]
173
+ @contact = attrs["Contact"] ? Contact.new(attrs["Contact"], reference: true) : nil
174
+ @date = parse_time(attrs["Date"])
175
+ @line_items = (attrs["LineItems"] || []).map { |li| LineItem.new(li) }
176
+ # … 30 more lines …
177
+ end
178
+
179
+ private
180
+
181
+ def parse_time(value)
182
+ # … 15 lines, duplicated in 9 files …
183
+ end
184
+ ```
185
+
186
+ After — one declaration per field, no `initialize`, no `parse_time`:
187
+
188
+ ```ruby
189
+ include Accounting::Resource
190
+
191
+ payload_key "Invoices"
192
+
193
+ attribute :invoice_id, xero: "InvoiceID", type: :guid
194
+ attribute :invoice_number, xero: "InvoiceNumber", type: :string
195
+ attribute :contact, xero: "Contact", type: :object, of: Contact
196
+ attribute :date, xero: "Date", type: :date
197
+ attribute :line_items, xero: "LineItems", type: :collection, of: LineItem
198
+ # …
199
+
200
+ def accounts_receivable? = type == "ACCREC"
201
+ def accounts_payable? = type == "ACCPAY"
202
+
203
+ def ==(other) = other.is_a?(Invoice) && other.invoice_id == invoice_id
204
+ alias eql? ==
205
+ def hash = [self.class, invoice_id].hash
206
+ ```
207
+
208
+ ### 5. Edge cases to preserve
209
+
210
+ - **Raw pass-through fields** that are currently kept as plain hashes/arrays
211
+ (e.g. `Invoice#invoice_addresses`, `Contact#bank_account_details`) — use
212
+ `type: :string` (misnomer but harmless) or the `hydrate:` escape hatch with
213
+ an identity lambda. Audit each during migration.
214
+ - **`reference: true` semantics** — nested `:object` attributes always hydrate
215
+ with `reference: true`; nested `:collection` attributes hydrate without it
216
+ (full objects). This matches today's behaviour per-file. If any class
217
+ currently deviates, that deviation gets a `hydrate:` escape hatch.
218
+ - **Resource-specific helpers** (`accounts_receivable?`, `reference?`,
219
+ `inspect`, `==`, `hash`) stay inline on each class.
220
+ - **Classes without list endpoints** (nested value types — `Address`, `Phone`,
221
+ etc.) use the DSL but don't call `payload_key`. `from_response` on them isn't
222
+ called; the method existing but unused is harmless.
223
+
224
+ ## Files
225
+
226
+ ### Create
227
+
228
+ - `lib/xero_kiwi/accounting/resource.rb` — the DSL mixin.
229
+ - `lib/xero_kiwi/accounting/hydrator.rb` — shared hydration + `parse_time`.
230
+ - `spec/xero_kiwi/accounting/resource_spec.rb` — DSL unit tests.
231
+ - `spec/xero_kiwi/accounting/hydrator_spec.rb` — hydrator unit tests.
232
+
233
+ ### Modify
234
+
235
+ - `lib/xero_kiwi.rb` — `require` the new files before any accounting class.
236
+ - Every `lib/xero_kiwi/accounting/*.rb` — migrate to `attribute` DSL, drop
237
+ `ATTRIBUTES`, drop hand-written `initialize`, drop private `parse_time`.
238
+ Concrete list: `contact.rb`, `contact_group.rb`, `contact_person.rb`,
239
+ `invoice.rb`, `credit_note.rb`, `prepayment.rb`, `overpayment.rb`,
240
+ `payment.rb`, `user.rb`, `branding_theme.rb`, `organisation.rb`, `address.rb`,
241
+ `phone.rb`, `external_link.rb`, `payment_term.rb`, `line_item.rb`, and any
242
+ other value classes in `lib/xero_kiwi/accounting/`.
243
+ - `lib/xero_kiwi/version.rb` — bump (0.2.1 or 0.3.0 — confirm).
244
+ - `CHANGELOG.md` — **Changed** entry describing the internal refactor.
245
+ - `Gemfile.lock` — rebuild after version bump.
246
+
247
+ ## Testing strategy
248
+
249
+ - **Hydrator unit specs:** each type dispatches correctly. `/Date(ms)/` parses.
250
+ ISO8601 with and without timezone parses. Empty/invalid strings return
251
+ `nil`. `:object` hydrates nested reference. `:collection` with `nil` →
252
+ `[]`, populated → `map`ped. `:collection` with `nil` + `hydrate:` runs the
253
+ lambda. Unknown type raises.
254
+ - **Resource unit specs:** declare a throwaway class inside the spec with a
255
+ couple of attributes of each type, hydrate a fixture hash, assert every
256
+ reader returns the expected value, `to_h` returns a keyed hash, `payload_key`
257
+ + `from_response` parse an envelope.
258
+ - **Existing accounting resource specs carry the real load.** They already
259
+ call `Class.new(fixture_hash)` and assert every reader — they should pass
260
+ unchanged. A regression in the DSL surfaces as real-resource spec failures,
261
+ which is exactly what we want.
262
+ - **Client integration specs** — untouched, must still pass green.
263
+
264
+ ## Verification
265
+
266
+ ```sh
267
+ bundle install
268
+ bundle exec rspec # full suite green
269
+ bundle exec rspec spec/xero_kiwi/accounting # DSL + hydrator + every resource
270
+ bundle exec rspec spec/xero_kiwi/client_spec.rb # list methods unaffected
271
+ bundle exec rubocop # style still clean
272
+ ```
273
+
274
+ Smoke check in IRB against a recorded fixture or live tenant:
275
+
276
+ ```ruby
277
+ require "xero_kiwi"
278
+
279
+ client = XeroKiwi::Client.new(access_token: ENV.fetch("XERO_TOKEN"))
280
+ invoices = client.invoices(tenant)
281
+ inv = invoices.first
282
+
283
+ inv.invoice_id # string
284
+ inv.date # Time
285
+ inv.contact # Accounting::Contact, reference? == true
286
+ inv.line_items # Array<Accounting::LineItem>
287
+ inv.to_h.keys # every attribute name
288
+ ```
289
+
290
+ ## Follow-up
291
+
292
+ Once this lands, the 0.3.0 querying plan picks up by:
293
+
294
+ 1. Adding a `query: true` option to `attribute`.
295
+ 2. Auto-populating `query_fields` on the class from attributes flagged
296
+ queryable.
297
+ 3. Building `Query::Filter` / `Query::Order` compilers against that map.
298
+ 4. Adding the `Page` return type, `each_*` helpers, and `modified_since`
299
+ support on the client.
300
+
301
+ None of which requires re-touching the resource files.
@@ -6,53 +6,22 @@ module XeroKiwi
6
6
  #
7
7
  # See: https://developer.xero.com/documentation/api/accounting/types#addresses
8
8
  class Address
9
- ATTRIBUTES = {
10
- address_type: "AddressType",
11
- address_line_1: "AddressLine1",
12
- address_line_2: "AddressLine2",
13
- address_line_3: "AddressLine3",
14
- address_line_4: "AddressLine4",
15
- city: "City",
16
- region: "Region",
17
- postal_code: "PostalCode",
18
- country: "Country",
19
- attention_to: "AttentionTo"
20
- }.freeze
21
-
22
- attr_reader(*ATTRIBUTES.keys)
23
-
24
- def initialize(attrs)
25
- attrs = attrs.transform_keys(&:to_s)
26
- @address_type = attrs["AddressType"]
27
- @address_line_1 = attrs["AddressLine1"]
28
- @address_line_2 = attrs["AddressLine2"]
29
- @address_line_3 = attrs["AddressLine3"]
30
- @address_line_4 = attrs["AddressLine4"]
31
- @city = attrs["City"]
32
- @region = attrs["Region"]
33
- @postal_code = attrs["PostalCode"]
34
- @country = attrs["Country"]
35
- @attention_to = attrs["AttentionTo"]
36
- end
9
+ include Resource
10
+
11
+ attribute :address_type, xero: "AddressType"
12
+ attribute :address_line_1, xero: "AddressLine1"
13
+ attribute :address_line_2, xero: "AddressLine2"
14
+ attribute :address_line_3, xero: "AddressLine3"
15
+ attribute :address_line_4, xero: "AddressLine4"
16
+ attribute :city, xero: "City"
17
+ attribute :region, xero: "Region"
18
+ attribute :postal_code, xero: "PostalCode"
19
+ attribute :country, xero: "Country"
20
+ attribute :attention_to, xero: "AttentionTo"
37
21
 
38
22
  def street? = address_type == "STREET"
39
23
  def pobox? = address_type == "POBOX"
40
24
  def delivery? = address_type == "DELIVERY"
41
-
42
- def to_h
43
- ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
44
- end
45
-
46
- def ==(other)
47
- other.is_a?(Address) && to_h == other.to_h
48
- end
49
- alias eql? ==
50
-
51
- def hash = to_h.hash
52
-
53
- def inspect
54
- "#<#{self.class} type=#{address_type.inspect} city=#{city.inspect} country=#{country.inspect}>"
55
- end
56
25
  end
57
26
  end
58
27
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "time"
4
-
5
3
  module XeroKiwi
6
4
  module Accounting
7
5
  # Represents an allocation of a credit note, prepayment, or overpayment
@@ -9,58 +7,15 @@ module XeroKiwi
9
7
  #
10
8
  # See: https://developer.xero.com/documentation/api/accounting/overpayments
11
9
  class Allocation
12
- ATTRIBUTES = {
13
- allocation_id: "AllocationID",
14
- amount: "Amount",
15
- date: "Date",
16
- invoice: "Invoice",
17
- is_deleted: "IsDeleted"
18
- }.freeze
19
-
20
- attr_reader(*ATTRIBUTES.keys)
21
-
22
- def initialize(attrs)
23
- attrs = attrs.transform_keys(&:to_s)
24
- @allocation_id = attrs["AllocationID"]
25
- @amount = attrs["Amount"]
26
- @date = parse_time(attrs["Date"])
27
- @invoice = attrs["Invoice"] ? Invoice.new(attrs["Invoice"], reference: true) : nil
28
- @is_deleted = attrs["IsDeleted"]
29
- end
30
-
31
- def to_h
32
- ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
33
- end
34
-
35
- def ==(other)
36
- other.is_a?(Allocation) && other.allocation_id == allocation_id
37
- end
38
- alias eql? ==
39
-
40
- def hash = [self.class, allocation_id].hash
41
-
42
- def inspect
43
- "#<#{self.class} allocation_id=#{allocation_id.inspect} " \
44
- "amount=#{amount.inspect}>"
45
- end
46
-
47
- private
48
-
49
- def parse_time(value)
50
- return nil if value.nil?
10
+ include Resource
51
11
 
52
- str = value.to_s.strip
53
- return nil if str.empty?
12
+ identity :allocation_id
54
13
 
55
- if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
56
- Time.at(match[1].to_i / 1000.0).utc
57
- else
58
- str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
59
- Time.iso8601(str)
60
- end
61
- rescue ArgumentError
62
- nil
63
- end
14
+ attribute :allocation_id, xero: "AllocationID", type: :guid
15
+ attribute :amount, xero: "Amount", type: :decimal
16
+ attribute :date, xero: "Date", type: :date
17
+ attribute :invoice, xero: "Invoice", type: :object, of: Invoice, reference: true
18
+ attribute :is_deleted, xero: "IsDeleted", type: :bool
64
19
  end
65
20
  end
66
21
  end
@@ -1,76 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "time"
4
-
5
3
  module XeroKiwi
6
4
  module Accounting
7
5
  # Represents a Xero Branding Theme returned by the Accounting API.
8
6
  #
9
7
  # See: https://developer.xero.com/documentation/api/accounting/brandingthemes
10
8
  class BrandingTheme
11
- ATTRIBUTES = {
12
- branding_theme_id: "BrandingThemeID",
13
- name: "Name",
14
- logo_url: "LogoUrl",
15
- type: "Type",
16
- sort_order: "SortOrder",
17
- created_date_utc: "CreatedDateUTC"
18
- }.freeze
19
-
20
- attr_reader(*ATTRIBUTES.keys)
21
-
22
- def self.from_response(payload)
23
- return [] if payload.nil?
24
-
25
- items = payload["BrandingThemes"]
26
- return [] if items.nil?
27
-
28
- items.map { |attrs| new(attrs) }
29
- end
30
-
31
- def initialize(attrs)
32
- attrs = attrs.transform_keys(&:to_s)
33
- @branding_theme_id = attrs["BrandingThemeID"]
34
- @name = attrs["Name"]
35
- @logo_url = attrs["LogoUrl"]
36
- @type = attrs["Type"]
37
- @sort_order = attrs["SortOrder"]
38
- @created_date_utc = parse_time(attrs["CreatedDateUTC"])
39
- end
40
-
41
- def to_h
42
- ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
43
- end
44
-
45
- def ==(other)
46
- other.is_a?(BrandingTheme) && other.branding_theme_id == branding_theme_id
47
- end
48
- alias eql? ==
49
-
50
- def hash = [self.class, branding_theme_id].hash
51
-
52
- def inspect
53
- "#<#{self.class} branding_theme_id=#{branding_theme_id.inspect} " \
54
- "name=#{name.inspect} type=#{type.inspect}>"
55
- end
56
-
57
- private
58
-
59
- def parse_time(value)
60
- return nil if value.nil?
9
+ include Resource
61
10
 
62
- str = value.to_s.strip
63
- return nil if str.empty?
11
+ payload_key "BrandingThemes"
12
+ identity :branding_theme_id
64
13
 
65
- if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
66
- Time.at(match[1].to_i / 1000.0).utc
67
- else
68
- str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
69
- Time.iso8601(str)
70
- end
71
- rescue ArgumentError
72
- nil
73
- end
14
+ attribute :branding_theme_id, xero: "BrandingThemeID", type: :guid
15
+ attribute :name, xero: "Name"
16
+ attribute :logo_url, xero: "LogoUrl"
17
+ attribute :type, xero: "Type"
18
+ attribute :sort_order, xero: "SortOrder"
19
+ attribute :created_date_utc, xero: "CreatedDateUTC", type: :date
74
20
  end
75
21
  end
76
22
  end