require 'net/http' require 'net/https' require 'hpricot' require 'ostruct' module FEDEX class FedExError < StandardError end class Client def initialize(options) @prefs = options @logger = RAILS_DEFAULT_LOGGER end def rate_list(to_zip, weight, boxes, home_delivery = false) response = self.rate_request(@prefs[:origin_zip], to_zip, weight, boxes, home_delivery) Hpricot(response).search('entry').collect do |service| OpenStruct.new( #:label => Services[service.at("service").inner_html], :service_code => service.at("service").inner_html, :cost => service.at("estimatedcharges/discountedcharges/netcharge").inner_html.to_f ) #|| service.at("transportationcharges/monetaryvalue").inner_html.to_f) end.sort_by(&:cost) end Services = { "priority" => "PRIORITYOVERNIGHT", "2day" => "FEDEX2DAY", "standard_overnight" => "STANDARDOVERNIGHT", "first_overnight" => "FIRSTOVERNIGHT", "express_saver" => "FEDEXEXPRESSSAVER", "1day_freight" => "FEDEX1DAYFREIGHT", "2day_freight" => "FEDEX2DAYFREIGHT", "3day_freight" => "FEDEX3DAYFREIGHT", "international_priority" => "INTERNATIONALPRIORITY", "international_economy" => "INTERNATIONALECONOMY", "international_first" => "INTERNATIONALFIRST", "international_priority_freight" => "INTERNATIONALPRIORITYFREIGHT", "international_economy_freight" => "INTERNATIONALECONOMYFREIGHT", "home_delivery" => "GROUNDHOMEDELIVERY", "ground_service" => "FEDEXGROUND", "international_ground_service" => "INTERNATIONALGROUND" } protected def send_request(data) uri = URI.parse(@prefs[:url]) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE @logger.info "Sending to FedEx:\n #{data}" if @prefs[:debug] body = http.post(uri.request_uri, data).body @logger.info "Received from FedEx:\n #{body}" if @prefs[:debug] if @error = Hpricot(body).at('error').inner_html rescue nil raise FedExError, @error end body end def rate_request(from_zip, destination, weight, boxes, home_delivery = false) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.FDXRateAvailableServicesRequest('xmlns:api' => 'http://www.fedex.com/fsmapi', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation' => 'FDXRateAvailableServicesRequest.xsd') do xml.RequestHeader do xml.AccountNumber @prefs[:account] xml.MeterNumber @prefs[:meter] #xml.CarrierCode 'ANY' end #xml.Service 'FEDEX2DAY' #xml.ShipDate '2008-10-30' xml.ReturnShipmentIndicator 'NONRETURN' xml.DropoffType 'REGULARPICKUP' xml.Packaging 'YOURPACKAGING' xml.WeightUnits 'LBS' xml.Weight weight xml.ListRate true #tells fedex to return list rates as well as discounted rates xml.OriginAddress do xml.StateOrProvinceCode 'CA' xml.PostalCode from_zip xml.CountryCode 'US' end if home_delivery == true xml.HomeDelivery do xml.Type 'APPOINTMENT' end end xml.DestinationAddress do xml.StateOrProvinceCode self.state_from_zip(destination) xml.PostalCode destination xml.CountryCode "US" end xml.Payment do xml.PayorType 'SENDER' end xml.PackageCount boxes end self.send_request(xml.target!) end def state_from_zip(zip) zip = zip.to_i { (99500...99929) => "AK", (35000...36999) => "AL", (71600...72999) => "AR", (75502...75505) => "AR", (85000...86599) => "AZ", (90000...96199) => "CA", (80000...81699) => "CO", (6000...6999) => "CT", (20000...20099) => "DC", (20200...20599) => "DC", (19700...19999) => "DE", (32000...33999) => "FL", (34100...34999) => "FL", (30000...31999) => "GA", (96700...96798) => "HI", (96800...96899) => "HI", (50000...52999) => "IA", (83200...83899) => "ID", (60000...62999) => "IL", (46000...47999) => "IN", (66000...67999) => "KS", (40000...42799) => "KY", (45275...45275) => "KY", (70000...71499) => "LA", (71749...71749) => "LA", (1000...2799) => "MA", (20331...20331) => "MD", (20600...21999) => "MD", (3801...3801) => "ME", (3804...3804) => "ME", (3900...4999) => "ME", (48000...49999) => "MI", (55000...56799) => "MN", (63000...65899) => "MO", (38600...39799) => "MS", (59000...59999) => "MT", (27000...28999) => "NC", (58000...58899) => "ND", (68000...69399) => "NE", (3000...3803) => "NH", (3809...3899) => "NH", (7000...8999) => "NJ", (87000...88499) => "NM", (89000...89899) => "NV", (400...599) => "NY", (6390...6390) => "NY", (9000...14999) => "NY", (43000...45999) => "OH", (73000...73199) => "OK", (73400...74999) => "OK", (97000...97999) => "OR", (15000...19699) => "PA", (2800...2999) => "RI", (6379...6379) => "RI", (29000...29999) => "SC", (57000...57799) => "SD", (37000...38599) => "TN", (72395...72395) => "TN", (73300...73399) => "TX", (73949...73949) => "TX", (75000...79999) => "TX", (88501...88599) => "TX", (84000...84799) => "UT", (20105...20199) => "VA", (20301...20301) => "VA", (20370...20370) => "VA", (22000...24699) => "VA", (5000...5999) => "VT", (98000...99499) => "WA", (49936...49936) => "WI", (53000...54999) => "WI", (24700...26899) => "WV", (82000...83199) => "WY" }.each do |range, state| return state if range.include? zip end raise ShippingError, "Invalid zip code" end end end