From d431493d12dbbbda806e779226d3aafc52bb551d Mon Sep 17 00:00:00 2001 From: Tim Connor Date: Mon, 1 Dec 2008 12:45:04 +1300 Subject: [PATCH] first commit --- LICENSE | 14 + README.textile | 112 ++++++++ Rakefile | 14 + lib/xero_gateway.rb | 34 +++ lib/xero_gateway/address.rb | 47 ++++ lib/xero_gateway/contact.rb | 54 ++++ lib/xero_gateway/dates.rb | 31 +++ lib/xero_gateway/gateway.rb | 263 +++++++++++++++++++ lib/xero_gateway/http.rb | 73 +++++ lib/xero_gateway/invoice.rb | 48 ++++ lib/xero_gateway/line_item.rb | 40 +++ lib/xero_gateway/messages/contact_message.rb | 107 ++++++++ lib/xero_gateway/messages/invoice_message.rb | 136 ++++++++++ lib/xero_gateway/money.rb | 30 +++ lib/xero_gateway/phone.rb | 38 +++ lib/xero_gateway/response.rb | 43 +++ test/integration/gateway_test.rb | 148 +++++++++++ test/integration/stub_responses/contact.xml | 1 + test/integration/stub_responses/contacts.xml | 1 + test/integration/stub_responses/invoice.xml | 1 + test/integration/stub_responses/invoices.xml | 1 + test/test_helper.rb | 22 ++ test/unit/messages/contact_message_test.rb | 66 +++++ test/unit/messages/invoice_message_test.rb | 76 ++++++ test/xsd/README | 2 + test/xsd/create_contact.xsd | 50 ++++ test/xsd/create_invoice.xsd | 98 +++++++ xero_gateway.gemspec | 39 +++ 28 files changed, 1589 insertions(+) create mode 100644 LICENSE create mode 100644 README.textile create mode 100644 Rakefile create mode 100644 lib/xero_gateway.rb create mode 100644 lib/xero_gateway/address.rb create mode 100644 lib/xero_gateway/contact.rb create mode 100644 lib/xero_gateway/dates.rb create mode 100644 lib/xero_gateway/gateway.rb create mode 100644 lib/xero_gateway/http.rb create mode 100644 lib/xero_gateway/invoice.rb create mode 100644 lib/xero_gateway/line_item.rb create mode 100644 lib/xero_gateway/messages/contact_message.rb create mode 100644 lib/xero_gateway/messages/invoice_message.rb create mode 100644 lib/xero_gateway/money.rb create mode 100644 lib/xero_gateway/phone.rb create mode 100644 lib/xero_gateway/response.rb create mode 100644 test/integration/gateway_test.rb create mode 100644 test/integration/stub_responses/contact.xml create mode 100644 test/integration/stub_responses/contacts.xml create mode 100644 test/integration/stub_responses/invoice.xml create mode 100644 test/integration/stub_responses/invoices.xml create mode 100644 test/test_helper.rb create mode 100644 test/unit/messages/contact_message_test.rb create mode 100644 test/unit/messages/invoice_message_test.rb create mode 100644 test/xsd/README create mode 100644 test/xsd/create_contact.xsd create mode 100644 test/xsd/create_invoice.xsd create mode 100644 xero_gateway.gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..86aa424e --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2008 Tim Connor + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + diff --git a/README.textile b/README.textile new file mode 100644 index 00000000..43cae633 --- /dev/null +++ b/README.textile @@ -0,0 +1,112 @@ +h1. Xero API wrapper + +h2. Introduction + +This library is designed to help ruby based applications communicate with the publicly available API for Xero. If you are unfamiliar with the API, you should first read the documentation, located here "https://network.xero.com/Help/Xero%20API%20Reference%201.0.htm":https://network.xero.com/Help/Xero%20API%20Reference%201.0.htm + +h2. Prerequisites + +To use the Xero API you must have a Xero API Key. IF you don't know what this is, and don't know how to get one, this library is probably not for you. + +h2. Usage + +

+    gateway = XeroGateway::Gateway.new(
+      :customer_key => "THE_CUSTOMER_KEY_GENERATED_FOR_YOUR_APP",
+      :api_key => "YOUR_XERO_API_KEY"
+    )
+
+ + +h2. Implemented interface methods + +h3. GET /api.xro/1.0/contact (get_contact_by_id) + +Example: +

+	result = gateway.get_contact_by_id(contact_id)
+	contact = result.contact if result.success?
+
+ +h3. GET /api.xro/1.0/contact (get_contact_by_number) + +Example: +

+	gateway.get_contact_by_number(contact_number)
+
+ +h3. GET /api.xro/1.0/contacts (get_contacts) + +Example: +

+	gateway.get_contacts(:type => :all, :sort => :name, :direction => :desc)
+
+ +h3. PUT /api.xro/1.0/contact + +Example: +

+    contact = XeroGateway::Contact.new
+	contact.name = "The contacts name"
+    contact.email = "whoever@something.com"
+    contact.phone.number = "555 123 4567"
+    contact.address.line_1 = "LINE 1 OF THE ADDRESS"
+    contact.address.line_2 = "LINE 2 OF THE ADDRESS"
+    contact.address.city = "WELLINGTON"
+    contact.address.region = "WELLINGTON"
+    contact.address.country = "NEW ZEALAND"
+    contact.address.post_code = "6021"
+	
+	gateway.create_contact(contact)
+
+ +h3. GET /api.xro/1.0/invoice (get_invoice_by_id) + +Example: +

+	gateway.get_invoice_by_id(invoice_id)
+
+ +h3. GET /api.xro/1.0/invoice (get_invoice_by_number) + +Example: +

+	gateway.get_invoice_by_number(invoice_number)
+
+ +h3. GET /api.xro/1.0/invoices (get_invoices) + +Example: +

+	gateway.get_invoices(modified_since = nil)
+
+ +h3. PUT /api.xro/1.0/invoice + +Example: +

