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