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.
- 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
|