xero_gateway-float 2.0.18 → 2.1.1

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.
Files changed (36) hide show
  1. data/Rakefile +7 -1
  2. data/examples/partner_app.rb +2 -2
  3. data/lib/xero_gateway/accounts_list.rb +0 -4
  4. data/lib/xero_gateway/bank_transaction.rb +0 -2
  5. data/lib/xero_gateway/contact.rb +6 -9
  6. data/lib/xero_gateway/credit_note.rb +1 -3
  7. data/lib/xero_gateway/error.rb +16 -0
  8. data/lib/xero_gateway/exceptions.rb +5 -0
  9. data/lib/xero_gateway/gateway.rb +184 -82
  10. data/lib/xero_gateway/http.rb +38 -32
  11. data/lib/xero_gateway/invoice.rb +17 -9
  12. data/lib/xero_gateway/journal_line.rb +102 -0
  13. data/lib/xero_gateway/line_item_calculations.rb +0 -4
  14. data/lib/xero_gateway/manual_journal.rb +163 -0
  15. data/lib/xero_gateway/oauth.rb +29 -23
  16. data/lib/xero_gateway/partner_app.rb +6 -1
  17. data/lib/xero_gateway/response.rb +2 -0
  18. data/lib/xero_gateway/tax_rate.rb +1 -1
  19. data/lib/xero_gateway.rb +3 -0
  20. data/test/integration/accounts_list_test.rb +4 -4
  21. data/test/integration/create_invoice_test.rb +6 -0
  22. data/test/integration/create_manual_journal_test.rb +35 -0
  23. data/test/integration/create_payments_test.rb +35 -0
  24. data/test/integration/get_invoice_test.rb +27 -12
  25. data/test/integration/get_manual_journal_test.rb +50 -0
  26. data/test/integration/get_manual_journals_test.rb +88 -0
  27. data/test/integration/update_manual_journal_test.rb +31 -0
  28. data/test/test_helper.rb +39 -1
  29. data/test/unit/bank_transaction_test.rb +1 -1
  30. data/test/unit/credit_note_test.rb +1 -1
  31. data/test/unit/gateway_test.rb +15 -15
  32. data/test/unit/invoice_test.rb +3 -2
  33. data/test/unit/manual_journal_test.rb +93 -0
  34. data/test/unit/payment_test.rb +34 -0
  35. data/xero_gateway.gemspec +2 -2
  36. metadata +11 -2
@@ -7,26 +7,28 @@ module XeroGateway
7
7
  def log(str)
8
8
  XeroGateway.log("HTTP : "+str)
9
9
  end
10
- def http_get(client, url, extra_params = {})
10
+
11
+ def http_get(client, url, extra_params = {}, headers = {})
11
12
  log "get | #{url} :: #{extra_params.inspect}"
12
- http_request(client, :get, url, nil, extra_params)
13
+ http_request(client, :get, url, nil, extra_params, headers)
13
14
  end
14
15
 
15
- def http_post(client, url, body, extra_params = {})
16
+ def http_post(client, url, body, extra_params = {}, headers = {})
16
17
  log "post | #{url} :: #{extra_params.inspect}"
17
- http_request(client, :post, url, body, extra_params)
18
+ http_request(client, :post, url, body, extra_params, headers)
18
19
  end
19
20
 
20
- def http_put(client, url, body, extra_params = {})
21
+ def http_put(client, url, body, extra_params = {}, headers = {})
21
22
  log "put | #{url} :: #{extra_params.inspect}"
22
- http_request(client, :put, url, body, extra_params)
23
+ http_request(client, :put, url, body, extra_params, headers)
23
24
  end
24
25
 
25
26
  private
26
-
27
- def http_request(client, method, url, body, params = {})
27
+
28
+ def http_request(client, method, url, body, params = {}, headers = {})
28
29
  # headers = {'Accept-Encoding' => 'gzip, deflate'}
29
- headers = { 'charset' => 'utf-8' }
30
+
31
+ headers = headers.merge!('charset' => 'utf-8')
30
32
 
31
33
  if method != :get
32
34
  headers['Content-Type'] ||= "application/x-www-form-urlencoded"
@@ -45,39 +47,43 @@ module XeroGateway
45
47
 
46
48
  # # Only setup @cached_http once on first use as loading the CA file is quite expensive computationally.
