xero_gateway 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
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