Class: Shipping::ShipengineLtlBase

Inherits:
ShipengineBase show all
Defined in:
app/services/shipping/shipengine_ltl_base.rb

Direct Known Subclasses

RlCarriersDisabled, Roadrunner, Saia, YrcFreight

Constant Summary collapse

COST_DISCREPANCY_THRESHOLD =
200.0
COST_DISCREPANCY_THRESHOLD_RATIO =

this is max ratio of discrepancy by total shipping cost

0.5
COST_DISCREPANCY_THRESHOLD_BY_TOTAL_VALUE_RATIO =

this is max ratio of discrepancy by total delivery value

0.05

Constants inherited from ShipengineBase

Shipping::ShipengineBase::MAX_LABEL_REFERENCE_LENGTH, Shipping::ShipengineBase::RATES_TIMEOUT_MS

Instance Attribute Summary

Attributes inherited from Base

#address, #address2, #address3, #address_residential, #attention_name, #billing_account, #billing_country, #billing_zip, #ci_comments, #city, #close_report_only, #cod_amount, #cod_collection_type, #company, #country, #currency_code, #data, #debug, #declared_value, #delivery_instructions, #delivery_total_value, #description, #discount_price, #dropoff_type, #email, #eta, #export_reason, #freight_class, #freightquote_authorization_url, #freightquote_client_id, #freightquote_client_secret, #freightquote_customer_code, #freightquote_events_url, #freightquote_rating_url, #freightquote_shipping_url, #freightquote_voiding_url, #handling_instructions, #has_loading_dock, #image_type, #include_first_class_mail_options, #insured_value, #is_construction_site, #is_trade_show, #label_type, #limited_access, #line_items, #master_tracking_number, #measure_height, #measure_length, #measure_units, #measure_width, #media_mail, #multiple_piece_shipping, #negotiated_rates, #package, #package_count, #package_sequence_number, #package_total, #packages, #packaging_type, #pay_type, #phone, #pickup_datetime, #pickup_instructions, #plain_response, #price, #rate_data, #reference_number_1, #reference_number_2, #reference_number_3, #reference_number_code_1, #reference_number_code_2, #required, #requires_appointment, #requires_inside_delivery, #requires_liftgate, #response, #return_to_address, #return_to_address2, #return_to_address3, #return_to_address_residential, #return_to_attention_name, #return_to_city, #return_to_company, #return_to_country, #return_to_email, #return_to_has_loading_dock, #return_to_is_construction_site, #return_to_is_trade_show, #return_to_limited_access, #return_to_name, #return_to_phone, #return_to_requires_appointment, #return_to_requires_inside_delivery, #return_to_requires_liftgate, #return_to_state, #return_to_zip, #rl_carriers_api_key, #rl_carriers_shipping_url, #saturday_delivery, #sender_address, #sender_address2, #sender_address3, #sender_address_residential, #sender_attention_name, #sender_city, #sender_company, #sender_country, #sender_email, #sender_has_loading_dock, #sender_is_construction_site, #sender_is_trade_show, #sender_limited_access, #sender_name, #sender_phone, #sender_requires_appointment, #sender_requires_inside_delivery, #sender_requires_liftgate, #sender_state, #sender_tax_identification_number, #sender_zip, #service_code, #service_type, #services, #ship_date, #shipengine_api_key, #shipengine_canadapost_account_id, #shipengine_canadapost_parent_account_number, #shipengine_canpar_account_id, #shipengine_dhl_express_account_id, #shipengine_fed_ex_account_id, #shipengine_fed_ex_ca_account_id, #shipengine_purolator_account_id, #shipengine_ups_account_id, #shipengine_ups_ca_account_id, #shipengine_usps_account_id, #shipper_address, #shipper_address2, #shipper_address3, #shipper_address_residential, #shipper_attention_name, #shipper_city, #shipper_company, #shipper_country, #shipper_email, #shipper_has_loading_dock, #shipper_is_construction_site, #shipper_is_trade_show, #shipper_limited_access, #shipper_name, #shipper_phone, #shipper_requires_appointment, #shipper_requires_inside_delivery, #shipper_requires_liftgate, #shipper_state, #shipper_zip, #signature_confirmation, #skip_png_download, #skip_rate_test, #special_instructions, #state, #tax_identification_number, #time_in_transit, #total_shipment_weight, #transaction_type, #weight, #weight_units, #zip

Instance Method Summary collapse

Methods inherited from ShipengineBase

#find_rates, #get_separate_labels_per_package_from_label_pdf_url, #start_tracking, #track, #valid_address?, #void

Methods inherited from Base

#fedex, #initialize, #purolator, state_from_zip, #ups, #ups_freight

Constructor Details

This class inherits a constructor from Shipping::Base

Instance Method Details

#label(_return_label = false, logger = nil) ⇒ Object

Raises:



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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
# File 'app/services/shipping/shipengine_ltl_base.rb', line 9

