xero_gateway-float 2.0.15

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 (71) hide show
  1. data/Gemfile +12 -0
  2. data/LICENSE +14 -0
  3. data/README.textile +357 -0
  4. data/Rakefile +14 -0
  5. data/examples/oauth.rb +25 -0
  6. data/examples/partner_app.rb +36 -0
  7. data/init.rb +1 -0
  8. data/lib/oauth/oauth_consumer.rb +14 -0
  9. data/lib/xero_gateway.rb +39 -0
  10. data/lib/xero_gateway/account.rb +95 -0
  11. data/lib/xero_gateway/accounts_list.rb +87 -0
  12. data/lib/xero_gateway/address.rb +96 -0
  13. data/lib/xero_gateway/bank_transaction.rb +178 -0
  14. data/lib/xero_gateway/ca-certificates.crt +2560 -0
  15. data/lib/xero_gateway/contact.rb +206 -0
  16. data/lib/xero_gateway/credit_note.rb +222 -0
  17. data/lib/xero_gateway/currency.rb +56 -0
  18. data/lib/xero_gateway/dates.rb +30 -0
  19. data/lib/xero_gateway/error.rb +18 -0
  20. data/lib/xero_gateway/exceptions.rb +46 -0
  21. data/lib/xero_gateway/gateway.rb +622 -0
  22. data/lib/xero_gateway/http.rb +138 -0
  23. data/lib/xero_gateway/http_encoding_helper.rb +49 -0
  24. data/lib/xero_gateway/invoice.rb +236 -0
  25. data/lib/xero_gateway/line_item.rb +125 -0
  26. data/lib/xero_gateway/line_item_calculations.rb +55 -0
  27. data/lib/xero_gateway/money.rb +16 -0
  28. data/lib/xero_gateway/oauth.rb +87 -0
  29. data/lib/xero_gateway/organisation.rb +75 -0
  30. data/lib/xero_gateway/partner_app.rb +30 -0
  31. data/lib/xero_gateway/payment.rb +40 -0
  32. data/lib/xero_gateway/phone.rb +77 -0
  33. data/lib/xero_gateway/private_app.rb +17 -0
  34. data/lib/xero_gateway/response.rb +41 -0
  35. data/lib/xero_gateway/tax_rate.rb +63 -0
  36. data/lib/xero_gateway/tracking_category.rb +87 -0
  37. data/test/integration/accounts_list_test.rb +109 -0
  38. data/test/integration/create_bank_transaction_test.rb +38 -0
  39. data/test/integration/create_contact_test.rb +66 -0
  40. data/test/integration/create_credit_note_test.rb +49 -0
  41. data/test/integration/create_invoice_test.rb +49 -0
  42. data/test/integration/get_accounts_test.rb +23 -0
  43. data/test/integration/get_bank_transaction_test.rb +51 -0
  44. data/test/integration/get_bank_transactions_test.rb +88 -0
  45. data/test/integration/get_contact_test.rb +28 -0
  46. data/test/integration/get_contacts_test.rb +40 -0
  47. data/test/integration/get_credit_note_test.rb +48 -0
  48. data/test/integration/get_credit_notes_test.rb +90 -0
  49. data/test/integration/get_currencies_test.rb +25 -0
  50. data/test/integration/get_invoice_test.rb +48 -0
  51. data/test/integration/get_invoices_test.rb +92 -0
  52. data/test/integration/get_organisation_test.rb +24 -0
  53. data/test/integration/get_tax_rates_test.rb +25 -0
  54. data/test/integration/get_tracking_categories_test.rb +27 -0
  55. data/test/integration/update_bank_transaction_test.rb +31 -0
  56. data/test/integration/update_contact_test.rb +31 -0
  57. data/test/integration/update_invoice_test.rb +31 -0
  58. data/test/test_helper.rb +179 -0
  59. data/test/unit/account_test.rb +47 -0
  60. data/test/unit/bank_transaction_test.rb +126 -0
  61. data/test/unit/contact_test.rb +97 -0
  62. data/test/unit/credit_note_test.rb +284 -0
  63. data/test/unit/currency_test.rb +31 -0
  64. data/test/unit/gateway_test.rb +119 -0
  65. data/test/unit/invoice_test.rb +326 -0
  66. data/test/unit/oauth_test.rb +116 -0
  67. data/test/unit/organisation_test.rb +38 -0
  68. data/test/unit/tax_rate_test.rb +38 -0
  69. data/test/unit/tracking_category_test.rb +52 -0
  70. data/xero_gateway.gemspec +15 -0
  71. metadata +164 -0
