xero_gateway 2.0.19 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/xero_gateway/contact.rb +3 -3
- data/lib/xero_gateway/error.rb +16 -0
- data/lib/xero_gateway/gateway.rb +100 -82
- data/lib/xero_gateway/http.rb +38 -34
- data/lib/xero_gateway/invoice.rb +13 -5
- data/lib/xero_gateway/oauth.rb +24 -23
- data/lib/xero_gateway/partner_app.rb +6 -1
- data/lib/xero_gateway/payment.rb +35 -8
- data/lib/xero_gateway/tax_rate.rb +1 -1
- data/lib/xero_gateway.rb +1 -0
- data/test/integration/create_invoice_test.rb +6 -0
- data/test/integration/create_payments_test.rb +35 -0
- data/test/integration/get_invoice_test.rb +27 -12
- data/test/unit/gateway_test.rb +15 -15
- data/test/unit/invoice_test.rb +2 -1
- data/test/unit/payment_test.rb +34 -0
- data/xero_gateway.gemspec +2 -2
- metadata +7 -7
data/lib/xero_gateway/contact.rb
CHANGED
@@ -140,7 +140,7 @@ module XeroGateway
|
|
140
140
|
b.Contact {
|
141
141
|
b.ContactID self.contact_id if self.contact_id
|
142
142
|
b.ContactNumber self.contact_number if self.contact_number
|
143
|
-
b.Name self.name
|
143
|
+
b.Name self.name if self.name
|
144
144
|
b.EmailAddress self.email if self.email
|
145
145
|
b.FirstName self.first_name if self.first_name
|
146
146
|
b.LastName self.last_name if self.last_name
|
@@ -154,10 +154,10 @@ module XeroGateway
|
|
154
154
|
b.DefaultCurrency if self.default_currency
|
155
155
|
b.Addresses {
|
156
156
|
addresses.each { |address| address.to_xml(b) }
|
157
|
-
}
|
157
|
+
} if self.addresses.any?
|
158
158
|
b.Phones {
|
159
159
|
phones.each { |phone| phone.to_xml(b) }
|
160
|
-
}
|
160
|
+
} if self.phones.any?
|
161
161
|
}
|
162
162
|
end
|
163
163
|
|
data/lib/xero_gateway/error.rb
CHANGED
@@ -14,5 +14,21 @@ module XeroGateway
|
|
14
14
|
end
|
15
15
|
return true
|
16
16
|
end
|
17
|
+
|
18
|
+
# pass a REXML::Element error object to
|
19
|
+
# have returned a new Error object
|
20
|
+
def self.parse(error_element)
|
21
|
+
description = REXML::XPath.first(error_element, "Description")
|
22
|
+
date = REXML::XPath.first(error_element, "//DateTime")
|
23
|
+
type = REXML::XPath.first(error_element, "//ExceptionType")
|
24
|
+
message = REXML::XPath.first(error_element, "//Message")
|
25
|
+
Error.new(
|
26
|
+
:description => (description.text if description),
|
27
|
+
:date_time => (date.text if date),
|
28
|
+
:type => (type.text if type),
|
29
|
+
:message => (message.text if message)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
17
33
|
end
|
18
34
|
end
|
data/lib/xero_gateway/gateway.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
module XeroGateway
|
2
|
-
|
2
|
+
|
3
3
|
class Gateway
|
4
4
|
include Http
|
5
5
|
include Dates
|
6
|
-
|
6
|
+
|
7
7
|
attr_accessor :client, :xero_url, :logger
|
8
|
-
|
8
|
+
|
9
9
|
extend Forwardable
|
10
10
|
def_delegators :client, :request_token, :access_token, :authorize_from_request, :authorize_from_access, :expires_at, :authorization_expires_at
|
11
11
|
|
12
12
|
#
|
13
13
|
# The consumer key and secret here correspond to those provided
|
14
|
-
# to you by Xero inside the API Previewer.
|
14
|
+
# to you by Xero inside the API Previewer.
|
15
15
|
def initialize(consumer_key, consumer_secret, options = {})
|
16
16
|
@xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
|
17
17
|
@client = OAuth.new(consumer_key, consumer_secret, options)
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
#
|
21
21
|
# Retrieve all contacts from Xero
|
22
22
|
#
|
@@ -34,27 +34,27 @@ module XeroGateway
|
|
34
34
|
|
35
35
|
request_params[:ContactID] = options[:contact_id] if options[:contact_id]
|
36
36
|
request_params[:ContactNumber] = options[:contact_number] if options[:contact_number]
|
37
|
-
request_params[:OrderBy] = options[:order] if options[:order]
|
37
|
+
request_params[:OrderBy] = options[:order] if options[:order]
|
38
38
|
request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
|
39
39
|
request_params[:where] = options[:where] if options[:where]
|
40
|
-
|
40
|
+
|
41
41
|
response_xml = http_get(@client, "#{@xero_url}/Contacts", request_params)
|
42
|
-
|
42
|
+
|
43
43
|
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/contacts'})
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
# Retrieve a contact from Xero
|
47
|
-
# Usage get_contact_by_id(contact_id)
|
47
|
+
# Usage get_contact_by_id(contact_id)
|
48
48
|
def get_contact_by_id(contact_id)
|
49
49
|
get_contact(contact_id)
|
50
50
|
end
|
51
51
|
|
52
52
|
# Retrieve a contact from Xero
|
53
|
-
# Usage get_contact_by_id(contact_id)
|
53
|
+
# Usage get_contact_by_id(contact_id)
|
54
54
|
def get_contact_by_number(contact_number)
|
55
55
|
get_contact(nil, contact_number)
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
# Factory method for building new Contact objects associated with this gateway.
|
59
59
|
def build_contact(contact = {})
|
60
60
|
case contact
|
@@ -63,11 +63,11 @@ module XeroGateway
|
|
63
63
|
end
|
64
64
|
contact
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
#
|
68
68
|
# Creates a contact in Xero
|
69
69
|
#
|
70
|
-
# Usage :
|
70
|
+
# Usage :
|
71
71
|
#
|
72
72
|
# contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT #{Time.now.to_i}")
|
73
73
|
# contact.email = "whoever@something.com"
|
@@ -85,24 +85,24 @@ module XeroGateway
|
|
85
85
|
def create_contact(contact)
|
86
86
|
save_contact(contact)
|
87
87
|
end
|
88
|
-
|
88
|
+
|
89
89
|
#
|
90
90
|
# Updates an existing Xero contact
|
91
91
|
#
|
92
|
-
# Usage :
|
92
|
+
# Usage :
|
93
93
|
#
|
94
94
|
# contact = xero_gateway.get_contact(some_contact_id)
|
95
95
|
# contact.email = "a_new_email_ddress"
|
96
96
|
#
|
97
|
-
# xero_gateway.update_contact(contact)
|
97
|
+
# xero_gateway.update_contact(contact)
|
98
98
|
def update_contact(contact)
|
99
99
|
raise "contact_id or contact_number is required for updating contacts" if contact.contact_id.nil? and contact.contact_number.nil?
|
100
100
|
save_contact(contact)
|
101
101
|
end
|
102
|
-
|
102
|
+
|
103
103
|
#
|
104
104
|
# Updates an array of contacts in a single API operation.
|
105
|
-
#
|
105
|
+
#
|
106
106
|
# Usage :
|
107
107
|
# contacts = [XeroGateway::Contact.new(:name => 'Joe Bloggs'), XeroGateway::Contact.new(:name => 'Jane Doe')]
|
108
108
|
# result = gateway.update_contacts(contacts)
|
@@ -116,7 +116,7 @@ module XeroGateway
|
|
116
116
|
contact.to_xml(b)
|
117
117
|
end
|
118
118
|
}
|
119
|
-
|
119
|
+
|
120
120
|
response_xml = http_post(@client, "#{@xero_url}/Contacts", request_xml, {})
|
121
121
|
|
122
122
|
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'POST/contacts'})
|
@@ -125,7 +125,7 @@ module XeroGateway
|
|
125
125
|
end
|
126
126
|
response
|
127
127
|
end
|
128
|
-
|
128
|
+
|
129
129
|
# Retrieves all invoices from Xero
|
130
130
|
#
|
131
131
|
# Usage : get_invoices
|
@@ -133,12 +133,12 @@ module XeroGateway
|
|
133
133
|
#
|
134
134
|
# Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
|
135
135
|
def get_invoices(options = {})
|
136
|
-
|
136
|
+
|
137
137
|
request_params = {}
|
138
|
-
|
138
|
+
|
139
139
|
request_params[:InvoiceID] = options[:invoice_id] if options[:invoice_id]
|
140
140
|
request_params[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number]
|
141
|
-
request_params[:OrderBy] = options[:order] if options[:order]
|
141
|
+
request_params[:OrderBy] = options[:order] if options[:order]
|
142
142
|
request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
|
143
143
|
|
144
144
|
request_params[:where] = options[:where] if options[:where]
|
@@ -147,21 +147,33 @@ module XeroGateway
|
|
147
147
|
|
148
148
|
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/Invoices'})
|
149
149
|
end
|
150
|
-
|
150
|
+
|
151
151
|
# Retrieves a single invoice
|
152
152
|
#
|
153
|
+
# You can get a PDF-formatted invoice by specifying :pdf as the format argument
|
154
|
+
#
|
153
155
|
# Usage : get_invoice("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
|
154
156
|
# get_invoice("OIT-12345") # By number
|
155
|
-
def get_invoice(invoice_id_or_number)
|
157
|
+
def get_invoice(invoice_id_or_number, format = :xml)
|
156
158
|
request_params = {}
|
157
|
-
|
159
|
+
headers = {}
|
160
|
+
|
161
|
+
headers.merge!("Accept" => "application/pdf") if format == :pdf
|
162
|
+
|
158
163
|
url = "#{@xero_url}/Invoices/#{URI.escape(invoice_id_or_number)}"
|
159
|
-
|
160
|
-
response_xml = http_get(@client, url, request_params)
|
161
164
|
|
162
|
-
|
165
|
+
response = http_get(@client, url, request_params, headers)
|
166
|
+
|
167
|
+
if format == :pdf
|
168
|
+
Tempfile.open(invoice_id_or_number) do |f|
|
169
|
+
f.write(response)
|
170
|
+
f
|
171
|
+
end
|
172
|
+
else
|
173
|
+
parse_response(response, {:request_params => request_params}, {:request_signature => 'GET/Invoice'})
|
174
|
+
end
|
163
175
|
end
|
164
|
-
|
176
|
+
|
165
177
|
# Factory method for building new Invoice objects associated with this gateway.
|
166
178
|
def build_invoice(invoice = {})
|
167
179
|
case invoice
|
@@ -170,12 +182,12 @@ module XeroGateway
|
|
170
182
|
end
|
171
183
|
invoice
|
172
184
|
end
|
173
|
-
|
185
|
+
|
174
186
|
# Creates an invoice in Xero based on an invoice object.
|
175
187
|
#
|
176
188
|
# Invoice and line item totals are calculated automatically.
|
177
189
|
#
|
178
|
-
# Usage :
|
190
|
+
# Usage :
|
179
191
|
#
|
180
192
|
# invoice = XeroGateway::Invoice.new({
|
181
193
|
# :invoice_type => "ACCREC",
|
@@ -186,7 +198,7 @@ module XeroGateway
|
|
186
198
|
# })
|
187
199
|
# invoice.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
|
188
200
|
# invoice.contact.phone.number = "12345"
|
189
|
-
# invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
|
201
|
+
# invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
|
190
202
|
# invoice.line_items << XeroGateway::LineItem.new(
|
191
203
|
# :description => "THE DESCRIPTION OF THE LINE ITEM",
|
192
204
|
# :unit_amount => 100,
|
@@ -203,12 +215,12 @@ module XeroGateway
|
|
203
215
|
#
|
204
216
|
# Updates an existing Xero invoice
|
205
217
|
#
|
206
|
-
# Usage :
|
218
|
+
# Usage :
|
207
219
|
#
|
208
220
|
# invoice = xero_gateway.get_invoice(some_invoice_id)
|
209
221
|
# invoice.due_date = Date.today
|
210
222
|
#
|
211
|
-
# xero_gateway.update_invoice(invoice)
|
223
|
+
# xero_gateway.update_invoice(invoice)
|
212
224
|
|
213
225
|
def update_invoice(invoice)
|
214
226
|
raise "invoice_id is required for updating invoices" if invoice.invoice_id.nil?
|
@@ -217,7 +229,7 @@ module XeroGateway
|
|
217
229
|
|
218
230
|
#
|
219
231
|
# Creates an array of invoices with a single API request.
|
220
|
-
#
|
232
|
+
#
|
221
233
|
# Usage :
|
222
234
|
# invoices = [XeroGateway::Invoice.new(...), XeroGateway::Invoice.new(...)]
|
223
235
|
# result = gateway.create_invoices(invoices)
|
@@ -229,8 +241,8 @@ module XeroGateway
|
|
229
241
|
invoice.to_xml(b)
|
230
242
|
end
|
231
243
|
}
|
232
|
-
|
233
|
-
response_xml = http_put(@client, "#{@xero_url}/Invoices", request_xml, {})
|
244
|
+
|
245
|
+
response_xml = http_put(@client, "#{@xero_url}/Invoices?SummarizeErrors=false", request_xml, {})
|
234
246
|
|
235
247
|
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/invoices'})
|
236
248
|
response.invoices.each_with_index do | response_invoice, index |
|
@@ -246,12 +258,12 @@ module XeroGateway
|
|
246
258
|
#
|
247
259
|
# Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
|
248
260
|
def get_credit_notes(options = {})
|
249
|
-
|
261
|
+
|
250
262
|
request_params = {}
|
251
|
-
|
263
|
+
|
252
264
|
request_params[:CreditNoteID] = options[:credit_note_id] if options[:credit_note_id]
|
253
265
|
request_params[:CreditNoteNumber] = options[:credit_note_number] if options[:credit_note_number]
|
254
|
-
request_params[:OrderBy] = options[:order] if options[:order]
|
266
|
+
request_params[:OrderBy] = options[:order] if options[:order]
|
255
267
|
request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
|
256
268
|
|
257
269
|
request_params[:where] = options[:where] if options[:where]
|
@@ -260,21 +272,21 @@ module XeroGateway
|
|
260
272
|
|
261
273
|
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNotes'})
|
262
274
|
end
|
263
|
-
|
275
|
+
|
264
276
|
# Retrieves a single credit_note
|
265
277
|
#
|
266
278
|
# Usage : get_credit_note("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
|
267
279
|
# get_credit_note("OIT-12345") # By number
|
268
280
|
def get_credit_note(credit_note_id_or_number)
|
269
281
|
request_params = {}
|
270
|
-
|
282
|
+
|
271
283
|
url = "#{@xero_url}/CreditNotes/#{URI.escape(credit_note_id_or_number)}"
|
272
|
-
|
284
|
+
|
273
285
|
response_xml = http_get(@client, url, request_params)
|
274
286
|
|
275
287
|
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNote'})
|
276
288
|
end
|
277
|
-
|
289
|
+
|
278
290
|
# Factory method for building new CreditNote objects associated with this gateway.
|
279
291
|
def build_credit_note(credit_note = {})
|
280
292
|
case credit_note
|
@@ -283,12 +295,12 @@ module XeroGateway
|
|
283
295
|
end
|
284
296
|
credit_note
|
285
297
|
end
|
286
|
-
|
298
|
+
|
287
299
|
# Creates an credit_note in Xero based on an credit_note object.
|
288
300
|
#
|
289
301
|
# CreditNote and line item totals are calculated automatically.
|
290
302
|
#
|
291
|
-
# Usage :
|
303
|
+
# Usage :
|
292
304
|
#
|
293
305
|
# credit_note = XeroGateway::CreditNote.new({
|
294
306
|
# :credit_note_type => "ACCREC",
|
@@ -299,7 +311,7 @@ module XeroGateway
|
|
299
311
|
# })
|
300
312
|
# credit_note.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
|
301
313
|
# credit_note.contact.phone.number = "12345"
|
302
|
-
# credit_note.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
|
314
|
+
# credit_note.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
|
303
315
|
# credit_note.line_items << XeroGateway::LineItem.new(
|
304
316
|
# :description => "THE DESCRIPTION OF THE LINE ITEM",
|
305
317
|
# :unit_amount => 100,
|
@@ -313,21 +325,21 @@ module XeroGateway
|
|
313
325
|
request_xml = credit_note.to_xml
|
314
326
|
response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml)
|
315
327
|
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_note'})
|
316
|
-
|
328
|
+
|
317
329
|
# Xero returns credit_notes inside an <CreditNotes> tag, even though there's only ever
|
318
330
|
# one for this request
|
319
331
|
response.response_item = response.credit_notes.first
|
320
|
-
|
332
|
+
|
321
333
|
if response.success? && response.credit_note && response.credit_note.credit_note_id
|
322
|
-
credit_note.credit_note_id = response.credit_note.credit_note_id
|
334
|
+
credit_note.credit_note_id = response.credit_note.credit_note_id
|
323
335
|
end
|
324
|
-
|
336
|
+
|
325
337
|
response
|
326
338
|
end
|
327
|
-
|
339
|
+
|
328
340
|
#
|
329
341
|
# Creates an array of credit_notes with a single API request.
|
330
|
-
#
|
342
|
+
#
|
331
343
|
# Usage :
|
332
344
|
# credit_notes = [XeroGateway::CreditNote.new(...), XeroGateway::CreditNote.new(...)]
|
333
345
|
# result = gateway.create_credit_notes(credit_notes)
|
@@ -339,7 +351,7 @@ module XeroGateway
|
|
339
351
|
credit_note.to_xml(b)
|
340
352
|
end
|
341
353
|
}
|
342
|
-
|
354
|
+
|
343
355
|
response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml, {})
|
344
356
|
|
345
357
|
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_notes'})
|
@@ -475,7 +487,7 @@ module XeroGateway
|
|
475
487
|
response_xml = http_get(@client, "#{xero_url}/Accounts")
|
476
488
|
parse_response(response_xml, {}, {:request_signature => 'GET/accounts'})
|
477
489
|
end
|
478
|
-
|
490
|
+
|
479
491
|
#
|
480
492
|
# Returns a XeroGateway::AccountsList object that makes working with
|
481
493
|
# the Xero list of accounts easier and allows caching the results.
|
@@ -500,7 +512,7 @@ module XeroGateway
|
|
500
512
|
response_xml = http_get(@client, "#{xero_url}/Organisation")
|
501
513
|
parse_response(response_xml, {}, {:request_signature => 'GET/organisation'})
|
502
514
|
end
|
503
|
-
|
515
|
+
|
504
516
|
#
|
505
517
|
# Gets all currencies for a specific organisation in Xero
|
506
518
|
#
|
@@ -508,7 +520,7 @@ module XeroGateway
|
|
508
520
|
response_xml = http_get(@client, "#{xero_url}/Currencies")
|
509
521
|
parse_response(response_xml, {}, {:request_signature => 'GET/currencies'})
|
510
522
|
end
|
511
|
-
|
523
|
+
|
512
524
|
#
|
513
525
|
# Gets all Tax Rates for a specific organisation in Xero
|
514
526
|
#
|
@@ -517,19 +529,33 @@ module XeroGateway
|
|
517
529
|
parse_response(response_xml, {}, {:request_signature => 'GET/tax_rates'})
|
518
530
|
end
|
519
531
|
|
532
|
+
#
|
533
|
+
# Create Payment record in Xero
|
534
|
+
#
|
535
|
+
def create_payment(payment)
|
536
|
+
b = Builder::XmlMarkup.new
|
537
|
+
|
538
|
+
request_xml = b.Payments do
|
539
|
+
payment.to_xml(b)
|
540
|
+
end
|
541
|
+
|
542
|
+
response_xml = http_put(@client, "#{xero_url}/Payments", request_xml)
|
543
|
+
parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/payments'})
|
544
|
+
end
|
545
|
+
|
520
546
|
private
|
521
547
|
|
522
|
-
def get_contact(contact_id = nil, contact_number = nil)
|
548
|
+
def get_contact(contact_id = nil, contact_number = nil)
|
523
549
|
request_params = contact_id ? { :contactID => contact_id } : { :contactNumber => contact_number }
|
524
550
|
response_xml = http_get(@client, "#{@xero_url}/Contacts/#{URI.escape(contact_id||contact_number)}", request_params)
|
525
551
|
|
526
552
|
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/contact'})
|
527
553
|
end
|
528
|
-
|
554
|
+
|
529
555
|
# Create or update a contact record based on if it has a contact_id or contact_number.
|
530
556
|
def save_contact(contact)
|
531
557
|
request_xml = contact.to_xml
|
532
|
-
|
558
|
+
|
533
559
|
response_xml = nil
|
534
560
|
create_or_save = nil
|
535
561
|
if contact.contact_id.nil? && contact.contact_number.nil?
|
@@ -550,7 +576,7 @@ module XeroGateway
|
|
550
576
|
# Create or update an invoice record based on if it has an invoice_id.
|
551
577
|
def save_invoice(invoice)
|
552
578
|
request_xml = invoice.to_xml
|
553
|
-
|
579
|
+
|
554
580
|
response_xml = nil
|
555
581
|
create_or_save = nil
|
556
582
|
if invoice.invoice_id.nil?
|
@@ -564,15 +590,15 @@ module XeroGateway
|
|
564
590
|
end
|
565
591
|
|
566
592
|
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => "#{create_or_save == :create ? 'PUT' : 'POST'}/invoice"})
|
567
|
-
|
593
|
+
|
568
594
|
# Xero returns invoices inside an <Invoices> tag, even though there's only ever
|
569
595
|
# one for this request
|
570
596
|
response.response_item = response.invoices.first
|
571
|
-
|
597
|
+
|
572
598
|
if response.success? && response.invoice && response.invoice.invoice_id
|
573
|
-
invoice.invoice_id = response.invoice.invoice_id
|
599
|
+
invoice.invoice_id = response.invoice.invoice_id
|
574
600
|
end
|
575
|
-
|
601
|
+
|
576
602
|
response
|
577
603
|
end
|
578
604
|
|
@@ -637,12 +663,12 @@ module XeroGateway
|
|
637
663
|
response = XeroGateway::Response.new
|
638
664
|
|
639
665
|
doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
640
|
-
|
666
|
+
|
641
667
|
# check for responses we don't understand
|
642
668
|
raise UnparseableResponse.new(doc.root.name) unless doc.root.name == "Response"
|
643
669
|
|
644
670
|
response_element = REXML::XPath.first(doc, "/Response")
|
645
|
-
|
671
|
+
|
646
672
|
response_element.children.reject { |e| e.is_a? REXML::Text }.each do |element|
|
647
673
|
case(element.name)
|
648
674
|
when "ID" then response.response_id = element.text
|
@@ -671,28 +697,20 @@ module XeroGateway
|
|
671
697
|
when "Currencies" then element.children.each {|child| response.response_item << Currency.from_xml(child) }
|
672
698
|
when "Organisations" then response.response_item = Organisation.from_xml(element.children.first) # Xero only returns the Authorized Organisation
|
673
699
|
when "TrackingCategories" then element.children.each {|child| response.response_item << TrackingCategory.from_xml(child) }
|
674
|
-
when "Errors" then element.children.
|
700
|
+
when "Errors" then response.errors = element.children.map { |error| Error.parse(error) }
|
675
701
|
end
|
676
702
|
end if response_element
|
677
|
-
|
703
|
+
|
678
704
|
# If a single result is returned don't put it in an array
|
679
705
|
if response.response_item.is_a?(Array) && response.response_item.size == 1
|
680
706
|
response.response_item = response.response_item.first
|
681
707
|
end
|
682
|
-
|
708
|
+
|
683
709
|
response.request_params = request[:request_params]
|
684
710
|
response.request_xml = request[:request_xml]
|
685
711
|
response.response_xml = raw_response
|
686
712
|
response
|
687
|
-
end
|
688
|
-
|
689
|
-
def parse_error(error_element, response)
|
690
|
-
response.errors << Error.new(
|
691
|
-
:description => REXML::XPath.first(error_element, "Description").text,
|
692
|
-
:date_time => REXML::XPath.first(error_element, "//DateTime").text,
|
693
|
-
:type => REXML::XPath.first(error_element, "//ExceptionType").text,
|
694
|
-
:message => REXML::XPath.first(error_element, "//Message").text
|
695
|
-
)
|
696
713
|
end
|
697
|
-
|
714
|
+
|
715
|
+
end
|
698
716
|
end
|
data/lib/xero_gateway/http.rb
CHANGED
@@ -3,25 +3,25 @@ module XeroGateway
|
|
3
3
|
OPEN_TIMEOUT = 10 unless defined? OPEN_TIMEOUT
|
4
4
|
READ_TIMEOUT = 60 unless defined? READ_TIMEOUT
|
5
5
|
ROOT_CA_FILE = File.join(File.dirname(__FILE__), 'ca-certificates.crt') unless defined? ROOT_CA_FILE
|
6
|
-
|
7
|
-
def http_get(client, url, extra_params = {})
|
8
|
-
http_request(client, :get, url, nil, extra_params)
|
6
|
+
|
7
|
+
def http_get(client, url, extra_params = {}, headers = {})
|
8
|
+
http_request(client, :get, url, nil, extra_params, headers)
|
9
9
|
end
|
10
10
|
|
11
|
-
def http_post(client, url, body, extra_params = {})
|
12
|
-
http_request(client, :post, url, body, extra_params)
|
11
|
+
def http_post(client, url, body, extra_params = {}, headers = {})
|
12
|
+
http_request(client, :post, url, body, extra_params, headers)
|
13
13
|
end
|
14
14
|
|
15
|
-
def http_put(client, url, body, extra_params = {})
|
16
|
-
http_request(client, :put, url, body, extra_params)
|
15
|
+
def http_put(client, url, body, extra_params = {}, headers = {})
|
16
|
+
http_request(client, :put, url, body, extra_params, headers)
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
private
|
20
|
-
|
21
|
-
def http_request(client, method, url, body, params = {})
|
20
|
+
|
21
|
+
def http_request(client, method, url, body, params = {}, headers = {})
|
22
22
|
# headers = {'Accept-Encoding' => 'gzip, deflate'}
|
23
23
|
|
24
|
-
headers =
|
24
|
+
headers = headers.merge!('charset' => 'utf-8')
|
25
25
|
|
26
26
|
if method != :get
|
27
27
|
headers['Content-Type'] ||= "application/x-www-form-urlencoded"
|
@@ -38,39 +38,43 @@ module XeroGateway
|
|
38
38
|
|
39
39
|
# # Only setup @cached_http once on first use as loading the CA file is quite expensive computationally.
|
40
40
|
# unless @cached_http && @cached_http.address == uri.host && @cached_http.port == uri.port
|
41
|
-
# @cached_http = Net::HTTP.new(uri.host, uri.port)
|
41
|
+
# @cached_http = Net::HTTP.new(uri.host, uri.port)
|
42
42
|
# @cached_http.open_timeout = OPEN_TIMEOUT
|
43
43
|
# @cached_http.read_timeout = READ_TIMEOUT
|
44
44
|
# @cached_http.use_ssl = true
|
45
|
-
#
|
45
|
+
#
|
46
46
|
# # Need to validate server's certificate against root certificate authority to prevent man-in-the-middle attacks.
|
47
47
|
# @cached_http.ca_file = ROOT_CA_FILE
|
48
48
|
# # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
49
49
|
# @cached_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
50
50
|
# @cached_http.verify_depth = 5
|
51
51
|
# end
|
52
|
-
|
52
|
+
|
53
53
|
logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{uri.request_uri} ") if self.logger
|
54
|
-
|
54
|
+
|
55
55
|
response = case method
|
56
56
|
when :get then client.get(uri.request_uri, headers)
|
57
57
|
when :post then client.post(uri.request_uri, { :xml => body }, headers)
|
58
58
|
when :put then client.put(uri.request_uri, { :xml => body }, headers)
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
61
|
if self.logger
|
62
62
|
logger.info("== [#{Time.now.to_s}] XeroGateway Response (#{response.code})")
|
63
|
-
|
63
|
+
|
64
64
|
unless response.code.to_i == 200
|
65
65
|
logger.info("== #{uri.request_uri} Response Body \n\n #{response.plain_body} \n == End Response Body")
|
66
66
|
end
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
case response.code.to_i
|
70
70
|
when 200
|
71
|
-
|
71
|
+
if RUBY_VERSION >= "1.9"
|
72
|
+
response.plain_body.force_encoding("UTF-8")
|
73
|
+
else
|
74
|
+
response.plain_body
|
75
|
+
end
|
72
76
|
when 400
|
73
|
-
handle_error!(body, response)
|
77
|
+
handle_error!(body, response)
|
74
78
|
when 401
|
75
79
|
handle_oauth_error!(response)
|
76
80
|
when 404
|
@@ -79,11 +83,11 @@ module XeroGateway
|
|
79
83
|
raise "Unknown response code: #{response.code.to_i}"
|
80
84
|
end
|
81
85
|
end
|
82
|
-
|
86
|
+
|
83
87
|
def handle_oauth_error!(response)
|
84
88
|
error_details = CGI.parse(response.plain_body)
|
85
89
|
description = error_details["oauth_problem_advice"].first
|
86
|
-
|
90
|
+
|
87
91
|
# see http://oauth.pbworks.com/ProblemReporting
|
88
92
|
# In addition to token_expired and token_rejected, Xero also returns
|
89
93
|
# 'rate limit exceeded' when more than 60 requests have been made in
|
@@ -96,32 +100,32 @@ module XeroGateway
|
|
96
100
|
else raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
|
97
101
|
end
|
98
102
|
end
|
99
|
-
|
103
|
+
|
100
104
|
def handle_error!(request_xml, response)
|
101
|
-
|
105
|
+
|
102
106
|
raw_response = response.plain_body
|
103
|
-
|
107
|
+
|
104
108
|
# Xero Gateway API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
|
105
109
|
# So let's ignore that :)
|
106
110
|
raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
|
107
|
-
|
111
|
+
|
108
112
|
doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
109
|
-
|
113
|
+
|
110
114
|
if doc.root.name == "ApiException"
|
111
115
|
|
112
|
-
raise ApiException.new(doc.root.elements["Type"].text,
|
116
|
+
raise ApiException.new(doc.root.elements["Type"].text,
|
113
117
|
doc.root.elements["Message"].text,
|
114
|
-
request_xml,
|
118
|
+
request_xml,
|
115
119
|
raw_response)
|
116
120
|
|
117
121
|
else
|
118
|
-
|
122
|
+
|
119
123
|
raise "Unparseable 400 Response: #{raw_response}"
|
120
|
-
|
124
|
+
|
121
125
|
end
|
122
|
-
|
126
|
+
|
123
127
|
end
|
124
|
-
|
128
|
+
|
125
129
|
def handle_object_not_found!(response, request_url)
|
126
130
|
case(request_url)
|
127
131
|
when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
|
@@ -130,6 +134,6 @@ module XeroGateway
|
|
130
134
|
else raise ObjectNotFound.new(request_url)
|
131
135
|
end
|
132
136
|
end
|
133
|
-
|
137
|
+
|
134
138
|
end
|
135
139
|
end
|
data/lib/xero_gateway/invoice.rb
CHANGED
@@ -30,13 +30,14 @@ module XeroGateway
|
|
30
30
|
attr_accessor :gateway
|
31
31
|
|
32
32
|
# Any errors that occurred when the #valid? method called.
|
33
|
-
|
33
|
+
# Or errors that were within the XML payload from Xero
|
34
|
+
attr_accessor :errors
|
34
35
|
|
35
36
|
# Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/INVOICES
|
36
37
|
attr_accessor :line_items_downloaded
|
37
38
|
|
38
39
|
# All accessible fields
|
39
|
-
attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_due, :amount_paid, :amount_credited, :sent_to_contact, :url
|
40
|
+
attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :branding_theme_id, :line_amount_types, :currency_code, :line_items, :contact, :payments, :fully_paid_on, :amount_due, :amount_paid, :amount_credited, :sent_to_contact, :url
|
40
41
|
|
41
42
|
|
42
43
|
def initialize(params = {})
|
@@ -66,6 +67,10 @@ module XeroGateway
|
|
66
67
|
def valid?
|
67
68
|
@errors = []
|
68
69
|
|
70
|
+
if !INVOICE_TYPE[invoice_type]
|
71
|
+
@errors << ['invoice_type', "must be one of #{INVOICE_TYPE.keys.join('/')}"]
|
72
|
+
end
|
73
|
+
|
69
74
|
if !invoice_id.nil? && invoice_id !~ GUID_REGEX
|
70
75
|
@errors << ['invoice_id', 'must be blank or a valid Xero GUID']
|
71
76
|
end
|
@@ -187,6 +192,7 @@ module XeroGateway
|
|
187
192
|
b.DueDate Invoice.format_date(self.due_date) if self.due_date
|
188
193
|
b.Status self.invoice_status if self.invoice_status
|
189
194
|
b.Reference self.reference if self.reference
|
195
|
+
b.BrandingThemeID self.branding_theme_id if self.branding_theme_id
|
190
196
|
b.LineAmountTypes self.line_amount_types
|
191
197
|
b.LineItems {
|
192
198
|
self.line_items.each do |line_item|
|
@@ -211,22 +217,24 @@ module XeroGateway
|
|
211
217
|
when "DueDate" then invoice.due_date = parse_date(element.text)
|
212
218
|
when "Status" then invoice.invoice_status = element.text
|
213
219
|
when "Reference" then invoice.reference = element.text
|
220
|
+
when "BrandingThemeID" then invoice.branding_theme_id = element.text
|
214
221
|
when "LineAmountTypes" then invoice.line_amount_types = element.text
|
215
222
|
when "LineItems" then element.children.each {|line_item| invoice.line_items_downloaded = true; invoice.line_items << LineItem.from_xml(line_item) }
|
216
223
|
when "SubTotal" then invoice.sub_total = BigDecimal.new(element.text)
|
217
224
|
when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
|
218
225
|
when "Total" then invoice.total = BigDecimal.new(element.text)
|
219
226
|
when "InvoiceID" then invoice.invoice_id = element.text
|
220
|
-
when "InvoiceNumber" then invoice.invoice_number = element.text
|
227
|
+
when "InvoiceNumber" then invoice.invoice_number = element.text
|
221
228
|
when "Payments" then element.children.each { | payment | invoice.payments << Payment.from_xml(payment) }
|
222
229
|
when "AmountDue" then invoice.amount_due = BigDecimal.new(element.text)
|
223
230
|
when "AmountPaid" then invoice.amount_paid = BigDecimal.new(element.text)
|
224
231
|
when "AmountCredited" then invoice.amount_credited = BigDecimal.new(element.text)
|
225
232
|
when "SentToContact" then invoice.sent_to_contact = (element.text.strip.downcase == "true")
|
226
233
|
when "Url" then invoice.url = element.text
|
234
|
+
when "ValidationErrors" then invoice.errors = element.children.map { |error| Error.parse(error) }
|
227
235
|
end
|
228
|
-
end
|
236
|
+
end
|
229
237
|
invoice
|
230
|
-
end
|
238
|
+
end
|
231
239
|
end
|
232
240
|
end
|
data/lib/xero_gateway/oauth.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
module XeroGateway
|
2
|
-
|
2
|
+
|
3
3
|
# Shamelessly based on the Twitter Gem's OAuth implementation by John Nunemaker
|
4
4
|
# Thanks!
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# http://twitter.rubyforge.org/
|
7
7
|
# http://github.com/jnunemaker/twitter/
|
8
|
-
|
8
|
+
|
9
9
|
class OAuth
|
10
|
-
|
10
|
+
|
11
11
|
class TokenExpired < StandardError; end
|
12
12
|
class TokenInvalid < StandardError; end
|
13
13
|
class RateLimitExceeded < StandardError; end
|
14
14
|
class UnknownError < StandardError; end
|
15
|
-
|
15
|
+
|
16
16
|
unless defined? XERO_CONSUMER_OPTIONS
|
17
17
|
XERO_CONSUMER_OPTIONS = {
|
18
18
|
:site => "https://api.xero.com",
|
@@ -21,54 +21,55 @@ module XeroGateway
|
|
21
21
|
:authorize_path => "/oauth/Authorize"
|
22
22
|
}.freeze
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
extend Forwardable
|
26
26
|
def_delegators :access_token, :get, :post, :put, :delete
|
27
|
-
|
28
|
-
attr_reader
|
29
|
-
|
27
|
+
|
28
|
+
attr_reader :ctoken, :csecret, :consumer_options, :authorization_expires_at
|
29
|
+
attr_accessor :session_handle
|
30
|
+
|
30
31
|
def initialize(ctoken, csecret, options = {})
|
31
32
|
@ctoken, @csecret = ctoken, csecret
|
32
33
|
@consumer_options = XERO_CONSUMER_OPTIONS.merge(options)
|
33
34
|
end
|
34
|
-
|
35
|
+
|
35
36
|
def consumer
|
36
37
|
@consumer ||= ::OAuth::Consumer.new(@ctoken, @csecret, consumer_options)
|
37
38
|
end
|
38
|
-
|
39
|
+
|
39
40
|
def request_token(params = {})
|
40
41
|
@request_token ||= consumer.get_request_token(params)
|
41
42
|
end
|
42
|
-
|
43
|
+
|
43
44
|
def authorize_from_request(rtoken, rsecret, params = {})
|
44
45
|
request_token = ::OAuth::RequestToken.new(consumer, rtoken, rsecret)
|
45
46
|
access_token = request_token.get_access_token(params)
|
46
47
|
@atoken, @asecret = access_token.token, access_token.secret
|
47
|
-
|
48
|
+
|
48
49
|
update_attributes_from_token(access_token)
|
49
50
|
end
|
50
|
-
|
51
|
+
|
51
52
|
def access_token
|
52
53
|
@access_token ||= ::OAuth::AccessToken.new(consumer, @atoken, @asecret)
|
53
54
|
end
|
54
|
-
|
55
|
+
|
55
56
|
def authorize_from_access(atoken, asecret)
|
56
57
|
@atoken, @asecret = atoken, asecret
|
57
58
|
end
|
58
|
-
|
59
|
+
|
59
60
|
# Renewing access tokens only works for Partner applications
|
60
61
|
def renew_access_token(access_token = nil, access_secret = nil, session_handle = nil)
|
61
62
|
access_token ||= @atoken
|
62
|
-
access_secret ||= @asecret
|
63
|
+
access_secret ||= @asecret
|
63
64
|
session_handle ||= @session_handle
|
64
|
-
|
65
|
+
|
65
66
|
old_token = ::OAuth::RequestToken.new(consumer, access_token, access_secret)
|
66
|
-
|
67
|
+
|
67
68
|
access_token = old_token.get_access_token({
|
68
69
|
:oauth_session_handle => session_handle,
|
69
70
|
:token => old_token
|
70
71
|
})
|
71
|
-
|
72
|
+
|
72
73
|
update_attributes_from_token(access_token)
|
73
74
|
rescue ::OAuth::Unauthorized => e
|
74
75
|
# If the original access token is for some reason invalid an OAuth::Unauthorized could be raised.
|
@@ -76,9 +77,9 @@ module XeroGateway
|
|
76
77
|
# situation the end user will need to re-authorize the application via the request token authorization URL
|
77
78
|
raise XeroGateway::OAuth::TokenInvalid.new(e.message)
|
78
79
|
end
|
79
|
-
|
80
|
+
|
80
81
|
private
|
81
|
-
|
82
|
+
|
82
83
|
# Update instance variables with those from the AccessToken.
|
83
84
|
def update_attributes_from_token(access_token)
|
84
85
|
@expires_at = Time.now + access_token.params[:oauth_expires_in].to_i
|
@@ -87,6 +88,6 @@ module XeroGateway
|
|
87
88
|
@atoken, @asecret = access_token.token, access_token.secret
|
88
89
|
@access_token = nil
|
89
90
|
end
|
90
|
-
|
91
|
+
|
91
92
|
end
|
92
93
|
end
|
@@ -6,7 +6,7 @@ module XeroGateway
|
|
6
6
|
NO_SSL_CLIENT_CERT_MESSAGE = "You need to provide a client ssl certificate and key pair (these are the ones you got from Entrust and should not be password protected) as :ssl_client_cert and :ssl_client_key (should be .crt or .pem files)"
|
7
7
|
NO_PRIVATE_KEY_ERROR_MESSAGE = "You need to provide your private key (corresponds to the public key you uploaded at api.xero.com) as :private_key_file (should be .crt or .pem files)"
|
8
8
|
|
9
|
-
def_delegators :client, :session_handle, :renew_access_token
|
9
|
+
def_delegators :client, :session_handle, :renew_access_token, :authorization_expires_at
|
10
10
|
|
11
11
|
def initialize(consumer_key, consumer_secret, options = {})
|
12
12
|
|
@@ -26,5 +26,10 @@ module XeroGateway
|
|
26
26
|
@xero_url = options[:xero_url] || "https://api-partner.xero.com/api.xro/2.0"
|
27
27
|
@client = OAuth.new(consumer_key, consumer_secret, options)
|
28
28
|
end
|
29
|
+
|
30
|
+
def set_session_handle(handle)
|
31
|
+
client.session_handle = handle
|
32
|
+
end
|
33
|
+
|
29
34
|
end
|
30
35
|
end
|
data/lib/xero_gateway/payment.rb
CHANGED
@@ -7,16 +7,16 @@ module XeroGateway
|
|
7
7
|
attr_reader :errors
|
8
8
|
|
9
9
|
# All accessible fields
|
10
|
-
attr_accessor :payment_id, :date, :amount, :reference, :currency_rate
|
11
|
-
|
10
|
+
attr_accessor :invoice_id, :invoice_number, :account_id, :code, :payment_id, :date, :amount, :reference, :currency_rate
|
11
|
+
|
12
12
|
def initialize(params = {})
|
13
13
|
@errors ||= []
|
14
|
-
|
14
|
+
|
15
15
|
params.each do |k,v|
|
16
16
|
self.send("#{k}=", v)
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def self.from_xml(payment_element)
|
21
21
|
payment = Payment.new
|
22
22
|
payment_element.children.each do | element |
|
@@ -26,18 +26,45 @@ module XeroGateway
|
|
26
26
|
when 'Amount' then payment.amount = BigDecimal.new(element.text)
|
27
27
|
when 'Reference' then payment.reference = element.text
|
28
28
|
when 'CurrencyRate' then payment.currency_rate = BigDecimal.new(element.text)
|
29
|
-
|
29
|
+
when 'Invoice' then payment.send("#{element.children.first.name.underscore}=", element.children.first.text)
|
30
|
+
when 'Account' then payment.send("#{element.children.first.name.underscore}=", element.children.first.text)
|
31
|
+
end
|
30
32
|
end
|
31
33
|
payment
|
32
|
-
end
|
33
|
-
|
34
|
+
end
|
35
|
+
|
34
36
|
def ==(other)
|
35
37
|
[:payment_id, :date, :amount].each do |field|
|
36
38
|
return false if send(field) != other.send(field)
|
37
39
|
end
|
38
40
|
return true
|
39
41
|
end
|
40
|
-
|
42
|
+
|
43
|
+
def to_xml(b = Builder::XmlMarkup.new)
|
44
|
+
b.Payment do
|
45
|
+
|
46
|
+
if self.invoice_id || self.invoice_number
|
47
|
+
b.Invoice do |i|
|
48
|
+
i.InvoiceID self.invoice_id if self.invoice_id
|
49
|
+
i.InvoiceNumber self.invoice_number if self.invoice_number
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if self.account_id || self.code
|
54
|
+
b.Account do |a|
|
55
|
+
a.AccountID self.account_id if self.account_id
|
56
|
+
a.Code self.code if self.code
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
b.Amount self.amount if self.amount
|
61
|
+
b.CurrencyRate self.currency_rate if self.currency_rate
|
62
|
+
b.Reference self.reference if self.reference
|
63
|
+
|
64
|
+
b.Date self.class.format_date(self.date || Date.today)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
41
68
|
|
42
69
|
end
|
43
70
|
end
|
@@ -47,7 +47,7 @@ module XeroGateway
|
|
47
47
|
attribute = element.name
|
48
48
|
underscored_attribute = element.name.underscore
|
49
49
|
|
50
|
-
|
50
|
+
next if !ATTRS.keys.include?(attribute)
|
51
51
|
|
52
52
|
case (ATTRS[attribute])
|
53
53
|
when :boolean then tax_rate.send("#{underscored_attribute}=", (element.text == "true"))
|
data/lib/xero_gateway.rb
CHANGED
@@ -33,6 +33,12 @@ class CreateInvoiceTest < Test::Unit::TestCase
|
|
33
33
|
example_invoice = dummy_invoice.dup
|
34
34
|
assert_equal true, example_invoice.valid?, "invoice is invalid - errors:\n\t#{example_invoice.errors.map { | error | "#{error[0]} #{error[1]}"}.join("\n\t")}"
|
35
35
|
end
|
36
|
+
|
37
|
+
def test_create_invoice_invalid_with_invalid_invoice_type
|
38
|
+
example_invoice = dummy_invoice.dup
|
39
|
+
example_invoice.invoice_type = "ABC"
|
40
|
+
assert_equal false, example_invoice.valid?
|
41
|
+
end
|
36
42
|
|
37
43
|
private
|
38
44
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
class CreatePaymentsTest < Test::Unit::TestCase
|
4
|
+
include TestHelper
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@gateway = XeroGateway::Gateway.new(CONSUMER_KEY, CONSUMER_SECRET)
|
8
|
+
|
9
|
+
if STUB_XERO_CALLS
|
10
|
+
@gateway.xero_url = "DUMMY_URL"
|
11
|
+
|
12
|
+
@gateway.stubs(:http_put).with {|client, url, body, params| url =~ /Invoices$/ }.returns(get_file_as_string("create_invoice.xml"))
|
13
|
+
@gateway.stubs(:http_put).with {|client, url, params| url =~ /Payments$/ }.returns(get_file_as_string("create_payments.xml"))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_create_payment
|
18
|
+
example_invoice = dummy_invoice.dup
|
19
|
+
|
20
|
+
result = @gateway.create_invoice(example_invoice)
|
21
|
+
|
22
|
+
payment = XeroGateway::Payment.new(
|
23
|
+
:invoice_id => result.invoice.invoice_id,
|
24
|
+
:amount => 500,
|
25
|
+
:reference => "Test Payment",
|
26
|
+
:date => Time.now,
|
27
|
+
:code => "601"
|
28
|
+
)
|
29
|
+
|
30
|
+
result = @gateway.create_payment(payment)
|
31
|
+
|
32
|
+
assert_kind_of XeroGateway::Response, result
|
33
|
+
assert result.success?
|
34
|
+
end
|
35
|
+
end
|
@@ -2,18 +2,25 @@ require File.dirname(__FILE__) + '/../test_helper'
|
|
2
2
|
|
3
3
|
class GetInvoiceTest < Test::Unit::TestCase
|
4
4
|
include TestHelper
|
5
|
-
|
5
|
+
|
6
|
+
INVOICE_GET_URL = /Invoices(\/[0-9a-z\-]+)?$/i
|
7
|
+
|
6
8
|
def setup
|
7
9
|
@gateway = XeroGateway::Gateway.new(CONSUMER_KEY, CONSUMER_SECRET)
|
8
|
-
|
10
|
+
|
9
11
|
if STUB_XERO_CALLS
|
10
12
|
@gateway.xero_url = "DUMMY_URL"
|
11
|
-
|
12
|
-
@gateway.stubs(:http_get).with
|
13
|
-
|
13
|
+
|
14
|
+
@gateway.stubs(:http_get).with do |client, url, params, headers|
|
15
|
+
url =~ INVOICE_GET_URL && headers["Accept"] == "application/pdf"
|
16
|
+
end.returns(get_file_as_string("get_invoice.pdf"))
|
17
|
+
|
18
|
+
@gateway.stubs(:http_get).with {|client, url, params, headers| url =~ INVOICE_GET_URL && headers["Accept"].blank? }.returns(get_file_as_string("invoice.xml"))
|
19
|
+
@gateway.stubs(:http_put).with {|client, url, body, params| url =~ /Invoices$/ }.returns(get_file_as_string("create_invoice.xml"))
|
20
|
+
|
14
21
|
end
|
15
22
|
end
|
16
|
-
|
23
|
+
|
17
24
|
def test_get_invoice
|
18
25
|
# Make sure there is an invoice in Xero to retrieve
|
19
26
|
invoice = @gateway.create_invoice(dummy_invoice).invoice
|
@@ -21,28 +28,36 @@ class GetInvoiceTest < Test::Unit::TestCase
|
|
21
28
|
result = @gateway.get_invoice(invoice.invoice_id)
|
22
29
|
assert result.success?
|
23
30
|
assert !result.request_params.nil?
|
24
|
-
assert !result.response_xml.nil?
|
31
|
+
assert !result.response_xml.nil?
|
25
32
|
assert_equal result.invoice.invoice_number, invoice.invoice_number
|
26
33
|
|
27
34
|
result = @gateway.get_invoice(invoice.invoice_number)
|
28
35
|
assert result.success?
|
29
36
|
assert !result.request_params.nil?
|
30
|
-
assert !result.response_xml.nil?
|
37
|
+
assert !result.response_xml.nil?
|
31
38
|
assert_equal result.invoice.invoice_id, invoice.invoice_id
|
32
39
|
end
|
33
|
-
|
40
|
+
|
34
41
|
def test_line_items_downloaded_set_correctly
|
35
42
|
# Make sure there is an invoice in Xero to retrieve.
|
36
43
|
example_invoice = @gateway.create_invoice(dummy_invoice).invoice
|
37
|
-
|
44
|
+
|
38
45
|
# No line items.
|
39
46
|
response = @gateway.get_invoice(example_invoice.invoice_id)
|
40
47
|
assert_equal(true, response.success?)
|
41
|
-
|
48
|
+
|
42
49
|
invoice = response.invoice
|
43
50
|
assert_kind_of(XeroGateway::LineItem, invoice.line_items.first)
|
44
51
|
assert_kind_of(XeroGateway::Invoice, invoice)
|
45
52
|
assert_equal(true, invoice.line_items_downloaded?)
|
46
53
|
end
|
47
|
-
|
54
|
+
|
55
|
+
def test_get_invoice_pdf
|
56
|
+
# Make sure there is an invoice in Xero to retrieve
|
57
|
+
example_invoice = @gateway.create_invoice(dummy_invoice).invoice
|
58
|
+
|
59
|
+
pdf_tempfile = @gateway.get_invoice(example_invoice.invoice_id, :pdf)
|
60
|
+
assert_equal get_file_as_string("get_invoice.pdf"), File.open(pdf_tempfile.path).read
|
61
|
+
end
|
62
|
+
|
48
63
|
end
|
data/test/unit/gateway_test.rb
CHANGED
@@ -6,28 +6,28 @@ class GatewayTest < Test::Unit::TestCase
|
|
6
6
|
def setup
|
7
7
|
@gateway = XeroGateway::Gateway.new(CONSUMER_KEY, CONSUMER_SECRET)
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
context "with error handling" do
|
11
|
-
|
11
|
+
|
12
12
|
should "handle token expired" do
|
13
13
|
XeroGateway::OAuth.any_instance.stubs(:get).returns(stub(:plain_body => get_file_as_string("token_expired"), :code => "401"))
|
14
|
-
|
14
|
+
|
15
15
|
assert_raises XeroGateway::OAuth::TokenExpired do
|
16
16
|
@gateway.get_accounts
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
should "handle invalid request tokens" do
|
21
21
|
XeroGateway::OAuth.any_instance.stubs(:get).returns(stub(:plain_body => get_file_as_string("invalid_request_token"), :code => "401"))
|
22
|
-
|
22
|
+
|
23
23
|
assert_raises XeroGateway::OAuth::TokenInvalid do
|
24
24
|
@gateway.get_accounts
|
25
25
|
end
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
should "handle invalid consumer key" do
|
29
29
|
XeroGateway::OAuth.any_instance.stubs(:get).returns(stub(:plain_body => get_file_as_string("invalid_consumer_key"), :code => "401"))
|
30
|
-
|
30
|
+
|
31
31
|
assert_raises XeroGateway::OAuth::TokenInvalid do
|
32
32
|
@gateway.get_accounts
|
33
33
|
end
|
@@ -48,10 +48,10 @@ class GatewayTest < Test::Unit::TestCase
|
|
48
48
|
@gateway.get_accounts
|
49
49
|
end
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
should "handle ApiExceptions" do
|
53
53
|
XeroGateway::OAuth.any_instance.stubs(:put).returns(stub(:plain_body => get_file_as_string("api_exception.xml"), :code => "400"))
|
54
|
-
|
54
|
+
|
55
55
|
assert_raises XeroGateway::ApiException do
|
56
56
|
@gateway.create_invoice(XeroGateway::Invoice.new)
|
57
57
|
end
|
@@ -86,17 +86,17 @@ class GatewayTest < Test::Unit::TestCase
|
|
86
86
|
|
87
87
|
assert_raises XeroGateway::UnparseableResponse do
|
88
88
|
@gateway.create_invoice(XeroGateway::Invoice.new)
|
89
|
-
end
|
89
|
+
end
|
90
90
|
end
|
91
|
-
|
91
|
+
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
def test_unknown_error_handling
|
95
95
|
if STUB_XERO_CALLS
|
96
96
|
@gateway.xero_url = "DUMMY_URL"
|
97
|
-
@gateway.stubs(:http_get).with {|client, url, params| url =~ /Invoices\/AN_INVALID_ID$/ }.returns(get_file_as_string("unknown_error.xml"))
|
97
|
+
@gateway.stubs(:http_get).with {|client, url, params| url =~ /Invoices\/AN_INVALID_ID$/ }.returns(get_file_as_string("unknown_error.xml"))
|
98
98
|
end
|
99
|
-
|
99
|
+
|
100
100
|
result = @gateway.get_invoice("AN_INVALID_ID")
|
101
101
|
assert !result.success?
|
102
102
|
assert_equal 1, result.errors.size
|
@@ -109,7 +109,7 @@ class GatewayTest < Test::Unit::TestCase
|
|
109
109
|
@gateway.xero_url = "DUMMY_URL"
|
110
110
|
@gateway.stubs(:http_get).with {|client, url, params| url =~ /Invoices\/UNKNOWN_INVOICE_NO$/ }.returns(get_file_as_string("invoice_not_found_error.xml"))
|
111
111
|
end
|
112
|
-
|
112
|
+
|
113
113
|
result = @gateway.get_invoice("UNKNOWN_INVOICE_NO")
|
114
114
|
assert !result.success?
|
115
115
|
assert_equal 1, result.errors.size
|
data/test/unit/invoice_test.rb
CHANGED
@@ -240,8 +240,9 @@ class InvoiceTest < Test::Unit::TestCase
|
|
240
240
|
end
|
241
241
|
|
242
242
|
def test_optional_params
|
243
|
-
invoice = create_test_invoice(:url => 'http://example.com')
|
243
|
+
invoice = create_test_invoice(:url => 'http://example.com', :branding_theme_id => 'a94a78db-5cc6-4e26-a52b-045237e56e6e')
|
244
244
|
assert_equal 'http://example.com', invoice.url
|
245
|
+
assert_equal 'a94a78db-5cc6-4e26-a52b-045237e56e6e', invoice.branding_theme_id
|
245
246
|
end
|
246
247
|
|
247
248
|
private
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../test_helper.rb')
|
2
|
+
|
3
|
+
class PaymentTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# Tests that a payment can be converted into XML that Xero can understand, and then converted back to a payment
|
6
|
+
def test_build_and_parse_xml
|
7
|
+
payment = create_test_payment
|
8
|
+
|
9
|
+
# Generate the XML message
|
10
|
+
payment_as_xml = payment.to_xml
|
11
|
+
|
12
|
+
# Parse the XML message and retrieve the account element
|
13
|
+
payment_element = REXML::XPath.first(REXML::Document.new(payment_as_xml), "/Payment")
|
14
|
+
|
15
|
+
# Build a new account from the XML
|
16
|
+
result_payment = XeroGateway::Payment.from_xml(payment_element)
|
17
|
+
|
18
|
+
# Check the details
|
19
|
+
assert_equal payment, result_payment
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def create_test_payment
|
26
|
+
XeroGateway::Payment.new.tap do |payment|
|
27
|
+
payment.invoice_id = "a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999"
|
28
|
+
payment.amount = 100.0
|
29
|
+
payment.date = Time.now.beginning_of_day
|
30
|
+
payment.reference = "Invoice Payment"
|
31
|
+
payment.code = 200
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/xero_gateway.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "xero_gateway"
|
3
|
-
s.version = "2.0
|
4
|
-
s.date = "2012-
|
3
|
+
s.version = "2.1.0"
|
4
|
+
s.date = "2012-11-19"
|
5
5
|
s.summary = "Enables ruby based applications to communicate with the Xero API"
|
6
6
|
s.email = "tim@connorsoftware.com"
|
7
7
|
s.homepage = "http://github.com/tlconnor/xero_gateway"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xero_gateway
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 11
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 2.0.19
|
10
|
+
version: 2.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tim Connor
|
@@ -16,8 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2012-
|
20
|
-
default_executable:
|
19
|
+
date: 2012-11-19 00:00:00 Z
|
21
20
|
dependencies:
|
22
21
|
- !ruby/object:Gem::Dependency
|
23
22
|
name: builder
|
@@ -118,6 +117,7 @@ files:
|
|
118
117
|
- test/integration/create_credit_note_test.rb
|
119
118
|
- test/integration/create_invoice_test.rb
|
120
119
|
- test/integration/create_manual_journal_test.rb
|
120
|
+
- test/integration/create_payments_test.rb
|
121
121
|
- test/integration/get_accounts_test.rb
|
122
122
|
- test/integration/get_bank_transaction_test.rb
|
123
123
|
- test/integration/get_bank_transactions_test.rb
|
@@ -148,10 +148,10 @@ files:
|
|
148
148
|
- test/unit/manual_journal_test.rb
|
149
149
|
- test/unit/oauth_test.rb
|
150
150
|
- test/unit/organisation_test.rb
|
151
|
+
- test/unit/payment_test.rb
|
151
152
|
- test/unit/tax_rate_test.rb
|
152
153
|
- test/unit/tracking_category_test.rb
|
153
154
|
- lib/xero_gateway/ca-certificates.crt
|
154
|
-
has_rdoc: true
|
155
155
|
homepage: http://github.com/tlconnor/xero_gateway
|
156
156
|
licenses: []
|
157
157
|
|
@@ -181,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
181
181
|
requirements: []
|
182
182
|
|
183
183
|
rubyforge_project:
|
184
|
-
rubygems_version: 1.
|
184
|
+
rubygems_version: 1.8.15
|
185
185
|
signing_key:
|
186
186
|
specification_version: 3
|
187
187
|
summary: Enables ruby based applications to communicate with the Xero API
|