Class: ActiveShipping::USPS
- Defined in:
- lib/active_shipping/carriers/usps.rb
Overview
After getting an API login from USPS (looks like '123YOURNAME456'), run the following test:
usps = USPS.new(:login => '123YOURNAME456', :test => true) usps.valid_credentials?
This will send a test request to the USPS test servers, which they ask you to do before they put your API key in production mode.
Defined Under Namespace
Classes: EventDetails
Constant Summary collapse
- ONLY_PREFIX_EVENTS =
['DELIVERED','OUT FOR DELIVERY']
- LIVE_DOMAIN =
'production.shippingapis.com'
- LIVE_RESOURCE =
'ShippingAPI.dll'
- TEST_DOMAINS =
indexed by security; e.g. TEST_DOMAINS[USE_SSL[:rates]]
{ # indexed by security; e.g. TEST_DOMAINS[USE_SSL[:rates]] true => 'secure.shippingapis.com', false => 'stg-production.shippingapis.com' }
- API_CODES =
{ :us_rates => 'RateV4', :world_rates => 'IntlRateV2', :test => 'CarrierPickupAvailability', :track => 'TrackV2' }
- USE_SSL =
{ :us_rates => false, :world_rates => false, :test => true, :track => false }
- CONTAINERS =
{ rectangular: 'RECTANGULAR', variable: 'VARIABLE', box: 'FLAT RATE BOX', box_large: 'LG FLAT RATE BOX', box_medium: 'MD FLAT RATE BOX', box_small: 'SM FLAT RATE BOX', envelope: 'FLAT RATE ENVELOPE', envelope_legal: 'LEGAL FLAT RATE ENVELOPE', envelope_padded: 'PADDED FLAT RATE ENVELOPE', envelope_gift_card: 'GIFT CARD FLAT RATE ENVELOPE', envelope_window: 'WINDOW FLAT RATE ENVELOPE', envelope_small: 'SM FLAT RATE ENVELOPE', package_service: 'PACKAGE SERVICE' }
- MAIL_TYPES =
{ :package => 'Package', :postcard => 'Postcards or aerogrammes', :matter_for_the_blind => 'Matter for the blind', :envelope => 'Envelope' }
- PACKAGE_PROPERTIES =
{ 'ZipOrigination' => :origin_zip, 'ZipDestination' => :destination_zip, 'Pounds' => :pounds, 'Ounces' => :ounces, 'Container' => :container, 'Size' => :size, 'Machinable' => :machinable, 'Zone' => :zone, 'Postage' => :postage, 'Restrictions' => :restrictions }
- POSTAGE_PROPERTIES =
{ 'MailService' => :service, 'Rate' => :rate }
- US_SERVICES =
{ :first_class => 'FIRST CLASS', :priority => 'PRIORITY', :express => 'EXPRESS', :bpm => 'BPM', :parcel => 'PARCEL', :media => 'MEDIA', :library => 'LIBRARY', :online => 'ONLINE', :plus => 'PLUS', :all => 'ALL' }
- DEFAULT_SERVICE =
Hash.new(:all).update( :base => :online, :plus => :plus )
- DOMESTIC_RATE_FIELD =
Hash.new('Rate').update( :base => 'CommercialRate', :plus => 'CommercialPlusRate' )
- INTERNATIONAL_RATE_FIELD =
Hash.new('Postage').update( :base => 'CommercialPostage', :plus => 'CommercialPlusPostage' )
- COMMERCIAL_FLAG_NAME =
{ :base => 'CommercialFlag', :plus => 'CommercialPlusFlag' }
- FIRST_CLASS_MAIL_TYPES =
{ :letter => 'LETTER', :flat => 'FLAT', :parcel => 'PARCEL', :post_card => 'POSTCARD', :package_service => 'PACKAGESERVICE' }
- ATTEMPTED_DELIVERY_CODES =
%w(02 53 54 55 56 H0)
- US_POSSESSIONS =
Array of U.S. possessions according to USPS: https://www.usps.com/ship/official-abbreviations.htm
%w(AS FM GU MH MP PW PR VI)
- COUNTRY_NAME_CONVERSIONS =
TODO: figure out how USPS likes to say "Ivory Coast"
Country names: http://pe.usps.gov/text/Imm/immctry.htm
{ "BA" => "Bosnia-Herzegovina", "CD" => "Congo, Democratic Republic of the", "CG" => "Congo (Brazzaville),Republic of the", "CI" => "Côte d'Ivoire (Ivory Coast)", "CK" => "Cook Islands (New Zealand)", "FK" => "Falkland Islands", "GB" => "Great Britain and Northern Ireland", "GE" => "Georgia, Republic of", "IR" => "Iran", "KN" => "Saint Kitts (St. Christopher and Nevis)", "KP" => "North Korea (Korea, Democratic People's Republic of)", "KR" => "South Korea (Korea, Republic of)", "LA" => "Laos", "LY" => "Libya", "MC" => "Monaco (France)", "MD" => "Moldova", "MK" => "Macedonia, Republic of", "MM" => "Burma", "PN" => "Pitcairn Island", "RU" => "Russia", "SK" => "Slovak Republic", "TK" => "Tokelau (Union) Group (Western Samoa)", "TW" => "Taiwan", "TZ" => "Tanzania", "VA" => "Vatican City", "VG" => "British Virgin Islands", "VN" => "Vietnam", "WF" => "Wallis and Futuna Islands", "WS" => "Western Samoa" }
- TRACKING_ODD_COUNTRY_NAMES =
{ 'TAIWAN' => 'TW', 'MACEDONIA THE FORMER YUGOSLAV REPUBLIC OF'=> 'MK', 'MICRONESIA FEDERATED STATES OF' => 'FM', 'MOLDOVA REPUBLIC OF' => 'MD', }
- RESPONSE_ERROR_MESSAGES =
[ /There is no record of that mail item/, /This Information has not been included in this Test Server\./, /Delivery status information is not available/ ]
- ESCAPING_AND_SYMBOLS =
/<\S*>/
- LEADING_USPS =
/^USPS /
- TRAILING_ASTERISKS =
/\*+$/
- SERVICE_NAME_SUBSTITUTIONS =
/#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}|#{TRAILING_ASTERISKS}/
- @@name =
"USPS"
Instance Attribute Summary
Attributes inherited from Carrier
Class Method Summary collapse
Instance Method Summary collapse
- #batch_find_tracking_info(tracking_infos, options = {}) ⇒ Object
- #build_tracking_batch_request(tracking_infos, options) ⇒ Object protected
- #build_tracking_request(tracking_number, options = {}) ⇒ Object protected
-
#build_us_rate_request(packages, origin_zip, destination_zip, options = {}) ⇒ Object
protected
options[:service] -- One of [:first_class, :priority, :express, :bpm, :parcel, :media, :library, :online, :plus, :all].
-
#build_world_rate_request(origin, packages, destination, options) ⇒ Object
protected
important difference with international rate requests: * services are not given in the request * package sizes are not given in the request * services are returned in the response along with restrictions of size * the size restrictions are returned AS AN ENGLISH SENTENCE (!?).
-
#canned_address_verification_works? ⇒ Boolean
protected
Once the address verification API is implemented, remove this and have valid_credentials? build the request using that instead.
- #commit(action, request, test = false) ⇒ Object protected
- #error_description_node(node) ⇒ Object protected
- #extract_event_details(node) ⇒ Object
- #find_country_code_case_insensitive(name) ⇒ Object protected
- #find_rates(origin, destination, packages, options = {}) ⇒ Object
- #find_tracking_info(tracking_number, options = {}) ⇒ Object
- #has_error?(node) ⇒ Boolean protected
- #maximum_address_field_length ⇒ Object
- #maximum_weight ⇒ Object
- #package_valid_for_max_dimensions(package, dimensions) ⇒ Object protected
- #package_valid_for_service(package, service_node) ⇒ Object protected
- #parse_rate_response(origin, destination, packages, response, options = {}) ⇒ Object protected
- #parse_tracking_info(response, node) ⇒ Object protected
- #parse_tracking_response(response, options = {}) ⇒ Object protected
- #rates_from_response_node(response_node, packages, options = {}) ⇒ Object protected
- #request_url(action, request, test) ⇒ Object protected
- #requirements ⇒ Object
- #response_message(document) ⇒ Object protected
- #response_status_node(node) ⇒ Object protected
- #strip_zip(zip) ⇒ Object protected
- #us_rates(origin, destination, packages, options = {}) ⇒ Object protected
- #valid_credentials? ⇒ Boolean
- #world_rates(origin, destination, packages, options = {}) ⇒ Object protected
Methods inherited from Carrier
#cancel_shipment, #create_shipment, default_location, #initialize, #save_request, #timestamp_from_business_day
Constructor Details
This class inherits a constructor from ActiveShipping::Carrier
Class Method Details
.package_machinable?(package, options = {}) ⇒ Boolean
from info at http://www.usps.com/businessmail101/mailcharacteristics/parcels.htm
package.options[:books] -- 25 lb. limit instead of 35 for books or other printed matter. Defaults to false.
201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/active_shipping/carriers/usps.rb', line 201 def self.package_machinable?(package, = {}) at_least_minimum = package.inches(:length) >= 6.0 && package.inches(:width) >= 3.0 && package.inches(:height) >= 0.25 && package.ounces >= 6.0 at_most_maximum = package.inches(:length) <= 34.0 && package.inches(:width) <= 17.0 && package.inches(:height) <= 17.0 && package.pounds <= (package.[:books] ? 25.0 : 35.0) at_least_minimum && at_most_maximum end |
.size_code_for(package) ⇒ Object
189 190 191 192 193 194 195 |
# File 'lib/active_shipping/carriers/usps.rb', line 189 def self.size_code_for(package) if package.inches(:max) <= 12 'REGULAR' else 'LARGE' end end |
Instance Method Details
#batch_find_tracking_info(tracking_infos, options = {}) ⇒ Object
182 183 184 185 186 187 |
# File 'lib/active_shipping/carriers/usps.rb', line 182 def batch_find_tracking_info(tracking_infos, = {}) = @options.update() tracking_request = build_tracking_batch_request(tracking_infos, ) response = commit(:track, tracking_request, [:test] || false) parse_tracking_response(response, fault_tolerant: true) end |
#build_tracking_batch_request(tracking_infos, options) ⇒ Object (protected)
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/active_shipping/carriers/usps.rb', line 287 def build_tracking_batch_request(tracking_infos, ) xml_builder = Nokogiri::XML::Builder.new do |xml| xml.TrackFieldRequest('USERID' => [:login]) do xml.Revision { xml.text('1') } xml.ClientIp { xml.text([:client_ip] || '127.0.0.1') } xml.SourceId { xml.text([:source_id] || 'active_shipping') } tracking_infos.each do |info| xml.TrackID('ID' => info[:number]) do xml.DestinationZipCode { xml.text(strip_zip(info[:destination_zip]))} if info[:destination_zip] if info[:mailing_date] formatted_date = info[:mailing_date].strftime('%Y-%m-%d') xml.MailingDate { xml.text(formatted_date)} end end end end end xml_builder.to_xml end |
#build_tracking_request(tracking_number, options = {}) ⇒ Object (protected)
279 280 281 282 283 284 285 |
# File 'lib/active_shipping/carriers/usps.rb', line 279 def build_tracking_request(tracking_number, = {}) build_tracking_batch_request([{ number: tracking_number, destination_zip: [:destination_zip], mailing_date: [:mailing_date] }], ) end |
#build_us_rate_request(packages, origin_zip, destination_zip, options = {}) ⇒ Object (protected)
options[:service] -- One of [:first_class, :priority, :express, :bpm, :parcel, :media, :library, :online, :plus, :all]. defaults to :all. options[:books] -- Either true or false. Packages of books or other printed matter have a lower weight limit to be considered machinable. package.options[:container] -- Can be :rectangular, :variable, or a flat rate container defined in CONTAINERS. package.options[:machinable] -- Either true or false. Overrides the detection of "machinability" entirely.
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
# File 'lib/active_shipping/carriers/usps.rb', line 347 def build_us_rate_request(packages, origin_zip, destination_zip, = {}) xml_builder = Nokogiri::XML::Builder.new do |xml| xml.RateV4Request('USERID' => @options[:login]) do Array(packages).each_with_index do |package, id| xml.Package('ID' => id) do commercial_type = commercial_type() default_service = DEFAULT_SERVICE[commercial_type] service = .fetch(:service, default_service).to_sym if commercial_type && service != default_service raise ArgumentError, "Commercial #{commercial_type} rates are only provided with the #{default_service.inspect} service." end xml.Service(US_SERVICES[service]) xml.FirstClassMailType(FIRST_CLASS_MAIL_TYPES[[:first_class_mail_type].try(:to_sym)]) xml.ZipOrigination(strip_zip(origin_zip)) xml.ZipDestination(strip_zip(destination_zip)) xml.Pounds(0) xml.Ounces("%0.1f" % [package.ounces, 1].max) size_code = USPS.size_code_for(package) container = CONTAINERS[package.[:container]] container ||= (package.cylinder? ? 'NONRECTANGULAR' : 'RECTANGULAR') if size_code == 'LARGE' xml.Container(container) xml.Size(size_code) xml.Width("%0.2f" % package.inches(:width)) xml.Length("%0.2f" % package.inches(:length)) xml.Height("%0.2f" % package.inches(:height)) xml.Girth("%0.2f" % package.inches(:girth)) is_machinable = if package..has_key?(:machinable) package.[:machinable] ? true : false else USPS.package_machinable?(package) end xml.Machinable(is_machinable.to_s.upcase) end end end end save_request(xml_builder.to_xml) end |
#build_world_rate_request(origin, packages, destination, options) ⇒ Object (protected)
important difference with international rate requests:
- services are not given in the request
- package sizes are not given in the request
- services are returned in the response along with restrictions of size
- the size restrictions are returned AS AN ENGLISH SENTENCE (!?)
package.options[:mail_type] -- one of [:package, :postcard, :matter_for_the_blind, :envelope]. Defaults to :package.
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/active_shipping/carriers/usps.rb', line 397 def build_world_rate_request(origin, packages, destination, ) country = COUNTRY_NAME_CONVERSIONS[destination.country.code(:alpha2).value] || destination.country.name xml_builder = Nokogiri::XML::Builder.new do |xml| xml.IntlRateV2Request('USERID' => @options[:login]) do xml.Revision(2) Array(packages).each_with_index do |package, id| xml.Package('ID' => id) do xml.Pounds(0) xml.Ounces([package.ounces, 1].max.ceil) # takes an integer for some reason, must be rounded UP xml.MailType(MAIL_TYPES[package.[:mail_type]] || 'Package') xml.GXG do xml.POBoxFlag(destination.po_box? ? 'Y' : 'N') xml.GiftFlag(package.gift? ? 'Y' : 'N') end value = if package.value && package.value > 0 && package.currency && package.currency != 'USD' 0.0 else (package.value || 0) / 100.0 end xml.ValueOfContents(value) xml.Country(country) xml.Container(package.cylinder? ? 'NONRECTANGULAR' : 'RECTANGULAR') xml.Size(USPS.size_code_for(package)) xml.Width("%0.2f" % [package.inches(:width), 0.01].max) xml.Length("%0.2f" % [package.inches(:length), 0.01].max) xml.Height("%0.2f" % [package.inches(:height), 0.01].max) xml.Girth("%0.2f" % [package.inches(:girth), 0.01].max) xml.OriginZip(origin.zip) if commercial_type = commercial_type() xml.public_send(COMMERCIAL_FLAG_NAME.fetch(commercial_type), 'Y') end if destination.zip.present? xml.AcceptanceDateTime(([:acceptance_time] || Time.now.utc).iso8601) xml.DestinationPostalCode(destination.zip) end end end end end save_request(xml_builder.to_xml) end |
#canned_address_verification_works? ⇒ Boolean (protected)
Once the address verification API is implemented, remove this and have valid_credentials? build the request using that instead.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/active_shipping/carriers/usps.rb', line 320 def canned_address_verification_works? return false unless @options[:login] request = <<-EOF <?xml version="1.0" encoding="UTF-8"?> <CarrierPickupAvailabilityRequest USERID="#{URI.encode(@options[:login])}"> <FirmName>Shopifolk</FirmName> <SuiteOrApt>Suite 0</SuiteOrApt> <Address2>18 Fair Ave</Address2> <Urbanization /> <City>San Francisco</City> <State>CA</State> <ZIP5>94110</ZIP5> <ZIP4>9411</ZIP4> </CarrierPickupAvailabilityRequest> EOF xml = Nokogiri.XML(commit(:test, request, true)) { |config| config.strict } xml.at('/CarrierPickupAvailabilityResponse/City').try(:text) == 'SAN FRANCISCO' && xml.at('/CarrierPickupAvailabilityResponse/Address2').try(:text) == '18 FAIR AVE' end |
#commit(action, request, test = false) ⇒ Object (protected)
683 684 685 |
# File 'lib/active_shipping/carriers/usps.rb', line 683 def commit(action, request, test = false) ssl_get(request_url(action, request, test)) end |
#error_description_node(node) ⇒ Object (protected)
657 658 659 |
# File 'lib/active_shipping/carriers/usps.rb', line 657 def error_description_node(node) node.xpath('Error/Description') end |
#extract_event_details(node) ⇒ Object
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/active_shipping/carriers/usps.rb', line 241 def extract_event_details(node) description = node.at('Event').text.upcase if prefix = ONLY_PREFIX_EVENTS.find { |p| description.start_with?(p) } description = prefix end time = if node.at('EventDate').text.present? = "#{node.at('EventDate').text}, #{node.at('EventTime').text}" Time.parse() else # Epoch time, because we need to sort properly by time Time.at(0) end event_code = node.at('EventCode').text city = node.at('EventCity').try(:text) state = node.at('EventState').try(:text) zip_code = node.at('EventZIPCode').try(:text) country_node = node.at('EventCountry') country = country_node ? country_node.text : '' country = 'UNITED STATES' if country.empty? # USPS returns upcased country names which ActiveUtils doesn't recognize without translation country = find_country_code_case_insensitive(country) zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec) location = Location.new(city: city, state: state, postal_code: zip_code, country: country) EventDetails.new(description, time, zoneless_time, location, event_code) end |
#find_country_code_case_insensitive(name) ⇒ Object (protected)
673 674 675 676 677 678 679 680 681 |
# File 'lib/active_shipping/carriers/usps.rb', line 673 def find_country_code_case_insensitive(name) upcase_name = name.upcase.gsub(' ', ', ') if special = TRACKING_ODD_COUNTRY_NAMES[upcase_name] return special end country = ActiveUtils::Country::COUNTRIES.detect { |c| c[:name].upcase == upcase_name } raise ActiveShipping::Error, "No country found for #{name}" unless country country[:alpha2] end |
#find_rates(origin, destination, packages, options = {}) ⇒ Object
217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/active_shipping/carriers/usps.rb', line 217 def find_rates(origin, destination, packages, = {}) = @options.merge() origin = Location.from(origin) destination = Location.from(destination) packages = Array(packages) domestic_codes = US_POSSESSIONS + ['US', nil] if domestic_codes.include?(destination.country_code(:alpha2)) us_rates(origin, destination, packages, ) else world_rates(origin, destination, packages, ) end end |
#find_tracking_info(tracking_number, options = {}) ⇒ Object
175 176 177 178 179 180 |
# File 'lib/active_shipping/carriers/usps.rb', line 175 def find_tracking_info(tracking_number, = {}) = @options.merge() tracking_request = build_tracking_request(tracking_number, ) response = commit(:track, tracking_request, [:test] || false) parse_tracking_response(response).first end |
#has_error?(node) ⇒ Boolean (protected)
665 666 667 |
# File 'lib/active_shipping/carriers/usps.rb', line 665 def has_error?(node) node.xpath('Error').length > 0 end |
#maximum_address_field_length ⇒ Object
272 273 274 275 |
# File 'lib/active_shipping/carriers/usps.rb', line 272 def maximum_address_field_length # https://www.usps.com/business/web-tools-apis/address-information-api.pdf 38 end |
#maximum_weight ⇒ Object
237 238 239 |
# File 'lib/active_shipping/carriers/usps.rb', line 237 def maximum_weight Mass.new(70, :pounds) end |
#package_valid_for_max_dimensions(package, dimensions) ⇒ Object (protected)
574 575 576 577 578 579 580 581 582 583 |
# File 'lib/active_shipping/carriers/usps.rb', line 574 def package_valid_for_max_dimensions(package, dimensions) ((not ([:length, :width, :height].map { |dim| dimensions[dim].nil? || dimensions[dim].to_f >= package.inches(dim).to_f }.include?(false))) and (dimensions[:weight].nil? || dimensions[:weight] >= package.pounds) and (dimensions[:length_plus_girth].nil? or dimensions[:length_plus_girth].to_f >= package.inches(:length) + package.inches(:girth)) and (dimensions[:length_plus_width_plus_height].nil? or dimensions[:length_plus_width_plus_height].to_f >= package.inches(:length) + package.inches(:width) + package.inches(:height))) end |
#package_valid_for_service(package, service_node) ⇒ Object (protected)
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 |
# File 'lib/active_shipping/carriers/usps.rb', line 518 def package_valid_for_service(package, service_node) return true if service_node.at('MaxWeight').nil? max_weight = service_node.at('MaxWeight').text.to_f name = service_node.at_xpath('SvcDescription | MailService').text.downcase if name =~ /flat.rate.box/ # domestic or international flat rate box # flat rate dimensions from http://www.usps.com/shipping/flatrate.htm return (package_valid_for_max_dimensions(package, :weight => max_weight, # domestic apparently has no weight restriction :length => 11.0, :width => 8.5, :height => 5.5) or package_valid_for_max_dimensions(package, :weight => max_weight, :length => 13.625, :width => 11.875, :height => 3.375)) elsif name =~ /flat.rate.envelope/ return package_valid_for_max_dimensions(package, :weight => max_weight, :length => 12.5, :width => 9.5, :height => 0.75) elsif service_node.at('MailService') # domestic non-flat rates return true else # international non-flat rates # Some sample english that this is required to parse: # # 'Max. length 46", width 35", height 46" and max. length plus girth 108"' # 'Max. length 24", Max. length, height, depth combined 36"' # sentence = CGI.unescapeHTML(service_node.at('MaxDimensions').text) tokens = sentence.downcase.split(/[^\d]*"/).reject(&:empty?) max_dimensions = {:weight => max_weight} single_axis_values = [] tokens.each do |token| axis_sum = [/length/, /width/, /height/, /depth/].sum { |regex| (token =~ regex) ? 1 : 0 } unless axis_sum == 0 value = token[/\d+$/].to_f if axis_sum == 3 max_dimensions[:length_plus_width_plus_height] = value elsif token =~ /girth/ and axis_sum == 1 max_dimensions[:length_plus_girth] = value else single_axis_values << value end end end single_axis_values.sort!.reverse! [:length, :width, :height].each_with_index do |axis, i| max_dimensions[axis] = single_axis_values[i] if single_axis_values[i] end package_valid_for_max_dimensions(package, max_dimensions) end end |
#parse_rate_response(origin, destination, packages, response, options = {}) ⇒ Object (protected)
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 |
# File 'lib/active_shipping/carriers/usps.rb', line 441 def parse_rate_response(origin, destination, packages, response, = {}) success = true = '' rate_hash = {} xml = Nokogiri.XML(response) if error = xml.at_xpath('/Error | //ServiceErrors/ServiceError') success = false = error.at('Description').text else xml.root.xpath('Package').each do |package| if package.at('Error') success = false = package.at('Error/Description').text break end end if success rate_hash = rates_from_response_node(xml, packages, ) unless rate_hash success = false = "Unknown root node in XML response: '#{xml.root.name}'" end end end if success rate_estimates = rate_hash.keys.map do |service_name| RateEstimate.new(origin, destination, @@name, "USPS #{service_name}", :package_rates => rate_hash[service_name][:package_rates], :service_code => rate_hash[service_name][:service_code], :currency => 'USD') end rate_estimates.reject! { |e| e.package_count != packages.length } rate_estimates = rate_estimates.sort_by(&:total_price) end RateResponse.new(success, , Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request) end |
#parse_tracking_info(response, node) ⇒ Object (protected)
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 |
# File 'lib/active_shipping/carriers/usps.rb', line 611 def parse_tracking_info(response, node) success = !has_error?(node) = (node) if success destination = nil shipment_events = [] tracking_details = node.xpath('TrackDetail') tracking_details << node.at('TrackSummary') tracking_number = node.attributes['ID'].value prediction_node = node.at('PredictedDeliveryDate') || node.at('ExpectedDeliveryDate') scheduled_delivery = prediction_node ? Time.parse(prediction_node.text) : nil tracking_details.each do |event| details = extract_event_details(event) if details.location shipment_events << ShipmentEvent.new(details.description, details.zoneless_time, details.location, details.description, details.event_code) end end shipment_events = shipment_events.sort_by(&:time) attempted_delivery_date = shipment_events.detect{ |shipment_event| ATTEMPTED_DELIVERY_CODES.include?(shipment_event.type_code) }.try(:time) if last_shipment = shipment_events.last status = last_shipment.status actual_delivery_date = last_shipment.time if last_shipment.delivered? end end TrackingResponse.new(success, , Hash.from_xml(response), :carrier => @@name, :xml => response, :request => last_request, :shipment_events => shipment_events, :destination => destination, :tracking_number => tracking_number, :status => status, :actual_delivery_date => actual_delivery_date, :attempted_delivery_date => attempted_delivery_date, :scheduled_delivery_date => scheduled_delivery ) end |
#parse_tracking_response(response, options = {}) ⇒ Object (protected)
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'lib/active_shipping/carriers/usps.rb', line 585 def parse_tracking_response(response, = {}) xml = Nokogiri.XML(response) if has_error?(xml) = error_description_node(xml).text # actually raises instead of returning by nature of TrackingResponse#initialize return TrackingResponse.new(false, , Hash.from_xml(response), carrier: @@name, xml: response, request: last_request) end # Responses are always returned in the order originally given. if [:fault_tolerant] xml.root.xpath('TrackInfo').map do |info| # Don't let one failure wreck the whole batch begin parse_tracking_info(response, info) rescue ResponseError => e e.response end end else xml.root.xpath('TrackInfo').map { |info| parse_tracking_info(response, info) } end end |
#rates_from_response_node(response_node, packages, options = {}) ⇒ Object (protected)
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 |
# File 'lib/active_shipping/carriers/usps.rb', line 484 def rates_from_response_node(response_node, packages, = {}) rate_hash = {} return false unless (root_node = response_node.at_xpath('/IntlRateV2Response | /RateV4Response')) commercial_type = commercial_type() service_node, service_code_node, service_name_node, rate_node = if root_node.name == 'RateV4Response' %w(Postage CLASSID MailService) << DOMESTIC_RATE_FIELD[commercial_type] else %w(Service ID SvcDescription) << INTERNATIONAL_RATE_FIELD[commercial_type] end root_node.xpath('Package').each do |package_node| this_package = packages[package_node['ID'].to_i] package_node.xpath(service_node).each do |service_response_node| service_name = service_response_node.at(service_name_node).text service_name.gsub!(SERVICE_NAME_SUBSTITUTIONS, '') # aggregate specific package rates into a service-centric RateEstimate # first package with a given service name will initialize these; # later packages with same service will add to them this_service = rate_hash[service_name] ||= {} this_service[:service_code] ||= service_response_node.attributes[service_code_node].value package_rates = this_service[:package_rates] ||= [] this_package_rate = {:package => this_package, :rate => Package.cents_from(rate_value(rate_node, service_response_node, commercial_type))} package_rates << this_package_rate if package_valid_for_service(this_package, service_response_node) end end rate_hash end |
#request_url(action, request, test) ⇒ Object (protected)
687 688 689 690 691 |
# File 'lib/active_shipping/carriers/usps.rb', line 687 def request_url(action, request, test) scheme = USE_SSL[action] ? 'https://' : 'http://' host = test ? TEST_DOMAINS[USE_SSL[action]] : LIVE_DOMAIN "#{scheme}#{host}/#{LIVE_RESOURCE}?API=#{API_CODES[action]}&XML=#{URI.encode(request)}" end |
#requirements ⇒ Object
213 214 215 |
# File 'lib/active_shipping/carriers/usps.rb', line 213 def requirements [:login] end |
#response_message(document) ⇒ Object (protected)
669 670 671 |
# File 'lib/active_shipping/carriers/usps.rb', line 669 def (document) response_status_node(document).text end |
#response_status_node(node) ⇒ Object (protected)
661 662 663 |
# File 'lib/active_shipping/carriers/usps.rb', line 661 def response_status_node(node) node.at('StatusSummary') || error_description_node(node) end |
#strip_zip(zip) ⇒ Object (protected)
693 694 695 |
# File 'lib/active_shipping/carriers/usps.rb', line 693 def strip_zip(zip) zip.to_s.scan(/\d{5}/).first || zip end |
#us_rates(origin, destination, packages, options = {}) ⇒ Object (protected)
307 308 309 310 311 |
# File 'lib/active_shipping/carriers/usps.rb', line 307 def us_rates(origin, destination, packages, = {}) request = build_us_rate_request(packages, origin.zip, destination.zip, ) # never use test mode; rate requests just won't work on test servers parse_rate_response(origin, destination, packages, commit(:us_rates, request, false), ) end |
#valid_credentials? ⇒ Boolean
232 233 234 235 |
# File 'lib/active_shipping/carriers/usps.rb', line 232 def valid_credentials? # Cannot test with find_rates because USPS doesn't allow that in test mode test_mode? ? canned_address_verification_works? : super end |
#world_rates(origin, destination, packages, options = {}) ⇒ Object (protected)
313 314 315 316 317 |
# File 'lib/active_shipping/carriers/usps.rb', line 313 def world_rates(origin, destination, packages, = {}) request = build_world_rate_request(origin, packages, destination, ) # never use test mode; rate requests just won't work on test servers parse_rate_response(origin, destination, packages, commit(:world_rates, request, false), ) end |