xeroizer 2.17.1 → 3.0.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 (136) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +246 -213
  3. data/lib/xeroizer/connection.rb +49 -0
  4. data/lib/xeroizer/exceptions.rb +4 -0
  5. data/lib/xeroizer/generic_application.rb +13 -5
  6. data/lib/xeroizer/http.rb +7 -80
  7. data/lib/xeroizer/http_response.rb +154 -0
  8. data/lib/xeroizer/models/bank_account.rb +1 -0
  9. data/lib/xeroizer/models/bank_transaction.rb +1 -0
  10. data/lib/xeroizer/models/batch_payment.rb +27 -0
  11. data/lib/xeroizer/models/branding_theme.rb +49 -9
  12. data/lib/xeroizer/models/contact.rb +12 -6
  13. data/lib/xeroizer/models/contact_group.rb +45 -0
  14. data/lib/xeroizer/models/credit_note.rb +24 -22
  15. data/lib/xeroizer/models/currency.rb +14 -2
  16. data/lib/xeroizer/models/from_bank_account.rb +1 -0
  17. data/lib/xeroizer/models/history_record.rb +72 -0
  18. data/lib/xeroizer/models/invoice.rb +17 -3
  19. data/lib/xeroizer/models/item.rb +2 -1
  20. data/lib/xeroizer/models/item_purchase_details.rb +1 -1
  21. data/lib/xeroizer/models/line_item.rb +17 -5
  22. data/lib/xeroizer/models/manual_journal.rb +2 -1
  23. data/lib/xeroizer/models/online_invoice.rb +37 -0
  24. data/lib/xeroizer/models/option.rb +1 -1
  25. data/lib/xeroizer/models/organisation.rb +2 -0
  26. data/lib/xeroizer/models/payment_service.rb +22 -0
  27. data/lib/xeroizer/models/payroll/address.rb +53 -0
  28. data/lib/xeroizer/models/payroll/bank_account.rb +18 -6
  29. data/lib/xeroizer/models/payroll/benefit_line.rb +26 -0
  30. data/lib/xeroizer/models/payroll/benefit_type.rb +45 -0
  31. data/lib/xeroizer/models/payroll/deduction_line.rb +32 -0
  32. data/lib/xeroizer/models/payroll/deduction_type.rb +49 -0
  33. data/lib/xeroizer/models/payroll/earnings_line.rb +39 -0
  34. data/lib/xeroizer/models/payroll/earnings_type.rb +53 -0
  35. data/lib/xeroizer/models/payroll/employee.rb +30 -8
  36. data/lib/xeroizer/models/payroll/leave_application.rb +27 -0
  37. data/lib/xeroizer/models/payroll/leave_line.rb +30 -0
  38. data/lib/xeroizer/models/payroll/leave_period.rb +15 -0
  39. data/lib/xeroizer/models/payroll/pay_items.rb +22 -0
  40. data/lib/xeroizer/models/payroll/pay_run.rb +33 -0
  41. data/lib/xeroizer/models/payroll/pay_schedule.rb +40 -0
  42. data/lib/xeroizer/models/payroll/pay_template.rb +24 -0
  43. data/lib/xeroizer/models/payroll/payment_method.rb +24 -0
  44. data/lib/xeroizer/models/payroll/paystub.rb +44 -0
  45. data/lib/xeroizer/models/payroll/reimbursement_line.rb +21 -0
  46. data/lib/xeroizer/models/payroll/reimbursement_type.rb +22 -0
  47. data/lib/xeroizer/models/payroll/salary_and_wage.rb +29 -0
  48. data/lib/xeroizer/models/payroll/super_line.rb +40 -0
  49. data/lib/xeroizer/models/payroll/tax_declaration.rb +50 -0
  50. data/lib/xeroizer/models/payroll/time_off_line.rb +20 -0
  51. data/lib/xeroizer/models/payroll/time_off_type.rb +32 -0
  52. data/lib/xeroizer/models/payroll/work_location.rb +25 -0
  53. data/lib/xeroizer/models/prepayment.rb +1 -0
  54. data/lib/xeroizer/models/purchase_order.rb +6 -6
  55. data/lib/xeroizer/models/quote.rb +76 -0
  56. data/lib/xeroizer/models/schedule.rb +1 -0
  57. data/lib/xeroizer/models/tax_component.rb +1 -0
  58. data/lib/xeroizer/models/to_bank_account.rb +1 -0
  59. data/lib/xeroizer/oauth.rb +12 -1
  60. data/lib/xeroizer/oauth2.rb +82 -0
  61. data/lib/xeroizer/oauth2_application.rb +49 -0
  62. data/lib/xeroizer/payroll_application.rb +8 -3
  63. data/lib/xeroizer/record/base.rb +11 -2
  64. data/lib/xeroizer/record/base_model.rb +1 -1
  65. data/lib/xeroizer/record/base_model_http_proxy.rb +37 -17
  66. data/lib/xeroizer/record/model_definition_helper.rb +1 -1
  67. data/lib/xeroizer/record/payroll_base.rb +4 -0
  68. data/lib/xeroizer/record/record_association_helper.rb +4 -4
  69. data/lib/xeroizer/record/validators/associated_validator.rb +1 -0
  70. data/lib/xeroizer/record/xml_helper.rb +18 -18
  71. data/lib/xeroizer/report/aged_receivables_by_contact.rb +1 -1
  72. data/lib/xeroizer/report/cell_xml_helper.rb +13 -13
  73. data/lib/xeroizer/response.rb +22 -17
  74. data/lib/xeroizer/version.rb +1 -1
  75. data/lib/xeroizer.rb +34 -4
  76. data/test/acceptance/about_creating_bank_transactions_test.rb +89 -81
  77. data/test/acceptance/about_creating_prepayment_test.rb +25 -30
  78. data/test/acceptance/about_fetching_bank_transactions_test.rb +12 -12
  79. data/test/acceptance/about_online_invoice_test.rb +25 -0
  80. data/test/acceptance/acceptance_test.rb +28 -26
  81. data/test/acceptance/bank_transfer_test.rb +12 -17
  82. data/test/acceptance/bulk_operations_test.rb +18 -16
  83. data/test/acceptance/connections_test.rb +11 -0
  84. data/test/stub_responses/bad_request.json +6 -0
  85. data/test/stub_responses/connections.json +16 -0
  86. data/test/stub_responses/expired_oauth2_token.json +6 -0
  87. data/test/stub_responses/generic_response_error.json +6 -0
  88. data/test/stub_responses/invalid_oauth2_request_token.json +6 -0
  89. data/test/stub_responses/invalid_tenant_header.json +6 -0
  90. data/test/stub_responses/object_not_found.json +6 -0
  91. data/test/stub_responses/organisations.xml +10 -0
  92. data/test/stub_responses/payment_service.xml +15 -0
  93. data/test/test_helper.rb +17 -12
  94. data/test/unit/generic_application_test.rb +21 -10
  95. data/test/unit/http_test.rb +282 -10
  96. data/test/unit/models/address_test.rb +2 -2
  97. data/test/unit/models/bank_transaction_model_parsing_test.rb +2 -2
  98. data/test/unit/models/bank_transaction_test.rb +1 -1
  99. data/test/unit/models/bank_transaction_validation_test.rb +1 -1
  100. data/test/unit/models/contact_test.rb +20 -11
  101. data/test/unit/models/credit_note_test.rb +8 -8
  102. data/test/unit/models/employee_test.rb +4 -4
  103. data/test/unit/models/invoice_test.rb +12 -12
  104. data/test/unit/models/journal_line_test.rb +6 -6
  105. data/test/unit/models/journal_test.rb +4 -4
  106. data/test/unit/models/line_item_sum_test.rb +1 -1
  107. data/test/unit/models/line_item_test.rb +29 -37
  108. data/test/unit/models/manual_journal_test.rb +3 -3
  109. data/test/unit/models/organisation_test.rb +16 -2
  110. data/test/unit/models/payment_service_test.rb +29 -0
  111. data/test/unit/models/phone_test.rb +7 -7
  112. data/test/unit/models/prepayment_test.rb +4 -4
  113. data/test/unit/models/repeating_invoice_test.rb +3 -3
  114. data/test/unit/models/tax_rate_test.rb +2 -2
  115. data/test/unit/oauth2_test.rb +171 -0
  116. data/test/unit/oauth_config_test.rb +1 -1
  117. data/test/unit/record/base_model_test.rb +13 -13
  118. data/test/unit/record/base_test.rb +73 -4
  119. data/test/unit/record/block_validator_test.rb +1 -1
  120. data/test/unit/record/connection_test.rb +60 -0
  121. data/test/unit/record/model_definition_test.rb +36 -36
  122. data/test/unit/record/parse_params_test.rb +59 -0
  123. data/test/unit/record/parse_where_hash_test.rb +13 -13
  124. data/test/unit/record/record_association_test.rb +14 -14
  125. data/test/unit/record/validators_test.rb +43 -43
  126. data/test/unit/record_definition_test.rb +7 -7
  127. data/test/unit/report_definition_test.rb +7 -7
  128. data/test/unit/report_test.rb +20 -20
  129. data/test/unit_test_helper.rb +16 -0
  130. metadata +117 -27
  131. data/lib/xeroizer/models/payroll/home_address.rb +0 -24
  132. data/lib/xeroizer/partner_application.rb +0 -51
  133. data/lib/xeroizer/private_application.rb +0 -25
  134. data/lib/xeroizer/public_application.rb +0 -21
  135. data/test/unit/oauth_test.rb +0 -118
  136. data/test/unit/private_application_test.rb +0 -20
