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.
- data/Gemfile +12 -0
- data/LICENSE +14 -0
- data/README.textile +357 -0
- data/Rakefile +14 -0
- data/examples/oauth.rb +25 -0
- data/examples/partner_app.rb +36 -0
- data/init.rb +1 -0
- data/lib/oauth/oauth_consumer.rb +14 -0
- data/lib/xero_gateway.rb +39 -0
- data/lib/xero_gateway/account.rb +95 -0
- data/lib/xero_gateway/accounts_list.rb +87 -0
- data/lib/xero_gateway/address.rb +96 -0
- data/lib/xero_gateway/bank_transaction.rb +178 -0
- data/lib/xero_gateway/ca-certificates.crt +2560 -0
- data/lib/xero_gateway/contact.rb +206 -0
- data/lib/xero_gateway/credit_note.rb +222 -0
- data/lib/xero_gateway/currency.rb +56 -0
- data/lib/xero_gateway/dates.rb +30 -0
- data/lib/xero_gateway/error.rb +18 -0
- data/lib/xero_gateway/exceptions.rb +46 -0
- data/lib/xero_gateway/gateway.rb +622 -0
- data/lib/xero_gateway/http.rb +138 -0
- data/lib/xero_gateway/http_encoding_helper.rb +49 -0
- data/lib/xero_gateway/invoice.rb +236 -0
- data/lib/xero_gateway/line_item.rb +125 -0
- data/lib/xero_gateway/line_item_calculations.rb +55 -0
- data/lib/xero_gateway/money.rb +16 -0
- data/lib/xero_gateway/oauth.rb +87 -0
- data/lib/xero_gateway/organisation.rb +75 -0
- data/lib/xero_gateway/partner_app.rb +30 -0
- data/lib/xero_gateway/payment.rb +40 -0
- data/lib/xero_gateway/phone.rb +77 -0
- data/lib/xero_gateway/private_app.rb +17 -0
- data/lib/xero_gateway/response.rb +41 -0
- data/lib/xero_gateway/tax_rate.rb +63 -0
- data/lib/xero_gateway/tracking_category.rb +87 -0
- data/test/integration/accounts_list_test.rb +109 -0
- data/test/integration/create_bank_transaction_test.rb +38 -0
- data/test/integration/create_contact_test.rb +66 -0
- data/test/integration/create_credit_note_test.rb +49 -0
- data/test/integration/create_invoice_test.rb +49 -0
- data/test/integration/get_accounts_test.rb +23 -0
- data/test/integration/get_bank_transaction_test.rb +51 -0
- data/test/integration/get_bank_transactions_test.rb +88 -0
- data/test/integration/get_contact_test.rb +28 -0
- data/test/integration/get_contacts_test.rb +40 -0
- data/test/integration/get_credit_note_test.rb +48 -0
- data/test/integration/get_credit_notes_test.rb +90 -0
- data/test/integration/get_currencies_test.rb +25 -0
- data/test/integration/get_invoice_test.rb +48 -0
- data/test/integration/get_invoices_test.rb +92 -0
- data/test/integration/get_organisation_test.rb +24 -0
- data/test/integration/get_tax_rates_test.rb +25 -0
- data/test/integration/get_tracking_categories_test.rb +27 -0
- data/test/integration/update_bank_transaction_test.rb +31 -0
- data/test/integration/update_contact_test.rb +31 -0
- data/test/integration/update_invoice_test.rb +31 -0
- data/test/test_helper.rb +179 -0
- data/test/unit/account_test.rb +47 -0
- data/test/unit/bank_transaction_test.rb +126 -0
- data/test/unit/contact_test.rb +97 -0
- data/test/unit/credit_note_test.rb +284 -0
- data/test/unit/currency_test.rb +31 -0
- data/test/unit/gateway_test.rb +119 -0
- data/test/unit/invoice_test.rb +326 -0
- data/test/unit/oauth_test.rb +116 -0
- data/test/unit/organisation_test.rb +38 -0
- data/test/unit/tax_rate_test.rb +38 -0
- data/test/unit/tracking_category_test.rb +52 -0
- data/xero_gateway.gemspec +15 -0
- 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
|