module ActiveMerchant module Fulfillment class WebgistixService < Service SERVICE_URLS = { :fulfillment => 'https://www.webgistix.com/XML/CreateOrder.asp', :inventory => 'https://www.webgistix.com/XML/GetInventory.asp', :tracking => 'https://www.webgistix.com/XML/GetTracking.asp' } TEST_URLS = SERVICE_URLS.merge({ :fulfillment => 'https://www.webgistix.com/XML/CreateOrderTest.asp' }) SUCCESS, DUPLICATE, FAILURE = 'True', 'Duplicate', 'False' SUCCESS_MESSAGE = 'Successfully submitted the order' FAILURE_MESSAGE = 'Failed to submit the order' DUPLICATE_MESSAGE = 'This order has already been successfully submitted' INVALID_LOGIN = 'Invalid Credentials' NOT_SHIPPED = 'Not Shipped' TRACKING_COMPANIES = %w(UPS FedEx USPS) # If a request is detected as a duplicate only the original data will be # used by Webgistix, and the subsequent responses will have a # :duplicate parameter set in the params hash. self.retry_safe = true # The first is the label, and the last is the code def self.shipping_methods [ ["UPS Ground Shipping", "Ground"], ["UPS Ground", "Ground"], ["UPS Standard Shipping (Canada Only)", "Standard"], ["UPS Standard Shipping (CA & MX Only)", "Standard"], ["UPS 3-Business Day", "3-Day Select"], ["UPS 2-Business Day", "2nd Day Air"], ["UPS 2-Business Day AM", "2nd Day Air AM"], ["UPS Next Day", "Next Day Air"], ["UPS Next Day Saver", "Next Day Air Saver"], ["UPS Next Day Early AM", "Next Day Air Early AM"], ["UPS Worldwide Express (Next Day)", "Worldwide Express"], ["UPS Worldwide Expedited (2nd Day)", "Worldwide Expedited"], ["UPS Worldwide Express Saver", "Worldwide Express Saver"], ["FedEx Priority Overnight", "FedEx Priority Overnight"], ["FedEx Standard Overnight", "FedEx Standard Overnight"], ["FedEx First Overnight", "FedEx First Overnight"], ["FedEx 2nd Day", "FedEx 2nd Day"], ["FedEx Express Saver", "FedEx Express Saver"], ["FedEx International Priority", "FedEx International Priority"], ["FedEx International Economy", "FedEx International Economy"], ["FedEx International First", "FedEx International First"], ["FedEx Ground", "FedEx Ground"], ["USPS Priority Mail", "Priority Mail"], ["USPS Priority Mail International", "Priority Mail International"], ["USPS Priority Mail Small Flat Rate Box", "Priority Mail Small Flat Rate Box"], ["USPS Priority Mail Medium Flat Rate Box", "Priority Mail Medium Flat Rate Box"], ["USPS Priority Mail Large Flat Rate Box", "Priority Mail Large Flat Rate Box"], ["USPS Priority Mail Flat Rate Envelope", "Priority Mail Flat Rate Envelope"], ["USPS First Class Mail", "First Class"], ["USPS First Class International", "First Class International"], ["USPS Express Mail", "Express"], ["USPS Express Mail International", "Express Mail International"], ["USPS Parcel Post", "Parcel"], ["USPS Media Mail", "Media Mail"] ].inject(ActiveSupport::OrderedHash.new){|h, (k,v)| h[k] = v; h} end # Pass in the login and password for the shipwire account. # Optionally pass in the :test => true to force test mode def initialize(options = {}) requires!(options, :login, :password) super end def fulfill(order_id, shipping_address, line_items, options = {}) requires!(options, :shipping_method) commit :fulfillment, build_fulfillment_request(order_id, shipping_address, line_items, options) end def fetch_stock_levels(options = {}) commit :inventory, build_inventory_request(options) end def fetch_tracking_data(order_ids, options = {}) commit :tracking, build_tracking_request(order_ids, options) end def valid_credentials? response = fulfill('', {}, [], :shipping_method => '') response.message != INVALID_LOGIN end def test_mode? true end private # # # Webgistix # 3 # # # Test Company # Joe Smith # 123 Main St. # # # Olean # NY # 14760 # United States # info@webgistix.com # 1-123-456-7890 # Ground # Test Order # 0 # # testitem # 2 # # # def build_fulfillment_request(order_id, shipping_address, line_items, options) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! 'OrderXML' do add_credentials(xml) add_order(xml, order_id, shipping_address, line_items, options) end xml.target! end # # # Webgistix # 3 # def build_inventory_request(options) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! 'InventoryXML' do add_credentials(xml) end end # # # Webgistix # 3 # # AB12345 # # # XY4567 # # def build_tracking_request(order_ids, options) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! 'TrackingXML' do add_credentials(xml) order_ids.each do |o_id| xml.tag! 'Tracking' do xml.tag! 'Order', o_id end end end end def add_credentials(xml) xml.tag! 'CustomerID', @options[:login] xml.tag! 'Password', @options[:password] end def add_order(xml, order_id, shipping_address, line_items, options) xml.tag! 'Order' do xml.tag! 'ReferenceNumber', order_id xml.tag! 'ShippingInstructions', options[:shipping_method] xml.tag! 'Approve', 1 xml.tag! 'OrderComments', options[:comment] unless options[:comment].blank? add_address(xml, shipping_address, options) Array(line_items).each_with_index do |line_item, index| add_item(xml, line_item, index) end end end def add_address(xml, address, options) xml.tag! 'Name', address[:name] xml.tag! 'Address1', address[:address1] xml.tag! 'Address2', address[:address2] unless address[:address2].blank? xml.tag! 'Address3', address[:address3] unless address[:address3].blank? xml.tag! 'City', address[:city] xml.tag! 'State', address[:state] xml.tag! 'ZipCode', address[:zip] xml.tag! 'Company', address[:company] unless address[:country].blank? country = Country.find(address[:country]) xml.tag! 'Country', country.name end xml.tag! 'Phone', address[:phone] xml.tag! 'Email', options[:email] unless options[:email].blank? end def add_item(xml, item, index) xml.tag! 'Item' do xml.tag! 'ItemID', item[:sku] unless item[:sku].blank? xml.tag! 'ItemQty', item[:quantity] unless item[:quantity].blank? end end def commit(action, request) url = test? ? TEST_URLS[action] : SERVICE_URLS[action] data = ssl_post(url, request, 'EndPointURL' => url, 'Content-Type' => 'text/xml; charset="utf-8"' ) response = parse_response(action, data) Response.new(success?(response), message_from(response), response, :test => test?) end def success?(response) response[:success] == SUCCESS || response[:success] == DUPLICATE end def message_from(response) if response[:duplicate] DUPLICATE_MESSAGE elsif success?(response) SUCCESS_MESSAGE elsif response[:error_0] == INVALID_LOGIN INVALID_LOGIN else FAILURE_MESSAGE end end def parse_response(action, xml) begin document = REXML::Document.new("#{xml}") rescue REXML::ParseException return {:success => FAILURE} end case action when :fulfillment parse_fulfillment_response(document) when :inventory parse_inventory_response(document) when :tracking parse_tracking_response(document) else raise ArgumentError, "Unknown action #{action}" end end def parse_fulfillment_response(document) response = parse_errors(document) # Check if completed if completed = REXML::XPath.first(document, '//Completed') completed.elements.each do |e| response[e.name.underscore.to_sym] = e.text end else response[:success] = FAILURE end response[:duplicate] = response[:success] == DUPLICATE response end def parse_inventory_response(document) response = parse_errors(document) response[:stock_levels] = {} document.root.each_element('//Item') do |node| # {ItemID => 'SOME-ID', ItemQty => '101'} params = node.elements.to_a.each_with_object({}) {|elem, hash| hash[elem.name] = elem.text} response[:stock_levels][params['ItemID']] = params['ItemQty'].to_i end response end def parse_tracking_response(document) response = parse_errors(document) response[:tracking_numbers] = {} response[:tracking_companies] = {} response[:tracking_urls] = {} document.root.each_element('//Shipment') do |node| # {InvoiceNumber => 'SOME-ID', ShipmentTrackingNumber => 'SOME-TRACKING-NUMBER'} params = node.elements.to_a.each_with_object({}) {|elem, hash| hash[elem.name] = elem.text} tracking = params['ShipmentTrackingNumber'] unless tracking == NOT_SHIPPED response[:tracking_numbers][params['InvoiceNumber']] ||= [] response[:tracking_numbers][params['InvoiceNumber']] << tracking end company = params['Method'].split[0] if params['Method'] if TRACKING_COMPANIES.include? company response[:tracking_companies][params['InvoiceNumber']] ||= [] response[:tracking_companies][params['InvoiceNumber']] << company end end response end def parse_errors(document) response = {} REXML::XPath.match(document, "//Errors/Error").to_a.each_with_index do |e, i| response["error_#{i}".to_sym] = e.text end response[:success] = response.empty? ? SUCCESS : FAILURE response end end end end