@@ -0,0 +1,49 @@
1
+ module Xeroizer
2
+ class Connection
3
+ attr_accessor :attributes
4
+ attr_reader :parent
5
+
6
+ class << self
7
+ def current_connections(client)
8
+ response = do_request(client)
9
+
10
+ if response.success?
11
+ JSON.parse(response.plain_body).map do |connection_json|
12
+ build(connection_json, client)
13
+ end
14
+ else
15
+ raise Xeroizer::OAuth::TokenInvalid, response.plain_body
16
+ end
17
+ end
18
+
19
+ def build(attributes, parent)
20
+ record = new(parent)
21
+ record.attributes = attributes
22
+ record
23
+ end
24
+
25
+ private
26
+
27
+ def do_request(client)
28
+ client.get('https://api.xero.com/connections')
29
+ end
30
+ end
31
+
32
+ def initialize(parent)
33
+ @parent = parent
34
+ @attributes = {}
35
+ end
36
+
37
+ def method_missing(name, *_args)
38
+ @attributes.send(:[], name.to_s.camelcase(:lower))
39
+ end
40
+
41
+ def delete
42
+ parent.delete("https://api.xero.com/connections/#{id}")
43
+ end
44
+
45
+ def to_h
46
+ attributes.transform_keys(&:underscore)
47
+ end
48
+ end
49
+ end
@@ -97,6 +97,8 @@ module Xeroizer
97
97
 
