xero_gateway-float 2.0.18 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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"))