xero_gateway 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/CHANGELOG.textile +51 -0
  2. data/LICENSE +14 -0
  3. data/README.textile +289 -0
  4. data/Rakefile +14 -0
  5. data/examples/oauth.rb +25 -0
  6. data/init.rb +1 -0
  7. data/lib/xero_gateway/account.rb +78 -0
  8. data/lib/xero_gateway/accounts_list.rb +77 -0
  9. data/lib/xero_gateway/address.rb +97 -0
  10. data/lib/xero_gateway/ca-certificates.crt +2560 -0
  11. data/lib/xero_gateway/contact.rb +206 -0
  12. data/lib/xero_gateway/currency.rb +56 -0
  13. data/lib/xero_gateway/dates.rb +25 -0
  14. data/lib/xero_gateway/error.rb +18 -0
  15. data/lib/xero_gateway/exceptions.rb +41 -0
  16. data/lib/xero_gateway/gateway.rb +363 -0
  17. data/lib/xero_gateway/http.rb +128 -0
  18. data/lib/xero_gateway/http_encoding_helper.rb +49 -0
  19. data/lib/xero_gateway/invoice.rb +278 -0
  20. data/lib/xero_gateway/line_item.rb +123 -0
  21. data/lib/xero_gateway/money.rb +16 -0
  22. data/lib/xero_gateway/oauth.rb +56 -0
  23. data/lib/xero_gateway/organisation.rb +61 -0
  24. data/lib/xero_gateway/payment.rb +40 -0
  25. data/lib/xero_gateway/phone.rb +77 -0
  26. data/lib/xero_gateway/private_app.rb +17 -0
  27. data/lib/xero_gateway/response.rb +37 -0
  28. data/lib/xero_gateway/tax_rate.rb +63 -0
  29. data/lib/xero_gateway/tracking_category.rb +62 -0
  30. data/lib/xero_gateway.rb +33 -0
  31. data/test/integration/accounts_list_test.rb +109 -0
  32. data/test/integration/create_contact_test.rb +66 -0
  33. data/test/integration/create_invoice_test.rb +49 -0
  34. data/test/integration/get_accounts_test.rb +23 -0
  35. data/test/integration/get_contact_test.rb +28 -0
  36. data/test/integration/get_contacts_test.rb +40 -0
  37. data/test/integration/get_currencies_test.rb +25 -0
  38. data/test/integration/get_invoice_test.rb +48 -0
  39. data/test/integration/get_invoices_test.rb +90 -0
  40. data/test/integration/get_organisation_test.rb +24 -0
  41. data/test/integration/get_tax_rates_test.rb +25 -0
  42. data/test/integration/get_tracking_categories_test.rb +26 -0
  43. data/test/integration/update_contact_test.rb +31 -0
  44. data/test/stub_responses/accounts.xml +1 -0
  45. data/test/stub_responses/api_exception.xml +153 -0
  46. data/test/stub_responses/contact.xml +1 -0
  47. data/test/stub_responses/contacts.xml +2189 -0
  48. data/test/stub_responses/create_invoice.xml +64 -0
  49. data/test/stub_responses/currencies.xml +16 -0
  50. data/test/stub_responses/invalid_api_key_error.xml +1 -0
  51. data/test/stub_responses/invalid_consumer_key +1 -0
  52. data/test/stub_responses/invalid_request_token +1 -0
  53. data/test/stub_responses/invoice.xml +1 -0
  54. data/test/stub_responses/invoice_not_found_error.xml +1 -0
  55. data/test/stub_responses/invoices.xml +1 -0
  56. data/test/stub_responses/organisation.xml +14 -0
  57. data/test/stub_responses/tax_rates.xml +52 -0
  58. data/test/stub_responses/token_expired +1 -0
  59. data/test/stub_responses/tracking_categories.xml +1 -0
  60. data/test/stub_responses/unknown_error.xml +1 -0
  61. data/test/test_helper.rb +81 -0
  62. data/test/unit/account_test.rb +34 -0
  63. data/test/unit/contact_test.rb +97 -0
  64. data/test/unit/currency_test.rb +31 -0
  65. data/test/unit/gateway_test.rb +79 -0
  66. data/test/unit/invoice_test.rb +302 -0
  67. data/test/unit/oauth_test.rb +110 -0
  68. data/test/unit/organisation_test.rb +34 -0
  69. data/test/unit/tax_rate_test.rb +38 -0
  70. data/test/unit/tracking_category_test.rb +30 -0
  71. data/test/xsd/README +2 -0
  72. data/test/xsd/create_contact.xsd +61 -0
  73. data/test/xsd/create_invoice.xsd +107 -0
  74. data/xero_gateway.gemspec +87 -0
  75. metadata +172 -0
