Class: Shipping::ShipengineLtlTracker
- Inherits:
-
Object
- Object
- Shipping::ShipengineLtlTracker
- 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
-
.candidates ⇒ ActiveRecord::Relation<Delivery>
Base set: invoiced ShipEngine-LTL deliveries with a PRO.
Instance Method Summary collapse
-
#delivered? ⇒ Boolean
A delivered scan (DE/SP) is already on file for the PRO.
-
#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). -
#initialize(delivery) ⇒ ShipengineLtlTracker
constructor
A new instance of ShipengineLtlTracker.
-
#past_backstop? ⇒ Boolean
Past the age backstop (anchored on confirmed pickup, falling back to last-updated when pickup wasn't recorded).
-
#poll! ⇒ Integer
Poll ShipEngine and persist any new events.
-
#pollable? ⇒ Boolean
Whether this delivery should still be polled now.
Constructor Details
#initialize(delivery) ⇒ ShipengineLtlTracker
Returns a new instance of ShipengineLtlTracker.
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
.candidates ⇒ ActiveRecord::Relation<Delivery>
Base set: invoiced ShipEngine-LTL deliveries with a PRO. Further narrowed
at poll time by #pollable? (delivered / age backstop).
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.
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).
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).
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.
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.
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 |