47
49
  # unless @cached_http && @cached_http.address == uri.host && @cached_http.port == uri.port
48
- # @cached_http = Net::HTTP.new(uri.host, uri.port)
50
+ # @cached_http = Net::HTTP.new(uri.host, uri.port)
49
51
  # @cached_http.open_timeout = OPEN_TIMEOUT
50
52
  # @cached_http.read_timeout = READ_TIMEOUT
51
53
  # @cached_http.use_ssl = true
52
- #
54
+ #
53
55
  # # Need to validate server's certificate against root certificate authority to prevent man-in-the-middle attacks.
54
56
  # @cached_http.ca_file = ROOT_CA_FILE
55
57
  # # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
56
58
  # @cached_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
57
59
  # @cached_http.verify_depth = 5
58
60
  # end
59
-
61
+
60
62
  logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{uri.request_uri} ") if self.logger
61
-
63
+
62
64
  response = case method
63
65
  when :get then client.get(uri.request_uri, headers)
64
66
  when :post then client.post(uri.request_uri, { :xml => body }, headers)
65
67
  when :put then client.put(uri.request_uri, { :xml => body }, headers)
66
68
  end
67
-
69
+
68
70
  if self.logger
69
71
  logger.info("== [#{Time.now.to_s}] XeroGateway Response (#{response.code})")
70
-
72
+
71
73
  unless response.code.to_i == 200
72
74
  logger.info("== #{uri.request_uri} Response Body \n\n #{response.plain_body} \n == End Response Body")
73
75
  end
74
76
  end
75
-
77
+
76
78
  case response.code.to_i
77
79
  when 200
78
- response.plain_body
80
+ if RUBY_VERSION >= "1.9"
81
+ response.plain_body.force_encoding("UTF-8")
82
+ else
83
+ response.plain_body
84
+ end
79
85
  when 400
80
- handle_error!(body, response)
86
+ handle_error!(body, response)
81
87
  when 401
82
88
  handle_oauth_error!(response)
83
89
  when 404
@@ -88,11 +94,11 @@ module XeroGateway
88
94
  raise "Unknown response code: #{response.code.to_i}"
89
95
  end
90
96
  end
91
-
97
+
92
98
  def handle_oauth_error!(response)
93
99
  error_details = CGI.parse(response.plain_body)
94
100
  description = error_details["oauth_problem_advice"].first
95
-
101
+
96
102
  # see http://oauth.pbworks.com/ProblemReporting
97
103
  # In addition to token_expired and token_rejected, Xero also returns
98
104
  # 'rate limit exceeded' when more than 60 requests have been made in
@@ -105,32 +111,32 @@ module XeroGateway
105
111
  else raise OAuth::UnknownError.new(error_details["oauth_problem"].first + ':' + description)
106
112
  end
107
113
  end
108
-
114
+
109
115
  def handle_error!(request_xml, response)
110
-
116
+
111
117
  raw_response = response.plain_body
112
-
118
+
113
119
  # Xero Gateway API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
114
120
  # So let's ignore that :)
115
121
  raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
116
-
122
+
117
123
  doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
118
-
124
+
119
125
  if doc.root.name == "ApiException"
120
126
 
