xeroizer 2.17.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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