98
98
  end
99
99
 
100
+ class RecordInvalid < XeroizerError; end
101
+
100
102
  class SettingTotalDirectlyNotSupported < XeroizerError
101
103
 
102
104
  def initialize(attribute_name)
@@ -153,5 +155,7 @@ module Xeroizer
153
155
  end
154
156
 
155
157
  end
158
+
159
+ class InvalidClientError < XeroizerError; end
156
160
 
157
161
  end
@@ -6,17 +6,20 @@ module Xeroizer
6
6
  include Http
7
7
  extend Record::ApplicationHelper
8
8
 
9
- attr_reader :client, :xero_url, :logger, :rate_limit_sleep, :rate_limit_max_attempts,
9
+ attr_reader :client, :logger, :rate_limit_sleep, :rate_limit_max_attempts,
10
10
  :default_headers, :unitdp, :before_request, :after_request, :around_request, :nonce_used_max_attempts
11
11
 
12
+ attr_accessor :xero_url
13
+
12
14
  extend Forwardable
13
15
  def_delegators :client, :access_token
14
16
 
15
17
  record :Account
16
18
  record :Allocation
17
19
  record :Attachment
18
- record :BrandingTheme
19
20
  record :Balances
21
+ record :BatchPayment
22
+ record :BrandingTheme
20
23
  record :Contact
21
24
  record :ContactGroup
22
25
  record :CreditNote