@@ -0,0 +1,55 @@
1
+ module XeroGateway
2
+ module LineItemCalculations
3
+
4
+ class Error < RuntimeError; end
5
+ class InvalidLineItemError < Error; end
6
+
7
+ def add_line_item(params = {})
8
+ line_item = nil
9
+ case params
10
+ when Hash then line_item = LineItem.new(params)
11
+ when LineItem then line_item = params
12
+ else raise InvalidLineItemError
13
+ end
14
+ @line_items << line_item
15
+ line_item
16
+ end
17
+
18
+ # Deprecated (but API for setter remains).
19
+ #
20
+ # As sub_total must equal SUM(line_item.line_amount) for the API call to pass, this is now
21
+ # automatically calculated in the sub_total method.
22
+ def sub_total=(value)
23
+ end
24
+
25
+ # Calculate the sub_total as the SUM(line_item.line_amount).
26
+ def sub_total
27
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.line_amount.to_s) }
28
+ end
29
+
30
+ # Deprecated (but API for setter remains).
31
+ #
32
+ # As total_tax must equal SUM(line_item.tax_amount) for the API call to pass, this is now
33
+ # automatically calculated in the total_tax method.
34
+ def total_tax=(value)
35
+ end
36
+
37
+ # Calculate the total_tax as the SUM(line_item.tax_amount).
38
+ def total_tax
39
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.tax_amount.to_s) }
40
+ end
41
+
42
+ # Deprecated (but API for setter remains).
43
+ #
44
+ # As total must equal sub_total + total_tax for the API call to pass, this is now
45
+ # automatically calculated in the total method.
46
+ def total=(value)
47
+ end
48
+
49
+ # Calculate the toal as sub_total + total_tax.
50
+ def total
51
+ sub_total + total_tax
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ module XeroGateway
2
+ module Money
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def format_money(amount)
9
+ if amount.class == BigDecimal
10
+ return amount.to_s("F")
11
+ end
12
+ return amount
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,87 @@
1
+ module XeroGateway
2
+
3
+ # Shamelessly based on the Twitter Gem's OAuth implementation by John Nunemaker
4
+ # Thanks!
5
+ #
6
+ # http://twitter.rubyforge.org/
7
+ # http://github.com/jnunemaker/twitter/
8
+
9
+ class OAuth
10
+
11
+ class TokenExpired < StandardError; end
12
+ class TokenInvalid < StandardError; end
13
+ class RateLimitExceeded < StandardError; end
14
+ class UnknownError < StandardError; end
15
+
16
+ unless defined? XERO_CONSUMER_OPTIONS
17
+ XERO_CONSUMER_OPTIONS = {
18
+ :site => "https://api.xero.com",
19
+ :request_token_path => "/oauth/RequestToken",
20
+ :access_token_path => "/oauth/AccessToken",
21
+ :authorize_path => "/oauth/Authorize"
22
+ }.freeze
23
+ end
24
+
25
+ extend Forwardable
26
+ def_delegators :access_token, :get, :post, :put, :delete
27
+
28
+ attr_reader :ctoken, :csecret, :consumer_options, :session_handle, :authorization_expires_at
29
+
30
+ def initialize(ctoken, csecret, options = {})
31
+ @ctoken, @csecret = ctoken, csecret
32
+ @consumer_options = XERO_CONSUMER_OPTIONS.merge(options)
33
+ end
34
+
35
+ def consumer
36
+ @consumer ||= ::OAuth::Consumer.new(@ctoken, @csecret, consumer_options)
37
+ end
38
+
39
+ def request_token(params = {})
40
+ @request_token ||= consumer.get_request_token(params)
41
+ end
42
+
43
+ def authorize_from_request(rtoken, rsecret, params = {})
44
+ request_token = ::OAuth::RequestToken.new(consumer, rtoken, rsecret)
45
+ access_token = request_token.get_access_token(params)
46
+ @atoken, @asecret = access_token.token, access_token.secret
47
+
48
+ update_attributes_from_token(access_token)
49
+ end
50
+
51
+ def access_token
52
+ @access_token ||= ::OAuth::AccessToken.new(consumer, @atoken, @asecret)
53
+ end
54
+
55
+ def authorize_from_access(atoken, asecret)
56
+ @atoken, @asecret = atoken, asecret
57
+ end
58
+
59
+ # Renewing access tokens only works for Partner applications
60
+ def renew_access_token(access_token = nil, access_secret = nil, session_handle = nil)
61
+ access_token ||= @atoken
62
+ access_secret ||= @asecret
63
+ session_handle ||= @session_handle
64
+
65
+ old_token = ::OAuth::RequestToken.new(consumer, access_token, access_secret)
66
+
67
+ access_token = old_token.get_access_token({
68
+ :oauth_session_handle => session_handle,
69
+ :token => old_token
70
+ })
71
+
72
+ update_attributes_from_token(access_token)
73
+ end
74
+
75
+ private
76
+
77
+ # Update instance variables with those from the AccessToken.
78
+ def update_attributes_from_token(access_token)
79
+ @expires_at = Time.now + access_token.params[:oauth_expires_in].to_i
80
+ @authorization_expires_at = Time.now + access_token.params[:oauth_authorization_expires_in].to_i
81
+ @session_handle = access_token.params[:oauth_session_handle]
82
+ @atoken, @asecret = access_token.token, access_token.secret
83
+ @access_token = nil
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,75 @@
1
+ module XeroGateway
2
+ class Organisation
3
+
4
+ unless defined? ATTRS
5
+ ATTRS = {
6
+ "Name" => :string, # Display name of organisation shown in Xero
7
+ "LegalName" => :string, # Organisation name shown on Reports
8
+ "PaysTax" => :boolean, # Boolean to describe if organisation is registered with a local tax authority i.e. true, false
9
+ "Version" => :string, # See Version Types
10
+ "BaseCurrency" => :string, # Default currency for organisation. See Currency types
11
+ "OrganisationType" => :string, # UNDOCUMENTED parameter, only returned for "real" (i.e non-demo) companies
12
+ "OrganisationStatus" => :string, # UNDOCUMENTED parameter
13
+ "IsDemoCompany" => :boolean, # UNDOCUMENTED parameter
14
+ "APIKey" => :string, # UNDOCUMENTED paramater, returned if organisations are linked via Xero Network
15
+ "CountryCode" => :string, # UNDOCUMENTED parameter
16
+ "TaxNumber" => :string,
17
+ "FinancialYearEndDay" => :string,
18
+ "FinancialYearEndMonth" => :string,
19
+ "PeriodLockDate" => :string,
20
+ "CreatedDateUTC" => :string
21
+ }
22
+ end
23
+
24
+ attr_accessor *ATTRS.keys.map(&:underscore)
25
+
26
+ def initialize(params = {})
27
+ params.each do |k,v|
28
+ self.send("#{k}=", v)
29
+ end
30
+ end
31
+
32
+ def ==(other)
33
+ ATTRS.keys.map(&:underscore).each do |field|
34
+ return false if send(field) != other.send(field)
35
+ end
36
+ return true
37
+ end
38
+
39
+ def to_xml
40
+ b = Builder::XmlMarkup.new
41
+
42
+ b.Organisation do
43
+ ATTRS.keys.each do |attr|
44
+ eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'")
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.from_xml(organisation_element)
50
+ Organisation.new.tap do |org|
51
+ organisation_element.children.each do |element|
52
+
53
+ attribute = element.name
54
+ underscored_attribute = element.name.underscore
55
+
56
+ if ATTRS.keys.include?(attribute)
57
+
58
+ case (ATTRS[attribute])
59
+ when :boolean then org.send("#{underscored_attribute}=", (element.text == "true"))
60
+ when :float then org.send("#{underscored_attribute}=", element.text.to_f)
61
+ else org.send("#{underscored_attribute}=", element.text)
62
+ end
63
+
64
+ else
65
+
66
+ warn "Ignoring unknown attribute: #{attribute}"
67
+
68
+ end
69
+
70
+ end
71
+ end
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,30 @@
1
+ module XeroGateway
2
+ class PartnerApp < Gateway
3
+
4
+ class CertificateRequired < StandardError; end
5
+
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
+ 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
+
9
+ def_delegators :client, :session_handle, :renew_access_token
10
+
11
+ def initialize(consumer_key, consumer_secret, options = {})
12
+
13
+ raise CertificateRequired.new(NO_SSL_CLIENT_CERT_MESSAGE) unless options[:ssl_client_cert]
14
+ raise CertificateRequired.new(NO_SSL_CLIENT_CERT_MESSAGE) unless options[:ssl_client_key]
15
+ raise CertificateRequired.new(NO_PRIVATE_KEY_ERROR_MESSAGE) unless options[:private_key_file]
16
+
17
+ options.merge!(
18
+ :site => "https://api-partner.network.xero.com",
19
+ :authorize_url => 'https://api.xero.com/oauth/Authorize',
20
+ :signature_method => 'RSA-SHA1',
21
+ :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read(options[:ssl_client_cert])),
22
+ :ssl_client_key => OpenSSL::PKey::RSA.new(File.read(options[:ssl_client_key])),
23
+ :private_key_file => options[:private_key_file]
24
+ )
25
+
26
+ @xero_url = options[:xero_url] || "https://api-partner.xero.com/api.xro/2.0"
27
+ @client = OAuth.new(consumer_key, consumer_secret, options)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ module XeroGateway
2
+ class Payment
3
+ include Money
4
+ include Dates
5
+
6
+ # Any errors that occurred when the #valid? method called.
7
+ attr_reader :errors
8
+
9
+ # All accessible fields
10
+ attr_accessor :date, :amount
11
+
12
+ def initialize(params = {})
13
+ @errors ||= []
14
+
15
+ params.each do |k,v|
16
+ self.send("#{k}=", v)
17
+ end
18
+ end
19
+
20
+ def self.from_xml(payment_element)
21
+ payment = Payment.new
22
+ payment_element.children.each do | element |
23
+ case element.name
24
+ when 'Date' then payment.date = parse_date_time(element.text)
25
+ when 'Amount' then payment.amount = BigDecimal.new(element.text)
26
+ end
27
+ end
28
+ payment
29
+ end
30
+
31
+ def ==(other)
32
+ [:date, :amount].each do |field|
33
+ return false if send(field) != other.send(field)
34
+ end
35
+ return true
36
+ end
37
+
38
+
39
+ end
40
+ end
@@ -0,0 +1,77 @@
1
+ module XeroGateway
2
+ class Phone
3
+
4
+ PHONE_TYPE = {
5
+ 'DEFAULT' => 'Default',
6
+ 'DDI' => 'Direct Dial-In',
7
+ 'MOBILE' => 'Mobile',
8
+ 'FAX' => 'Fax'
9
+ } unless defined?(PHONE_TYPE)
10
+
11
+ # Any errors that occurred when the #valid? method called.
12
+ attr_reader :errors
13
+
14
+ attr_accessor :phone_type, :number, :area_code, :country_code
15
+
16
+ def initialize(params = {})
17
+ @errors ||= []
18
+
19
+ params = {
20
+ :phone_type => "DEFAULT"
21
+ }.merge(params)
22
+
23
+ params.each do |k,v|
24
+ self.send("#{k}=", v)
25
+ end
26
+ end
27
+
28
+ # Validate the Phone record according to what will be valid by the gateway.
29
+ #
30
+ # Usage:
31
+ # phone.valid? # Returns true/false
32
+ #
33
+ # Additionally sets phone.errors array to an array of field/error.
34
+ def valid?
35
+ @errors = []
36
+
37
+ unless number
38
+ @errors << ['number', "can't be blank"]
39
+ end
40
+
41
+ if phone_type && !PHONE_TYPE[phone_type]
42
+ @errors << ['phone_type', "must be one of #{PHONE_TYPE.keys.join('/')}"]
43
+ end
44
+
45
+ @errors.size == 0
46
+ end
47
+
48
+ def to_xml(b = Builder::XmlMarkup.new)
49
+ b.Phone {
50
+ b.PhoneType phone_type
51
+ b.PhoneNumber number
52
+ b.PhoneAreaCode area_code if area_code
53
+ b.PhoneCountryCode country_code if country_code
54
+ }
55
+ end
56
+
57
+ def self.from_xml(phone_element)
58
+ phone = Phone.new
59
+ phone_element.children.each do |element|
60
+ case(element.name)
61
+ when "PhoneType" then phone.phone_type = element.text
62
+ when "PhoneNumber" then phone.number = element.text
63
+ when "PhoneAreaCode" then phone.area_code = element.text
64
+ when "PhoneCountryCode" then phone.country_code = element.text
65
+ end
66
+ end
67
+ phone
68
+ end
69
+
70
+ def ==(other)
71
+ [:phone_type, :number, :area_code, :country_code].each do |field|
72
+ return false if send(field) != other.send(field)
73
+ end
74
+ return true
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ module XeroGateway
2
+ class PrivateApp < Gateway
3
+ #
4
+ # The consumer key and secret here correspond to those provided
5
+ # to you by Xero inside the API Previewer.
6
+ def initialize(consumer_key, consumer_secret, path_to_private_key, options = {})
7
+ options.merge!(
8
+ :signature_method => 'RSA-SHA1',
9
+ :private_key_file => path_to_private_key
10
+ )
11
+
12
+ @xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
13
+ @client = OAuth.new(consumer_key, consumer_secret, options)
14
+ @client.authorize_from_access(consumer_key, consumer_secret)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module XeroGateway
2
+ class Response
3
+ attr_accessor :response_id, :status, :errors, :provider, :date_time, :response_item, :request_params, :request_xml, :response_xml
4
+
5
+ def array_wrapped_response_item
6
+ Array(response_item)
7
+ end
8
+
9
+ alias_method :invoice, :response_item
10
+ alias_method :credit_note, :response_item
11
+ alias_method :bank_transaction, :response_item
12
+ alias_method :contact, :response_item
13
+ alias_method :organisation, :response_item
14
+ alias_method :invoices, :array_wrapped_response_item
15
+ alias_method :credit_notes, :array_wrapped_response_item
16
+ alias_method :bank_transactions, :array_wrapped_response_item
17
+ alias_method :contacts, :array_wrapped_response_item
18
+ alias_method :accounts, :array_wrapped_response_item
19
+ alias_method :tracking_categories, :array_wrapped_response_item
20
+ alias_method :tax_rates, :array_wrapped_response_item
21
+ alias_method :currencies, :array_wrapped_response_item
22
+
23
+ def initialize(params = {})
24
+ params.each do |k,v|
25
+ self.send("#{k}=", v)
26
+ end
27
+
28
+ @errors ||= []
29
+ @response_item ||= []
30
+ end
31
+
32
+
33
+ def success?
34
+ status == "OK"
35
+ end
36
+
37
+ def error
38
+ errors.blank? ? nil : errors[0]
39
+ end
40
+ end
41
+ end