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