xero_gateway 2.0.2

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 (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