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