@@ -0,0 +1,123 @@
1
+ require File.join(File.dirname(__FILE__), 'account')
2
+
3
+ module XeroGateway
4
+ class LineItem
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 :line_item_id, :description, :quantity, :unit_amount, :tax_type, :tax_amount, :account_code, :tracking
14
+
15
+ def initialize(params = {})
16
+ @errors ||= []
17
+ @tracking ||= []
18
+ @quantity = 1
19
+ @unit_amount = BigDecimal.new('0')
20
+
21
+ params.each do |k,v|
22
+ self.send("#{k}=", v)
23
+ end
24
+ end
25
+
26
+ # Validate the LineItem record according to what will be valid by the gateway.
27
+ #
28
+ # Usage:
29
+ # line_item.valid? # Returns true/false
30
+ #
31
+ # Additionally sets line_item.errors array to an array of field/error.
32
+ def valid?
33
+ @errors = []
34
+
35
+ if !line_item_id.nil? && line_item_id !~ GUID_REGEX
36
+ @errors << ['line_item_id', 'must be blank or a valid Xero GUID']
37
+ end
38
+
39
+ unless description
40
+ @errors << ['description', "can't be blank"]
41
+ end
42
+
43
+ if tax_type && !TAX_TYPE[tax_type]
44
+ @errors << ['tax_type', "must be one of #{TAX_TYPE.keys.join('/')}"]
45
+ end
46
+
47
+ @errors.size == 0
48
+ end
49
+
50
+ def has_tracking?
51
+ return false if tracking.nil?
52
+
53
+ if tracking.is_a?(Array)
54
+ return tracking.any?
55
+ else
56
+ return tracking.is_a?(TrackingCategory)
57
+ end
58
+ end
59
+
60
+ # Deprecated (but API for setter remains).
61
+ #
62
+ # As line_amount must equal quantity * unit_amount for the API call to pass, this is now
63
+ # automatically calculated in the line_amount method.
64
+ def line_amount=(value)
65
+ end
66
+
67
+ # Calculate the line_amount as quantity * unit_amount as this value must be correct
68
+ # for the API call to succeed.
69
+ def line_amount
70
+ quantity * unit_amount
71
+ end
72
+
73
+ def to_xml(b = Builder::XmlMarkup.new)
74
+ b.LineItem {
75
+ b.Description description
76
+ b.Quantity quantity if quantity
77
+ b.UnitAmount LineItem.format_money(unit_amount)
78
+ b.TaxType tax_type if tax_type
79
+ b.TaxAmount tax_amount if tax_amount
80
+ b.LineAmount line_amount if line_amount
81
+ b.AccountCode account_code if account_code
82
+ if has_tracking?
83
+ b.Tracking {
84
+ # Due to strange retardness in the Xero API, the XML structure for a tracking category within
85
+ # an invoice is different to a standalone tracking category.
86
+ # This means rather than going category.to_xml we need to call the special category.to_xml_for_invoice_messages
87
+ (tracking.is_a?(TrackingCategory) ? [tracking] : tracking).each do |category|
88
+ category.to_xml_for_invoice_messages(b)
89
+ end
90
+ }
91
+ end
92
+ }
93
+ end
94
+
95
+ def self.from_xml(line_item_element)
96
+ line_item = LineItem.new
97
+ line_item_element.children.each do |element|
98
+ case(element.name)
99
+ when "LineItemID" then line_item.line_item_id = element.text
100
+ when "Description" then line_item.description = element.text
101
+ when "Quantity" then line_item.quantity = element.text.to_i
102
+ when "UnitAmount" then line_item.unit_amount = BigDecimal.new(element.text)
103
+ when "TaxType" then line_item.tax_type = element.text
104
+ when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text)
105
+ when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text)
106
+ when "AccountCode" then line_item.account_code = element.text
107
+ when "Tracking" then
108
+ element.children.each do | tracking_element |
109
+ line_item.tracking << TrackingCategory.from_xml(tracking_element)
110
+ end
111
+ end
112
+ end
113
+ line_item
114
+ end
115
+
116
+ def ==(other)
117
+ [:description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code].each do |field|
118
+ return false if send(field) != other.send(field)
119
+ end
120
+ return true
121
+ end
122
+ end
123
+ 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,56 @@
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
+
14
+ unless defined? XERO_CONSUMER_OPTIONS
15
+ XERO_CONSUMER_OPTIONS = {
16
+ :site => "https://api.xero.com",
17
+ :request_token_path => "/oauth/RequestToken",
18
+ :access_token_path => "/oauth/AccessToken",
19
+ :authorize_path => "/oauth/Authorize"
20
+ }.freeze
21
+ end
22
+
23
+ extend Forwardable
24
+ def_delegators :access_token, :get, :post, :put, :delete
25
+
26
+ attr_reader :ctoken, :csecret, :consumer_options
27
+
28
+ def initialize(ctoken, csecret, options = {})
29
+ @ctoken, @csecret = ctoken, csecret
30
+ @consumer_options = XERO_CONSUMER_OPTIONS.merge(options)
31
+ end
32
+
33
+ def consumer
34
+ @consumer ||= ::OAuth::Consumer.new(@ctoken, @csecret, consumer_options)
35
+ end
36
+
37
+ def request_token(params = {})
38
+ @request_token ||= consumer.get_request_token(params)
39
+ end
40
+
41
+ def authorize_from_request(rtoken, rsecret, params = {})
42
+ request_token = ::OAuth::RequestToken.new(consumer, rtoken, rsecret)
43
+ access_token = request_token.get_access_token(params)
44
+ @atoken, @asecret = access_token.token, access_token.secret
45
+ end
46
+
47
+ def access_token
48
+ @access_token ||= ::OAuth::AccessToken.new(consumer, @atoken, @asecret)
49
+ end
50
+
51
+ def authorize_from_access(atoken, asecret)
52
+ @atoken, @asecret = atoken, asecret
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,61 @@
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
+ "APIKey" => :string # UNDOCUMENTED paramater, returned if organisations are linked via Xero Network
13
+ }
14
+ end
15
+
16
+ attr_accessor *ATTRS.keys.map(&:underscore)
17
+
18
+ def initialize(params = {})
19
+ params.each do |k,v|
20
+ self.send("#{k}=", v)
21
+ end
22
+ end
23
+
24
+ def ==(other)
25
+ ATTRS.keys.map(&:underscore).each do |field|
26
+ return false if send(field) != other.send(field)
27
+ end
28
+ return true
29
+ end
30
+
31
+ def to_xml
32
+ b = Builder::XmlMarkup.new
33
+
34
+ b.Organisation do
35
+ ATTRS.keys.each do |attr|
36
+ eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'")
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.from_xml(organisation_element)
42
+ Organisation.new.tap do |org|
43
+ organisation_element.children.each do |element|
44
+
45
+ attribute = element.name
46
+ underscored_attribute = element.name.underscore
47
+
48
+ raise "Unknown attribute: #{attribute}" unless ATTRS.keys.include?(attribute)
49
+
50
+ case (ATTRS[attribute])
51
+ when :boolean then org.send("#{underscored_attribute}=", (element.text == "true"))
52
+ when :float then org.send("#{underscored_attribute}=", element.text.to_f)
53
+ else org.send("#{underscored_attribute}=", element.text)
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+
60
+ end
61
+ 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,37 @@
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 :contact, :response_item
11
+ alias_method :organisation, :response_item
12
+ alias_method :invoices, :array_wrapped_response_item
13
+ alias_method :contacts, :array_wrapped_response_item
14
+ alias_method :accounts, :array_wrapped_response_item
15
+ alias_method :tracking_categories, :array_wrapped_response_item
16
+ alias_method :tax_rates, :array_wrapped_response_item
17
+ alias_method :currencies, :array_wrapped_response_item
18
+
19
+ def initialize(params = {})
20
+ params.each do |k,v|
21
+ self.send("#{k}=", v)
22
+ end
23
+
24
+ @errors ||= []
25
+ @response_item ||= []
26
+ end
27
+
28
+
29
+ def success?
30
+ status == "OK"
31
+ end
32
+
33
+ def error
34
+ errors.blank? ? nil : errors[0]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ module XeroGateway
2
+ class TaxRate
3
+
4
+ unless defined? ATTRS
5
+ ATTRS = {
6
+ "Name" => :string,
7
+ "TaxType" => :string,
8
+ "CanApplyToAssets" => :boolean,
9
+ "CanApplyToEquity" => :boolean,
10
+ "CanApplyToExpenses" => :boolean,
11
+ "CanApplyToLiabilities" => :boolean,
12
+ "CanApplyToRevenue" => :boolean,
13
+ "DisplayTaxRate" => :float,
14
+ "EffectiveRate" => :float
15
+ }
16
+ end
17
+
18
+ attr_accessor *ATTRS.keys.map(&:underscore)
19
+
20
+ def initialize(params = {})
21
+ params.each do |k,v|
22
+ self.send("#{k}=", v)
23
+ end
24
+ end
25
+
26
+ def ==(other)
27
+ ATTRS.keys.map(&:underscore).each do |field|
28
+ return false if send(field) != other.send(field)
29
+ end
30
+ return true
31
+ end
32
+
33
+ def to_xml
34
+ b = Builder::XmlMarkup.new
35
+
36
+ b.TaxRate do
37
+ ATTRS.keys.each do |attr|
38
+ eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'")
39
+ end
40
+ end
41
+ end
42
+
43
+ def self.from_xml(tax_rate_element)
44
+ TaxRate.new.tap do |tax_rate|
45
+ tax_rate_element.children.each do |element|
46
+
47
+ attribute = element.name
48
+ underscored_attribute = element.name.underscore
49
+
50
+ raise "Unknown attribute: #{attribute}" unless ATTRS.keys.include?(attribute)
51
+
52
+ case (ATTRS[attribute])
53
+ when :boolean then tax_rate.send("#{underscored_attribute}=", (element.text == "true"))
54
+ when :float then tax_rate.send("#{underscored_attribute}=", element.text.to_f)
55
+ else tax_rate.send("#{underscored_attribute}=", element.text)
56
+ end
57
+
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+ end