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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Branding Theme returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/brandingthemes
10
+ 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?
61
+
62
+ str = value.to_s.strip
63
+ return nil if str.empty?
64
+
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
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Contact returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/contacts
10
+ class Contact
11
+ ATTRIBUTES = {
12
+ contact_id: "ContactID",
13
+ contact_number: "ContactNumber",
14
+ account_number: "AccountNumber",
15
+ contact_status: "ContactStatus",
16
+ name: "Name",
17
+ first_name: "FirstName",
18
+ last_name: "LastName",
19
+ email_address: "EmailAddress",
20
+ bank_account_details: "BankAccountDetails",
21
+ company_number: "CompanyNumber",
22
+ tax_number: "TaxNumber",
23
+ tax_number_type: "TaxNumberType",
24
+ accounts_receivable_tax_type: "AccountsReceivableTaxType",
25
+ accounts_payable_tax_type: "AccountsPayableTaxType",
26
+ addresses: "Addresses",
27
+ phones: "Phones",
28
+ is_supplier: "IsSupplier",
29
+ is_customer: "IsCustomer",
30
+ default_currency: "DefaultCurrency",
31
+ updated_date_utc: "UpdatedDateUTC",
32
+ contact_persons: "ContactPersons",
33
+ xero_network_key: "XeroNetworkKey",
34
+ merged_to_contact_id: "MergedToContactID",
35
+ sales_default_account_code: "SalesDefaultAccountCode",
36
+ purchases_default_account_code: "PurchasesDefaultAccountCode",
37
+ sales_tracking_categories: "SalesTrackingCategories",
38
+ purchases_tracking_categories: "PurchasesTrackingCategories",
39
+ sales_default_line_amount_type: "SalesDefaultLineAmountType",
40
+ purchases_default_line_amount_type: "PurchasesDefaultLineAmountType",
41
+ tracking_category_name: "TrackingCategoryName",
42
+ tracking_option_name: "TrackingOptionName",
43
+ payment_terms: "PaymentTerms",
44
+ contact_groups: "ContactGroups",
45
+ website: "Website",
46
+ branding_theme: "BrandingTheme",
47
+ batch_payments: "BatchPayments",
48
+ discount: "Discount",
49
+ balances: "Balances",
50
+ has_attachments: "HasAttachments"
51
+ }.freeze
52
+
53
+ attr_reader(*ATTRIBUTES.keys)
54
+
55
+ def self.from_response(payload)
56
+ return [] if payload.nil?
57
+
58
+ items = payload["Contacts"]
59
+ return [] if items.nil?
60
+
61
+ items.map { |attrs| new(attrs) }
62
+ end
63
+
64
+ def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
65
+ attrs = attrs.transform_keys(&:to_s)
66
+ @is_reference = reference
67
+ @contact_id = attrs["ContactID"]
68
+ @contact_number = attrs["ContactNumber"]
69
+ @account_number = attrs["AccountNumber"]
70
+ @contact_status = attrs["ContactStatus"]
71
+ @name = attrs["Name"]
72
+ @first_name = attrs["FirstName"]
73
+ @last_name = attrs["LastName"]
74
+ @email_address = attrs["EmailAddress"]
75
+ @bank_account_details = attrs["BankAccountDetails"]
76
+ @company_number = attrs["CompanyNumber"]
77
+ @tax_number = attrs["TaxNumber"]
78
+ @tax_number_type = attrs["TaxNumberType"]
79
+ @accounts_receivable_tax_type = attrs["AccountsReceivableTaxType"]
80
+ @accounts_payable_tax_type = attrs["AccountsPayableTaxType"]
81
+ @addresses = (attrs["Addresses"] || []).map { |a| Address.new(a) }
82
+ @phones = (attrs["Phones"] || []).map { |p| Phone.new(p) }
83
+ @is_supplier = attrs["IsSupplier"]
84
+ @is_customer = attrs["IsCustomer"]
85
+ @default_currency = attrs["DefaultCurrency"]
86
+ @updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
87
+ @contact_persons = (attrs["ContactPersons"] || []).map { |cp| ContactPerson.new(cp) }
88
+ @xero_network_key = attrs["XeroNetworkKey"]
89
+ @merged_to_contact_id = attrs["MergedToContactID"]
90
+ @sales_default_account_code = attrs["SalesDefaultAccountCode"]
91
+ @purchases_default_account_code = attrs["PurchasesDefaultAccountCode"]
92
+ @sales_tracking_categories = (attrs["SalesTrackingCategories"] || []).map { |t| TrackingCategory.new(t) }
93
+ @purchases_tracking_categories = (attrs["PurchasesTrackingCategories"] || []).map { |t| TrackingCategory.new(t) }
94
+ @sales_default_line_amount_type = attrs["SalesDefaultLineAmountType"]
95
+ @purchases_default_line_amount_type = attrs["PurchasesDefaultLineAmountType"]
96
+ @tracking_category_name = attrs["TrackingCategoryName"]
97
+ @tracking_option_name = attrs["TrackingOptionName"]
98
+ @payment_terms = PaymentTerms.from_hash(attrs["PaymentTerms"])
99
+ @contact_groups = (attrs["ContactGroups"] || []).map { |cg| ContactGroup.new(cg, reference: true) }
100
+ @website = attrs["Website"]
101
+ @branding_theme = attrs["BrandingTheme"] ? BrandingTheme.new(attrs["BrandingTheme"]) : nil
102
+ @batch_payments = attrs["BatchPayments"]
103
+ @discount = attrs["Discount"]
104
+ @balances = attrs["Balances"]
105
+ @has_attachments = attrs["HasAttachments"]
106
+ end
107
+
108
+ def reference? = @is_reference
109
+
110
+ def supplier? = is_supplier == true
111
+
112
+ def customer? = is_customer == true
113
+
114
+ def active? = contact_status == "ACTIVE"
115
+
116
+ def archived? = contact_status == "ARCHIVED"
117
+
118
+ def to_h
119
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
120
+ end
121
+
122
+ def ==(other)
123
+ other.is_a?(Contact) && other.contact_id == contact_id
124
+ end
125
+ alias eql? ==
126
+
127
+ def hash = [self.class, contact_id].hash
128
+
129
+ def inspect
130
+ "#<#{self.class} contact_id=#{contact_id.inspect} " \
131
+ "name=#{name.inspect} contact_status=#{contact_status.inspect}>"
132
+ end
133
+
134
+ private
135
+
136
+ def parse_time(value)
137
+ return nil if value.nil?
138
+
139
+ str = value.to_s.strip
140
+ return nil if str.empty?
141
+
142
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
143
+ Time.at(match[1].to_i / 1000.0).utc
144
+ else
145
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
146
+ Time.iso8601(str)
147
+ end
148
+ rescue ArgumentError
149
+ nil
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XeroKiwi
4
+ module Accounting
5
+ # Represents a Xero Contact Group returned by the Accounting API.
6
+ #
7
+ # See: https://developer.xero.com/documentation/api/accounting/contactgroups
8
+ class ContactGroup
9
+ ATTRIBUTES = {
10
+ contact_group_id: "ContactGroupID",
11
+ name: "Name",
12
+ status: "Status",
13
+ contacts: "Contacts"
14
+ }.freeze
15
+
16
+ attr_reader(*ATTRIBUTES.keys)
17
+
18
+ def self.from_response(payload)
19
+ return [] if payload.nil?
20
+
21
+ items = payload["ContactGroups"]
22
+ return [] if items.nil?
23
+
24
+ items.map { |attrs| new(attrs) }
25
+ end
26
+
27
+ def initialize(attrs, reference: false)
28
+ attrs = attrs.transform_keys(&:to_s)
29
+ @is_reference = reference
30
+ @contact_group_id = attrs["ContactGroupID"]
31
+ @name = attrs["Name"]
32
+ @status = attrs["Status"]
33
+ @contacts = (attrs["Contacts"] || []).map { |c| Contact.new(c, reference: true) }
34
+ end
35
+
36
+ def reference? = @is_reference
37
+
38
+ def active? = status == "ACTIVE"
39
+
40
+ def to_h
41
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
42
+ end
43
+
44
+ def ==(other)
45
+ other.is_a?(ContactGroup) && other.contact_group_id == contact_group_id
46
+ end
47
+ alias eql? ==
48
+
49
+ def hash = [self.class, contact_group_id].hash
50
+
51
+ def inspect
52
+ "#<#{self.class} contact_group_id=#{contact_group_id.inspect} " \
53
+ "name=#{name.inspect} status=#{status.inspect}>"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XeroKiwi
4
+ module Accounting
5
+ # Represents a contact person nested within a Xero Contact.
6
+ #
7
+ # See: https://developer.xero.com/documentation/api/accounting/contacts
8
+ class ContactPerson
9
+ ATTRIBUTES = {
10
+ first_name: "FirstName",
11
+ last_name: "LastName",
12
+ email_address: "EmailAddress",
13
+ include_in_emails: "IncludeInEmails"
14
+ }.freeze
15
+
16
+ attr_reader(*ATTRIBUTES.keys)
17
+
18
+ def initialize(attrs)
19
+ attrs = attrs.transform_keys(&:to_s)
20
+ @first_name = attrs["FirstName"]
21
+ @last_name = attrs["LastName"]
22
+ @email_address = attrs["EmailAddress"]
23
+ @include_in_emails = attrs["IncludeInEmails"]
24
+ end
25
+
26
+ def include_in_emails? = include_in_emails == true
27
+
28
+ def to_h
29
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
30
+ end
31
+
32
+ def ==(other)
33
+ other.is_a?(ContactPerson) && to_h == other.to_h
34
+ end
35
+ alias eql? ==
36
+
37
+ def hash = to_h.hash
38
+
39
+ def inspect
40
+ "#<#{self.class} first_name=#{first_name.inspect} " \
41
+ "last_name=#{last_name.inspect} email_address=#{email_address.inspect}>"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Credit Note returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/creditnotes
10
+ class CreditNote
11
+ ATTRIBUTES = {
12
+ credit_note_id: "CreditNoteID",
13
+ credit_note_number: "CreditNoteNumber",
14
+ type: "Type",
15
+ contact: "Contact",
16
+ date: "Date",
17
+ status: "Status",
18
+ line_amount_types: "LineAmountTypes",
19
+ line_items: "LineItems",
20
+ sub_total: "SubTotal",
21
+ total_tax: "TotalTax",
22
+ total: "Total",
23
+ cis_deduction: "CISDeduction",
24
+ updated_date_utc: "UpdatedDateUTC",
25
+ currency_code: "CurrencyCode",
26
+ currency_rate: "CurrencyRate",
27
+ fully_paid_on_date: "FullyPaidOnDate",
28
+ reference: "Reference",
29
+ sent_to_contact: "SentToContact",
30
+ remaining_credit: "RemainingCredit",
31
+ allocations: "Allocations",
32
+ branding_theme_id: "BrandingThemeID",
33
+ has_attachments: "HasAttachments"
34
+ }.freeze
35
+
36
+ attr_reader(*ATTRIBUTES.keys)
37
+
38
+ def self.from_response(payload)
39
+ return [] if payload.nil?
40
+
41
+ items = payload["CreditNotes"]
42
+ return [] if items.nil?
43
+
44
+ items.map { |attrs| new(attrs) }
45
+ end
46
+
47
+ def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
48
+ attrs = attrs.transform_keys(&:to_s)
49
+ @is_reference = reference
50
+ @credit_note_id = attrs["CreditNoteID"]
51
+ @credit_note_number = attrs["CreditNoteNumber"]
52
+ @type = attrs["Type"]
53
+ @contact = attrs["Contact"] ? Contact.new(attrs["Contact"], reference: true) : nil
54
+ @date = parse_time(attrs["Date"])
55
+ @status = attrs["Status"]
56
+ @line_amount_types = attrs["LineAmountTypes"]
57
+ @line_items = (attrs["LineItems"] || []).map { |li| LineItem.new(li) }
58
+ @sub_total = attrs["SubTotal"]
59
+ @total_tax = attrs["TotalTax"]
60
+ @total = attrs["Total"]
61
+ @cis_deduction = attrs["CISDeduction"]
62
+ @updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
63
+ @currency_code = attrs["CurrencyCode"]
64
+ @currency_rate = attrs["CurrencyRate"]
65
+ @fully_paid_on_date = parse_time(attrs["FullyPaidOnDate"])
66
+ @reference = attrs["Reference"]
67
+ @sent_to_contact = attrs["SentToContact"]
68
+ @remaining_credit = attrs["RemainingCredit"]
69
+ @allocations = (attrs["Allocations"] || []).map { |a| Allocation.new(a) }
70
+ @branding_theme_id = attrs["BrandingThemeID"]
71
+ @has_attachments = attrs["HasAttachments"]
72
+ end
73
+
74
+ def accounts_receivable? = type == "ACCRECCREDIT"
75
+
76
+ def reference? = @is_reference
77
+
78
+ def accounts_payable? = type == "ACCPAYCREDIT"
79
+
80
+ def to_h
81
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
82
+ end
83
+
84
+ def ==(other)
85
+ other.is_a?(CreditNote) && other.credit_note_id == credit_note_id
86
+ end
87
+ alias eql? ==
88
+
89
+ def hash = [self.class, credit_note_id].hash
90
+
91
+ def inspect
92
+ "#<#{self.class} credit_note_id=#{credit_note_id.inspect} " \
93
+ "type=#{type.inspect} status=#{status.inspect} total=#{total.inspect}>"
94
+ end
95
+
96
+ private
97
+
98
+ def parse_time(value)
99
+ return nil if value.nil?
100
+
101
+ str = value.to_s.strip
102
+ return nil if str.empty?
103
+
104
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
105
+ Time.at(match[1].to_i / 1000.0).utc
106
+ else
107
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
108
+ Time.iso8601(str)
109
+ end
110
+ rescue ArgumentError
111
+ nil
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XeroKiwi
4
+ module Accounting
5
+ # A Xero external link (social/web profile). Used by Organisation.
6
+ #
7
+ # See: https://developer.xero.com/documentation/api/accounting/types#externallinks
8
+ class ExternalLink
9
+ ATTRIBUTES = {
10
+ link_type: "LinkType",
11
+ url: "Url"
12
+ }.freeze
13
+
14
+ attr_reader(*ATTRIBUTES.keys)
15
+
16
+ def initialize(attrs)
17
+ attrs = attrs.transform_keys(&:to_s)
18
+ @link_type = attrs["LinkType"]
19
+ @url = attrs["Url"]
20
+ end
21
+
22
+ def to_h
23
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
24
+ end
25
+
26
+ def ==(other)
27
+ other.is_a?(ExternalLink) && to_h == other.to_h
28
+ end
29
+ alias eql? ==
30
+
31
+ def hash = to_h.hash
32
+
33
+ def inspect
34
+ "#<#{self.class} type=#{link_type.inspect} url=#{url.inspect}>"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module XeroKiwi
6
+ module Accounting
7
+ # Represents a Xero Invoice returned by the Accounting API.
8
+ #
9
+ # See: https://developer.xero.com/documentation/api/accounting/invoices
10
+ class Invoice
11
+ ATTRIBUTES = {
12
+ invoice_id: "InvoiceID",
13
+ invoice_number: "InvoiceNumber",
14
+ type: "Type",
15
+ contact: "Contact",
16
+ date: "Date",
17
+ due_date: "DueDate",
18
+ status: "Status",
19
+ line_amount_types: "LineAmountTypes",
20
+ line_items: "LineItems",
21
+ sub_total: "SubTotal",
22
+ total_tax: "TotalTax",
23
+ total: "Total",
24
+ total_discount: "TotalDiscount",
25
+ updated_date_utc: "UpdatedDateUTC",
26
+ currency_code: "CurrencyCode",
27
+ currency_rate: "CurrencyRate",
28
+ reference: "Reference",
29
+ branding_theme_id: "BrandingThemeID",
30
+ url: "Url",
31
+ sent_to_contact: "SentToContact",
32
+ expected_payment_date: "ExpectedPaymentDate",
33
+ planned_payment_date: "PlannedPaymentDate",
34
+ has_attachments: "HasAttachments",
35
+ repeating_invoice_id: "RepeatingInvoiceID",
36
+ payments: "Payments",
37
+ credit_notes: "CreditNotes",
38
+ prepayments: "Prepayments",
39
+ overpayments: "Overpayments",
40
+ amount_due: "AmountDue",
41
+ amount_paid: "AmountPaid",
42
+ amount_credited: "AmountCredited",
43
+ cis_deduction: "CISDeduction",
44
+ fully_paid_on_date: "FullyPaidOnDate",
45
+ sales_tax_calculation_type_code: "SalesTaxCalculationTypeCode",
46
+ invoice_addresses: "InvoiceAddresses"
47
+ }.freeze
48
+
49
+ attr_reader(*ATTRIBUTES.keys)
50
+
51
+ def self.from_response(payload)
52
+ return [] if payload.nil?
53
+
54
+ items = payload["Invoices"]
55
+ return [] if items.nil?
56
+
57
+ items.map { |attrs| new(attrs) }
58
+ end
59
+
60
+ def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
61
+ attrs = attrs.transform_keys(&:to_s)
62
+ @is_reference = reference
63
+ @invoice_id = attrs["InvoiceID"]
64
+ @invoice_number = attrs["InvoiceNumber"]
65
+ @type = attrs["Type"]
66
+ @contact = attrs["Contact"] ? Contact.new(attrs["Contact"], reference: true) : nil
67
+ @date = parse_time(attrs["Date"])
68
+ @due_date = parse_time(attrs["DueDate"])
69
+ @status = attrs["Status"]
70
+ @line_amount_types = attrs["LineAmountTypes"]
71
+ @line_items = (attrs["LineItems"] || []).map { |li| LineItem.new(li) }
72
+ @sub_total = attrs["SubTotal"]
73
+ @total_tax = attrs["TotalTax"]
74
+ @total = attrs["Total"]
75
+ @total_discount = attrs["TotalDiscount"]
76
+ @updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
77
+ @currency_code = attrs["CurrencyCode"]
78
+ @currency_rate = attrs["CurrencyRate"]
79
+ @reference = attrs["Reference"]
80
+ @branding_theme_id = attrs["BrandingThemeID"]
81
+ @url = attrs["Url"]
82
+ @sent_to_contact = attrs["SentToContact"]
83
+ @expected_payment_date = parse_time(attrs["ExpectedPaymentDate"])
84
+ @planned_payment_date = parse_time(attrs["PlannedPaymentDate"])
85
+ @has_attachments = attrs["HasAttachments"]
86
+ @repeating_invoice_id = attrs["RepeatingInvoiceID"]
87
+ @payments = (attrs["Payments"] || []).map { |p| Payment.new(p, reference: true) }
88
+ @credit_notes = (attrs["CreditNotes"] || []).map { |cn| CreditNote.new(cn, reference: true) }
89
+ @prepayments = (attrs["Prepayments"] || []).map { |p| Prepayment.new(p, reference: true) }
90
+ @overpayments = (attrs["Overpayments"] || []).map { |o| Overpayment.new(o, reference: true) }
91
+ @amount_due = attrs["AmountDue"]
92
+ @amount_paid = attrs["AmountPaid"]
93
+ @amount_credited = attrs["AmountCredited"]
94
+ @cis_deduction = attrs["CISDeduction"]
95
+ @fully_paid_on_date = parse_time(attrs["FullyPaidOnDate"])
96
+ @sales_tax_calculation_type_code = attrs["SalesTaxCalculationTypeCode"]
97
+ @invoice_addresses = attrs["InvoiceAddresses"] || []
98
+ end
99
+
100
+ def reference? = @is_reference
101
+
102
+ def accounts_receivable? = type == "ACCREC"
103
+
104
+ def accounts_payable? = type == "ACCPAY"
105
+
106
+ def to_h
107
+ ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
108
+ end
109
+
110
+ def ==(other)
111
+ other.is_a?(Invoice) && other.invoice_id == invoice_id
112
+ end
113
+ alias eql? ==
114
+
115
+ def hash = [self.class, invoice_id].hash
116
+
117
+ def inspect
118
+ "#<#{self.class} invoice_id=#{invoice_id.inspect} " \
119
+ "invoice_number=#{invoice_number.inspect} type=#{type.inspect} " \
120
+ "status=#{status.inspect} total=#{total.inspect}>"
121
+ end
122
+
123
+ private
124
+
125
+ def parse_time(value)
126
+ return nil if value.nil?
127
+
128
+ str = value.to_s.strip
129
+ return nil if str.empty?
130
+
131
+ if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
132
+ Time.at(match[1].to_i / 1000.0).utc
133
+ else
134
+ str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
135
+ Time.iso8601(str)
136
+ end
137
+ rescue ArgumentError
138
+ nil
139
+ end
140
+ end
141
+ end
142
+ end