tlconnor-xero_gateway 1.0.3 → 1.0.4
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 +11 -0
- data/README.textile +21 -8
- data/lib/xero_gateway.rb +14 -13
- data/lib/xero_gateway/account.rb +17 -2
- data/lib/xero_gateway/accounts_list.rb +77 -0
- data/lib/xero_gateway/address.rb +62 -3
- data/lib/xero_gateway/ca-certificates.crt +2560 -0
- data/lib/xero_gateway/contact.rb +110 -62
- data/lib/xero_gateway/error.rb +1 -2
- data/lib/xero_gateway/gateway.rb +55 -17
- data/lib/xero_gateway/http.rb +42 -46
- data/lib/xero_gateway/invoice.rb +132 -75
- data/lib/xero_gateway/line_item.rb +93 -7
- data/lib/xero_gateway/phone.rb +56 -2
- data/lib/xero_gateway/response.rb +1 -2
- data/lib/xero_gateway/tracking_category.rb +13 -12
- data/test/integration/accounts_list_test.rb +111 -0
- data/test/integration/create_contact_test.rb +46 -6
- data/test/integration/create_invoice_test.rb +29 -6
- data/test/test_helper.rb +4 -5
- data/test/unit/contact_test.rb +43 -0
- data/test/unit/invoice_test.rb +215 -22
- data/xero_gateway.gemspec +5 -2
- metadata +8 -3
data/lib/xero_gateway/contact.rb
CHANGED
@@ -2,12 +2,29 @@ module XeroGateway
|
|
2
2
|
class Contact
|
3
3
|
include Dates
|
4
4
|
|
5
|
-
|
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
|
6
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, :email, :addresses, :phones, :updated_at
|
22
|
+
|
7
23
|
def initialize(params = {})
|
24
|
+
@errors ||= []
|
25
|
+
|
8
26
|
params = {}.merge(params)
|
9
27
|
params.each do |k,v|
|
10
|
-
self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
|
11
28
|
self.send("#{k}=", v)
|
12
29
|
end
|
13
30
|
|
@@ -23,6 +40,21 @@ module XeroGateway
|
|
23
40
|
self.addresses[0] ||= Address.new
|
24
41
|
end
|
25
42
|
|
43
|
+
# Helper method to add a new address object to this contact.
|
44
|
+
#
|
45
|
+
# Usage:
|
46
|
+
# contact.add_address({
|
47
|
+
# :address_type => 'STREET',
|
48
|
+
# :line_1 => '100 Queen Street',
|
49
|
+
# :city => 'Brisbane',
|
50
|
+
# :region => 'QLD',
|
51
|
+
# :post_code => '4000',
|
52
|
+
# :country => 'Australia'
|
53
|
+
# })
|
54
|
+
def add_address(address_params)
|
55
|
+
self.addresses << Address.new(address_params)
|
56
|
+
end
|
57
|
+
|
26
58
|
def phone=(phone)
|
27
59
|
self.phones = [phone]
|
28
60
|
end
|
@@ -35,45 +67,86 @@ module XeroGateway
|
|
35
67
|
end
|
36
68
|
end
|
37
69
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
70
|
+
# Helper method to add a new phone object to this contact.
|
71
|
+
#
|
72
|
+
# Usage:
|
73
|
+
# contact.add_phone({
|
74
|
+
# :phone_type => 'MOBILE',
|
75
|
+
# :number => '0400123123'
|
76
|
+
# })
|
77
|
+
def add_phone(phone_params = {})
|
78
|
+
self.phones << Phone.new(phone_params)
|
43
79
|
end
|
44
80
|
|
45
|
-
|
46
|
-
|
81
|
+
# Validate the Contact record according to what will be valid by the gateway.
|
82
|
+
#
|
83
|
+
# Usage:
|
84
|
+
# contact.valid? # Returns true/false
|
85
|
+
#
|
86
|
+
# Additionally sets contact.errors array to an array of field/error.
|
87
|
+
def valid?
|
88
|
+
@errors = []
|
89
|
+
|
90
|
+
if !contact_id.nil? && contact_id !~ GUID_REGEX
|
91
|
+
@errors << ['contact_id', 'must be blank or a valid Xero GUID']
|
92
|
+
end
|
93
|
+
|
94
|
+
if status && !CONTACT_STATUS[status]
|
95
|
+
@errors << ['status', "must be one of #{CONTACT_STATUS.keys.join('/')}"]
|
96
|
+
end
|
97
|
+
|
98
|
+
unless name
|
99
|
+
@errors << ['name', "can't be blank"]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Make sure all addresses are correct.
|
103
|
+
unless addresses.all? { | address | address.valid? }
|
104
|
+
@errors << ['addresses', 'at least one address is invalid']
|
105
|
+
end
|
106
|
+
|
107
|
+
# Make sure all phone numbers are correct.
|
108
|
+
unless phones.all? { | phone | phone.valid? }
|
109
|
+
@errors << ['phones', 'at leaset one phone is invalid']
|
110
|
+
end
|
47
111
|
|
112
|
+
@errors.size == 0
|
113
|
+
end
|
114
|
+
|
115
|
+
# General purpose create/save method.
|
116
|
+
# If contact_id and contact_number are nil then create, otherwise, attempt to save.
|
117
|
+
def save
|
118
|
+
if contact_id.nil? && contact_number.nil?
|
119
|
+
create
|
120
|
+
else
|
121
|
+
update
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Creates this contact record (using gateway.create_contact) with the associated gateway.
|
126
|
+
# If no gateway set, raise a Xero::Contact::NoGatewayError exception.
|
127
|
+
def create
|
128
|
+
raise NoGatewayError unless gateway
|
129
|
+
gateway.create_contact(self)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Creates this contact record (using gateway.update_contact) with the associated gateway.
|
133
|
+
# If no gateway set, raise a Xero::Contact::NoGatewayError exception.
|
134
|
+
def update
|
135
|
+
raise NoGatewayError unless gateway
|
136
|
+
gateway.update_contact(self)
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_xml(b = Builder::XmlMarkup.new)
|
48
140
|
b.Contact {
|
49
141
|
b.ContactID self.contact_id if self.contact_id
|
50
142
|
b.ContactNumber self.contact_number if self.contact_number
|
51
143
|
b.Name self.name
|
52
144
|
b.EmailAddress self.email if self.email
|
53
145
|
b.Addresses {
|
54
|
-
|
55
|
-
b.Address {
|
56
|
-
b.AddressType address.address_type
|
57
|
-
b.AddressLine1 address.line_1 if address.line_1
|
58
|
-
b.AddressLine2 address.line_2 if address.line_2
|
59
|
-
b.AddressLine3 address.line_3 if address.line_3
|
60
|
-
b.AddressLine4 address.line_4 if address.line_4
|
61
|
-
b.City address.city if address.city
|
62
|
-
b.Region address.region if address.region
|
63
|
-
b.PostalCode address.post_code if address.post_code
|
64
|
-
b.Country address.country if address.country
|
65
|
-
}
|
66
|
-
end
|
146
|
+
addresses.each { |address| address.to_xml(b) }
|
67
147
|
}
|
68
148
|
b.Phones {
|
69
|
-
|
70
|
-
b.Phone {
|
71
|
-
b.PhoneType phone.phone_type
|
72
|
-
b.PhoneNumber phone.number
|
73
|
-
b.PhoneAreaCode phone.area_code if phone.area_code
|
74
|
-
b.PhoneCountryCode phone.country_code if phone.country_code
|
75
|
-
}
|
76
|
-
end
|
149
|
+
phones.each { |phone| phone.to_xml(b) }
|
77
150
|
}
|
78
151
|
}
|
79
152
|
end
|
@@ -88,44 +161,19 @@ module XeroGateway
|
|
88
161
|
when "ContactStatus" then contact.status = element.text
|
89
162
|
when "Name" then contact.name = element.text
|
90
163
|
when "EmailAddress" then contact.email = element.text
|
91
|
-
when "Addresses" then element.children.each {|
|
92
|
-
when "Phones" then element.children.each {|
|
164
|
+
when "Addresses" then element.children.each {|address_element| contact.addresses << Address.from_xml(address_element)}
|
165
|
+
when "Phones" then element.children.each {|phone_element| contact.phones << Phone.from_xml(phone_element)}
|
93
166
|
end
|
94
167
|
end
|
95
168
|
contact
|
96
169
|
end
|
97
170
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
address = Address.new
|
102
|
-
address_element.children.each do |element|
|
103
|
-
case(element.name)
|
104
|
-
when "AddressType" then address.address_type = element.text
|
105
|
-
when "AddressLine1" then address.line_1 = element.text
|
106
|
-
when "AddressLine2" then address.line_2 = element.text
|
107
|
-
when "AddressLine3" then address.line_3 = element.text
|
108
|
-
when "AddressLine4" then address.line_4 = element.text
|
109
|
-
when "City" then address.city = element.text
|
110
|
-
when "Region" then address.region = element.text
|
111
|
-
when "PostalCode" then address.post_code = element.text
|
112
|
-
when "Country" then address.country = element.text
|
113
|
-
end
|
114
|
-
end
|
115
|
-
address
|
116
|
-
end
|
117
|
-
|
118
|
-
def self.parse_phone(phone_element)
|
119
|
-
phone = Phone.new
|
120
|
-
phone_element.children.each do |element|
|
121
|
-
case(element.name)
|
122
|
-
when "PhoneType" then phone.phone_type = element.text
|
123
|
-
when "PhoneNumber" then phone.number = element.text
|
124
|
-
when "PhoneAreaCode" then phone.area_code = element.text
|
125
|
-
when "PhoneCountryCode" then phone.country_code = element.text
|
126
|
-
end
|
171
|
+
def ==(other)
|
172
|
+
[:contact_number, :status, :name, :email, :addresses, :phones].each do |field|
|
173
|
+
return false if send(field) != other.send(field)
|
127
174
|
end
|
128
|
-
|
175
|
+
return true
|
129
176
|
end
|
177
|
+
|
130
178
|
end
|
131
179
|
end
|
data/lib/xero_gateway/error.rb
CHANGED
@@ -4,7 +4,6 @@ module XeroGateway
|
|
4
4
|
|
5
5
|
def initialize(params = {})
|
6
6
|
params.each do |k,v|
|
7
|
-
self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
|
8
7
|
self.send("#{k}=", v)
|
9
8
|
end
|
10
9
|
end
|
@@ -16,4 +15,4 @@ module XeroGateway
|
|
16
15
|
return true
|
17
16
|
end
|
18
17
|
end
|
19
|
-
end
|
18
|
+
end
|
data/lib/xero_gateway/gateway.rb
CHANGED
@@ -35,6 +35,15 @@ module XeroGateway
|
|
35
35
|
get_contact(nil, contact_number)
|
36
36
|
end
|
37
37
|
|
38
|
+
# Factory method for building new Contact objects associated with this gateway.
|
39
|
+
def build_contact(contact = {})
|
40
|
+
case contact
|
41
|
+
when Contact then contact.gateway = self
|
42
|
+
when Hash then contact = Contact.new(contact.merge({:gateway => self}))
|
43
|
+
end
|
44
|
+
contact
|
45
|
+
end
|
46
|
+
|
38
47
|
#
|
39
48
|
# Creates a contact in Xero
|
40
49
|
#
|
@@ -54,10 +63,7 @@ module XeroGateway
|
|
54
63
|
#
|
55
64
|
# create_contact(contact)
|
56
65
|
def create_contact(contact)
|
57
|
-
|
58
|
-
response_xml = http_put("#{@xero_url}/contact", request_xml, {})
|
59
|
-
|
60
|
-
parse_response(response_xml, :request_xml => request_xml)
|
66
|
+
save_contact(contact)
|
61
67
|
end
|
62
68
|
|
63
69
|
#
|
@@ -71,13 +77,9 @@ module XeroGateway
|
|
71
77
|
# xero_gateway.update_contact(contact)
|
72
78
|
def update_contact(contact)
|
73
79
|
raise "contact_id or contact_number is required for updating contacts" if contact.contact_id.nil? and contact.contact_number.nil?
|
74
|
-
|
75
|
-
request_xml = contact.to_xml
|
76
|
-
response_xml = http_post("#{@xero_url}/contact", request_xml, {})
|
77
|
-
|
78
|
-
parse_response(response_xml, :request_xml => request_xml)
|
80
|
+
save_contact(contact)
|
79
81
|
end
|
80
|
-
|
82
|
+
|
81
83
|
# Retrieves an invoice from Xero based on its GUID
|
82
84
|
#
|
83
85
|
# Usage : get_invoice_by_id("8c69117a-60ae-4d31-9eb4-7f5a76bc4947")
|
@@ -103,8 +105,19 @@ module XeroGateway
|
|
103
105
|
|
104
106
|
parse_response(response_xml, :request_params => request_params)
|
105
107
|
end
|
108
|
+
|
109
|
+
# Factory method for building new Invoice objects associated with this gateway.
|
110
|
+
def build_invoice(invoice = {})
|
111
|
+
case invoice
|
112
|
+
when Invoice then invoice.gateway = self
|
113
|
+
when Hash then invoice = Invoice.new(invoice.merge(:gateway => self))
|
114
|
+
end
|
115
|
+
invoice
|
116
|
+
end
|
106
117
|
|
107
|
-
# Creates an invoice in Xero based on an invoice object
|
118
|
+
# Creates an invoice in Xero based on an invoice object.
|
119
|
+
#
|
120
|
+
# Invoice and line item totals are calculated automatically.
|
108
121
|
#
|
109
122
|
# Usage :
|
110
123
|
#
|
@@ -113,10 +126,7 @@ module XeroGateway
|
|
113
126
|
# :due_date => 1.month.from_now,
|
114
127
|
# :invoice_number => "YOUR INVOICE NUMBER",
|
115
128
|
# :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
|
116
|
-
# :includes_tax => false
|
117
|
-
# :sub_total => 1000,
|
118
|
-
# :total_tax => 125,
|
119
|
-
# :total => 1125
|
129
|
+
# :includes_tax => false
|
120
130
|
# })
|
121
131
|
# invoice.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
|
122
132
|
# invoice.contact.phone.number = "12345"
|
@@ -125,7 +135,6 @@ module XeroGateway
|
|
125
135
|
# :description => "THE DESCRIPTION OF THE LINE ITEM",
|
126
136
|
# :unit_amount => 100,
|
127
137
|
# :tax_amount => 12.5,
|
128
|
-
# :line_amount => 125,
|
129
138
|
# :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
|
130
139
|
# :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
|
131
140
|
# )
|
@@ -135,7 +144,10 @@ module XeroGateway
|
|
135
144
|
request_xml = invoice.to_xml
|
136
145
|
response_xml = http_put("#{@xero_url}/invoice", request_xml)
|
137
146
|
|
138
|
-
parse_response(response_xml, :request_xml => request_xml)
|
147
|
+
response = parse_response(response_xml, :request_xml => request_xml)
|
148
|
+
invoice.invoice_id = response.invoice.invoice_id if response.invoice && response.invoice.invoice_id
|
149
|
+
|
150
|
+
response
|
139
151
|
end
|
140
152
|
|
141
153
|
#
|
@@ -145,6 +157,14 @@ module XeroGateway
|
|
145
157
|
response_xml = http_get("#{xero_url}/accounts")
|
146
158
|
parse_response(response_xml)
|
147
159
|
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Returns a XeroGateway::AccountsList object that makes working with
|
163
|
+
# the Xero list of accounts easier and allows caching the results.
|
164
|
+
#
|
165
|
+
def get_accounts_list(load_on_init = true)
|
166
|
+
AccountsList.new(self, load_on_init)
|
167
|
+
end
|
148
168
|
|
149
169
|
#
|
150
170
|
# Gets all tracking categories for a specific organization in Xero.
|
@@ -170,6 +190,24 @@ module XeroGateway
|
|
170
190
|
|
171
191
|
parse_response(response_xml, :request_params => request_params)
|
172
192
|
end
|
193
|
+
|
194
|
+
# Create or update a contact record based on if it has a contact_id or contact_number.
|
195
|
+
def save_contact(contact)
|
196
|
+
request_xml = contact.to_xml
|
197
|
+
|
198
|
+
response_xml = nil
|
199
|
+
if contact.contact_id.nil? && contact.contact_number.nil?
|
200
|
+
# Create new contact record.
|
201
|
+
response_xml = http_put("#{@xero_url}/contact", request_xml, {})
|
202
|
+
else
|
203
|
+
# Update existing contact record.
|
204
|
+
response_xml = http_post("#{@xero_url}/contact", request_xml, {})
|
205
|
+
end
|
206
|
+
|
207
|
+
response = parse_response(response_xml, :request_xml => request_xml)
|
208
|
+
contact.contact_id = response.contacts.contact_id if response.contacts && response.contacts.contact_id
|
209
|
+
response
|
210
|
+
end
|
173
211
|
|
174
212
|
def parse_response(response_xml, request = {})
|
175
213
|
doc = REXML::Document.new(response_xml)
|
data/lib/xero_gateway/http.rb
CHANGED
@@ -2,58 +2,54 @@ module XeroGateway
|
|
2
2
|
module Http
|
3
3
|
OPEN_TIMEOUT = 10 unless defined? OPEN_TIMEOUT
|
4
4
|
READ_TIMEOUT = 60 unless defined? READ_TIMEOUT
|
5
|
-
|
5
|
+
ROOT_CA_FILE = File.join(File.dirname(__FILE__), 'ca-certificates.crt') unless defined? ROOT_CA_FILE
|
6
|
+
|
6
7
|
def http_get(url, extra_params = {})
|
7
|
-
|
8
|
-
params = params.merge(extra_params).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&")
|
9
|
-
|
10
|
-
uri = URI.parse(url + "?" + params)
|
11
|
-
|
12
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
13
|
-
http.open_timeout = OPEN_TIMEOUT
|
14
|
-
http.read_timeout = READ_TIMEOUT
|
15
|
-
http.use_ssl = true
|
16
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
17
|
-
|
18
|
-
http.get(uri.request_uri).body
|
8
|
+
http_request(:get, url, nil, extra_params)
|
19
9
|
end
|
20
10
|
|
21
11
|
def http_post(url, body, extra_params = {})
|
22
|
-
|
23
|
-
headers['Content-Type'] ||= "application/x-www-form-urlencoded"
|
24
|
-
|
25
|
-
params = {:apiKey => @api_key, :xeroKey => @customer_key}
|
26
|
-
params = params.merge(extra_params).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&")
|
27
|
-
|
28
|
-
uri = URI.parse(url + "?" + params)
|
29
|
-
|
30
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
31
|
-
http.open_timeout = OPEN_TIMEOUT
|
32
|
-
http.read_timeout = READ_TIMEOUT
|
33
|
-
http.use_ssl = true
|
34
|
-
|
35
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
36
|
-
|
37
|
-
http.post(uri.request_uri, body, headers).body
|
12
|
+
http_request(:post, url, body, extra_params)
|
38
13
|
end
|
39
14
|
|
40
15
|
def http_put(url, body, extra_params = {})
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
16
|
+
http_request(:put, url, body, extra_params)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def http_request(method, url, body, extra_params = {})
|
22
|
+
headers = {}
|
23
|
+
|
24
|
+
if method != :get
|
25
|
+
headers['Content-Type'] ||= "application/x-www-form-urlencoded"
|
26
|
+
end
|
27
|
+
|
28
|
+
params = {:apiKey => @api_key, :xeroKey => @customer_key}
|
29
|
+
params = params.merge(extra_params).map {|key,value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"}.join("&")
|
30
|
+
|
31
|
+
uri = URI.parse(url + "?" + params)
|
32
|
+
|
33
|
+
# Only setup @cached_http once on first use as loading the CA file is quite expensive computationally.
|
34
|
+
unless @cached_http && @cached_http.address == uri.host && @cached_http.port == uri.port
|
35
|
+
@cached_http = Net::HTTP.new(uri.host, uri.port)
|
36
|
+
@cached_http.open_timeout = OPEN_TIMEOUT
|
37
|
+
@cached_http.read_timeout = READ_TIMEOUT
|
38
|
+
@cached_http.use_ssl = true
|
39
|
+
|
40
|
+
# Need to validate server's certificate against root certificate authority to prevent man-in-the-middle attacks.
|
41
|
+
@cached_http.ca_file = ROOT_CA_FILE
|
42
|
+
# http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
43
|
+
@cached_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
44
|
+
@cached_http.verify_depth = 5
|
45
|
+
end
|
46
|
+
|
47
|
+
case method
|
48
|
+
when :get then @cached_http.get(uri.request_uri, headers).body
|
49
|
+
when :post then @cached_http.post(uri.request_uri, body, headers).body
|
50
|
+
when :put then @cached_http.put(uri.request_uri, body, headers).body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
58
54
|
end
|
59
55
|
end
|