tlconnor-xero_gateway 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,12 +2,29 @@ module XeroGateway
2
2
  class Contact
3
3
  include Dates
4
4
 
5
- attr_accessor :contact_id, :contact_number, :status, :name, :email, :addresses, :phones, :updated_at
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
- def ==(other)
39
- [:contact_number, :status, :name, :email, :addresses, :phones].each do |field|
40
- return false if send(field) != other.send(field)
41
- end
42
- return true
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
- def to_xml
46
- b = Builder::XmlMarkup.new
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
- self.addresses.each do |address|
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
- self.phones.each do |phone|
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 {|address| contact.addresses << parse_address(address)}
92
- when "Phones" then element.children.each {|phone| contact.phones << parse_phone(phone)}
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
- private
99
-
100
- def self.parse_address(address_element)
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
- phone
175
+ return true
129
176
  end
177
+
130
178
  end
131
179
  end
@@ -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
@@ -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
- request_xml = contact.to_xml
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)
@@ -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
- params = {:apiKey => @api_key, :xeroKey => @customer_key}
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
- headers = {}
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
- headers = {}
42
- headers['Content-Type'] ||= "application/x-www-form-urlencoded"
43
-
44
- params = {:apiKey => @api_key, :xeroKey => @customer_key}
45
- params = params.merge(extra_params).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&")
46
-
47
- uri = URI.parse(url + "?" + params)
48
-
49
- http = Net::HTTP.new(uri.host, uri.port)
50
- http.open_timeout = OPEN_TIMEOUT
51
- http.read_timeout = READ_TIMEOUT
52
- http.use_ssl = true
53
-
54
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
55
-
56
- http.put(uri.request_uri, body, headers).body
57
- end
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