Class: FriendlyShipping::Services::Usps::ParsePackageRate
- Inherits:
-
Object
- Object
- FriendlyShipping::Services::Usps::ParsePackageRate
- Defined in:
- lib/friendly_shipping/services/usps/parse_package_rate.rb
Constant Summary collapse
- ESCAPING_AND_SYMBOLS =
USPS returns all the info about a rate in a long string with a bit of gibberish.
/<\S*>/
- 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.
/^USPS /
- SERVICE_NAME_SUBSTITUTIONS =
This combines all the things we want to filter out.
/#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/
- 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. { flat_rate_boxes: 'Flat Rate Boxes', large_flat_rate_box: 'Large Flat Rate Box', medium_flat_rate_box: 'Medium Flat Rate Box', small_flat_rate_box: 'Small Flat Rate Box', regional_rate_box_a: 'Regional Rate Box A', regional_rate_box_b: 'Regional Rate Box B', regional_rate_box_c: 'Regional Rate Box C', flat_rate_envelope: 'Flat Rate Envelope', legal_flat_rate_envelope: 'Legal Flat Rate Envelope', padded_flat_rate_envelope: 'Padded Flat Rate Envelope', gift_card_flat_rate_envelope: 'Gift Card Flat Rate Envelope', window_flat_rate_envelope: 'Window Flat Rate Envelope', small_flat_rate_envelope: 'Small Flat Rate Envelope', large_envelope: 'Large Envelope', parcel: 'Parcel', postcards: 'Postcards' }.map { |k, v| "(?<#{k}>#{v})" }.join("|").freeze
- HOLD_FOR_PICKUP =
We use this for identifying rates that use the Hold for Pickup service.
/Hold for Pickup/i
- 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.
/(?<days>\d)-Day/
- 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.
/MILITARY/i
- SERVICE_CODE_TAG =
The tags used in the rate node that we get information from.
'CLASSID'
- SERVICE_NAME_TAG =
'MailService'
- RATE_TAG =
'Rate'
- COMMERCIAL_RATE_TAG =
'CommercialRate'
- COMMERCIAL_PLUS_RATE_TAG =
'CommercialPlusRate'
- DIMENSIONAL_WEIGHT_RATE =
'DimensionalWeightRate'
- FEES =
'.//Fees/Fee'
- CURRENCY =
Money::Currency.new('USD').freeze
Class Method Summary collapse
Class Method Details
.call(rate_node, package, package_options) ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/friendly_shipping/services/usps/parse_package_rate.rb', line 63 def call(rate_node, package, ) # "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 = .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 commercial_rate = rate_node.at(COMMERCIAL_RATE_TAG)&.text.to_d commercial_rate.zero? ? rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d : commercial_rate 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 dimensional_weight_rate = rate_node.at(DIMENSIONAL_WEIGHT_RATE)&.text&.to_i fees = rate_node.xpath(FEES).map do |fee_node| type = fee_node.at('FeeType').text price = fee_node.at('FeePrice').text.to_d { type: type, price: Money.new(price * CURRENCY.subunit_to_unit, CURRENCY) } end # 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, dimensional_weight_rate: dimensional_weight_rate, fees: fees } ) end |