@@ -25,15 +28,19 @@ module Xeroizer
25
28
  record :ExpenseClaim
26
29
  record :Invoice
27
30
  record :InvoiceReminder
31
+ record :HistoryRecord
32
+ record :OnlineInvoice
28
33
  record :Item
29
34
  record :Journal
30
35
  record :LineItem
31
36
  record :ManualJournal
32
37
  record :Organisation
33
38
  record :Payment
39
+ record :PaymentService
34
40
  record :Prepayment
35
41
  record :Overpayment
36
42
  record :PurchaseOrder
43
+ record :Quote
37
44
  record :Receipt
38
45
  record :RepeatingInvoice
39
46
  record :Schedule
@@ -60,7 +67,8 @@ module Xeroizer
60
67
  # @see PublicApplication
61
68
  # @see PrivateApplication
62
69
  # @see PartnerApplication
63
- def initialize(consumer_key, consumer_secret, options = {})
70
+ def initialize(client, options = {})
71
+ raise Xeroizer::InvalidClientError.new unless [OAuth, OAuth2].member?(client.class)
64
72
  @xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
65
73
  @rate_limit_sleep = options[:rate_limit_sleep] || false
66
74
  @rate_limit_max_attempts = options[:rate_limit_max_attempts] || 5
@@ -69,7 +77,7 @@ module Xeroizer
69
77
  @before_request = options.delete(:before_request)
70
78
  @after_request = options.delete(:after_request)
71
79
  @around_request = options.delete(:around_request)
72
- @client = OAuth.new(consumer_key, consumer_secret, options.merge({default_headers: default_headers}))
80
+ @client = client
73
81
  @logger = options[:logger] || false
74
82
  @unitdp = options[:unitdp] || 2
75
83
  end
@@ -79,6 +87,6 @@ module Xeroizer
79
87
  xero_client.xero_url = options[:xero_url] || "https://api.xero.com/payroll.xro/1.0"
80
88
  @payroll ||= PayrollApplication.new(xero_client)
81
89
  end
82
-
90
+
83
91
  end
84
92
  end
data/lib/xeroizer/http.rb CHANGED
@@ -15,7 +15,7 @@
15
15
  module Xeroizer
16
16
  module Http
17
17
  class BadResponse < XeroizerError; end
18
- RequestInfo = Struct.new(:url, :headers, :params, :body)
18
+ RequestInfo = Struct.new(:url, :headers, :params, :body, :method)
19
19
 