+    invoice = XeroGateway::Invoice.new({
+      :invoice_type => "ACCREC",
+      :due_date => 1.month.from_now,
+      :invoice_number => "YOUR INVOICE NUMBER",
+      :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)",
+      :tax_inclusive => true,
+      :includes_tax => false,
+      :sub_total => 1000,
+      :total_tax => 125,
+      :total => 1250
+    })
+    invoice.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT")
+    invoice.contact.phone.number = "12345"
+    invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS"    
+    invoice.line_items << XeroGateway::LineItem.new(
+      :description => "THE DESCRIPTION OF THE LINE ITEM",
+      :unit_amount => 1000,
+      :tax_amount => 125,
+      :line_amount => 1000,
+      :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM",
+      :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM"
+    )
+	
+	gateway.create_invoice(invoice)
+
diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..fa205217 --- /dev/null +++ b/Rakefile @@ -0,0 +1,14 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the xero gateway.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + diff --git a/lib/xero_gateway.rb b/lib/xero_gateway.rb new file mode 100644 index 00000000..27ba5e04 --- /dev/null +++ b/lib/xero_gateway.rb @@ -0,0 +1,34 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +require "CGI" +require "URI" +require 'net/https' +require "rexml/document" +require "builder" +require "bigdecimal" + +require File.dirname(__FILE__) + "/xero_gateway/http" +require File.dirname(__FILE__) + "/xero_gateway/dates" +require File.dirname(__FILE__) + "/xero_gateway/money" +require File.dirname(__FILE__) + "/xero_gateway/response" +require File.dirname(__FILE__) + "/xero_gateway/line_item" +require File.dirname(__FILE__) + "/xero_gateway/invoice" +require File.dirname(__FILE__) + "/xero_gateway/contact" +require File.dirname(__FILE__) + "/xero_gateway/address" +require File.dirname(__FILE__) + "/xero_gateway/phone" +require File.dirname(__FILE__) + "/xero_gateway/messages/contact_message" +require File.dirname(__FILE__) + "/xero_gateway/messages/invoice_message" +require File.dirname(__FILE__) + "/xero_gateway/gateway" diff --git a/lib/xero_gateway/address.rb b/lib/xero_gateway/address.rb new file mode 100644 index 00000000..e3e793a2 --- /dev/null +++ b/lib/xero_gateway/address.rb @@ -0,0 +1,47 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + class Address + attr_accessor :address_type, :line_1, :line_2, :line_3, :line_4, :city, :region, :post_code, :country + + def initialize(params = {}) + params = { + :address_type => "DEFAULT" + }.merge(params) + + params.each do |k,v| + self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair + self.send("#{k}=", v) + end + end + + def self.parse(string) + address = Address.new + + string.split("\r\n").each_with_index do |line, index| + address.instance_variable_set("@line_#{index+1}", line) + end + address + end + + def ==(other) + equal = true + [:address_type, :line_1, :line_2, :line_3, :line_4, :city, :region, :post_code, :country].each do |field| + equal &&= (send(field) == other.send(field)) + end + return equal + end + end +end \ No newline at end of file diff --git a/lib/xero_gateway/contact.rb b/lib/xero_gateway/contact.rb new file mode 100644 index 00000000..6e88b295 --- /dev/null +++ b/lib/xero_gateway/contact.rb @@ -0,0 +1,54 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + class Contact + attr_accessor :id, :contact_number, :status, :name, :email, :addresses, :phones, :updated_at + + def initialize(params = {}) + params = {}.merge(params) + params.each do |k,v| + self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair + self.send("#{k}=", v) + end + + @phones ||= [] + @addresses ||= [] + end + + def address=(address) + self.addresses = [address] + end + + def address + self.addresses[0] ||= Address.new + end + + def phone=(phone) + self.phones = [phone] + end + + def phone + self.phones[0] ||= Phone.new + end + + def ==(other) + equal = true + [:contact_number, :status, :name, :email, :addresses, :phones].each do |field| + equal &&= (send(field) == other.send(field)) + end + return equal + end + end +end \ No newline at end of file diff --git a/lib/xero_gateway/dates.rb b/lib/xero_gateway/dates.rb new file mode 100644 index 00000000..fc42222a --- /dev/null +++ b/lib/xero_gateway/dates.rb @@ -0,0 +1,31 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + module Dates + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def format_date_time(time) + return time.strftime("%Y-%m-%dT%H:%M:%S") + end + + def parse_date_time(time) + 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) + end + end + end +end diff --git a/lib/xero_gateway/gateway.rb b/lib/xero_gateway/gateway.rb new file mode 100644 index 00000000..0c8de7c2 --- /dev/null +++ b/lib/xero_gateway/gateway.rb @@ -0,0 +1,263 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + class Gateway + include Http + + attr_accessor :xero_url, :customer_key, :api_key + + def initialize(params) + @xero_url = params[:xero_url] || "https://networktest.xero.com/api.xro/1.0" + @customer_key = params[:customer_key] + @api_key = params[:api_key] + end + + # Retrieve all contacts from Xero + # Usage get_contacts(:type => :all, :sort => :name, :direction => :desc) + def get_contacts(options = {}) + request_params = {} + request_params[:type] = options[:type] if options[:type] + request_params[:sortBy] = options[:sort] if options[:sort] + request_params[:direction] = options[:direction] if options[:direction] + + response_xml = http_get("#{@xero_url}/contacts", request_params) + + doc = REXML::Document.new(response_xml) + + # Create the response object + response = build_response(doc) + + # Add the contacts to the response + if response.success? + response.response_item = [] + REXML::XPath.each(doc, "/Response/Contacts/Contact") do |contact_element| + response.response_item << XeroGateway::Messages::ContactMessage.from_xml(contact_element) + end + end + + # Add the request and response XML to the response object + response.request_params = request_params + response.response_xml = response_xml + response + end + + # Retrieve a contact from Xero + # Usage get_contact_by_id(contact_id) + def get_contact_by_id(contact_id) + get_contact(contact_id) + end + + # Retrieve a contact from Xero + # Usage get_contact_by_id(contact_id) + def get_contact_by_number(contact_number) + get_contact(nil, contact_number) + end + + + # Creates a contact in Xero + # + # Usage : + # + # contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT #{Time.now.to_i}") + # contact.email = "whoever@something.com" + # contact.phone.number = "12345" + # contact.address.line_1 = "LINE 1 OF THE ADDRESS" + # contact.address.line_2 = "LINE 2 OF THE ADDRESS" + # contact.address.line_3 = "LINE 3 OF THE ADDRESS" + # contact.address.line_4 = "LINE 4 OF THE ADDRESS" + # contact.address.city = "WELLINGTON" + # contact.address.region = "WELLINGTON" + # contact.address.country = "NEW ZEALAND" + # contact.address.post_code = "6021" + # + # create_contact(contact) + def create_contact(contact) + request_xml = XeroGateway::Messages::ContactMessage.build_xml(contact) + response_xml = http_put("#{@xero_url}/contact", request_xml, {}) + + doc = REXML::Document.new(response_xml) + + # Create the response object + response = build_response(doc) + + # Add the invoice to the response + if response.success? + response.response_item = XeroGateway::Messages::ContactMessage.from_xml(REXML::XPath.first(doc, "/Response/Contact")) + end + + # Add the request and response XML to the response object + response.request_xml = request_xml + response.response_xml = response_xml + + response + end + + # Retrieves an invoice from Xero based on its GUID + # + # Usage : get_invoice_by_id("8c69117a-60ae-4d31-9eb4-7f5a76bc4947") + def get_invoice_by_id(invoice_id) + get_invoice(invoice_id) + end + + # Retrieves an invoice from Xero based on its number + # + # Usage : get_invoice_by_number("OIT00526") + def get_invoice_by_number(invoice_number) + get_invoice(nil, invoice_number) + end + + # Retrieves all invoices from Xero + # + # Usage : get_invoices + # get_invoices(modified_since) + def get_invoices(modified_since = nil) + request_params = modified_since ? {:modifiedSince => modified_since.strftime("%Y%m%d")} : {} + + response_xml = http_get("#{@xero_url}/invoices", request_params) + + doc = REXML::Document.new(response_xml) + + # Create the response object + response = build_response(doc) + + # Add the invoices to the response + if response.success? + response.response_item = [] + REXML::XPath.first(doc, "/Response/Invoices").children.each do |invoice_element| + response.response_item << XeroGateway::Messages::InvoiceMessage.from_xml(invoice_element) + end + end + + # Add the request and response XML to the response object + response.request_params = request_params + response.response_xml = response_xml + response + end + + # Creates an invoice in Xero based on an invoice object + # + # Usage : + # + # invoice = XeroGateway::Invoice.new({ + # :invoice_type => "ACCREC", + # :due_date => 1.month.from_now, + # :invoice_number => "YOUR INVOICE NUMBER", + # :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)", + # :includes_tax => false, + # :sub_total => 1000, + # :total_tax => 125, + # :total => 1125 + # }) + # invoice.contact = XeroGateway::Contact.new(:name => "THE NAME OF THE CONTACT") + # invoice.contact.phone.number = "12345" + # invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS" + # invoice.line_items << XeroGateway::LineItem.new( + # :description => "THE DESCRIPTION OF THE LINE ITEM", + # :unit_amount => 100, + # :tax_amount => 12.5, + # :line_amount => 125, + # :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM", + # :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM" + # ) + # + # create_invoice(invoice) + def create_invoice(invoice) + request_xml = XeroGateway::Messages::InvoiceMessage.build_xml(invoice) + response_xml = http_put("#{@xero_url}/invoice", request_xml) + + doc = REXML::Document.new(response_xml) + + # Create the response object + response = build_response(doc) + + # Add the invoice to the response + if response.success? + response.response_item = XeroGateway::Messages::InvoiceMessage.from_xml(REXML::XPath.first(doc, "/Response/Invoice")) + end + + # Add the request and response XML to the response object + response.request_xml = request_xml + response.response_xml = response_xml + + response + end + + + + + + private + + def get_invoice(invoice_id = nil, invoice_number = nil) + request_params = invoice_id ? {:invoiceID => invoice_id} : {:invoiceNumber => invoice_number} + response_xml = http_get("#{@xero_url}/invoice", request_params) + + doc = REXML::Document.new(response_xml) + + # Create the response object + response = build_response(doc) + + # Add the invoice to the response + response.response_item = XeroGateway::Messages::InvoiceMessage.from_xml(REXML::XPath.first(doc, "/Response/Invoice")) if response.success? + + # Add the request and response XML to the response object + response.request_params = request_params + response.response_xml = response_xml + response + end + + def get_contact(contact_id = nil, contact_number = nil) + request_params = contact_id ? {:contactID => contact_id} : {:contactNumber => contact_number} + response_xml = http_get("#{@xero_url}/contact", request_params) + + doc = REXML::Document.new(response_xml) + + # Create the response object + response = build_response(doc) + + # Add the invoice to the response + response.response_item = XeroGateway::Messages::ContactMessage.from_xml(REXML::XPath.first(doc, "/Response/Contact")) if response.success? + + # Add the request and response XML to the response object + response.request_params = request_params + response.response_xml = response_xml + response + end + + + + def build_response(response_document) + response = XeroGateway::Response.new({ + :id => REXML::XPath.first(response_document, "/Response/ID").text, + :status => REXML::XPath.first(response_document, "/Response/Status").text, + :provider => REXML::XPath.first(response_document, "/Response/ProviderName").text, + :date_time => REXML::XPath.first(response_document, "/Response/DateTimeUTC").text, + }) + + # Add any errors to the response object + if !response.success? + REXML::XPath.each(response_document, "/Response/Error") do |error| + response.errors << { + :date_time => REXML::XPath.first(error, "/DateTime").text, + :type => REXML::XPath.first(error, "/ExceptionType").text, + :message => REXML::XPath.first(error, "/Message").text + } + end + end + + response + end + end +end diff --git a/lib/xero_gateway/http.rb b/lib/xero_gateway/http.rb new file mode 100644 index 00000000..1a973a7b --- /dev/null +++ b/lib/xero_gateway/http.rb @@ -0,0 +1,73 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + module Http + OPEN_TIMEOUT = 10 + READ_TIMEOUT = 60 + + def http_get(url, extra_params = {}) + params = {:apiKey => @api_key, :xeroKey => @customer_key} + params = params.merge(extra_params).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&") + + uri = URI.parse(url + "?" + params) + + http = Net::HTTP.new(uri.host, uri.port) + http.open_timeout = OPEN_TIMEOUT + http.read_timeout = READ_TIMEOUT + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + + http.get(uri.request_uri).body + end + + def http_post(url, body, extra_params = {}) + headers = {} + headers['Content-Type'] ||= "application/x-www-form-urlencoded" + + params = {:apiKey => @api_key, :xeroKey => @customer_key} + params = params.merge(extra_params).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&") + + uri = URI.parse(url + "?" + params) + + http = Net::HTTP.new(uri.host, uri.port) + http.open_timeout = OPEN_TIMEOUT + http.read_timeout = READ_TIMEOUT + http.use_ssl = true + + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + + http.post(uri.request_uri, body, headers).body + end + + def http_put(url, body, extra_params = {}) + headers = {} + headers['Content-Type'] ||= "application/x-www-form-urlencoded" + + params = {:apiKey => @api_key, :xeroKey => @customer_key} + params = params.merge(extra_params).map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&") + + uri = URI.parse(url + "?" + params) + + http = Net::HTTP.new(uri.host, uri.port) + http.open_timeout = OPEN_TIMEOUT + http.read_timeout = READ_TIMEOUT + http.use_ssl = true + + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + + http.put(uri.request_uri, body, headers).body + end + end +end \ No newline at end of file diff --git a/lib/xero_gateway/invoice.rb b/lib/xero_gateway/invoice.rb new file mode 100644 index 00000000..f525ce8c --- /dev/null +++ b/lib/xero_gateway/invoice.rb @@ -0,0 +1,48 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + class Invoice + # All accessible fields + attr_accessor :id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :tax_inclusive, :includes_tax, :sub_total, :total_tax, :total, :line_items, :contact + + def initialize(params = {}) + params = { + :contact => Contact.new, + :date => Time.now, + :includes_tax => true, + :tax_inclusive => true + }.merge(params) + + params.each do |k,v| + self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair + self.send("#{k}=", v) + end + + @line_items ||= [] + end + + def ==(other) + equal = true + ["invoice_number", "invoice_type", "invoice_status", "reference", "tax_inclusive", "includes_tax", "sub_total", "total_tax", "total", "contact", "line_items"].each do |field| + equal &&= (send(field) == other.send(field)) + end + ["date", "due_date"].each do |field| + equal &&= (send(field).to_s == other.send(field).to_s) + end + + return equal + end + end +end diff --git a/lib/xero_gateway/line_item.rb b/lib/xero_gateway/line_item.rb new file mode 100644 index 00000000..4eaf77a9 --- /dev/null +++ b/lib/xero_gateway/line_item.rb @@ -0,0 +1,40 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + class LineItem + # All accessible fields + attr_accessor :id, :description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code, :tracking_category, :tracking_option + + def initialize(params = {}) + params = { + :quantity => 1 + }.merge(params) + + params.each do |k,v| + self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair + self.send("#{k}=", v) + end + end + + def ==(other) + return true + equal = true + [:description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code, :tracking_category, :tracking_option].each do |field| + equal &&= (send(field) == other.send(field)) + end + return equal + end + end +end diff --git a/lib/xero_gateway/messages/contact_message.rb b/lib/xero_gateway/messages/contact_message.rb new file mode 100644 index 00000000..8052ed33 --- /dev/null +++ b/lib/xero_gateway/messages/contact_message.rb @@ -0,0 +1,107 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + module Messages + class ContactMessage + include Dates + + def self.build_xml(contact) + b = Builder::XmlMarkup.new + + b.Contact { + b.ContactID contact.id if contact.id + b.ContactNumber contact.contact_number if contact.contact_number + b.Name contact.name + b.EmailAddress contact.email if contact.email + b.Addresses { + contact.addresses.each do |address| + b.Address { + b.AddressType address.address_type + b.AddressLine1 address.line_1 if address.line_1 + b.AddressLine2 address.line_2 if address.line_2 + b.AddressLine3 address.line_3 if address.line_3 + b.AddressLine4 address.line_4 if address.line_4 + b.City address.city if address.city + b.Region address.region if address.region + b.PostalCode address.post_code if address.post_code + b.Country address.country if address.country + } + end + } + b.Phones { + contact.phones.each do |phone| + b.Phone { + b.PhoneType phone.phone_type + b.PhoneNumber phone.number + b.PhoneAreaCode phone.area_code if phone.area_code + b.PhoneCountryCode phone.country_code if phone.country_code + } + end + } + } + end + + # Take a Contact element and convert it into an Contact object + def self.from_xml(contact_element) + contact = Contact.new + contact_element.children.each do |element| + case(element.name) + when "ContactID" then contact.id = element.text + when "ContactNumber" then contact.contact_number = element.text + when "ContactStatus" then contact.status = element.text + when "Name" then contact.name = element.text + when "EmailAddress" then contact.email = element.text + when "Addresses" then element.children.each {|address| contact.addresses << parse_address(address)} + when "Phones" then element.children.each {|phone| contact.phones << parse_phone(phone)} + end + end + contact + end + + private + + def self.parse_address(address_element) + address = Address.new + address_element.children.each do |element| + case(element.name) + when "AddressType" then address.address_type = element.text + when "AddressLine1" then address.line_1 = element.text + when "AddressLine2" then address.line_2 = element.text + when "AddressLine3" then address.line_3 = element.text + when "AddressLine4" then address.line_4 = element.text + when "City" then address.city = element.text + when "Region" then address.region = element.text + when "PostalCode" then address.post_code = element.text + when "Country" then address.country = element.text + end + end + address + end + + def self.parse_phone(phone_element) + phone = Phone.new + phone_element.children.each do |element| + case(element.name) + when "PhoneType" then phone.phone_type = element.text + when "PhoneNumber" then phone.number = element.text + when "PhoneAreaCode" then phone.area_code = element.text + when "PhoneCountryCode" then phone.country_code = element.text + end + end + phone + end + end + end +end \ No newline at end of file diff --git a/lib/xero_gateway/messages/invoice_message.rb b/lib/xero_gateway/messages/invoice_message.rb new file mode 100644 index 00000000..5ba5ae7f --- /dev/null +++ b/lib/xero_gateway/messages/invoice_message.rb @@ -0,0 +1,136 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + module Messages + class InvoiceMessage + include Dates + include Money + + def self.build_xml(invoice) + b = Builder::XmlMarkup.new + + b.Invoice { + b.InvoiceType invoice.invoice_type + b.Contact { + b.ContactID invoice.contact.id if invoice.contact.id + b.Name invoice.contact.name + b.EmailAddress invoice.contact.email if invoice.contact.email + b.Addresses { + invoice.contact.addresses.each do |address| + b.Address { + b.AddressType address.address_type + b.AddressLine1 address.line_1 if address.line_1 + b.AddressLine2 address.line_2 if address.line_2 + b.AddressLine3 address.line_3 if address.line_3 + b.AddressLine4 address.line_4 if address.line_4 + b.City address.city if address.city + b.Region address.region if address.region + b.PostalCode address.post_code if address.post_code + b.Country address.country if address.country + } + end + } + b.Phones { + invoice.contact.phones.each do |phone| + b.Phone { + b.PhoneType phone.phone_type + b.PhoneNumber phone.number + b.PhoneAreaCode phone.area_code if phone.area_code + b.PhoneCountryCode phone.country_code if phone.country_code + } + end + } + } + b.InvoiceDate format_date_time(invoice.date) + b.DueDate format_date_time(invoice.due_date) if invoice.due_date + b.InvoiceNumber invoice.invoice_number + b.Reference invoice.reference if invoice.reference + b.TaxInclusive invoice.tax_inclusive if invoice.tax_inclusive + b.IncludesTax invoice.includes_tax + b.SubTotal format_money(invoice.sub_total) if invoice.sub_total + b.TotalTax format_money(invoice.total_tax) if invoice.total_tax + b.Total format_money(invoice.total) if invoice.total + b.LineItems { + invoice.line_items.each do |line_item| + b.LineItem { + b.Description line_item.description + b.Quantity line_item.quantity if line_item.quantity + b.UnitAmount format_money(line_item.unit_amount) + b.TaxType line_item.tax_type if line_item.tax_type + b.TaxAmount format_money(line_item.tax_amount) if line_item.tax_amount + b.LineAmount format_money(line_item.line_amount) + b.AccountCode line_item.account_code || 200 + b.Tracking { + b.TrackingCategory { + b.Name line_item.tracking_category + b.Option line_item.tracking_option + } + } + } + end + } + } + end + + # Take an Invoice element and convert it into an Invoice object + def self.from_xml(invoice_element) + invoice = Invoice.new + invoice_element.children.each do |element| + case(element.name) + when "InvoiceStatus" then invoice.invoice_status = element.text + when "InvoiceID" then invoice.id = element.text + when "InvoiceNumber" then invoice.invoice_number = element.text + when "InvoiceType" then invoice.invoice_type = element.text + when "InvoiceDate" then invoice.date = parse_date_time(element.text) + when "DueDate" then invoice.due_date = parse_date_time(element.text) + when "Reference" then invoice.reference = element.text + when "TaxInclusive" then invoice.tax_inclusive = (element.text == "true") + when "IncludesTax" then invoice.includes_tax = (element.text == "true") + when "SubTotal" then invoice.sub_total = BigDecimal.new(element.text) + when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text) + when "Total" then invoice.total = BigDecimal.new(element.text) + when "Contact" then invoice.contact = ContactMessage.from_xml(element) + when "LineItems" then element.children.each {|line_item| invoice.line_items << parse_line_item(line_item)} + end + end + invoice + end + + private + + def self.parse_line_item(line_item_element) + line_item = LineItem.new + line_item_element.children.each do |element| + case(element.name) + when "LineItemID" then line_item.id = element.text + when "Description" then line_item.description = element.text + when "Quantity" then line_item.quantity = element.text.to_i + when "UnitAmount" then line_item.unit_amount = BigDecimal.new(element.text) + when "TaxType" then line_item.tax_type = element.text + when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text) + when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text) + when "AccountCode" then line_item.account_code = element.text + when "Tracking" then + if element.elements['TrackingCategory'] + line_item.tracking_category = element.elements['TrackingCategory/Name'].text + line_item.tracking_option = element.elements['TrackingCategory/Option'].text + end + end + end + line_item + end + end + end +end \ No newline at end of file diff --git a/lib/xero_gateway/money.rb b/lib/xero_gateway/money.rb new file mode 100644 index 00000000..e6972d17 --- /dev/null +++ b/lib/xero_gateway/money.rb @@ -0,0 +1,30 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + module Money + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def format_money(amount) + if amount.class == BigDecimal + return amount.to_s("F") + end + return amount + end + end + end +end diff --git a/lib/xero_gateway/phone.rb b/lib/xero_gateway/phone.rb new file mode 100644 index 00000000..35104103 --- /dev/null +++ b/lib/xero_gateway/phone.rb @@ -0,0 +1,38 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + class Phone + attr_accessor :phone_type, :number, :area_code, :country_code + + def initialize(params = {}) + params = { + :phone_type => "DEFAULT" + }.merge(params) + + params.each do |k,v| + self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair + self.send("#{k}=", v) + end + end + + def ==(other) + equal = true + [:phone_type, :number, :area_code, :country_code].each do |field| + equal &&= (send(field) == other.send(field)) + end + return equal + end + end +end \ No newline at end of file diff --git a/lib/xero_gateway/response.rb b/lib/xero_gateway/response.rb new file mode 100644 index 00000000..898132e2 --- /dev/null +++ b/lib/xero_gateway/response.rb @@ -0,0 +1,43 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +module XeroGateway + class Response + attr_accessor :id, :status, :errors, :provider, :date_time, :response_item, :request_params, :request_xml, :response_xml + + alias_method :invoice, :response_item + alias_method :invoices, :response_item + alias_method :contact, :response_item + alias_method :contacts, :response_item + + + + def initialize(params = {}) + params.each do |k,v| + self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair + self.send("#{k}=", v) + end + + @errors ||= [] + end + + def success? + status == "OK" + end + + def error + errors.blank? ? nil : errors[0] + end + end +end \ No newline at end of file diff --git a/test/integration/gateway_test.rb b/test/integration/gateway_test.rb new file mode 100644 index 00000000..ed95c47f --- /dev/null +++ b/test/integration/gateway_test.rb @@ -0,0 +1,148 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +require File.dirname(__FILE__) + '/../test_helper' + +class GatewayTest < Test::Unit::TestCase + # If false, the tests will be run against the Xero test environment + STUB_XERO_CALLS = true + + # If the requests are not stubbed, enter your API key and you test company customer key here + API_KEY = "OWFKZTA4YZNHYWNKNDDJM2JKNZQWOW" + CUSTOMER_KEY = "YWZIMZQ3ZGVJMME1NDCWNTK3YWZMNW" + + def setup + @gateway = XeroGateway::Gateway.new( + :customer_key => CUSTOMER_KEY, + :api_key => API_KEY + ) + + if STUB_XERO_CALLS + @gateway.xero_url = "DUMMY_URL" + # Stub out the HTTP request + @gateway.stubs(:http_get).with {|url, params| url =~ /contact$/ }.returns(get_file_as_string("contact.xml")) + @gateway.stubs(:http_get).with {|url, params| url =~ /contacts$/ }.returns(get_file_as_string("contacts.xml")) + @gateway.stubs(:http_get).with {|url, params| url =~ /invoice$/ }.returns(get_file_as_string("invoice.xml")) + @gateway.stubs(:http_get).with {|url, params| url =~ /invoices$/ }.returns(get_file_as_string("invoices.xml")) + @gateway.stubs(:http_put).with {|url, body, params| url =~ /invoice$/ }.returns(get_file_as_string("invoice.xml")) + @gateway.stubs(:http_put).with {|url, body, params| url =~ /contact$/ }.returns(get_file_as_string("contact.xml")) + + + end + end + + def dummy_invoice + invoice = XeroGateway::Invoice.new({ + :invoice_type => "ACCREC", + :due_date => Date.today + 20, + :invoice_number => STUB_XERO_CALLS ? "INV-0001" : "#{Time.now.to_i}", + :reference => "YOUR REFERENCE (NOT NECESSARILY UNIQUE!)", + :sub_total => 1000, + :total_tax => 125, + :total => 1125 + }) + invoice.contact = dummy_contact + invoice.line_items << XeroGateway::LineItem.new( + :description => "THE DESCRIPTION OF THE LINE ITEM", + :unit_amount => 1000, + :tax_amount => 125, + :line_amount => 1000, + :tracking_category => "THE TRACKING CATEGORY FOR THE LINE ITEM", + :tracking_option => "THE TRACKING OPTION FOR THE LINE ITEM" + ) + invoice + end + + def dummy_contact + contact = XeroGateway::Contact.new(:name => STUB_XERO_CALLS ? "CONTACT NAME" : "THE NAME OF THE CONTACT #{Time.now.to_i}") + contact.contact_number = STUB_XERO_CALLS ? "12345" : "#{Time.now.to_i}" + contact.email = "whoever@something.com" + contact.phone.number = "12345" + contact.address.line_1 = "LINE 1 OF THE ADDRESS" + contact.address.line_2 = "LINE 2 OF THE ADDRESS" + contact.address.line_3 = "LINE 3 OF THE ADDRESS" + contact.address.line_4 = "LINE 4 OF THE ADDRESS" + contact.address.city = "WELLINGTON" + contact.address.region = "WELLINGTON" + contact.address.country = "NEW ZEALAND" + contact.address.post_code = "6021" + + contact + end + + def get_file_as_string(filename) + data = '' + f = File.open(File.dirname(__FILE__) + "/stub_responses/" + filename, "r") + f.each_line do |line| + data += line + end + f.close + return data + end + + def test_create_and_get_contact + contact = dummy_contact + + create_contact_result = @gateway.create_contact(contact) + assert create_contact_result.success? + + contact_from_create_request = create_contact_result.contact + assert contact_from_create_request.name == contact.name + + get_contact_by_id_result = @gateway.get_contact_by_id(contact_from_create_request.id) + assert get_contact_by_id_result.success? + assert get_contact_by_id_result.contact.name == contact.name + + get_contact_by_number_result = @gateway.get_contact_by_number(contact.contact_number) + assert get_contact_by_number_result.success? + assert get_contact_by_number_result.contact.name == contact.name + end + + def test_create_and_get_invoice + invoice = dummy_invoice + + result = @gateway.create_invoice(invoice) + assert result.success? + + invoice_from_create_request = result.invoice + assert invoice_from_create_request.invoice_number == invoice.invoice_number + + result = @gateway.get_invoice_by_id(invoice_from_create_request.id) + assert result.success? + assert result.invoice.invoice_number == invoice_from_create_request.invoice_number + + result = @gateway.get_invoice_by_number(invoice_from_create_request.invoice_number) + assert result.success? + assert result.invoice.id == invoice_from_create_request.id + end + + def test_get_contacts + result = @gateway.get_contacts + assert result.success? + assert result.contacts.size > 0 + end + + def test_get_invoices + # Create a test invoice + invoice = dummy_invoice + @gateway.create_invoice(invoice) + + # Check that it is returned + result = @gateway.get_invoices + assert result.success? + assert result.invoices.collect {|response_invoice| response_invoice.invoice_number}.include?(invoice.invoice_number) + end + + +end \ No newline at end of file diff --git a/test/integration/stub_responses/contact.xml b/test/integration/stub_responses/contact.xml new file mode 100644 index 00000000..765e6766 --- /dev/null +++ b/test/integration/stub_responses/contact.xml @@ -0,0 +1 @@ +a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999OKYOUR PROVIDER2008-10-09T02:40:54.3997437Za99a9aaa-9999-99a9-9aa9-aaaaaa9a999912345ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
\ No newline at end of file diff --git a/test/integration/stub_responses/contacts.xml b/test/integration/stub_responses/contacts.xml new file mode 100644 index 00000000..04031b2c --- /dev/null +++ b/test/integration/stub_responses/contacts.xml @@ -0,0 +1 @@ +a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999OKYOUR PROVIDER2008-10-09T02:40:54.3997437Za99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
\ No newline at end of file diff --git a/test/integration/stub_responses/invoice.xml b/test/integration/stub_responses/invoice.xml new file mode 100644 index 00000000..8342dc57 --- /dev/null +++ b/test/integration/stub_responses/invoice.xml @@ -0,0 +1 @@ +a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999OKYOUR PROVIDER2008-10-09T00:59:11.1341229ZACCRECa99a9aaa-9999-99a9-9aa9-aaaaaa9a9999a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
2008-10-03T00:00:002008-10-20T00:00:00INV-0001true1000.0000125.00001125.0000AUTHORISEDe5a8a4ee-85ee-4532-a79f-a552ec63a0d7A LINE ITEM100.012.34OUTPUT125.00001125.0000Region
\ No newline at end of file diff --git a/test/integration/stub_responses/invoices.xml b/test/integration/stub_responses/invoices.xml new file mode 100644 index 00000000..73325fc0 --- /dev/null +++ b/test/integration/stub_responses/invoices.xml @@ -0,0 +1 @@ +a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999OKYOUR PROVIDER2008-10-09T00:59:11.1341229ZACCRECa99a9aaa-9999-99a9-9aa9-aaaaaa9a9999a99a9aaa-9999-99a9-9aa9-aaaaaa9a9999ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
2008-10-03T00:00:002008-10-20T00:00:00INV-0001true1000.0000125.00001125.0000AUTHORISEDe5a8a4ee-85ee-4532-a79f-a552ec63a0d7A LINE ITEM100.012.34OUTPUT125.00001125.0000Region
ACCRECa11a1aaa-1111-11a1-1aa1-aaaaaa1a1111a11a1aaa-1111-11a1-1aa1-aaaaaa1a1111ACTIVECONTACT NAMEbob@example.com
POBOXLINE 1 OF THE ADDRESSSomewhereSome Country
MOBILE1234567123DEFAULTFAXDDI
2008-10-03T00:00:002008-10-20T00:00:00INV-0001true1000.0000125.00001125.0000AUTHORISEDe5a8a4ee-85ee-4532-a79f-a552ec63a0d7A LINE ITEM100.012.34OUTPUT125.00001125.0000Region
\ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..a8a51519 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +require "rubygems" + +require 'test/unit' +require 'mocha' + +require 'libxml' + +require File.dirname(__FILE__) + '/../lib/xero_gateway.rb' diff --git a/test/unit/messages/contact_message_test.rb b/test/unit/messages/contact_message_test.rb new file mode 100644 index 00000000..57215b96 --- /dev/null +++ b/test/unit/messages/contact_message_test.rb @@ -0,0 +1,66 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +require File.join(File.dirname(__FILE__), '../../test_helper.rb') + +class ContactMessageTest < Test::Unit::TestCase + def setup + @schema = LibXML::XML::Schema.document(LibXML::XML::Document.file(File.join(File.dirname(__FILE__), '../../xsd/create_contact.xsd'))) + end + + # Tests that the XML generated from a contact object validates against the Xero XSD + def test_build_xml + contact = create_test_contact + + message = XeroGateway::Messages::ContactMessage.build_xml(contact) + + # Check that the document matches the XSD + assert LibXML::XML::Parser.string(message).parse.validate_schema(@schema), "The XML document generated did not validate against the XSD" + end + + # Tests that a contact can be converted into XML that Xero can understand, and then converted back to a contact + def test_build_and_parse_xml + contact = create_test_contact + + # Generate the XML message + contact_as_xml = XeroGateway::Messages::ContactMessage.build_xml(contact) + + # Parse the XML message and retrieve the contact element + contact_element = REXML::XPath.first(REXML::Document.new(contact_as_xml), "/Contact") + + # Build a new contact from the XML + result_contact = XeroGateway::Messages::ContactMessage.from_xml(contact_element) + + # Check the contact details + assert_equal contact, result_contact + end + + + private + + def create_test_contact + contact = XeroGateway::Contact.new(:id => "55555") + contact.contact_number = "aaa111" + contact.name = "CONTACT NAME" + contact.email = "someone@somewhere.com" + contact.address.address_type = "THE ADDRESS TYPE FOR THE CONTACT" + contact.address.line_1 = "LINE 1 OF THE ADDRESS" + contact.address.line_2 = "LINE 2 OF THE ADDRESS" + contact.address.line_3 = "LINE 3 OF THE ADDRESS" + contact.address.line_4 = "LINE 4 OF THE ADDRESS" + contact.phone.number = "12345" + + contact + end +end \ No newline at end of file diff --git a/test/unit/messages/invoice_message_test.rb b/test/unit/messages/invoice_message_test.rb new file mode 100644 index 00000000..a7f06a9b --- /dev/null +++ b/test/unit/messages/invoice_message_test.rb @@ -0,0 +1,76 @@ +# Copyright (c) 2008 Tim Connor +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +require File.join(File.dirname(__FILE__), '../../test_helper.rb') + +class InvoiceMessageTest < Test::Unit::TestCase + def setup + @schema = LibXML::XML::Schema.document(LibXML::XML::Document.file(File.join(File.dirname(__FILE__), '../../xsd/create_invoice.xsd'))) + end + + # Tests that the XML generated from an invoice object validates against the Xero XSD + def test_build_xml + invoice = create_test_invoice + + message = XeroGateway::Messages::InvoiceMessage.build_xml(invoice) + + # Check that the document matches the XSD + assert LibXML::XML::Parser.string(message).parse.validate_schema(@schema), "The XML document generated did not validate against the XSD" + end + + # Tests that an invoice can be converted into XML that Xero can understand, and then converted back to an invoice + def test_build_and_parse_xml + invoice = create_test_invoice + + # Generate the XML message + invoice_as_xml = XeroGateway::Messages::InvoiceMessage.build_xml(invoice) + + # Parse the XML message and retrieve the invoice element + invoice_element = REXML::XPath.first(REXML::Document.new(invoice_as_xml), "/Invoice") + + # Build a new invoice from the XML + result_invoice = XeroGateway::Messages::InvoiceMessage.from_xml(invoice_element) + + assert_equal(invoice, result_invoice) + end + + + private + + def create_test_invoice + invoice = XeroGateway::Invoice.new(:invoice_type => "THE INVOICE TYPE") + invoice.date = Time.now + invoice.due_date = Time.now + 10 + invoice.invoice_number = "12345" + invoice.reference = "MY REFERENCE FOR THIS INVOICE" + invoice.includes_tax = false + invoice.sub_total = BigDecimal.new("1000") + invoice.total_tax = BigDecimal.new("125") + invoice.total = BigDecimal.new("1125") + + invoice.contact = XeroGateway::Contact.new(:id => 55555) + invoice.contact.name = "CONTACT NAME" + invoice.contact.address.address_type = "THE ADDRESS TYPE FOR THE CONTACT" + invoice.contact.address.line_1 = "LINE 1 OF THE ADDRESS" + invoice.contact.phone.number = "12345" + + invoice.line_items << XeroGateway::LineItem.new({ + :description => "A LINE ITEM", + :unit_amount => BigDecimal.new("100"), + :tax_amount => BigDecimal.new("12.5"), + :line_amount => BigDecimal.new("125") + }) + invoice + end +end \ No newline at end of file diff --git a/test/xsd/README b/test/xsd/README new file mode 100644 index 00000000..3484c4f3 --- /dev/null +++ b/test/xsd/README @@ -0,0 +1,2 @@ +These XML schemas are taken directly from https://network.xero.com/Help/Xero%20API%20Reference%201.0.htm +and all XML generated to be sent to Xero should match these schemas \ No newline at end of file diff --git a/test/xsd/create_contact.xsd b/test/xsd/create_contact.xsd new file mode 100644 index 00000000..05f1d47a --- /dev/null +++ b/test/xsd/create_contact.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/xsd/create_invoice.xsd b/test/xsd/create_invoice.xsd new file mode 100644 index 00000000..d32ae52f --- /dev/null +++ b/test/xsd/create_invoice.xsd @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/xero_gateway.gemspec b/xero_gateway.gemspec new file mode 100644 index 00000000..e44db1e0 --- /dev/null +++ b/xero_gateway.gemspec @@ -0,0 +1,39 @@ +Gem::Specification.new do |s| + s.name = "xero-gateway" + s.version = "1.0.0" + s.date = "2008-11-27" + s.summary = "Enable ruby based applications to communicate with the Xero API" + s.email = "tlconnor@gmail.com" + s.homepage = "http://github.com/tlconnor/xero-gateway" + s.description = "Enable ruby based applications to communicate with the Xero API" + s.has_rdoc = false + s.authors = ["Tim Connor"] + s.files = ["README.textile", + "LICENSE", + "Rakefile", + "lib/xero_gateway/address.rb", + "lib/xero_gateway/contact.rb", + "lib/xero_gateway/dates.rb", + "lib/xero_gateway/gateway.rb", + "lib/xero_gateway/http.rb", + "lib/xero_gateway/invoice.rb", + "lib/xero_gateway/line_item.rb", + "lib/xero_gateway/messages/contact_message.rb", + "lib/xero_gateway/messages/invoice_message.rb", + "lib/xero_gateway/money.rb", + "lib/xero_gateway/phone.rb", + "lib/xero_gateway/response.rb", + "lib/xero_gateway.rb", + "test/test_helper.rb", + "test/unit/messages/contact_message_test.rb", + "test/unit/messages/invoice_message_test.rb", + "test/integration/gateway_test.rb", + "test/integration/stub_responses/contact.xml", + "test/integration/stub_responses/contacts.xml", + "test/integration/stub_responses/invoice.xml", + "test/integration/stub_responses/invoices.xml", + "test/xsd/README", + "test/xsd/create_contact.xsd", + "test/xsd/create_invoice.xsd", + "xero_gateway.gemspec"] +end