Class: Shipping::ShipengineLtlTracker

Inherits:
Object
  • Object
show all
Defined in:
app/services/shipping/shipengine_ltl_tracker.rb

Overview

Polls ShipEngine's LTL tracking endpoint for a freight delivery's PRO and
projects the result into ShipmentEvent rows — the SAME per-scan table the
ShipEngine package tracking webhook writes to. Reusing ShipmentEvent (and
therefore ShipmentEventStatusSummary) means the delivery-level LTL status
icon is rendered by exactly the same status→icon/tooltip logic as parcel
shipments, with no parallel UI.

Driven hourly by ShipengineLtlTrackingWorker. Unlike package tracking
(push, via ShipEngine's tracking webhook) ShipEngine LTL has no webhook, so
this is poll-based — mirroring the Freightquote LTL sweep pattern.

Scope / stop conditions (see ShipengineLtlTracker.candidates / #pollable?):

  • carrier is a ShipEngine LTL carrier and the delivery is invoiced
    with a PRO populated;
  • STOP (don't poll) once a delivered event (DE/SP) is on file;
  • STOP at the age backstop (POLL_MAX_AGE past pickup) so refused /
    undeliverable / stuck freight doesn't poll forever — exceptions surface
    via the red status icon for ops but are not treated as auto-terminal
    (an EX can be transient, e.g. a weather hold that later delivers).

Constant Summary collapse

STATUS_CODE_BY_LABEL =

ShipEngine LTL tracking returns the long-form status (e.g. "in_transit");
ShipmentEvent stores the 2-letter code. Invert the canonical label map so
the projection shares one vocabulary with the package path.

ShipmentEvent::STATUS_CODE_LABELS.invert.freeze
POLL_MAX_AGE =

Stop polling this long after pickup regardless of status — the backstop
for non-delivered terminal outcomes (refused, undeliverable, lost). Matches
the Freightquote sweep's 30-day lookback.

30.days
TRACKING_TZ =

Display/parse timezone for the carrier's date + time-of-day fields, which
ShipEngine returns split and without an offset. Central is this codebase's
canonical server tz (Time.current / Date.current).

'America/Chicago'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(delivery) ⇒ ShipengineLtlTracker

Returns a new instance of ShipengineLtlTracker.

Parameters:



55
56
57
58
# File 'app/services/shipping/shipengine_ltl_tracker.rb', line 55

def initialize(delivery)
  @delivery = delivery
  @pro = delivery.ltl_pro_number.to_s.strip
end

Class Method Details

.candidatesActiveRecord::Relation<Delivery>

Base set: invoiced ShipEngine-LTL deliveries with a PRO. Further narrowed
at poll time by #pollable? (delivered / age backstop).

Returns:



47
48
49
50
51
52
# File 'app/services/shipping/shipengine_ltl_tracker.rb', line 47

def self.candidates
  Delivery.invoiced
          .where.not(ltl_pro_number: [nil, ''])
          .joins(:shipping_option)
          .where(shipping_options: { carrier: ShippingOption::SHIPENGINE_LTL_CARRIERS })
end

Instance Method Details

#delivered?Boolean

Returns a delivered scan (DE/SP) is already on file for the PRO.

Returns:

  • (Boolean)

    a delivered scan (DE/SP) is already on file for the PRO.



70
71
72
73
74
# File 'app/services/shipping/shipengine_ltl_tracker.rb', line 70

def delivered?
  return false if @pro.blank?

  ShipmentEvent.for_tracking_number(@pro).delivered.exists?
end

#event_rows(result) ⇒ Array<Hash>

Map a ShipEngine LTL tracking payload into ShipmentEvent attribute hashes
— one per events[] entry, plus a synthetic delivered row when the
payload carries a top-level actual-delivery date but no discrete
delivered scan (so the icon flips green and polling stops).

Parameters:

  • result (Hash)

    the track payload (HashWithIndifferentAccess)

Returns:

  • (Array<Hash>)

    ShipmentEvent attribute hashes



102
103
104
105
106
107
108
109
110
111
112
113
# File 'app/services/shipping/shipengine_ltl_tracker.rb', line 102

def event_rows(result)
  data = result.is_a?(Hash) ? result.with_indifferent_access : {}.with_indifferent_access
  rows = Array(data[:events]).filter_map { |event| event_row(event) }

  actual = data.dig(:delivery, :actual, :date)
  already_delivered = rows.any? { |r| ShipmentEvent::DELIVERED_STATUS_CODES.include?(r[:status_code]) }
  if actual.present? && !already_delivered
    rows << synthetic_delivered_row(actual, data.dig(:delivery, :actual, :time),
                                    data.dig(:delivery, :signature, :name))
  end
  rows
end

#past_backstop?Boolean

Returns past the age backstop (anchored on confirmed pickup,
falling back to last-updated when pickup wasn't recorded).

Returns:

  • (Boolean)

    past the age backstop (anchored on confirmed pickup,
    falling back to last-updated when pickup wasn't recorded).



78
79
80
81
# File 'app/services/shipping/shipengine_ltl_tracker.rb', line 78

def past_backstop?
  anchor = @delivery.confirmed_pickup_date || @delivery.updated_at&.to_date
  anchor.present? && anchor < POLL_MAX_AGE.ago.to_date
end

#poll!Integer

Poll ShipEngine and persist any new events.

Returns:

  • (Integer)

    number of ShipmentEvent rows inserted (excludes dedup hits)



86
87
88
89
90
91
92
93
# File 'app/services/shipping/shipengine_ltl_tracker.rb', line 86

def poll!
  return 0 unless pollable?

  result = WyShipping.shipengine_ltl_tracking(@delivery)
  return 0 if result.blank?

  persist_events(result)
end

#pollable?Boolean

Returns whether this delivery should still be polled now.

Returns:

  • (Boolean)

    whether this delivery should still be polled now.



61
62
63
64
65
66
67
# File 'app/services/shipping/shipengine_ltl_tracker.rb', line 61

def pollable?
  return false if @pro.blank?
  return false if delivered?
  return false if past_backstop?

  true
end