20
20
  ACCEPT_MIME_MAP = {
21
21
  :pdf => 'application/pdf',
@@ -53,7 +53,7 @@ module Xeroizer
53
53
 
54
54
  private
55
55
 
56
- def http_request(client, method, url, body, params = {})
56
+ def http_request(client, method, url, request_body, params = {})
57
57
  # headers = {'Accept-Encoding' => 'gzip, deflate'}
58
58
 
59
59
  headers = self.default_headers.merge({ 'charset' => 'utf-8' })
@@ -89,14 +89,14 @@ module Xeroizer
89
89
 
90
90
  attempts = 0
91
91
 
92
- request_info = RequestInfo.new(url, headers, params, body)
92
+ request_info = RequestInfo.new(url, headers, params, request_body, method)
93
93
  before_request.call(request_info) if before_request
94
94
 
95
95
  begin
96
96
  attempts += 1
97
97
  logger.info("XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri}") if self.logger
98
98
 
99
- raw_body = params.delete(:raw_body) ? body : {:xml => body}
99
+ raw_body = params.delete(:raw_body) ? request_body : {:xml => request_body}
100
100
 
101
101
  response = with_around_request(request_info) do
102
102
  case method
@@ -109,20 +109,7 @@ module Xeroizer
109
109
  log_response(response, uri)
110
110
  after_request.call(request_info, response) if after_request
111
111
 
112
- case response.code.to_i
113
- when 200
114
- response.plain_body
115
- when 400
116
- handle_error!(response, body)
117
- when 401
118
- handle_oauth_error!(response)
119
- when 404
120
- handle_object_not_found!(response, url)
121
- when 503
122
- handle_oauth_error!(response)
123
- else
124
- handle_unknown_response_error!(response)
125
- end
112
+ HttpResponse.from_response(response, request_body, url).body
126
113
  rescue Xeroizer::OAuth::NonceUsed => exception
127
114
  raise if attempts > nonce_used_max_attempts
128
115
  logger.info("Nonce used: " + exception.to_s) if self.logger
@@ -157,75 +144,15 @@ module Xeroizer
157
144
  end
158
145
  end
159
146
 
160
- def handle_oauth_error!(response)
161
- error_details = CGI.parse(response.plain_body)
162
- description = error_details["oauth_problem_advice"].first
163
- problem = error_details["oauth_problem"].first
164
-
165
- # see http://oauth.pbworks.com/ProblemReporting
166
- # In addition to token_expired and token_rejected, Xero also returns
167
- # 'rate limit exceeded' when more than 60 requests have been made in
168
- # a second.
169
- if problem
170
- case problem
171
- when "token_expired" then raise OAuth::TokenExpired.new(description)
172
- when "token_rejected" then raise OAuth::TokenInvalid.new(description)
173
- when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
174
- when "consumer_key_unknown" then raise OAuth::ConsumerKeyUnknown.new(description)
175
- when "nonce_used" then raise OAuth::NonceUsed.new(description)
176
- when "organisation offline" then raise OAuth::OrganisationOffline.new(description)
177
- else raise OAuth::UnknownError.new(problem + ':' + description)
178
- end
179
- else
180
- raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.")
181
- end
182
- end
183
-
184
- def handle_error!(response, request_body)
185
-
186
- raw_response = response.plain_body
187
-
188
- # XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
189
- # So let's ignore that :)
190
- raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
191
-
192
- # doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
193
- doc = Nokogiri::XML(raw_response)
194
-
195
- if doc && doc.root && doc.root.name == "ApiException"
196
-
197
- raise ApiException.new(doc.root.xpath("Type").text,
198
- doc.root.xpath("Message").text,
199
- raw_response,
200
- doc,
201
- request_body)
202
-
203
- else
204
- raise BadResponse.new("Unparseable 400 Response: #{raw_response}")
205
- end
206
- end
207
-
208
- def handle_object_not_found!(response, request_url)
209
- case request_url
210
- when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
211
- when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.")
212
- else raise ObjectNotFound.new(request_url)
213
- end
214
- end
215
-
216
- def handle_unknown_response_error!(response)
217
- raise BadResponse.new("Unknown response code: #{response.code.to_i}")
218
- end
219
-
220
147
  def sleep_for(seconds = 1)
221
148
  sleep seconds
222
149
  end
223
150
 
224
151
  # unitdp query string parameter to be added to request params
225
152
  # when the application option has been set and the model has line items
226
- # http://developer.xero.com/documentation/advanced-docs/rounding-in-xero/#unitamount
153
+ # https://developer.xero.com/documentation/api-guides/rounding-in-xero#unitamount
227
154
  def unitdp_param(request_url)
228
- models = [/Invoices/, /CreditNotes/, /BankTransactions/, /Receipts/]
155
+ models = [/Invoices/, /CreditNotes/, /BankTransactions/, /Receipts/, /Items/, /Overpayments/, /Prepayments/]
229
156
  self.unitdp == 4 && models.any?{ |m| request_url =~ m } ? {:unitdp => 4} : {}
230
157
  end
231
158
 
@@ -0,0 +1,154 @@
1
+ module Xeroizer
2
+ class BadResponse < XeroizerError; end
3
+
4
+ class XmlErrorResponse
5
+ def initialize(response, request_body, url)
6
+ @response = response
7
+ @request_body = request_body
8
+ @url = url
9
+ end
10
+
11
+ def raise_error!
12
+ case response.code.to_i
13
+ when 400
14
+ raise_bad_request!
15
+ when 401
16
+ raise_error
17
+ when 403
18
+ raise_error
19
+ when 404
20
+ raise_not_found!
21
+ when 429
22
+ raise_rate_limit_exceeded!
23
+ when 503
24
+ raise_error
25
+ else
26
+ raise_unknown_response_error!
27
+ end
28
+ end
29
+
30
+ def raise_error
31
+ description, problem = parse
32
+
33
+ # see http://oauth.pbworks.com/ProblemReporting
34
+ # In addition to token_expired and token_rejected, Xero also returns
35
+ # 'rate limit exceeded' when more than 60 requests have been made in
36
+ # a second.
37
+ if problem
38
+ case problem
39
+ when "token_expired" then raise OAuth::TokenExpired.new(description)
40
+ when "token_rejected" then raise OAuth::TokenInvalid.new(description)
41
+ when "rate limit exceeded" then raise OAuth::RateLimitExceeded.new(description)
42
+ when "consumer_key_unknown" then raise OAuth::ConsumerKeyUnknown.new(description)
43
+ when "nonce_used" then raise OAuth::NonceUsed.new(description)
44
+ when "organisation offline" then raise OAuth::OrganisationOffline.new(description)
45
+ else raise OAuth::UnknownError.new(problem + ':' + description)
46
+ end
47
+ else
48
+ raise OAuth::UnknownError.new("Xero API may be down or the way OAuth errors are provided by Xero may have changed.")
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :request_body, :response, :url
55
+
56
+ def parse
57
+ error_details = CGI.parse(response.plain_body)
58
+ description = error_details["oauth_problem_advice"].first
59
+ problem = error_details["oauth_problem"].first
60
+ [description, problem]
61
+ end
62
+
63
+ def raise_bad_request!
64
+
65
+ raw_response = response.plain_body
66
+
67
+ # XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
68
+ # So let's ignore that :)
69
+ raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
70
+
71
+ # doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
72
+ doc = Nokogiri::XML(raw_response)
73
+
74
+ if doc && doc.root && (doc.root.name == "ApiException" || doc.root.name == 'Response')
75
+
76
+ raise ApiException.new(doc.root.xpath("Type").text,
77
+ doc.root.xpath("Message").text,
78
+ raw_response,
79
+ doc,
80
+ request_body)
81
+
82
+ else
83
+ raise Xeroizer::BadResponse.new("Unparseable 400 Response: #{raw_response}")
84
+ end
85
+ end
86
+
87
+ def raise_not_found!
88
+ case url
89
+ when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
90
+ when /CreditNotes/ then raise CreditNoteNotFoundError.new("Credit Note not found in Xero.")
91
+ else raise ObjectNotFound.new(url)
92
+ end
93
+ end
94
+
95
+ def raise_rate_limit_exceeded!
96
+ retry_after = response.response.headers["retry-after"].to_i
97
+ daily_limit_remaining = response.response.headers["x-daylimit-remaining"].to_i
98
+
99
+ description = "Rate limit exceeded: #{daily_limit_remaining} requests left for the day, #{retry_after} seconds until you can make another request"
100
+ raise OAuth::RateLimitExceeded.new(description, retry_after: retry_after, daily_limit_remaining: daily_limit_remaining)
101
+ end
102
+
103
+ def raise_unknown_response_error!
104
+ raise Xeroizer::BadResponse.new("Unknown response code: #{response.code.to_i}")
105
+ end
106
+ end
107
+
108
+ class HttpResponse
109
+ def self.from_response(response, request_body, url)
110
+ new(response, request_body, url)
111
+ end
112
+
113
+ def initialize(response, request_body, url)
114
+ @response = response
115
+ @request_body = request_body
116
+ @url = url
117
+ end
118
+
119
+ def body
120
+ response_code = response.code.to_i
121
+ return nil if response_code == 204
122
+ raise_error! unless response.code.to_i == 200
123
+ response.plain_body
124
+ end
125
+
126
+ private
127
+
128
+ def raise_error!
129
+ begin
130
+ error_details = JSON.parse(response.plain_body)
131
+ description = error_details["Detail"]
132
+ case response.code.to_i
133
+ when 400
134
+ raise Xeroizer::BadResponse.new(description)
135
+ when 401
136
+ raise OAuth::TokenExpired.new(description) if description.include?("TokenExpired")
137
+ raise OAuth::TokenInvalid.new(description)
138
+ when 403
139
+ message = "Possible xero-tenant-id header issue. Xero Error: #{description}"
140
+ raise OAuth::Forbidden.new(message)
141
+ when 404
142
+ raise Xeroizer::ObjectNotFound.new(url)
143
+ else
144
+ raise Xeroizer::OAuth::UnknownError.new(description)
145
+ end
146
+ rescue JSON::ParserError
147
+ XmlErrorResponse.new(response, request_body, url).raise_error!
148
+ end
149
+ end
150
+
151
+ attr_reader :request_body, :response, :url
152
+ end
153
+ end
154
+
@@ -7,6 +7,7 @@ module Xeroizer
7
7
  class BankAccount < Base
8
8
  guid :account_id
9
9
  string :code
10
+ string :name
10
11
  end
11
12
  end
12
13
  end
@@ -40,6 +40,7 @@ module Xeroizer
40
40
  string :status
41
41
  string :currency_code
42
42
  decimal :currency_rate
43
+ boolean :has_attachments
43
44
 
44
45
  alias_method :reconciled?, :is_reconciled
45
46
 
@@ -0,0 +1,27 @@
1
+ module Xeroizer
2
+ module Record
3
+
4
+ class BatchPaymentModel < BaseModel
5
+ set_permissions :read, :write
6
+ end
7
+
8
+ class BatchPayment < Base
9
+ set_primary_key :batch_payment_id
10
+ list_contains_summary_only false
11
+
12
+ guid :batch_payment_id
13
+ string :reference
14
+ string :details
15
+ date :date
16
+ string :type
17
+ string :status
18
+ decimal :total_amount
19
+ boolean :is_reconciled
20
+
21
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
22
+
23
+ belongs_to :account
24
+ has_many :payments
25
+ end
26
+ end
27
+ end
@@ -1,22 +1,62 @@
1
+ require "xeroizer/models/payment_service"
2
+
1
3
  module Xeroizer
2
4
  module Record
3
-
5
+
4
6
  class BrandingThemeModel < BaseModel
5
-
6
- set_permissions :read
7
-
7
+
8
+ set_permissions :read, :write
9
+
10
+ public
11
+
12
+ def payment_services(id)
13
+ @payment_services ||= @application.http_get(@application.client, payment_services_endpoint(id))
14
+ end
15
+
16
+ def add_payment_service(id:, payment_service_id:)
17
+ xml = {
18
+ PaymentService: {
19
+ PaymentServiceID: payment_service_id
20
+ }
21
+ }.to_xml
22
+
23
+ @application.http_post(@application.client, payment_services_endpoint(id), xml)
24
+ end
25
+
26
+ private
27
+
28
+ def payment_services_endpoint(id)
29
+ "#{url}/#{id}/PaymentServices"
30
+ end
31
+
8
32
  end
9
-
33
+
10
34
  class BrandingTheme < Base
11
-
35
+
12
36
  set_primary_key :branding_theme_id
13
-
37
+
14
38
  guid :branding_theme_id
15
39
  string :name
16
40
  integer :sort_order
17
41
  datetime_utc :created_date_utc, :api_name => 'CreatedDateUTC'
18
42
 
43
+ # Unfortunately, this part of the API does not work the same as the rest.
44
+ # You cannot POST child records to Branding Themes.
45
+ #
46
+ # The endpoints are:
47
+ # GET /BrandingThemes/{BrandingThemeID}/PaymentServices
48
+ # POST /BrandingThemes/{BrandingThemeID}/PaymentServices
49
+ #
50
+ # has_one :payment_service, :model_name => 'PaymentService', :list_complete => true
51
+
52
+ def payment_services
53
+ parent.payment_services(id)
54
+ end
55
+
56
+ def add_payment_service(payment_service_id)
57
+ parent.add_payment_service(id: id, payment_service_id: payment_service_id)
58
+ end
19
59
  end
20
-
60
+
21
61
  end
22
- end
62
+ end
@@ -2,6 +2,7 @@ require "xeroizer/models/contact_person"
2
2
  require "xeroizer/models/balances"
3
3
  require "xeroizer/models/batch_payments"
4
4
  require "xeroizer/models/payment_terms"
5
+ require "xeroizer/models/history_record"
5
6
 
6
7
  module Xeroizer
7
8
  module Record
@@ -11,6 +12,7 @@ module Xeroizer
11
12
  set_permissions :read, :write, :update
12
13
 
13
14
  include AttachmentModel::Extensions
15
+ include HistoryRecordModel::Extensions
14
16
 
15
17
  end
16
18
 
@@ -19,10 +21,12 @@ module Xeroizer
19
21
  CONTACT_STATUS = {
20
22
  'ACTIVE' => 'Active',
21
23
  'DELETED' => 'Deleted',
22
- 'ARCHIVED' => 'Archived'
24
+ 'ARCHIVED' => 'Archived',
25
+ 'GDPRREQUEST' => 'GDPR Request'
23
26
  } unless defined?(CONTACT_STATUS)
24
27
 
25
28
  include Attachment::Extensions
29
+ include HistoryRecord::Extensions
26
30
 
27
31
  set_primary_key :contact_id
28
32
  set_possible_primary_keys :contact_id, :contact_number
@@ -50,20 +54,22 @@ module Xeroizer
50
54
  string :website # read only
51
55
  decimal :discount # read only
52
56
  boolean :has_attachments
57
+ string :xero_network_key
53
58
 
54
59
  has_many :addresses, :list_complete => true
55
60
  has_many :phones, :list_complete => true
56
- has_many :contact_groups
57
- has_many :contact_persons, :internal_name => :contact_people
61
+ has_many :contact_groups, :list_complete => true
62
+ has_many :contact_persons, :internal_name => :contact_people, :list_complete => true
58
63
 
59
- has_many :sales_tracking_categories, :model_name => 'ContactSalesTrackingCategory'
60
- has_many :purchases_tracking_categories, :model_name => 'ContactPurchasesTrackingCategory'
64
+ has_many :sales_tracking_categories, :model_name => 'ContactSalesTrackingCategory', :list_complete => true
65
+ has_many :purchases_tracking_categories, :model_name => 'ContactPurchasesTrackingCategory', :list_complete => true
61
66
 
62
67
  has_one :balances, :model_name => 'Balances', :list_complete => true
63
68
  has_one :batch_payments, :model_name => 'BatchPayments', :list_complete => true
64
69
  has_one :payment_terms, :model_name => 'PaymentTerms', :list_complete => true
70
+ has_one :branding_theme, :list_complete => true
65
71
 
66
- validates_presence_of :name, :unless => Proc.new { | contact | contact.contact_id.present?}
72
+ validates_presence_of :name, :unless => Proc.new { | contact | contact.contact_id.present? || contact.contact_number.present? }
67
73
  validates_inclusion_of :contact_status, :in => CONTACT_STATUS.keys, :allow_blanks => true
68
74
  validates_associated :addresses, allow_blanks: true
69
75
 
@@ -11,8 +11,53 @@ module Xeroizer
11
11
  string :name
12
12
  string :status
13
13
 
14
+ set_primary_key :contact_group_id
15
+ list_contains_summary_only true
14
16
  has_many :contacts, :list_complete => true
15
17
 
18
+ # Adding Contact uses different API endpoint
19
+ # https://developer.xero.com/documentation/api/contactgroups#PUT
20
+ def add_contact(contact)
21
+ @contacts ||= []
22
+ @contacts << contact
23
+ end
24
+
25
+ def delete
26
+ status = 'DELETED'
27
+ end
28
+
29
+ def name=(value)
30
+ @modified = true unless @attributes[:name].nil? or @attributes[:name] == value
31
+ @attributes[:name] = value
32
+ end
33
+
34
+ def status=(value)
35
+ @modified = true unless @attributes[:status].nil? or @attributes[:status] == value
36
+ @attributes[:status] = value
37
+ end
38
+
39
+ def save!
40
+ super if new_record? or @modified
41
+ @modified = false
42
+ if @contacts
43
+ req = cg_xml
44
+ app = parent.application
45
+ res = app.http_put(app.client, "#{parent.url}/#{CGI.escape(id)}/Contacts", req)
46
+ parse_save_response(res)
47
+ end
48
+ end
49
+
50
+ def cg_xml
51
+ b = Builder::XmlMarkup.new(:indent => 2)
52
+ b.tag!('Contacts') do
53
+ @contacts.each do |c|
54
+ b.tag!('Contact') do
55
+ b.tag!('ContactID', c.id)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
16
61
  end
17
62
 
18
63
  end