xero_gateway 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.textile +51 -0
- data/LICENSE +14 -0
- data/README.textile +289 -0
- data/Rakefile +14 -0
- data/examples/oauth.rb +25 -0
- data/init.rb +1 -0
- data/lib/xero_gateway/account.rb +78 -0
- data/lib/xero_gateway/accounts_list.rb +77 -0
- data/lib/xero_gateway/address.rb +97 -0
- data/lib/xero_gateway/ca-certificates.crt +2560 -0
- data/lib/xero_gateway/contact.rb +206 -0
- data/lib/xero_gateway/currency.rb +56 -0
- data/lib/xero_gateway/dates.rb +25 -0
- data/lib/xero_gateway/error.rb +18 -0
- data/lib/xero_gateway/exceptions.rb +41 -0
- data/lib/xero_gateway/gateway.rb +363 -0
- data/lib/xero_gateway/http.rb +128 -0
- data/lib/xero_gateway/http_encoding_helper.rb +49 -0
- data/lib/xero_gateway/invoice.rb +278 -0
- data/lib/xero_gateway/line_item.rb +123 -0
- data/lib/xero_gateway/money.rb +16 -0
- data/lib/xero_gateway/oauth.rb +56 -0
- data/lib/xero_gateway/organisation.rb +61 -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 +37 -0
- data/lib/xero_gateway/tax_rate.rb +63 -0
- data/lib/xero_gateway/tracking_category.rb +62 -0
- data/lib/xero_gateway.rb +33 -0
- data/test/integration/accounts_list_test.rb +109 -0
- data/test/integration/create_contact_test.rb +66 -0
- data/test/integration/create_invoice_test.rb +49 -0
- data/test/integration/get_accounts_test.rb +23 -0
- data/test/integration/get_contact_test.rb +28 -0
- data/test/integration/get_contacts_test.rb +40 -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 +90 -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 +26 -0
- data/test/integration/update_contact_test.rb +31 -0
- data/test/stub_responses/accounts.xml +1 -0
- data/test/stub_responses/api_exception.xml +153 -0
- data/test/stub_responses/contact.xml +1 -0
- data/test/stub_responses/contacts.xml +2189 -0
- data/test/stub_responses/create_invoice.xml +64 -0
- data/test/stub_responses/currencies.xml +16 -0
- data/test/stub_responses/invalid_api_key_error.xml +1 -0
- data/test/stub_responses/invalid_consumer_key +1 -0
- data/test/stub_responses/invalid_request_token +1 -0
- data/test/stub_responses/invoice.xml +1 -0
- data/test/stub_responses/invoice_not_found_error.xml +1 -0
- data/test/stub_responses/invoices.xml +1 -0
- data/test/stub_responses/organisation.xml +14 -0
- data/test/stub_responses/tax_rates.xml +52 -0
- data/test/stub_responses/token_expired +1 -0
- data/test/stub_responses/tracking_categories.xml +1 -0
- data/test/stub_responses/unknown_error.xml +1 -0
- data/test/test_helper.rb +81 -0
- data/test/unit/account_test.rb +34 -0
- data/test/unit/contact_test.rb +97 -0
- data/test/unit/currency_test.rb +31 -0
- data/test/unit/gateway_test.rb +79 -0
- data/test/unit/invoice_test.rb +302 -0
- data/test/unit/oauth_test.rb +110 -0
- data/test/unit/organisation_test.rb +34 -0
- data/test/unit/tax_rate_test.rb +38 -0
- data/test/unit/tracking_category_test.rb +30 -0
- data/test/xsd/README +2 -0
- data/test/xsd/create_contact.xsd +61 -0
- data/test/xsd/create_invoice.xsd +107 -0
- data/xero_gateway.gemspec +87 -0
- metadata +172 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
module XeroGateway
|
2
|
+
class Contact
|
3
|
+
include Dates
|
4
|
+
|
5
|
+
class Error < RuntimeError; end
|
6
|
+
class NoGatewayError < Error; end
|
7
|
+
|
8
|
+
GUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ unless defined?(GUID_REGEX)
|
9
|
+
|
10
|
+
CONTACT_STATUS = {
|
11
|
+
'ACTIVE' => 'Active',
|
12
|
+
'DELETED' => 'Deleted'
|
13
|
+
} unless defined?(CONTACT_STATUS)
|
14
|
+
|
15
|
+
# Xero::Gateway associated with this contact.
|
16
|
+
attr_accessor :gateway
|
17
|
+
|
18
|
+
# Any errors that occurred when the #valid? method called.
|
19
|
+
attr_reader :errors
|
20
|
+
|
21
|
+
attr_accessor :contact_id, :contact_number, :status, :name, :first_name, :last_name, :email, :addresses, :phones, :updated_at,
|
22
|
+
:bank_account_details, :tax_number, :accounts_receivable_tax_type, :accounts_payable_tax_type, :is_customer, :is_supplier,
|
23
|
+
:default_currency, :contact_groups
|
24
|
+
|
25
|
+
|
26
|
+
def initialize(params = {})
|
27
|
+
@errors ||= []
|
28
|
+
|
29
|
+
params = {}.merge(params)
|
30
|
+
params.each do |k,v|
|
31
|
+
self.send("#{k}=", v)
|
32
|
+
end
|
33
|
+
|
34
|
+
@phones ||= []
|
35
|
+
@addresses ||= []
|
36
|
+
end
|
37
|
+
|
38
|
+
def address=(address)
|
39
|
+
self.addresses = [address]
|
40
|
+
end
|
41
|
+
|
42
|
+
def address
|
43
|
+
self.addresses[0] ||= Address.new
|
44
|
+
end
|
45
|
+
|
46
|
+
# Helper method to add a new address object to this contact.
|
47
|
+
#
|
48
|
+
# Usage:
|
49
|
+
# contact.add_address({
|
50
|
+
# :address_type => 'STREET',
|
51
|
+
# :line_1 => '100 Queen Street',
|
52
|
+
# :city => 'Brisbane',
|
53
|
+
# :region => 'QLD',
|
54
|
+
# :post_code => '4000',
|
55
|
+
# :country => 'Australia'
|
56
|
+
# })
|
57
|
+
def add_address(address_params)
|
58
|
+
self.addresses << Address.new(address_params)
|
59
|
+
end
|
60
|
+
|
61
|
+
def phone=(phone)
|
62
|
+
self.phones = [phone]
|
63
|
+
end
|
64
|
+
|
65
|
+
def phone
|
66
|
+
if @phones.size > 1
|
67
|
+
@phones.detect {|p| p.phone_type == 'DEFAULT'} || phones[0]
|
68
|
+
else
|
69
|
+
@phones[0] ||= Phone.new
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Helper method to add a new phone object to this contact.
|
74
|
+
#
|
75
|
+
# Usage:
|
76
|
+
# contact.add_phone({
|
77
|
+
# :phone_type => 'MOBILE',
|
78
|
+
# :number => '0400123123'
|
79
|
+
# })
|
80
|
+
def add_phone(phone_params = {})
|
81
|
+
self.phones << Phone.new(phone_params)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Validate the Contact record according to what will be valid by the gateway.
|
85
|
+
#
|
86
|
+
# Usage:
|
87
|
+
# contact.valid? # Returns true/false
|
88
|
+
#
|
89
|
+
# Additionally sets contact.errors array to an array of field/error.
|
90
|
+
def valid?
|
91
|
+
@errors = []
|
92
|
+
|
93
|
+
if !contact_id.nil? && contact_id !~ GUID_REGEX
|
94
|
+
@errors << ['contact_id', 'must be blank or a valid Xero GUID']
|
95
|
+
end
|
96
|
+
|
97
|
+
if status && !CONTACT_STATUS[status]
|
98
|
+
@errors << ['status', "must be one of #{CONTACT_STATUS.keys.join('/')}"]
|
99
|
+
end
|
100
|
+
|
101
|
+
unless name
|
102
|
+
@errors << ['name', "can't be blank"]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Make sure all addresses are correct.
|
106
|
+
unless addresses.all? { | address | address.valid? }
|
107
|
+
@errors << ['addresses', 'at least one address is invalid']
|
108
|
+
end
|
109
|
+
|
110
|
+
# Make sure all phone numbers are correct.
|
111
|
+
unless phones.all? { | phone | phone.valid? }
|
112
|
+
@errors << ['phones', 'at leaset one phone is invalid']
|
113
|
+
end
|
114
|
+
|
115
|
+
@errors.size == 0
|
116
|
+
end
|
117
|
+
|
118
|
+
# General purpose create/save method.
|
119
|
+
# If contact_id and contact_number are nil then create, otherwise, attempt to save.
|
120
|
+
def save
|
121
|
+
if contact_id.nil? && contact_number.nil?
|
122
|
+
create
|
123
|
+
else
|
124
|
+
update
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates this contact record (using gateway.create_contact) with the associated gateway.
|
129
|
+
# If no gateway set, raise a Xero::Contact::NoGatewayError exception.
|
130
|
+
def create
|
131
|
+
raise NoGatewayError unless gateway
|
132
|
+
gateway.create_contact(self)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Creates this contact record (using gateway.update_contact) with the associated gateway.
|
136
|
+
# If no gateway set, raise a Xero::Contact::NoGatewayError exception.
|
137
|
+
def update
|
138
|
+
raise NoGatewayError unless gateway
|
139
|
+
gateway.update_contact(self)
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_xml(b = Builder::XmlMarkup.new)
|
143
|
+
b.Contact {
|
144
|
+
b.ContactID self.contact_id if self.contact_id
|
145
|
+
b.ContactNumber self.contact_number if self.contact_number
|
146
|
+
b.Name self.name
|
147
|
+
b.EmailAddress self.email if self.email
|
148
|
+
b.FirstName self.first_name if self.first_name
|
149
|
+
b.LastName self.last_name if self.last_name
|
150
|
+
b.BankAccountDetails self.bank_account_details if self.bank_account_details
|
151
|
+
b.TaxNumber self.tax_number if self.tax_number
|
152
|
+
b.AccountsReceivableTaxType self.accounts_receivable_tax_type if self.accounts_receivable_tax_type
|
153
|
+
b.AccountsPayableTaxType self.accounts_payable_tax_type if self.accounts_payable_tax_type
|
154
|
+
b.ContactGroups if self.contact_groups
|
155
|
+
b.IsCustomer true if self.is_customer
|
156
|
+
b.IsSupplier true if self.is_supplier
|
157
|
+
b.DefaultCurrency if self.default_currency
|
158
|
+
b.Addresses {
|
159
|
+
addresses.each { |address| address.to_xml(b) }
|
160
|
+
}
|
161
|
+
b.Phones {
|
162
|
+
phones.each { |phone| phone.to_xml(b) }
|
163
|
+
}
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Take a Contact element and convert it into an Contact object
|
168
|
+
def self.from_xml(contact_element, gateway = nil)
|
169
|
+
contact = Contact.new(:gateway => gateway)
|
170
|
+
contact_element.children.each do |element|
|
171
|
+
case(element.name)
|
172
|
+
when "ContactID" then contact.contact_id = element.text
|
173
|
+
when "ContactNumber" then contact.contact_number = element.text
|
174
|
+
when "ContactStatus" then contact.status = element.text
|
175
|
+
when "Name" then contact.name = element.text
|
176
|
+
when "FirstName" then contact.first_name = element.text
|
177
|
+
when "LastName" then contact.last_name = element.text
|
178
|
+
when "EmailAddress" then contact.email = element.text
|
179
|
+
when "Addresses" then element.children.each {|address_element| contact.addresses << Address.from_xml(address_element)}
|
180
|
+
when "Phones" then element.children.each {|phone_element| contact.phones << Phone.from_xml(phone_element)}
|
181
|
+
when "FirstName" then contact.first_name = element.text
|
182
|
+
when "LastName" then contact.last_name = element.text
|
183
|
+
when "BankAccountDetails" then contact.bank_account_details = element.text
|
184
|
+
when "TaxNumber" then contact.tax_number = element.text
|
185
|
+
when "AccountsReceivableTaxType" then contact.accounts_receivable_tax_type = element.text
|
186
|
+
when "AccountsPayableTaxType" then contact.accounts_payable_tax_type = element.text
|
187
|
+
when "ContactGroups" then contact.contact_groups = element.text
|
188
|
+
when "IsCustomer" then contact.is_customer = (element.text == "true")
|
189
|
+
when "IsSupplier" then contact.is_supplier = (element.text == "true")
|
190
|
+
when "DefaultCurrency" then contact.default_currency = element.text
|
191
|
+
end
|
192
|
+
end
|
193
|
+
contact
|
194
|
+
end
|
195
|
+
|
196
|
+
def ==(other)
|
197
|
+
[ :contact_id, :contact_number, :status, :name, :first_name, :last_name, :email, :addresses, :phones, :updated_at,
|
198
|
+
:bank_account_details, :tax_number, :accounts_receivable_tax_type, :accounts_payable_tax_type, :is_customer, :is_supplier,
|
199
|
+
:default_currency, :contact_groups ].each do |field|
|
200
|
+
return false if send(field) != other.send(field)
|
201
|
+
end
|
202
|
+
return true
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module XeroGateway
|
2
|
+
class Currency
|
3
|
+
|
4
|
+
unless defined? ATTRS
|
5
|
+
ATTRS = {
|
6
|
+
"Code" => :string, # 3 letter alpha code for the currency – see list of currency codes
|
7
|
+
"Description" => :string, # Name of Currency
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor *ATTRS.keys.map(&:underscore)
|
12
|
+
|
13
|
+
def initialize(params = {})
|
14
|
+
params.each do |k,v|
|
15
|
+
self.send("#{k}=", v)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
ATTRS.keys.map(&:underscore).each do |field|
|
21
|
+
return false if send(field) != other.send(field)
|
22
|
+
end
|
23
|
+
return true
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_xml
|
27
|
+
b = Builder::XmlMarkup.new
|
28
|
+
|
29
|
+
b.Currency do
|
30
|
+
ATTRS.keys.each do |attr|
|
31
|
+
eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.from_xml(currency_element)
|
37
|
+
Currency.new.tap do |currency|
|
38
|
+
currency_element.children.each do |element|
|
39
|
+
|
40
|
+
attribute = element.name
|
41
|
+
underscored_attribute = element.name.underscore
|
42
|
+
|
43
|
+
raise "Unknown attribute: #{attribute}" unless ATTRS.keys.include?(attribute)
|
44
|
+
|
45
|
+
case (ATTRS[attribute])
|
46
|
+
when :boolean then currency.send("#{underscored_attribute}=", (element.text == "true"))
|
47
|
+
when :float then currency.send("#{underscored_attribute}=", element.text.to_f)
|
48
|
+
else currency.send("#{underscored_attribute}=", element.text)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module XeroGateway
|
2
|
+
module Dates
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def format_date(time)
|
9
|
+
return time.strftime("%Y-%m-%d")
|
10
|
+
end
|
11
|
+
|
12
|
+
def format_date_time(time)
|
13
|
+
return time.strftime("%Y%m%d%H%M%S")
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_date(time)
|
17
|
+
Date.civil(time[0..3].to_i, time[5..6].to_i, time[8..9].to_i)
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_date_time(time)
|
21
|
+
Time.local(time[0..3].to_i, time[5..6].to_i, time[8..9].to_i, time[11..12].to_i, time[14..15].to_i, time[17..18].to_i)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module XeroGateway
|
2
|
+
class Error
|
3
|
+
attr_accessor :description, :date_time, :type, :message
|
4
|
+
|
5
|
+
def initialize(params = {})
|
6
|
+
params.each do |k,v|
|
7
|
+
self.send("#{k}=", v)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
[:description, :date_time, :type, :message].each do |field|
|
13
|
+
return false if send(field) != other.send(field)
|
14
|
+
end
|
15
|
+
return true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module XeroGateway
|
2
|
+
class ApiException < StandardError
|
3
|
+
|
4
|
+
def initialize(type, message, xml)
|
5
|
+
@type = type
|
6
|
+
@message = message
|
7
|
+
@xml = xml
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"#{@type}: #{@message} \n Generated by the following XML: \n #{@xml}"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
class UnparseableResponse < StandardError
|
17
|
+
|
18
|
+
def initialize(root_element_name)
|
19
|
+
@root_element_name = root_element_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def message
|
23
|
+
"A root element of #{@root_element_name} was returned, and we don't understand that!"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class ObjectNotFound < StandardError
|
29
|
+
|
30
|
+
def initialize(api_endpoint)
|
31
|
+
@api_endpoint = api_endpoint
|
32
|
+
end
|
33
|
+
|
34
|
+
def message
|
35
|
+
"Couldn't find object for API Endpoint #{@api_endpoint}"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class InvoiceNotFoundError < StandardError; end
|
41
|
+
end
|
@@ -0,0 +1,363 @@
|
|
1
|
+
module XeroGateway
|
2
|
+
|
3
|
+
class Gateway
|
4
|
+
include Http
|
5
|
+
include Dates
|
6
|
+
|
7
|
+
attr_accessor :client, :xero_url, :logger
|
8
|
+
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :client, :request_token, :access_token, :authorize_from_request, :authorize_from_access
|
11
|
+
|
12
|
+
#
|
13
|
+
# The consumer key and secret here correspond to those provided
|
14
|
+
# to you by Xero inside the API Previewer.
|
15
|
+
def initialize(consumer_key, consumer_secret, options = {})
|
16
|
+
@xero_url = options[:xero_url] || "https://api.xero.com/api.xro/2.0"
|
17
|
+
@client = OAuth.new(consumer_key, consumer_secret, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Retrieve all contacts from Xero
|
22
|
+
#
|
23
|
+
# Usage : get_contacts(:order => :name)
|
24
|
+
# get_contacts(:updated_after => Time)
|
25
|
+
#
|
26
|
+
# Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
|
27
|
+
def get_contacts(options = {})
|
28
|
+
request_params = {}
|
29
|
+
|
30
|
+
request_params[:ContactID] = options[:contact_id] if options[:contact_id]
|
31
|
+
request_params[:ContactNumber] = options[:contact_number] if options[:contact_number]
|
32
|
+
request_params[:OrderBy] = options[:order] if options[:order]
|
33
|
+
request_params[:ModifiedAfter] = Gateway.format_date_time(options[:updated_after]) if options[:updated_after]
|
34
|
+
request_params[:where] = options[:where] if options[:where]
|
35
|
+
|
36
|
+
response_xml = http_get(@client, "#{@xero_url}/Contacts", request_params)
|
37
|
+
|
38
|
+
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/contacts'})
|
39
|
+
end
|
40
|
+
|
41
|
+
# Retrieve a contact from Xero
|
42
|
+
# Usage get_contact_by_id(contact_id)
|
43
|
+
def get_contact_by_id(contact_id)
|
44
|
+
get_contact(contact_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Retrieve a contact from Xero
|
48
|
+
# Usage get_contact_by_id(contact_id)
|
49
|
+
def get_contact_by_number(contact_number)
|
50
|
+
get_contact(nil, contact_number)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Factory method for building new Contact objects associated with this gateway.
|
54
|
+
def build_contact(contact = {})
|
55
|
+
case contact
|
56
|
+
when Contact then contact.gateway = self
|
57
|
+
when Hash then contact = Contact.new(contact.merge({:gateway => self}))
|
58
|
+
end
|
59
|
+
contact
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Creates a contact in Xero
|
64
|
+
#
|
65
|
+
# Usage :
|
66
|
+
#
|
67
|
+
# contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT #{Time.now.to_i}")
|
68
|
+
# contact.email = "whoever@something.com"
|
69
|
+
# contact.phone.number = "12345"
|
70
|
+
# contact.address.line_1 = "LINE 1 OF THE ADDRESS"
|
71
|
+
# contact.address.line_2 = "LINE 2 OF THE ADDRESS"
|
72
|
+
# contact.address.line_3 = "LINE 3 OF THE ADDRESS"
|
73
|
+
# contact.address.line_4 = "LINE 4 OF THE ADDRESS"
|
74
|
+
# contact.address.city = "WELLINGTON"
|
75
|
+
# contact.address.region = "WELLINGTON"
|
76
|
+
# contact.address.country = "NEW ZEALAND"
|
77
|
+
# contact.address.post_code = "6021"
|
78
|
+
#
|
79
|
+
# create_contact(contact)
|
80
|
+
def create_contact(contact)
|
81
|
+
save_contact(contact)
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Updates an existing Xero contact
|
86
|
+
#
|
87
|
+
# Usage :
|
88
|
+
#
|
89
|
+
# contact = xero_gateway.get_contact(some_contact_id)
|
90
|
+
# contact.email = "a_new_email_ddress"
|
91
|
+
#
|
92
|
+
# xero_gateway.update_contact(contact)
|
93
|
+
def update_contact(contact)
|
94
|
+
raise "contact_id or contact_number is required for updating contacts" if contact.contact_id.nil? and contact.contact_number.nil?
|
95
|
+
save_contact(contact)
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Updates an array of contacts in a single API operation.
|
100
|
+
#
|
101
|
+
# Usage :
|
102
|
+
# contacts = [XeroGateway::Contact.new(:name => 'Joe Bloggs'), XeroGateway::Contact.new(:name => 'Jane Doe')]
|
103
|
+
# result = gateway.update_contacts(contacts)
|
104
|
+
#
|
105
|
+
# Will update contacts with matching contact_id, contact_number or name or create if they don't exist.
|
106
|
+
#
|
107
|
+
def update_contacts(contacts)
|
108
|
+
b = Builder::XmlMarkup.new
|
109
|
+
request_xml = b.Contacts {
|
110
|
+
contacts.each do | contact |
|
111
|
+
contact.to_xml(b)
|
112
|
+
end
|
113
|
+
}
|
114
|
+
|
115
|
+
response_xml = http_post(@client, "#{@xero_url}/Contacts", request_xml, {})
|
116
|
+
|
117
|
+
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'POST/contacts'})
|
118
|
+
response.contacts.each_with_index do | response_contact, index |
|
119
|
+
contacts[index].contact_id = response_contact.contact_id if response_contact && response_contact.contact_id
|
120
|
+
end
|
121
|
+
response
|
122
|
+
end
|
123
|
+
|
124
|
+
# Retrieves all invoices from Xero
|
125
|
+
#
|
126
|
+
# Usage : get_invoices
|
127
|
+
# get_invoices(:invoice_id => " 297c2dc5-cc47-4afd-8ec8-74990b8761e9")
|
128
|
+
#
|
129
|
+
# Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
|
130
|
+
def get_invoices(options = {})
|
131
|
+
|
132
|
+
request_params = {}
|
133
|
+
|
134
|
+
request_params[:InvoiceID] = options[:invoice_id] if options[:invoice_id]
|
135
|
+
request_params[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number]
|
136
|
+
request_params[:OrderBy] = options[:order] if options[:order]
|
137
|
+
request_params[:ModifiedAfter] = options[:modified_since]
|
138
|
+
|
139
|
+
request_params[:where] = options[:where] if options[:where]
|
140
|
+
|
141
|
+
response_xml = http_get(@client, "#{@xero_url}/Invoices", request_params)
|
142
|
+
|
143
|
+
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/Invoices'})
|
144
|
+
end
|
145
|
+
|
146
|
+
# Retrieves a single invoice
|
147
|
+
#
|
148
|
+
# Usage : get_invoice("297c2dc5-cc47-4afd-8ec8-74990b8761e9") # By ID
|
149
|
+
# get_invoice("OIT-12345") # By number
|
150
|
+
def get_invoice(invoice_id_or_number)
|
151
|
+
request_params = {}
|
152
|
+
|
153
|
+
url = "#{@xero_url}/Invoices/#{URI.escape(invoice_id_or_number)}"
|
154
|
+
|
155
|
+
response_xml = http_get(@client, url, request_params)
|
156
|
+
|
157
|
+
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/Invoice'})
|
158
|
+
end
|
159
|
+
|
160
|
+
# Factory method for building new Invoice objects associated with this gateway.
|
161
|
+
def build_invoice(invoice = {})
|
162
|
+
case invoice
|
163
|
+
when Invoice then invoice.gateway = self
|
164
|
+
when Hash then invoice = Invoice.new(invoice.merge(:gateway => self))
|
165
|
+
end
|
166
|
+
invoice
|
167
|
+
end
|
168
|
+
|
169
|
+
# Creates an invoice in Xero based on an invoice object.
|
170
|
+
#
|
171
|
+
# Invoice and line item totals are calculated automatically.
|
172
|
+
#
|
173
|
+
# Usage :
|
174
|
+
#
|
175
|
+
# invoice = XeroGateway::Invoice.new({
|
176
|
+
# :invoice_type => "ACCREC",
|
177
|
+
# :due_date => 1.month.from_now,
|
178
|
+
# :invoice_number => "YOUR INVOICE NUMBER",
|
179
|
+
# :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
|
180
|
+
# :line_amount_types => "Inclusive"
|
181
|
+
# })
|
182
|
+
# invoice.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
|
183
|
+
# invoice.contact.phone.number = "12345"
|
184
|
+
# invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS"
|
185
|
+
# invoice.line_items << XeroGateway::LineItem.new(
|
186
|
+
# :description => "THE DESCRIPTION OF THE LINE ITEM",
|
187
|
+
# :unit_amount => 100,
|
188
|
+
# :tax_amount => 12.5,
|
189
|
+
# :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
|
190
|
+
# :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
|
191
|
+
# )
|
192
|
+
#
|
193
|
+
# create_invoice(invoice)
|
194
|
+
def create_invoice(invoice)
|
195
|
+
request_xml = invoice.to_xml
|
196
|
+
response_xml = http_put(@client, "#{@xero_url}/Invoices", request_xml)
|
197
|
+
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/invoice'})
|
198
|
+
|
199
|
+
# Xero returns invoices inside an <Invoices> tag, even though there's only ever
|
200
|
+
# one for this request
|
201
|
+
response.response_item = response.invoices.first
|
202
|
+
|
203
|
+
if response.success? && response.invoice && response.invoice.invoice_id
|
204
|
+
invoice.invoice_id = response.invoice.invoice_id
|
205
|
+
end
|
206
|
+
|
207
|
+
response
|
208
|
+
end
|
209
|
+
|
210
|
+
#
|
211
|
+
# Creates an array of invoices with a single API request.
|
212
|
+
#
|
213
|
+
# Usage :
|
214
|
+
# invoices = [XeroGateway::Invoice.new(...), XeroGateway::Invoice.new(...)]
|
215
|
+
# result = gateway.create_invoices(invoices)
|
216
|
+
#
|
217
|
+
def create_invoices(invoices)
|
218
|
+
b = Builder::XmlMarkup.new
|
219
|
+
request_xml = b.Invoices {
|
220
|
+
invoices.each do | invoice |
|
221
|
+
invoice.to_xml(b)
|
222
|
+
end
|
223
|
+
}
|
224
|
+
|
225
|
+
response_xml = http_put(@client, "#{@xero_url}/Invoices", request_xml, {})
|
226
|
+
|
227
|
+
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/invoices'})
|
228
|
+
response.invoices.each_with_index do | response_invoice, index |
|
229
|
+
invoices[index].invoice_id = response_invoice.invoice_id if response_invoice && response_invoice.invoice_id
|
230
|
+
end
|
231
|
+
response
|
232
|
+
end
|
233
|
+
|
234
|
+
#
|
235
|
+
# Gets all accounts for a specific organization in Xero.
|
236
|
+
#
|
237
|
+
def get_accounts
|
238
|
+
response_xml = http_get(@client, "#{xero_url}/Accounts")
|
239
|
+
parse_response(response_xml, {}, {:request_signature => 'GET/accounts'})
|
240
|
+
end
|
241
|
+
|
242
|
+
#
|
243
|
+
# Returns a XeroGateway::AccountsList object that makes working with
|
244
|
+
# the Xero list of accounts easier and allows caching the results.
|
245
|
+
#
|
246
|
+
def get_accounts_list(load_on_init = true)
|
247
|
+
AccountsList.new(self, load_on_init)
|
248
|
+
end
|
249
|
+
|
250
|
+
#
|
251
|
+
# Gets all tracking categories for a specific organization in Xero.
|
252
|
+
#
|
253
|
+
def get_tracking_categories
|
254
|
+
response_xml = http_get(@client, "#{xero_url}/TrackingCategories")
|
255
|
+
|
256
|
+
parse_response(response_xml, {}, {:request_signature => 'GET/TrackingCategories'})
|
257
|
+
end
|
258
|
+
|
259
|
+
#
|
260
|
+
# Gets Organisation details
|
261
|
+
#
|
262
|
+
def get_organisation
|
263
|
+
response_xml = http_get(@client, "#{xero_url}/Organisation")
|
264
|
+
parse_response(response_xml, {}, {:request_signature => 'GET/organisation'})
|
265
|
+
end
|
266
|
+
|
267
|
+
#
|
268
|
+
# Gets all currencies for a specific organisation in Xero
|
269
|
+
#
|
270
|
+
def get_currencies
|
271
|
+
response_xml = http_get(@client, "#{xero_url}/Currencies")
|
272
|
+
parse_response(response_xml, {}, {:request_signature => 'GET/currencies'})
|
273
|
+
end
|
274
|
+
|
275
|
+
#
|
276
|
+
# Gets all Tax Rates for a specific organisation in Xero
|
277
|
+
#
|
278
|
+
def get_tax_rates
|
279
|
+
response_xml = http_get(@client, "#{xero_url}/TaxRates")
|
280
|
+
parse_response(response_xml, {}, {:request_signature => 'GET/tax_rates'})
|
281
|
+
end
|
282
|
+
|
283
|
+
private
|
284
|
+
|
285
|
+
def get_contact(contact_id = nil, contact_number = nil)
|
286
|
+
request_params = contact_id ? { :contactID => contact_id } : { :contactNumber => contact_number }
|
287
|
+
response_xml = http_get(@client, "#{@xero_url}/Contacts/#{URI.escape(contact_id||contact_number)}", request_params)
|
288
|
+
|
289
|
+
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/contact'})
|
290
|
+
end
|
291
|
+
|
292
|
+
# Create or update a contact record based on if it has a contact_id or contact_number.
|
293
|
+
def save_contact(contact)
|
294
|
+
request_xml = contact.to_xml
|
295
|
+
|
296
|
+
response_xml = nil
|
297
|
+
create_or_save = nil
|
298
|
+
if contact.contact_id.nil? && contact.contact_number.nil?
|
299
|
+
# Create new contact record.
|
300
|
+
response_xml = http_put(@client, "#{@xero_url}/Contacts", request_xml, {})
|
301
|
+
create_or_save = :create
|
302
|
+
else
|
303
|
+
# Update existing contact record.
|
304
|
+
response_xml = http_post(@client, "#{@xero_url}/Contacts", request_xml, {})
|
305
|
+
create_or_save = :save
|
306
|
+
end
|
307
|
+
|
308
|
+
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => "#{create_or_save == :create ? 'PUT' : 'POST'}/contact"})
|
309
|
+
contact.contact_id = response.contact.contact_id if response.contact && response.contact.contact_id
|
310
|
+
response
|
311
|
+
end
|
312
|
+
|
313
|
+
def parse_response(raw_response, request = {}, options = {})
|
314
|
+
|
315
|
+
response = XeroGateway::Response.new
|
316
|
+
|
317
|
+
doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
318
|
+
|
319
|
+
# check for responses we don't understand
|
320
|
+
raise UnparseableResponse.new(doc.root.name) unless doc.root.name == "Response"
|
321
|
+
|
322
|
+
response_element = REXML::XPath.first(doc, "/Response")
|
323
|
+
|
324
|
+
response_element.children.reject { |e| e.is_a? REXML::Text }.each do |element|
|
325
|
+
case(element.name)
|
326
|
+
when "ID" then response.response_id = element.text
|
327
|
+
when "Status" then response.status = element.text
|
328
|
+
when "ProviderName" then response.provider = element.text
|
329
|
+
when "DateTimeUTC" then response.date_time = element.text
|
330
|
+
when "Contact" then response.response_item = Contact.from_xml(element, self)
|
331
|
+
when "Invoice" then response.response_item = Invoice.from_xml(element, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"})
|
332
|
+
when "Contacts" then element.children.each {|child| response.response_item << Contact.from_xml(child, self) }
|
333
|
+
when "Invoices" then element.children.each {|child| response.response_item << Invoice.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/Invoices"}) }
|
334
|
+
when "Accounts" then element.children.each {|child| response.response_item << Account.from_xml(child) }
|
335
|
+
when "TaxRates" then element.children.each {|child| response.response_item << TaxRate.from_xml(child) }
|
336
|
+
when "Currencies" then element.children.each {|child| response.response_item << Currency.from_xml(child) }
|
337
|
+
when "Organisations" then response.response_item = Organisation.from_xml(element.children.first) # Xero only returns the Authorized Organisation
|
338
|
+
when "TrackingCategories" then element.children.each {|child| response.response_item << TrackingCategory.from_xml(child) }
|
339
|
+
when "Errors" then element.children.each { |error| parse_error(error, response) }
|
340
|
+
end
|
341
|
+
end if response_element
|
342
|
+
|
343
|
+
# If a single result is returned don't put it in an array
|
344
|
+
if response.response_item.is_a?(Array) && response.response_item.size == 1
|
345
|
+
response.response_item = response.response_item.first
|
346
|
+
end
|
347
|
+
|
348
|
+
response.request_params = request[:request_params]
|
349
|
+
response.request_xml = request[:request_xml]
|
350
|
+
response.response_xml = raw_response
|
351
|
+
response
|
352
|
+
end
|
353
|
+
|
354
|
+
def parse_error(error_element, response)
|
355
|
+
response.errors << Error.new(
|
356
|
+
:description => REXML::XPath.first(error_element, "Description").text,
|
357
|
+
:date_time => REXML::XPath.first(error_element, "//DateTime").text,
|
358
|
+
:type => REXML::XPath.first(error_element, "//ExceptionType").text,
|
359
|
+
:message => REXML::XPath.first(error_element, "//Message").text
|
360
|
+
)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|