xero-kiwi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +2 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +89 -0
  6. data/Rakefile +89 -0
  7. data/docs/accounting/address.md +54 -0
  8. data/docs/accounting/branding-theme.md +92 -0
  9. data/docs/accounting/contact-group.md +91 -0
  10. data/docs/accounting/contact.md +166 -0
  11. data/docs/accounting/credit-note.md +97 -0
  12. data/docs/accounting/external-link.md +33 -0
  13. data/docs/accounting/invoice.md +134 -0
  14. data/docs/accounting/organisation.md +119 -0
  15. data/docs/accounting/overpayment.md +94 -0
  16. data/docs/accounting/payment-terms.md +58 -0
  17. data/docs/accounting/payment.md +99 -0
  18. data/docs/accounting/phone.md +45 -0
  19. data/docs/accounting/prepayment.md +111 -0
  20. data/docs/accounting/user.md +109 -0
  21. data/docs/client.md +174 -0
  22. data/docs/connections.md +166 -0
  23. data/docs/errors.md +271 -0
  24. data/docs/getting-started.md +138 -0
  25. data/docs/oauth.md +508 -0
  26. data/docs/retries-and-rate-limits.md +224 -0
  27. data/docs/tokens.md +339 -0
  28. data/lib/xero_kiwi/accounting/address.rb +58 -0
  29. data/lib/xero_kiwi/accounting/allocation.rb +66 -0
  30. data/lib/xero_kiwi/accounting/branding_theme.rb +76 -0
  31. data/lib/xero_kiwi/accounting/contact.rb +153 -0
  32. data/lib/xero_kiwi/accounting/contact_group.rb +57 -0
  33. data/lib/xero_kiwi/accounting/contact_person.rb +45 -0
  34. data/lib/xero_kiwi/accounting/credit_note.rb +115 -0
  35. data/lib/xero_kiwi/accounting/external_link.rb +38 -0
  36. data/lib/xero_kiwi/accounting/invoice.rb +142 -0
  37. data/lib/xero_kiwi/accounting/line_item.rb +64 -0
  38. data/lib/xero_kiwi/accounting/organisation.rb +138 -0
  39. data/lib/xero_kiwi/accounting/overpayment.rb +107 -0
  40. data/lib/xero_kiwi/accounting/payment.rb +105 -0
  41. data/lib/xero_kiwi/accounting/payment_terms.rb +77 -0
  42. data/lib/xero_kiwi/accounting/phone.rb +46 -0
  43. data/lib/xero_kiwi/accounting/prepayment.rb +109 -0
  44. data/lib/xero_kiwi/accounting/tracking_category.rb +42 -0
  45. data/lib/xero_kiwi/accounting/user.rb +80 -0
  46. data/lib/xero_kiwi/client.rb +576 -0
  47. data/lib/xero_kiwi/connection.rb +78 -0
  48. data/lib/xero_kiwi/errors.rb +34 -0
  49. data/lib/xero_kiwi/identity.rb +40 -0
  50. data/lib/xero_kiwi/oauth/id_token.rb +102 -0
  51. data/lib/xero_kiwi/oauth/pkce.rb +51 -0
  52. data/lib/xero_kiwi/oauth.rb +232 -0
  53. data/lib/xero_kiwi/token.rb +99 -0
  54. data/lib/xero_kiwi/token_refresher.rb +53 -0
  55. data/lib/xero_kiwi/version.rb +5 -0
  56. data/lib/xero_kiwi.rb +33 -0
  57. data/llms-full.txt +3351 -0
  58. data/llms.txt +56 -0
  59. data/sig/xero_kiwi.rbs +4 -0
  60. metadata +164 -0
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XeroKiwi
4
+ module Accounting
5
+ # Represents a line item nested within Xero documents (invoices, credit
6
+ # notes, prepayments, overpayments).
7
+ #
8
+ # See: https://developer.xero.com/documentation/api/accounting/invoices
9
+ class LineItem
10
+ ATTRIBUTES = {
11
+ line_item_id: "LineItemID",
12
+ description: "Description",
13
+ quantity: "Quantity",
14
+ unit_amount: "UnitAmount",
15
+ item_code: "ItemCode",
16
+ account_code: "AccountCode",
17
+ account_id: "AccountId",
18
+ tax_type: "TaxType",
19
+ tax_amount: "TaxAmount",
20
+ line_amount: "LineAmount",
21
+ discount_rate: "DiscountRate",
22
+ discount_amount: "DiscountAmount",
23
+ tracking: "Tracking",
24
+ item: "Item"
25
+ }.freeze
26
+
27
+ attr_reader(*ATTRIBUTES.keys)
28
+
29
+ def initialize(attrs) # rubocop:disable Metrics/AbcSize
30
+ attrs = attrs.transform_keys(&:to_s)
31
+ @line_item_id = attrs["LineItemID"]
32
+ @description = attrs["Description"]
33
+ @quantity = attrs["Quantity"]
34
+ @unit_amount = attrs["UnitAmount"]
35
+ @item_code = attrs["ItemCode"]
36
+ @account_code = attrs["AccountCode"]
37
+ @account_id = attrs["AccountId"]
38
+ @tax_type = attrs["TaxType"]
39
+ @tax_amount = attrs["TaxAmount"]
40
+ @line_amount = attrs["LineAmount"]
41
+ @discount_rate = attrs["DiscountRate"]
42
+ @discount_amount = attrs["DiscountAmount"]
43
+ @tracking = (attrs["Tracking"] || []).map { |t| TrackingCategory.new(t) }
44
+ @item = attrs["Item"]
45
+ end
46
+
47
+ def to_h
48
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
49
+ end
50
+
51
+ def ==(other)
52
+ other.is_a?(LineItem) && to_h == other.to_h
53
+ end
54
+ alias eql? ==
55
+
56
+ def hash = to_h.hash
57
+
58
+ def inspect
59
+ "#<#{self.class} description=#{description.inspect} " \
60
+ "line_amount=#{line_amount.inspect}>"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Organisation (tenant) returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/organisation
10
+ class Organisation
11
+ ATTRIBUTES = {
12
+ organisation_id: "OrganisationID",
13
+ api_key: "APIKey",
14
+ name: "Name",
15
+ legal_name: "LegalName",
16
+ pays_tax: "PaysTax",
17
+ version: "Version",
18
+ organisation_type: "OrganisationType",
19
+ base_currency: "BaseCurrency",
20
+ country_code: "CountryCode",
21
+ is_demo_company: "IsDemoCompany",
22
+ organisation_status: "OrganisationStatus",
23
+ registration_number: "RegistrationNumber",
24
+ employer_identification_number: "EmployerIdentificationNumber",
25
+ tax_number: "TaxNumber",
26
+ financial_year_end_day: "FinancialYearEndDay",
27
+ financial_year_end_month: "FinancialYearEndMonth",
28
+ sales_tax_basis: "SalesTaxBasis",
29
+ sales_tax_period: "SalesTaxPeriod",
30
+ default_sales_tax: "DefaultSalesTax",
31
+ default_purchases_tax: "DefaultPurchasesTax",
32
+ period_lock_date: "PeriodLockDate",
33
+ end_of_year_lock_date: "EndOfYearLockDate",
34
+ created_date_utc: "CreatedDateUTC",
35
+ timezone: "Timezone",
36
+ organisation_entity_type: "OrganisationEntityType",
37
+ short_code: "ShortCode",
38
+ organisation_class: "Class",
39
+ edition: "Edition",
40
+ line_of_business: "LineOfBusiness",
41
+ addresses: "Addresses",
42
+ phones: "Phones",
43
+ external_links: "ExternalLinks",
44
+ payment_terms: "PaymentTerms"
45
+ }.freeze
46
+
47
+ attr_reader(*ATTRIBUTES.keys)
48
+
49
+ def self.from_response(payload)
50
+ return nil if payload.nil?
51
+
52
+ items = payload["Organisations"]
53
+ return nil if items.nil? || items.empty?
54
+
55
+ new(items.first)
56
+ end
57
+
58
+ def initialize(attrs) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
59
+ attrs = attrs.transform_keys(&:to_s)
60
+ @organisation_id = attrs["OrganisationID"]
61
+ @api_key = attrs["APIKey"]
62
+ @name = attrs["Name"]
63
+ @legal_name = attrs["LegalName"]
64
+ @pays_tax = attrs["PaysTax"]
65
+ @version = attrs["Version"]
66
+ @organisation_type = attrs["OrganisationType"]
67
+ @base_currency = attrs["BaseCurrency"]
68
+ @country_code = attrs["CountryCode"]
69
+ @is_demo_company = attrs["IsDemoCompany"]
70
+ @organisation_status = attrs["OrganisationStatus"]
71
+ @registration_number = attrs["RegistrationNumber"]
72
+ @employer_identification_number = attrs["EmployerIdentificationNumber"]
73
+ @tax_number = attrs["TaxNumber"]
74
+ @financial_year_end_day = attrs["FinancialYearEndDay"]
75
+ @financial_year_end_month = attrs["FinancialYearEndMonth"]
76
+ @sales_tax_basis = attrs["SalesTaxBasis"]
77
+ @sales_tax_period = attrs["SalesTaxPeriod"]
78
+ @default_sales_tax = attrs["DefaultSalesTax"]
79
+ @default_purchases_tax = attrs["DefaultPurchasesTax"]
80
+ @period_lock_date = parse_time(attrs["PeriodLockDate"])
81
+ @end_of_year_lock_date = parse_time(attrs["EndOfYearLockDate"])
82
+ @created_date_utc = parse_time(attrs["CreatedDateUTC"])
83
+ @timezone = attrs["Timezone"]
84
+ @organisation_entity_type = attrs["OrganisationEntityType"]
85
+ @short_code = attrs["ShortCode"]
86
+ @organisation_class = attrs["Class"]
87
+ @edition = attrs["Edition"]
88
+ @line_of_business = attrs["LineOfBusiness"]
89
+ @addresses = (attrs["Addresses"] || []).map { |a| Address.new(a) }
90
+ @phones = (attrs["Phones"] || []).map { |p| Phone.new(p) }
91
+ @external_links = (attrs["ExternalLinks"] || []).map { |l| ExternalLink.new(l) }
92
+ @payment_terms = PaymentTerms.from_hash(attrs["PaymentTerms"])
93
+ end
94
+
95
+ def demo_company? = is_demo_company == true
96
+
97
+ def to_h
98
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
99
+ end
100
+
101
+ def ==(other)
102
+ other.is_a?(Organisation) && other.organisation_id == organisation_id
103
+ end
104
+ alias eql? ==
105
+
106
+ def hash = [self.class, organisation_id].hash
107
+
108
+ def inspect
109
+ "#<#{self.class} organisation_id=#{organisation_id.inspect} " \
110
+ "name=#{name.inspect} organisation_type=#{organisation_type.inspect}>"
111
+ end
112
+
113
+ private
114
+
115
+ # Xero uses two timestamp formats depending on the endpoint:
116
+ #
117
+ # - ISO 8601: "2019-07-09T23:40:30.1833130" (connections API)
118
+ # - .NET JSON: "/Date(1574275974000)/" (accounting API)
119
+ #
120
+ # Handle both, always returning UTC.
121
+ def parse_time(value)
122
+ return nil if value.nil?
123
+
124
+ str = value.to_s.strip
125
+ return nil if str.empty?
126
+
127
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
128
+ Time.at(match[1].to_i / 1000.0).utc
129
+ else
130
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
131
+ Time.iso8601(str)
132
+ end
133
+ rescue ArgumentError
134
+ nil
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Overpayment returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/overpayments
10
+ class Overpayment
11
+ ATTRIBUTES = {
12
+ overpayment_id: "OverpaymentID",
13
+ type: "Type",
14
+ contact: "Contact",
15
+ date: "Date",
16
+ status: "Status",
17
+ line_amount_types: "LineAmountTypes",
18
+ line_items: "LineItems",
19
+ sub_total: "SubTotal",
20
+ total_tax: "TotalTax",
21
+ total: "Total",
22
+ updated_date_utc: "UpdatedDateUTC",
23
+ currency_code: "CurrencyCode",
24
+ currency_rate: "CurrencyRate",
25
+ remaining_credit: "RemainingCredit",
26
+ allocations: "Allocations",
27
+ payments: "Payments",
28
+ has_attachments: "HasAttachments",
29
+ reference: "Reference"
30
+ }.freeze
31
+
32
+ attr_reader(*ATTRIBUTES.keys)
33
+
34
+ def self.from_response(payload)
35
+ return [] if payload.nil?
36
+
37
+ items = payload["Overpayments"]
38
+ return [] if items.nil?
39
+
40
+ items.map { |attrs| new(attrs) }
41
+ end
42
+
43
+ def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
44
+ attrs = attrs.transform_keys(&:to_s)
45
+ @is_reference = reference
46
+ @overpayment_id = attrs["OverpaymentID"]
47
+ @type = attrs["Type"]
48
+ @contact = attrs["Contact"] ? Contact.new(attrs["Contact"], reference: true) : nil
49
+ @date = parse_time(attrs["Date"])
50
+ @status = attrs["Status"]
51
+ @line_amount_types = attrs["LineAmountTypes"]
52
+ @line_items = (attrs["LineItems"] || []).map { |li| LineItem.new(li) }
53
+ @sub_total = attrs["SubTotal"]
54
+ @total_tax = attrs["TotalTax"]
55
+ @total = attrs["Total"]
56
+ @updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
57
+ @currency_code = attrs["CurrencyCode"]
58
+ @currency_rate = attrs["CurrencyRate"]
59
+ @remaining_credit = attrs["RemainingCredit"]
60
+ @allocations = (attrs["Allocations"] || []).map { |a| Allocation.new(a) }
61
+ @payments = (attrs["Payments"] || []).map { |p| Payment.new(p, reference: true) }
62
+ @has_attachments = attrs["HasAttachments"]
63
+ @reference = attrs["Reference"]
64
+ end
65
+
66
+ def receive? = type == "RECEIVE-OVERPAYMENT"
67
+
68
+ def spend? = type == "SPEND-OVERPAYMENT"
69
+
70
+ def reference? = @is_reference
71
+
72
+ def to_h
73
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
74
+ end
75
+
76
+ def ==(other)
77
+ other.is_a?(Overpayment) && other.overpayment_id == overpayment_id
78
+ end
79
+ alias eql? ==
80
+
81
+ def hash = [self.class, overpayment_id].hash
82
+
83
+ def inspect
84
+ "#<#{self.class} overpayment_id=#{overpayment_id.inspect} " \
85
+ "type=#{type.inspect} status=#{status.inspect} total=#{total.inspect}>"
86
+ end
87
+
88
+ private
89
+
90
+ def parse_time(value)
91
+ return nil if value.nil?
92
+
93
+ str = value.to_s.strip
94
+ return nil if str.empty?
95
+
96
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
97
+ Time.at(match[1].to_i / 1000.0).utc
98
+ else
99
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
100
+ Time.iso8601(str)
101
+ end
102
+ rescue ArgumentError
103
+ nil
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Payment returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/payments
10
+ class Payment
11
+ ATTRIBUTES = {
12
+ payment_id: "PaymentID",
13
+ date: "Date",
14
+ currency_rate: "CurrencyRate",
15
+ amount: "Amount",
16
+ bank_amount: "BankAmount",
17
+ reference: "Reference",
18
+ is_reconciled: "IsReconciled",
19
+ status: "Status",
20
+ payment_type: "PaymentType",
21
+ updated_date_utc: "UpdatedDateUTC",
22
+ batch_payment_id: "BatchPaymentID",
23
+ batch_payment: "BatchPayment",
24
+ account: "Account",
25
+ invoice: "Invoice",
26
+ credit_note: "CreditNote",
27
+ prepayment: "Prepayment",
28
+ overpayment: "Overpayment",
29
+ has_account: "HasAccount"
30
+ }.freeze
31
+
32
+ attr_reader(*ATTRIBUTES.keys)
33
+
34
+ def self.from_response(payload)
35
+ return [] if payload.nil?
36
+
37
+ items = payload["Payments"]
38
+ return [] if items.nil?
39
+
40
+ items.map { |attrs| new(attrs) }
41
+ end
42
+
43
+ def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
44
+ attrs = attrs.transform_keys(&:to_s)
45
+ @is_reference = reference
46
+ @payment_id = attrs["PaymentID"]
47
+ @date = parse_time(attrs["Date"])
48
+ @currency_rate = attrs["CurrencyRate"]
49
+ @amount = attrs["Amount"]
50
+ @bank_amount = attrs["BankAmount"]
51
+ @reference = attrs["Reference"]
52
+ @is_reconciled = attrs["IsReconciled"]
53
+ @status = attrs["Status"]
54
+ @payment_type = attrs["PaymentType"]
55
+ @updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
56
+ @batch_payment_id = attrs["BatchPaymentID"]
57
+ @batch_payment = attrs["BatchPayment"]
58
+ @account = attrs["Account"]
59
+ @invoice = attrs["Invoice"] ? Invoice.new(attrs["Invoice"], reference: true) : nil
60
+ @credit_note = attrs["CreditNote"] ? CreditNote.new(attrs["CreditNote"], reference: true) : nil
61
+ @prepayment = attrs["Prepayment"] ? Prepayment.new(attrs["Prepayment"], reference: true) : nil
62
+ @overpayment = attrs["Overpayment"] ? Overpayment.new(attrs["Overpayment"], reference: true) : nil
63
+ @has_account = attrs["HasAccount"]
64
+ end
65
+
66
+ def reference? = @is_reference
67
+
68
+ def reconciled? = is_reconciled == true
69
+
70
+ def to_h
71
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
72
+ end
73
+
74
+ def ==(other)
75
+ other.is_a?(Payment) && other.payment_id == payment_id
76
+ end
77
+ alias eql? ==
78
+
79
+ def hash = [self.class, payment_id].hash
80
+
81
+ def inspect
82
+ "#<#{self.class} payment_id=#{payment_id.inspect} " \
83
+ "payment_type=#{payment_type.inspect} status=#{status.inspect} amount=#{amount.inspect}>"
84
+ end
85
+
86
+ private
87
+
88
+ def parse_time(value)
89
+ return nil if value.nil?
90
+
91
+ str = value.to_s.strip
92
+ return nil if str.empty?
93
+
94
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
95
+ Time.at(match[1].to_i / 1000.0).utc
96
+ else
97
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
98
+ Time.iso8601(str)
99
+ end
100
+ rescue ArgumentError
101
+ nil
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XeroKiwi
4
+ module Accounting
5
+ # Xero payment terms for an Organisation or Contact, containing separate
6
+ # terms for bills (payable) and sales (receivable).
7
+ #
8
+ # See: https://developer.xero.com/documentation/api/accounting/types#paymentterms
9
+ class PaymentTerms
10
+ attr_reader :bills, :sales
11
+
12
+ def self.from_hash(hash)
13
+ return nil if hash.nil?
14
+
15
+ new(hash)
16
+ end
17
+
18
+ def initialize(attrs)
19
+ attrs = attrs.transform_keys(&:to_s)
20
+ @bills = PaymentTerm.from_hash(attrs["Bills"])
21
+ @sales = PaymentTerm.from_hash(attrs["Sales"])
22
+ end
23
+
24
+ def to_h
25
+ { bills: bills&.to_h, sales: sales&.to_h }
26
+ end
27
+
28
+ def ==(other)
29
+ other.is_a?(PaymentTerms) && bills == other.bills && sales == other.sales
30
+ end
31
+ alias eql? ==
32
+
33
+ def hash = [self.class, bills, sales].hash
34
+
35
+ def inspect
36
+ "#<#{self.class} bills=#{bills.inspect} sales=#{sales.inspect}>"
37
+ end
38
+ end
39
+
40
+ # A single payment term (either bills or sales side).
41
+ class PaymentTerm
42
+ ATTRIBUTES = {
43
+ day: "Day",
44
+ type: "Type"
45
+ }.freeze
46
+
47
+ attr_reader(*ATTRIBUTES.keys)
48
+
49
+ def self.from_hash(hash)
50
+ return nil if hash.nil? || hash.empty?
51
+
52
+ new(hash)
53
+ end
54
+
55
+ def initialize(attrs)
56
+ attrs = attrs.transform_keys(&:to_s)
57
+ @day = attrs["Day"]
58
+ @type = attrs["Type"]
59
+ end
60
+
61
+ def to_h
62
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
63
+ end
64
+
65
+ def ==(other)
66
+ other.is_a?(PaymentTerm) && day == other.day && type == other.type
67
+ end
68
+ alias eql? ==
69
+
70
+ def hash = [self.class, day, type].hash
71
+
72
+ def inspect
73
+ "#<#{self.class} day=#{day.inspect} type=#{type.inspect}>"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XeroKiwi
4
+ module Accounting
5
+ # A Xero phone number. Used by Organisation, Contact, and other resources.
6
+ #
7
+ # See: https://developer.xero.com/documentation/api/accounting/types#phones
8
+ class Phone
9
+ ATTRIBUTES = {
10
+ phone_type: "PhoneType",
11
+ phone_number: "PhoneNumber",
12
+ phone_area_code: "PhoneAreaCode",
13
+ phone_country_code: "PhoneCountryCode"
14
+ }.freeze
15
+
16
+ attr_reader(*ATTRIBUTES.keys)
17
+
18
+ def initialize(attrs)
19
+ attrs = attrs.transform_keys(&:to_s)
20
+ @phone_type = attrs["PhoneType"]
21
+ @phone_number = attrs["PhoneNumber"]
22
+ @phone_area_code = attrs["PhoneAreaCode"]
23
+ @phone_country_code = attrs["PhoneCountryCode"]
24
+ end
25
+
26
+ def default? = phone_type == "DEFAULT"
27
+ def mobile? = phone_type == "MOBILE"
28
+ def fax? = phone_type == "FAX"
29
+
30
+ def to_h
31
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
32
+ end
33
+
34
+ def ==(other)
35
+ other.is_a?(Phone) && to_h == other.to_h
36
+ end
37
+ alias eql? ==
38
+
39
+ def hash = to_h.hash
40
+
41
+ def inspect
42
+ "#<#{self.class} type=#{phone_type.inspect} number=#{phone_number.inspect}>"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Prepayment returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/prepayments
10
+ class Prepayment
11
+ ATTRIBUTES = {
12
+ prepayment_id: "PrepaymentID",
13
+ type: "Type",
14
+ contact: "Contact",
15
+ date: "Date",
16
+ status: "Status",
17
+ line_amount_types: "LineAmountTypes",
18
+ line_items: "LineItems",
19
+ sub_total: "SubTotal",
20
+ total_tax: "TotalTax",
21
+ total: "Total",
22
+ updated_date_utc: "UpdatedDateUTC",
23
+ currency_code: "CurrencyCode",
24
+ currency_rate: "CurrencyRate",
25
+ invoice_number: "InvoiceNumber",
26
+ remaining_credit: "RemainingCredit",
27
+ allocations: "Allocations",
28
+ payments: "Payments",
29
+ has_attachments: "HasAttachments",
30
+ fully_paid_on_date: "FullyPaidOnDate"
31
+ }.freeze
32
+
33
+ attr_reader(*ATTRIBUTES.keys)
34
+
35
+ def self.from_response(payload)
36
+ return [] if payload.nil?
37
+
38
+ items = payload["Prepayments"]
39
+ return [] if items.nil?
40
+
41
+ items.map { |attrs| new(attrs) }
42
+ end
43
+
44
+ def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
45
+ attrs = attrs.transform_keys(&:to_s)
46
+ @is_reference = reference
47
+ @prepayment_id = attrs["PrepaymentID"]
48
+ @type = attrs["Type"]
49
+ @contact = attrs["Contact"] ? Contact.new(attrs["Contact"], reference: true) : nil
50
+ @date = parse_time(attrs["Date"])
51
+ @status = attrs["Status"]
52
+ @line_amount_types = attrs["LineAmountTypes"]
53
+ @line_items = (attrs["LineItems"] || []).map { |li| LineItem.new(li) }
54
+ @sub_total = attrs["SubTotal"]
55
+ @total_tax = attrs["TotalTax"]
56
+ @total = attrs["Total"]
57
+ @updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
58
+ @currency_code = attrs["CurrencyCode"]
59
+ @currency_rate = attrs["CurrencyRate"]
60
+ @invoice_number = attrs["InvoiceNumber"]
61
+ @remaining_credit = attrs["RemainingCredit"]
62
+ @allocations = (attrs["Allocations"] || []).map { |a| Allocation.new(a) }
63
+ @payments = (attrs["Payments"] || []).map { |p| Payment.new(p, reference: true) }
64
+ @has_attachments = attrs["HasAttachments"]
65
+ @fully_paid_on_date = parse_time(attrs["FullyPaidOnDate"])
66
+ end
67
+
68
+ def reference? = @is_reference
69
+
70
+ def receive? = type == "RECEIVE-PREPAYMENT"
71
+
72
+ def spend? = type == "SPEND-PREPAYMENT"
73
+
74
+ def to_h
75
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
76
+ end
77
+
78
+ def ==(other)
79
+ other.is_a?(Prepayment) && other.prepayment_id == prepayment_id
80
+ end
81
+ alias eql? ==
82
+
83
+ def hash = [self.class, prepayment_id].hash
84
+
85
+ def inspect
86
+ "#<#{self.class} prepayment_id=#{prepayment_id.inspect} " \
87
+ "type=#{type.inspect} status=#{status.inspect} total=#{total.inspect}>"
88
+ end
89
+
90
+ private
91
+
92
+ def parse_time(value)
93
+ return nil if value.nil?
94
+
95
+ str = value.to_s.strip
96
+ return nil if str.empty?
97
+
98
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
99
+ Time.at(match[1].to_i / 1000.0).utc
100
+ else
101
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
102
+ Time.iso8601(str)
103
+ end
104
+ rescue ArgumentError
105
+ nil
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XeroKiwi
4
+ module Accounting
5
+ # Represents a tracking category assignment on a line item or contact.
6
+ #
7
+ # See: https://developer.xero.com/documentation/api/accounting/invoices
8
+ class TrackingCategory
9
+ ATTRIBUTES = {
10
+ tracking_category_id: "TrackingCategoryID",
11
+ tracking_option_id: "TrackingOptionID",
12
+ name: "Name",
13
+ option: "Option"
14
+ }.freeze
15
+
16
+ attr_reader(*ATTRIBUTES.keys)
17
+
18
+ def initialize(attrs)
19
+ attrs = attrs.transform_keys(&:to_s)
20
+ @tracking_category_id = attrs["TrackingCategoryID"]
21
+ @tracking_option_id = attrs["TrackingOptionID"]
22
+ @name = attrs["Name"]
23
+ @option = attrs["Option"]
24
+ end
25
+
26
+ def to_h
27
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
28
+ end
29
+
30
+ def ==(other)
31
+ other.is_a?(TrackingCategory) && to_h == other.to_h
32
+ end
33
+ alias eql? ==
34
+
35
+ def hash = to_h.hash
36
+
37
+ def inspect
38
+ "#<#{self.class} name=#{name.inspect} option=#{option.inspect}>"
39
+ end
40
+ end
41
+ end
42
+ end