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.
Files changed (75) hide show
  1. data/CHANGELOG.textile +51 -0
  2. data/LICENSE +14 -0
  3. data/README.textile +289 -0
  4. data/Rakefile +14 -0
  5. data/examples/oauth.rb +25 -0
  6. data/init.rb +1 -0
  7. data/lib/xero_gateway/account.rb +78 -0
  8. data/lib/xero_gateway/accounts_list.rb +77 -0
  9. data/lib/xero_gateway/address.rb +97 -0
  10. data/lib/xero_gateway/ca-certificates.crt +2560 -0
  11. data/lib/xero_gateway/contact.rb +206 -0
  12. data/lib/xero_gateway/currency.rb +56 -0
  13. data/lib/xero_gateway/dates.rb +25 -0
  14. data/lib/xero_gateway/error.rb +18 -0
  15. data/lib/xero_gateway/exceptions.rb +41 -0
  16. data/lib/xero_gateway/gateway.rb +363 -0
  17. data/lib/xero_gateway/http.rb +128 -0
  18. data/lib/xero_gateway/http_encoding_helper.rb +49 -0
  19. data/lib/xero_gateway/invoice.rb +278 -0
  20. data/lib/xero_gateway/line_item.rb +123 -0
  21. data/lib/xero_gateway/money.rb +16 -0
  22. data/lib/xero_gateway/oauth.rb +56 -0
  23. data/lib/xero_gateway/organisation.rb +61 -0
  24. data/lib/xero_gateway/payment.rb +40 -0
  25. data/lib/xero_gateway/phone.rb +77 -0
  26. data/lib/xero_gateway/private_app.rb +17 -0
  27. data/lib/xero_gateway/response.rb +37 -0
  28. data/lib/xero_gateway/tax_rate.rb +63 -0
  29. data/lib/xero_gateway/tracking_category.rb +62 -0
  30. data/lib/xero_gateway.rb +33 -0
  31. data/test/integration/accounts_list_test.rb +109 -0
  32. data/test/integration/create_contact_test.rb +66 -0
  33. data/test/integration/create_invoice_test.rb +49 -0
  34. data/test/integration/get_accounts_test.rb +23 -0
  35. data/test/integration/get_contact_test.rb +28 -0
  36. data/test/integration/get_contacts_test.rb +40 -0
  37. data/test/integration/get_currencies_test.rb +25 -0
  38. data/test/integration/get_invoice_test.rb +48 -0
  39. data/test/integration/get_invoices_test.rb +90 -0
  40. data/test/integration/get_organisation_test.rb +24 -0
  41. data/test/integration/get_tax_rates_test.rb +25 -0
  42. data/test/integration/get_tracking_categories_test.rb +26 -0
  43. data/test/integration/update_contact_test.rb +31 -0
  44. data/test/stub_responses/accounts.xml +1 -0
  45. data/test/stub_responses/api_exception.xml +153 -0
  46. data/test/stub_responses/contact.xml +1 -0
  47. data/test/stub_responses/contacts.xml +2189 -0
  48. data/test/stub_responses/create_invoice.xml +64 -0
  49. data/test/stub_responses/currencies.xml +16 -0
  50. data/test/stub_responses/invalid_api_key_error.xml +1 -0
  51. data/test/stub_responses/invalid_consumer_key +1 -0
  52. data/test/stub_responses/invalid_request_token +1 -0
  53. data/test/stub_responses/invoice.xml +1 -0
  54. data/test/stub_responses/invoice_not_found_error.xml +1 -0
  55. data/test/stub_responses/invoices.xml +1 -0
  56. data/test/stub_responses/organisation.xml +14 -0
  57. data/test/stub_responses/tax_rates.xml +52 -0
  58. data/test/stub_responses/token_expired +1 -0
  59. data/test/stub_responses/tracking_categories.xml +1 -0
  60. data/test/stub_responses/unknown_error.xml +1 -0
  61. data/test/test_helper.rb +81 -0
  62. data/test/unit/account_test.rb +34 -0
  63. data/test/unit/contact_test.rb +97 -0
  64. data/test/unit/currency_test.rb +31 -0
  65. data/test/unit/gateway_test.rb +79 -0
  66. data/test/unit/invoice_test.rb +302 -0
  67. data/test/unit/oauth_test.rb +110 -0
  68. data/test/unit/organisation_test.rb +34 -0
  69. data/test/unit/tax_rate_test.rb +38 -0
  70. data/test/unit/tracking_category_test.rb +30 -0
  71. data/test/xsd/README +2 -0
  72. data/test/xsd/create_contact.xsd +61 -0
  73. data/test/xsd/create_invoice.xsd +107 -0
  74. data/xero_gateway.gemspec +87 -0
  75. 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