xeroizer 2.17.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +246 -213
- data/lib/xeroizer/connection.rb +49 -0
- data/lib/xeroizer/exceptions.rb +4 -0
- data/lib/xeroizer/generic_application.rb +13 -5
- data/lib/xeroizer/http.rb +7 -80
- data/lib/xeroizer/http_response.rb +154 -0
- data/lib/xeroizer/models/bank_account.rb +1 -0
- data/lib/xeroizer/models/bank_transaction.rb +1 -0
- data/lib/xeroizer/models/batch_payment.rb +27 -0
- data/lib/xeroizer/models/branding_theme.rb +49 -9
- data/lib/xeroizer/models/contact.rb +12 -6
- data/lib/xeroizer/models/contact_group.rb +45 -0
- data/lib/xeroizer/models/credit_note.rb +24 -22
- data/lib/xeroizer/models/currency.rb +14 -2
- data/lib/xeroizer/models/from_bank_account.rb +1 -0
- data/lib/xeroizer/models/history_record.rb +72 -0
- data/lib/xeroizer/models/invoice.rb +17 -3
- data/lib/xeroizer/models/item.rb +2 -1
- data/lib/xeroizer/models/item_purchase_details.rb +1 -1
- data/lib/xeroizer/models/line_item.rb +17 -5
- data/lib/xeroizer/models/manual_journal.rb +2 -1
- data/lib/xeroizer/models/online_invoice.rb +37 -0
- data/lib/xeroizer/models/option.rb +1 -1
- data/lib/xeroizer/models/organisation.rb +2 -0
- data/lib/xeroizer/models/payment_service.rb +22 -0
- data/lib/xeroizer/models/payroll/address.rb +53 -0
- data/lib/xeroizer/models/payroll/bank_account.rb +18 -6
- data/lib/xeroizer/models/payroll/benefit_line.rb +26 -0
- data/lib/xeroizer/models/payroll/benefit_type.rb +45 -0
- data/lib/xeroizer/models/payroll/deduction_line.rb +32 -0
- data/lib/xeroizer/models/payroll/deduction_type.rb +49 -0
- data/lib/xeroizer/models/payroll/earnings_line.rb +39 -0
- data/lib/xeroizer/models/payroll/earnings_type.rb +53 -0
- data/lib/xeroizer/models/payroll/employee.rb +30 -8
- data/lib/xeroizer/models/payroll/leave_application.rb +27 -0
- data/lib/xeroizer/models/payroll/leave_line.rb +30 -0
- data/lib/xeroizer/models/payroll/leave_period.rb +15 -0
- data/lib/xeroizer/models/payroll/pay_items.rb +22 -0
- data/lib/xeroizer/models/payroll/pay_run.rb +33 -0
- data/lib/xeroizer/models/payroll/pay_schedule.rb +40 -0
- data/lib/xeroizer/models/payroll/pay_template.rb +24 -0
- data/lib/xeroizer/models/payroll/payment_method.rb +24 -0
- data/lib/xeroizer/models/payroll/paystub.rb +44 -0
- data/lib/xeroizer/models/payroll/reimbursement_line.rb +21 -0
- data/lib/xeroizer/models/payroll/reimbursement_type.rb +22 -0
- data/lib/xeroizer/models/payroll/salary_and_wage.rb +29 -0
- data/lib/xeroizer/models/payroll/super_line.rb +40 -0
- data/lib/xeroizer/models/payroll/tax_declaration.rb +50 -0
- data/lib/xeroizer/models/payroll/time_off_line.rb +20 -0
- data/lib/xeroizer/models/payroll/time_off_type.rb +32 -0
- data/lib/xeroizer/models/payroll/work_location.rb +25 -0
- data/lib/xeroizer/models/prepayment.rb +1 -0
- data/lib/xeroizer/models/purchase_order.rb +6 -6
- data/lib/xeroizer/models/quote.rb +76 -0
- data/lib/xeroizer/models/schedule.rb +1 -0
- data/lib/xeroizer/models/tax_component.rb +1 -0
- data/lib/xeroizer/models/to_bank_account.rb +1 -0
- data/lib/xeroizer/oauth.rb +12 -1
- data/lib/xeroizer/oauth2.rb +82 -0
- data/lib/xeroizer/oauth2_application.rb +49 -0
- data/lib/xeroizer/payroll_application.rb +8 -3
- data/lib/xeroizer/record/base.rb +11 -2
- data/lib/xeroizer/record/base_model.rb +1 -1
- data/lib/xeroizer/record/base_model_http_proxy.rb +37 -17
- data/lib/xeroizer/record/model_definition_helper.rb +1 -1
- data/lib/xeroizer/record/payroll_base.rb +4 -0
- data/lib/xeroizer/record/record_association_helper.rb +4 -4
- data/lib/xeroizer/record/validators/associated_validator.rb +1 -0
- data/lib/xeroizer/record/xml_helper.rb +18 -18
- data/lib/xeroizer/report/aged_receivables_by_contact.rb +1 -1
- data/lib/xeroizer/report/cell_xml_helper.rb +13 -13
- data/lib/xeroizer/response.rb +22 -17
- data/lib/xeroizer/version.rb +1 -1
- data/lib/xeroizer.rb +34 -4
- data/test/acceptance/about_creating_bank_transactions_test.rb +89 -81
- data/test/acceptance/about_creating_prepayment_test.rb +25 -30
- data/test/acceptance/about_fetching_bank_transactions_test.rb +12 -12
- data/test/acceptance/about_online_invoice_test.rb +25 -0
- data/test/acceptance/acceptance_test.rb +28 -26
- data/test/acceptance/bank_transfer_test.rb +12 -17
- data/test/acceptance/bulk_operations_test.rb +18 -16
- data/test/acceptance/connections_test.rb +11 -0
- data/test/stub_responses/bad_request.json +6 -0
- data/test/stub_responses/connections.json +16 -0
- data/test/stub_responses/expired_oauth2_token.json +6 -0
- data/test/stub_responses/generic_response_error.json +6 -0
- data/test/stub_responses/invalid_oauth2_request_token.json +6 -0
- data/test/stub_responses/invalid_tenant_header.json +6 -0
- data/test/stub_responses/object_not_found.json +6 -0
- data/test/stub_responses/organisations.xml +10 -0
- data/test/stub_responses/payment_service.xml +15 -0
- data/test/test_helper.rb +17 -12
- data/test/unit/generic_application_test.rb +21 -10
- data/test/unit/http_test.rb +282 -10
- data/test/unit/models/address_test.rb +2 -2
- data/test/unit/models/bank_transaction_model_parsing_test.rb +2 -2
- data/test/unit/models/bank_transaction_test.rb +1 -1
- data/test/unit/models/bank_transaction_validation_test.rb +1 -1
- data/test/unit/models/contact_test.rb +20 -11
- data/test/unit/models/credit_note_test.rb +8 -8
- data/test/unit/models/employee_test.rb +4 -4
- data/test/unit/models/invoice_test.rb +12 -12
- data/test/unit/models/journal_line_test.rb +6 -6
- data/test/unit/models/journal_test.rb +4 -4
- data/test/unit/models/line_item_sum_test.rb +1 -1
- data/test/unit/models/line_item_test.rb +29 -37
- data/test/unit/models/manual_journal_test.rb +3 -3
- data/test/unit/models/organisation_test.rb +16 -2
- data/test/unit/models/payment_service_test.rb +29 -0
- data/test/unit/models/phone_test.rb +7 -7
- data/test/unit/models/prepayment_test.rb +4 -4
- data/test/unit/models/repeating_invoice_test.rb +3 -3
- data/test/unit/models/tax_rate_test.rb +2 -2
- data/test/unit/oauth2_test.rb +171 -0
- data/test/unit/oauth_config_test.rb +1 -1
- data/test/unit/record/base_model_test.rb +13 -13
- data/test/unit/record/base_test.rb +73 -4
- data/test/unit/record/block_validator_test.rb +1 -1
- data/test/unit/record/connection_test.rb +60 -0
- data/test/unit/record/model_definition_test.rb +36 -36
- data/test/unit/record/parse_params_test.rb +59 -0
- data/test/unit/record/parse_where_hash_test.rb +13 -13
- data/test/unit/record/record_association_test.rb +14 -14
- data/test/unit/record/validators_test.rb +43 -43
- data/test/unit/record_definition_test.rb +7 -7
- data/test/unit/report_definition_test.rb +7 -7
- data/test/unit/report_test.rb +20 -20
- data/test/unit_test_helper.rb +16 -0
- metadata +117 -27
- data/lib/xeroizer/models/payroll/home_address.rb +0 -24
- data/lib/xeroizer/partner_application.rb +0 -51
- data/lib/xeroizer/private_application.rb +0 -25
- data/lib/xeroizer/public_application.rb +0 -21
- data/test/unit/oauth_test.rb +0 -118
- 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
|
data/lib/xeroizer/exceptions.rb
CHANGED
@@ -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, :
|
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(
|
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 =
|
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,
|
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,
|
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) ?
|
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
|
-
|
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
|
-
#
|
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
|
+
|
@@ -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
|