def label(_return_label = false, logger = nil)
  logger ||= Rails.logger

  @required = [:shipengine_api_key]
  @required += %i[phone company address city state zip]
  @required += %i[sender_phone sender_email sender_company sender_address sender_city sender_state sender_zip]
  @required += %i[packages service_code]
  @required += [:billing_account] if %w[bill_third_party freight_collect].include?(@pay_type)
  @required += %i[billing_zip billing_country] if ['bill_third_party'].include?(@pay_type)
  @required += [:rate_data]

  @country ||= 'US'
  @sender_country ||= 'US'

  service_code_hash = service_code_map.detect { |h| h[:hw_service_code] == @service_code } || {}
  # deal with transitional back and forth to match service codes
  se_service_code = service_code_hash[:service_code]
  if se_service_code.nil?
    service_code_hash = service_code_map.detect { |h| h[:hw_service_code] == @service_code.split('SHIPENGINE_').last } || {}
    se_service_code = service_code_hash[:service_code]
  end
  if se_service_code.nil?
    service_code_hash = service_code_map.detect { |h| h[:hw_service_code] == "SHIPENGINE_#{@service_code}" } || {}
    se_service_code = service_code_hash[:service_code]
  end

  ####################################################################################

  quote_id = @rate_data['quote_id'] # quote id fallback will be original quote_id
  quote_expiration = @rate_data['quote_expiration']

  # Here we may need to re-rate quote, because you need to buy a specific Shipengine LTL quote_id and all the relevant criteria like packages, weights, addresses and options have to match the quote id you are 'buying'. This pattern is different than our other carriers which give you an estimate and then you buy the label using the same (one hopes) criteria

  original_service_code = @rate_data['service_code'] || se_service_code
  if original_service_code.nil?
    # no match
    raise ShippingError, 'Cannot generate shipment no service_code found, please HOLD order and refresh shipping rates/methods'
  end

  original_total_price = @rate_data['total_price'].to_f
  # So let's re-find rates, but with the new criteria for the label
  estimate_res = find_rates
  # find the matching rate based on service code
  new_total_price = nil
  estimate = nil
  estimate = estimate_res[:rates].detect { |e| e.dig(:rate_data, :service_code) == original_service_code }

  if estimate.present? # we have a matching rate using the service code matching
    new_total_price = estimate.dig(:rate_data, :total_price).to_f
    if (new_total_price && original_total_price &&
        ((diff = (new_total_price - original_total_price).abs) < COST_DISCREPANCY_THRESHOLD) &&
        (diff / original_total_price < COST_DISCREPANCY_THRESHOLD_RATIO)
       ) ||
       ((@delivery_total_value.to_f > 0.0) &&
        (@delivery_total_value.to_f > COST_DISCREPANCY_THRESHOLD * 10.0) &&
        (diff / @delivery_total_value.to_f < COST_DISCREPANCY_THRESHOLD_BY_TOTAL_VALUE_RATIO)
       )
      # we match and the price is proportionately close enough, so grab new quote id and carrier option id
      quote_id = estimate.dig(:rate_data, :quote_id)
    elsif @skip_rate_test # here we have an ST, and don't care about comparing rates, so take the matching rate
      quote_id = estimate.dig(:rate_data, :quote_id)
    end
  elsif quote_expiration && Date.current >= Date.parse(quote_expiration)
    raise ShippingError, 'Rate has expired, please HOLD order and refresh shipping rates/methods'
  end
  # no match or pricing threshold exceeded, test for quote expiration and let it go

  logger.debug "Shipping Freightquote @rate_data: #{@rate_data}"

  raise ShippingError, 'Missing data, please HOLD order and refresh shipping rates/methods' if quote_id.blank?

  ####################################################################################

  label_options = get_label_options_hash

  logger.debug("Shipping #{carrier} label Request", carrier: carrier)

  res = get_label(quote_id, label_options)

  logger.debug("Shipping #{carrier} label Response", carrier: carrier)

  # {
  #     "confirmation_number": "55667788",
  #     "documents": [
  #         {
  #             "format": "pdf",
  #             "image": "JVBERi0xLjQKMSAwIG9iago8PAo...",
  #             "type": "bill_of_lading"
  #         }
  #     ],
  #     "message": null,
  #     "pickup_id": "43f2a746-0f2c-4377-b30e-4d0a37c39444",
  #     "pro_number": "1234578",
  #     "pickup_id": "2292961b-c796-435f-9997-100725b1393a",
  #     "quote_id": "4776018b-6b48-4d8f-8789-4e2d0610accc",
  #     "warnings": [
  #       {
  #       "external_code": "10000008",
  #       "message": "Some of the accessorials submitted are not supported by the carrier and were not used."
  #       }
  #     ]
  # }

  msg = ([res.dig(:message)] + res.dig(:warnings).map { |w| "Code: #{w[:external_code]}, Message: #{w[:message]}" }).join(' ')

  raise ShippingError, msg unless res[:confirmation_number].present? && res[:pickup_id].present? && res[:documents].present? && res[:pro_number].present?

  total_price = new_total_price || original_total_price
  response = {}
  response[:tracking_number] = res[:pro_number]
  encoded_image = res.dig(:documents).detect { |doc| doc[:type] == 'bill_of_lading' }[:image]
  response[:bol_image] = Tempfile.new('bol')
  response[:bol_image].binmode
  response[:bol_image].write Base64.decode64(encoded_image)
  response[:bol_image].rewind
  response[:bol_image].flush
  response[:bol_image].fsync

  # allows for things like response.tracking_number?
  def response.method_missing(name, *args)
    has_key?(name) ? self[name] : super
  end

  { labels: [response], shipment_identification_number: res[:pro_number], carrier_bol: res[:confirmation_number], pickup_confirmation_number: res[:pickup_id], shipengine_label_id: res[:pickup_id], total_charges: total_price,
    ship_request_xml: label_options.to_hash.inspect, ship_reply_xml: res.to_hash.inspect }
end

#schedule_pickup(deliveries, datetime, logger = nil) ⇒ Object



136
137
# File 'app/services/shipping/shipengine_ltl_base.rb', line 136

def schedule_pickup(deliveries, datetime, logger = nil)
end