121
- raise ApiException.new(doc.root.elements["Type"].text,
127
+ raise ApiException.new(doc.root.elements["Type"].text,
122
128
  doc.root.elements["Message"].text,
123
- request_xml,
129
+ request_xml,
124
130
  raw_response)
125
131
 
126
132
  else
127
-
133
+
128
134
  raise "Unparseable 400 Response: #{raw_response}"
129
-
135
+
130
136
  end
131
-
137
+
132
138
  end
133
-
139
+
134
140
  def handle_object_not_found!(response, request_url)
135
141
  case(request_url)
136
142
  when /Invoices/ then raise InvoiceNotFoundError.new("Invoice not found in Xero.")
@@ -139,6 +145,6 @@ module XeroGateway
139
145
  else raise ObjectNotFound.new(request_url)
140
146
  end
141
147
  end
142
-
148
+
143
149
  end
144
150
  end
@@ -4,8 +4,6 @@ module XeroGateway
4
4
  include Money
5
5
  include LineItemCalculations
6
6
 
7
- class NoGatewayError < Error; end
8
-
9
7
  INVOICE_TYPE = {
10
8
  'ACCREC' => 'Accounts Receivable',
11
9
  'ACCPAY' => 'Accounts Payable'
@@ -32,13 +30,16 @@ module XeroGateway
32
30
  attr_accessor :gateway
33
31
 
34
32
  # Any errors that occurred when the #valid? method called.
35
- attr_reader :errors
33
+ # Or errors that were within the XML payload from Xero
34
+ attr_accessor :errors
36
35
 
37
36
  # Represents whether the line_items have been downloaded when getting from GET /API.XRO/2.0/INVOICES
38
37
  attr_accessor :line_items_downloaded
39
38
 
40
39
  # All accessible fields
41
- 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, :sub_total, :total_tax, :total, :updated_date_utc
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, :updated_date_utc
41
+ # Are these buggy?
42
+ #attr_accessor :sub_total, :total_tax, :total
42
43
 
43
44
 
44
45
  def initialize(params = {})
@@ -68,6 +69,10 @@ module XeroGateway
68
69
  def valid?
69
70
  @errors = []
70
71
 
72
+ if !INVOICE_TYPE[invoice_type]
73
+ @errors << ['invoice_type', "must be one of #{INVOICE_TYPE.keys.join('/')}"]
74
+ end
75
+
71
76
  if !invoice_id.nil? && invoice_id !~ GUID_REGEX
72
77
  @errors << ['invoice_id', 'must be blank or a valid Xero GUID']
73
78
  end
@@ -165,14 +170,14 @@ module XeroGateway
165
170
  end
166
171
 
167
172
  # Creates this invoice record (using gateway.create_invoice) with the associated gateway.
168
- # If no gateway set, raise a Xero::Invoice::NoGatewayError exception.
173
+ # If no gateway set, raise a NoGatewayError exception.
169
174
  def create
170
175
  raise NoGatewayError unless gateway
171
176
  gateway.create_invoice(self)
172
177
  end
173
178
 
174
179
  # Updates this invoice record (using gateway.update_invoice) with the associated gateway.
175
- # If no gateway set, raise a Xero::Invoice::NoGatewayError exception.
180
+ # If no gateway set, raise a NoGatewayError exception.
176
181
  def update
177
182
  raise NoGatewayError unless gateway
178
183
  gateway.update_invoice(self)
@@ -189,6 +194,7 @@ module XeroGateway
189
194
  b.DueDate Invoice.format_date(self.due_date) if self.due_date
190
195
  b.Status self.invoice_status if self.invoice_status
191
196
  b.Reference self.reference if self.reference
197
+ b.BrandingThemeID self.branding_theme_id if self.branding_theme_id
192
198
  b.LineAmountTypes self.line_amount_types
193
199
  b.LineItems {
194
200
  self.line_items.each do |line_item|
@@ -214,13 +220,14 @@ module XeroGateway
214
220
  when "FullyPaidOnDate" then invoice.fully_paid_on = parse_date(element.text)
215
221
  when "Status" then invoice.invoice_status = element.text
216
222
  when "Reference" then invoice.reference = element.text
223
+ when "BrandingThemeID" then invoice.branding_theme_id = element.text
217
224
  when "LineAmountTypes" then invoice.line_amount_types = element.text
218
225
  when "LineItems" then element.children.each {|line_item| invoice.line_items_downloaded = true; invoice.line_items << LineItem.from_xml(line_item) }
219
226
  when "SubTotal" then invoice.sub_total = BigDecimal.new(element.text)
220
227
  when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
221
228
  when "Total" then invoice.total = BigDecimal.new(element.text)
222
229
  when "InvoiceID" then invoice.invoice_id = element.text
223
- when "InvoiceNumber" then invoice.invoice_number = element.text
230
+ when "InvoiceNumber" then invoice.invoice_number = element.text
224
231
  when "Payments" then element.children.each { | payment | invoice.payments << Payment.from_xml(payment) }
225
232
  when "AmountDue" then invoice.amount_due = BigDecimal.new(element.text)
226
233
  when "AmountPaid" then invoice.amount_paid = BigDecimal.new(element.text)
@@ -228,9 +235,10 @@ module XeroGateway
228
235
  when "SentToContact" then invoice.sent_to_contact = (element.text.strip.downcase == "true")
229
236
  when "Url" then invoice.url = element.text
230
237
  when "UpdatedDateUTC" then invoice.updated_date_utc = parse_utc_date_time(element.text)
238
+ when "ValidationErrors" then invoice.errors = element.children.map { |error| Error.parse(error) }
231
239
  end
232
- end
240
+ end
233
241
  invoice
234
- end
242
+ end
235
243
  end
236
244
  end
@@ -0,0 +1,102 @@
1
+ require File.join(File.dirname(__FILE__), 'account')
2
+
3
+ module XeroGateway
4
+ class JournalLine
5
+ include Money
6
+
7
+ TAX_TYPE = Account::TAX_TYPE unless defined?(TAX_TYPE)
8
+
9
+ # Any errors that occurred when the #valid? method called.
10
+ attr_reader :errors
11
+
12
+ # All accessible fields
13
+ attr_accessor :journal_line_id, :line_amount, :account_code, :description, :tax_type, :tracking
14
+
15
+ def initialize(params = {})
16
+ @errors ||= []
17
+ @tracking ||= []
18
+
19
+ params.each do |k,v|
20
+ self.send("#{k}=", v)
21
+ end
22
+ end
23
+
24
+ # Validate the JournalLineItem record according to what will be valid by the gateway.
25
+ #
26
+ # Usage:
27
+ # journal_line_item.valid? # Returns true/false
28
+ #
29
+ # Additionally sets journal_line_item.errors array to an array of field/error.
30
+ def valid?
31
+ @errors = []
32
+
33
+ if !journal_line_id.nil? && journal_line_id !~ GUID_REGEX
34
+ @errors << ['journal_line_id', 'must be blank or a valid Xero GUID']
35
+ end
36
+
37
+ unless line_amount
38
+ @errors << ['line_amount', "can't be blank"]
39
+ end
40
+
41
+ unless account_code
42
+ @errors << ['account_code', "can't be blank"]
43
+ end
44
+
45
+ @errors.size == 0
46
+ end
47
+
48
+ def has_tracking?
49
+ return false if tracking.nil?
50
+
51
+ if tracking.is_a?(Array)
52
+ return tracking.any?
53
+ else
54
+ return tracking.is_a?(TrackingCategory)
55
+ end
56
+ end
57
+
58
+ def to_xml(b = Builder::XmlMarkup.new)
59
+ b.JournalLine {
60
+ b.LineAmount line_amount # mandatory
61
+ b.AccountCode account_code # mandatory
62
+ b.Description description if description # optional
63
+ b.TaxType tax_type if tax_type # optional
64
+ if has_tracking?
65
+ b.Tracking { # optional
66
+ # Due to strange retardness in the Xero API, the XML structure for a tracking category within
67
+ # an invoice is different to a standalone tracking category.
68
+ # This means rather than going category.to_xml we need to call the special category.to_xml_for_invoice_messages
69
+ (tracking.is_a?(TrackingCategory) ? [tracking] : tracking).each do |category|
70
+ category.to_xml_for_invoice_messages(b)
71
+ end
72
+ }
73
+ end
74
+ }
75
+ end
76
+
77
+ def self.from_xml(journal_line_element)
78
+ journal_line = JournalLine.new
79
+ journal_line_element.children.each do |element|
80
+ case(element.name)
81
+ when "LineAmount" then journal_line.line_amount = BigDecimal.new(element.text)
82
+ when "AccountCode" then journal_line.account_code = element.text
83
+ when "JournalLineID" then journal_line.journal_line_id = element.text
84
+ when "Description" then journal_line.description = element.text
85
+ when "TaxType" then journal_line.tax_type = element.text
86
+ when "Tracking" then
87
+ element.children.each do | tracking_element |
88
+ journal_line.tracking << TrackingCategory.from_xml(tracking_element)
89
+ end
90
+ end
91
+ end
92
+ journal_line
93
+ end
94
+
95
+ def ==(other)
96
+ [:description, :line_amount, :account_code, :tax_type].each do |field|
97
+ return false if send(field) != other.send(field)
98
+ end
99
+ return true
100
+ end
101
+ end
102
+ end
@@ -1,9 +1,5 @@
1
1
  module XeroGateway
2
2
  module LineItemCalculations
3
-
4
- class Error < RuntimeError; end
5
- class InvalidLineItemError < Error; end
6
-
7
3
  def add_line_item(params = {})
8
4
  line_item = nil
9
5
  case params
@@ -0,0 +1,163 @@
1
+ module XeroGateway
2
+ class ManualJournal
3
+ include Dates
4
+
5
+ GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
6
+
7
+ STATUSES = {
8
+ 'DRAFT' => 'Draft Manual Journal',
9
+ 'POSTED' => 'Posted Manual Journal',
10
+ 'DELETED' => 'Deleted Draft Manual Journal',
11
+ 'VOIDED' => 'Voided Posted Manual Journal'
12
+ } unless defined?(STATUSES)
13
+
14
+ # Xero::Gateway associated with this invoice.
15
+ attr_accessor :gateway
16
+
17
+ # Any errors that occurred when the #valid? method called.
18
+ attr_reader :errors
19
+
20
+ # Represents whether the journal lines have been downloaded when getting from GET /API.XRO/2.0/ManualJournals
21
+ attr_accessor :journal_lines_downloaded
22
+
23
+ # accessible fields
24
+ attr_accessor :manual_journal_id, :narration, :date, :status, :journal_lines, :url, :show_on_cash_basis_reports
25
+
26
+ def initialize(params = {})
27
+ @errors ||= []
28
+ @payments ||= []
29
+
30
+ # Check if the line items have been downloaded.
31
+ @journal_lines_downloaded = (params.delete(:journal_lines_downloaded) == true)
32
+
33
+ params.each do |k,v|
34
+ self.send("#{k}=", v)
35
+ end
36
+
37
+ @journal_lines ||= []
38
+ end
39
+
40
+ def ==(other)
41
+ ['narration', 'status', 'journal_lines', 'show_on_cash_basis_reports'].each do |field|
42
+ return false if send(field) != other.send(field)
43
+ end
44
+
45
+ ["date"].each do |field|
46
+ return false if send(field).to_s != other.send(field).to_s
47
+ end
48
+ return true
49
+ end
50
+
51
+ # Validate the ManualJournal record according to what will be valid by the gateway.
52
+ #
53
+ # Usage:
54
+ # manual_journal.valid? # Returns true/false
55
+ #
56
+ # Additionally sets manual_journal.errors array to an array of field/error.
57
+ def valid?
58
+ @errors = []
59
+
60
+ if !manual_journal_id.nil? && manual_journal_id !~ GUID_REGEX
61
+ @errors << ['manual_journal_id', 'must be blank or a valid Xero GUID']
62
+ end
63
+
64
+ if narration.blank?
65
+ @errors << ['narration', "can't be blank"]
66
+ end
67
+
68
+ unless date
69
+ @errors << ['date', "can't be blank"]
70
+ end
71
+
72
+ # Make sure all journal_items are valid.
73
+ unless journal_lines.all? { | journal_line | journal_line.valid? }
74
+ @errors << ['journal_lines', "at least one journal line invalid"]
75
+ end
76
+
77
+ # make sure there are at least 2 journal lines
78
+ unless journal_lines.length > 1
79
+ @errors << ['journal_lines', "journal must contain at least two individual journal lines"]
80
+ end
81
+
82
+ if journal_lines.length > 100
83
+ @errors << ['journal_lines', "journal must contain less than one hundred journal lines"]
84
+ end
85
+
86
+ unless journal_lines.sum(&:line_amount).to_f == 0.0
87
+ @errors << ['journal_lines', "the total debits must be equal to total credits"]
88
+ end
89
+
90
+ @errors.size == 0
91
+ end
92
+
93
+
94
+ def journal_lines_downloaded?
95
+ @journal_lines_downloaded
96
+ end
97
+
98
+ # If line items are not downloaded, then attempt a download now (if this record was found to begin with).
99
+ def journal_lines
100
+ if journal_lines_downloaded?
101
+ @journal_lines
102
+
103
+ elsif manual_journal_id =~ GUID_REGEX && @gateway
104
+ # There is a manual_journal_id so we can assume this record was loaded from Xero.
105
+ # Let's attempt to download the journal_line records (if there is a gateway)
106
+
107
+ response = @gateway.get_manual_journal(manual_journal_id)
108
+ raise ManualJournalNotFoundError, "Manual Journal with ID #{manual_journal_id} not found in Xero." unless response.success? && response.manual_journal.is_a?(XeroGateway::ManualJournal)
109
+
110
+ @journal_lines = response.manual_journal.journal_lines
111
+ @journal_lines_downloaded = true
112
+
113
+ @journal_lines
114
+
115
+ # Otherwise, this is a new manual journal, so return the journal_lines reference.
116
+ else
117
+ @journal_lines
118
+ end
119
+ end
120
+
121
+ def to_xml(b = Builder::XmlMarkup.new)
122
+ b.ManualJournal {
123
+ b.ManualJournalID manual_journal_id if manual_journal_id
124
+ b.Narration narration
125
+ b.JournalLines {
126
+ self.journal_lines.each do |journal_line|
127
+ journal_line.to_xml(b)
128
+ end
129
+ }
130
+ b.Date ManualJournal.format_date(date || Date.today)
131
+ b.Status status if status
132
+ b.Url url if url
133
+ }
134
+ end
135
+
136
+ def self.from_xml(manual_journal_element, gateway = nil, options = {})
137
+ manual_journal = ManualJournal.new(options.merge({:gateway => gateway}))
138
+ manual_journal_element.children.each do |element|
139
+ case(element.name)
140
+ when "ManualJournalID" then manual_journal.manual_journal_id = element.text
141
+ when "Date" then manual_journal.date = parse_date(element.text)
142
+ when "Status" then manual_journal.status = element.text
143
+ when "Narration" then manual_journal.narration = element.text
144
+ when "JournalLines" then element.children.each {|journal_line| manual_journal.journal_lines_downloaded = true; manual_journal.journal_lines << JournalLine.from_xml(journal_line) }
145
+ when "Url" then manual_journal.url = element.text
146
+ end
147
+ end
148
+ manual_journal
149
+ end # from_xml
150
+
151
+ def add_journal_line(params = {})
152
+ journal_line = nil
153
+ case params
154
+ when Hash then journal_line = JournalLine.new(params)
155
+ when JournalLine then journal_line = params
156
+ else raise InvalidLineItemError
157
+ end
158
+ @journal_lines << journal_line
159
+ journal_line
160
+ end
161
+
162
+ end
163
+ end
@@ -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,59 +21,65 @@ 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 :ctoken, :csecret, :consumer_options, :session_handle, :authorization_expires_at
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)
74
+ rescue ::OAuth::Unauthorized => e
75
+ # If the original access token is for some reason invalid an OAuth::Unauthorized could be raised.
76
+ # In this case raise a XeroGateway::OAuth::TokenInvalid which can be captured by the caller. In this
77
+ # situation the end user will need to re-authorize the application via the request token authorization URL
78
+ raise XeroGateway::OAuth::TokenInvalid.new(e.message)
73
79
  end
74
-
80
+
75
81
  private
76
-
82
+
77
83
  # Update instance variables with those from the AccessToken.
78
84
  def update_attributes_from_token(access_token)
79
85
  @expires_at = Time.now + access_token.params[:oauth_expires_in].to_i
@@ -82,6 +88,6 @@ module XeroGateway
82
88
  @atoken, @asecret = access_token.token, access_token.secret
83
89
  @access_token = nil
84
90
  end
85
-
91
+
86
92
  end
87
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
@@ -9,11 +9,13 @@ module XeroGateway
9
9
  alias_method :invoice, :response_item
10
10
  alias_method :credit_note, :response_item
11
11
  alias_method :bank_transaction, :response_item
12
+ alias_method :manual_journal, :response_item
12
13
  alias_method :contact, :response_item
13
14
  alias_method :organisation, :response_item
14
15
  alias_method :invoices, :array_wrapped_response_item
15
16
  alias_method :credit_notes, :array_wrapped_response_item
16
17
  alias_method :bank_transactions, :array_wrapped_response_item
18
+ alias_method :manual_journals, :array_wrapped_response_item
17
19
  alias_method :contacts, :array_wrapped_response_item
18
20
  alias_method :accounts, :array_wrapped_response_item
19
21
  alias_method :tracking_categories, :array_wrapped_response_item
@@ -47,7 +47,7 @@ module XeroGateway
47
47
  attribute = element.name
48
48
  underscored_attribute = element.name.underscore
49
49
 
50
- raise "Unknown attribute: #{attribute}" unless ATTRS.keys.include?(attribute)
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"))