xero_gateway-float 2.0.15

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