class FriendlyShipping::Services::Usps::ParsePackageRate

Constants

BOX_REGEX

Often we get a multitude of rates for the same service given some combination of Box type and (see below) and “Hold for Pickup” service. This creates a regular expression with groups named after the keys from the `Usps::CONTAINERS` constant. Unfortunately, the keys don't correspond directly to the codes we use when serializing the request.

COMMERCIAL_PLUS_RATE_TAG
COMMERCIAL_RATE_TAG
CURRENCY
DAYS_TO_DELIVERY

For most rate options, USPS will return how many business days it takes to deliver this package in the format “{1,2,3}-Day”. We can filter this out using the below Regex.

ESCAPING_AND_SYMBOLS

USPS returns all the info about a rate in a long string with a bit of gibberish.

HOLD_FOR_PICKUP

We use this for identifying rates that use the Hold for Pickup service.

LEADING_USPS

At the beginning of the long String, USPS keeps a copy of its own name. We know we're dealing with them though, so we can filter that out, too.

MILITARY

When delivering to military ZIP codes, we don't actually get a timing estimate, but instead the string “Military”. We use this to indicate that this rate is for a military zip code in the rates' data Hash.

RATE_TAG
SERVICE_CODE_TAG

The tags used in the rate node that we get information from.

SERVICE_NAME_SUBSTITUTIONS

This combines all the things we want to filter out.

SERVICE_NAME_TAG

Public Class Methods

call(rate_node, package, package_options) click to toggle source
# File lib/friendly_shipping/services/usps/parse_package_rate.rb, line 61
def call(rate_node, package, package_options)
  # "A mail class identifier for the postage returned. Not necessarily unique within a <Package/>."
  # (from the USPS docs). We save this on the data Hash, but do not use it for identifying shipping methods.
  service_code = rate_node.attributes[SERVICE_CODE_TAG].value

  # The long string discussed above.
  service_name = rate_node.at(SERVICE_NAME_TAG).text

  # Does this rate assume Hold for Pickup service?
  hold_for_pickup = service_name.match?(HOLD_FOR_PICKUP)

  # Is the destination a military ZIP code?
  military = service_name.match?(MILITARY)

  # If we get a days-to-delivery indication, save it in the `days_to_delivery` variable.
  days_to_delivery_match = service_name.match(DAYS_TO_DELIVERY)
  days_to_delivery = if days_to_delivery_match
                       days_to_delivery_match.named_captures.values.first.to_i
                     end

  # Clean up the long string
  service_name.gsub!(SERVICE_NAME_SUBSTITUTIONS, '')

  # Some USPS services only offer commercial pricing. Unfortunately, USPS then returns a retail rate of 0.
  # In these cases, return the commercial rate instead of the normal rate.
  #
  # Some rates are available in both commercial and retail pricing - if we want the commercial pricing here,
  # we need to specify the commercial_pricing property on the `Physical::Package`.
  #
  commercial_rate_requested_or_rate_is_zero = package_options.commercial_pricing || rate_node.at(RATE_TAG).text.to_d.zero?
  commercial_rate_available = rate_node.at(COMMERCIAL_RATE_TAG) || rate_node.at(COMMERCIAL_PLUS_RATE_TAG)

  rate_value =
    if commercial_rate_requested_or_rate_is_zero && commercial_rate_available
      rate_node.at(COMMERCIAL_RATE_TAG)&.text&.to_d || rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d
    else
      rate_node.at(RATE_TAG).text.to_d
    end

  # The rate expressed as a RubyMoney objext
  rate = Money.new(rate_value * CURRENCY.subunit_to_unit, CURRENCY)

  # Which shipping method does this rate belong to? We first try to match a rate to a shipping method
  # by class ID (the CLASSID attribute in the USPS API rate response). Not every shipping method
  # has a class ID defined, and a shipping method can have multiple class IDs (for example, Priority
  # Express has different class IDs for standard, hold for pickup, and Sunday/Holiday delivery).
  #
  # If we don't find a match for class ID, we next try to match a rate to a shipping method using the
  # shipping method's service code. The USPS API rate response includes a name for each rate in the
  # MailService element. We match to see if the name starts with the given value. For example:
  #   `Priority Mail Express 2-day™`
  #
  shipping_method =
    SHIPPING_METHODS.detect { |sm| sm.data[:class_ids]&.include?(service_code) } ||
    SHIPPING_METHODS.detect { |sm| service_name.tr('-', ' ').upcase.starts_with?(sm.service_code) }

  # We find out the box name using a bit of Regex magic using named captures. See the `BOX_REGEX`
  # constant above.
  box_name_match = service_name.match(/#{BOX_REGEX}/)
  box_name = box_name_match ? box_name_match.named_captures.compact.keys.last.to_sym : :variable

  # Combine all the gathered information in a FriendlyShipping::Rate object.
  # Careful: This rate is only for one package within the shipment, and we get multiple
  # rates per package for the different shipping method/box/hold for pickup combinations.
  FriendlyShipping::Rate.new(
    shipping_method: shipping_method,
    amounts: { package.id => rate },
    data: {
      package: package,
      box_name: box_name,
      hold_for_pickup: hold_for_pickup,
      days_to_delivery: days_to_delivery,
      military: military,
      full_mail_service: service_name,
      service_code: service_code
    }
  )
end