Class: Shipping::SpeedeeTracker
- Inherits:
-
Object
- Object
- Shipping::SpeedeeTracker
- Defined in:
- app/services/shipping/speedee_tracker.rb
Overview
Projects Spee-Dee (Dispatch Science) tracking into ShipmentEvent rows —
the SAME per-scan table parcel + ShipEngine LTL tracking write. Because
each Spee-Dee shipment's tracking_number is the carrier barcode, writing
these rows lights up the existing parcel status icon and Tracking Events
tab with no new UI — exactly like ShipengineLtlTracker.
SOURCE: GET /orders/{id}/status — {"status":"Delivered", "pickedUpDate":"…","deliveredDate":"…"}. #status_rows synthesizes one
AC (picked up) and/or one DE (delivered) event per shipment barcode from
those carrier timestamps; the idempotency key dedups repeat polls. The AC
event is suppressed when pickedUpDate is just the delivery scan echoed
back (see the note in #status_rows).
Why order status and not the per-item scan log: Spee-Dee's drivers do not
item-scan through Dispatch Science. Verified in production 2026-08-03 —
three DELIVERED orders (SD6225002/SD6222078/SD6222187) all returned []
from GET /orders/{id}/trackeditemslog, while /status carried real
pickedUpDate/deliveredDate values and /items confirmed each item barcode
equals our finalized shipment.tracking_number. The scan-log parser
(#event_rows, deleted with this pivot) is in git history if Spee-Dee
ever adopts driver item scanning. Granularity is coarse (no in-transit
scans), which fits a 1–3-day regional carrier.
Stop conditions (see #pollable?): a delivered scan on file, or the
POLL_MAX_AGE age backstop past label time.
Constant Summary collapse
- CARRIER =
'SpeedeeDelivery'- SCAC =
Spee-Dee's SCAC (per config/initializers/carrier_codes.rb) — stored on the
event as carrier_code for parity with the other trackers (informational). 'SDED'- STATUS_EVENTS =
The two carrier timestamps GET /orders/
{id}/status exposes, and the
ShipmentEvent status each becomes. [ { date_key: 'pickedUpDate', status_code: 'AC', carrier_status: 'PickedUp', label: 'Picked Up' }, { date_key: 'deliveredDate', status_code: 'DE', carrier_status: 'Delivered', label: 'Delivered' } ].freeze
- POLL_MAX_AGE =
30.days
Class Method Summary collapse
-
.candidates ⇒ ActiveRecord::Relation<Delivery>
Spee-Dee deliveries with a DS orderId that are recent enough to still be moving.
Instance Method Summary collapse
-
#delivered? ⇒ Boolean
A delivered scan is already on file for every shipment.
-
#initialize(delivery) ⇒ SpeedeeTracker
constructor
A new instance of SpeedeeTracker.
-
#past_backstop? ⇒ Boolean
Past the age backstop (ship-labeled time, falling back to last-updated when not recorded).
-
#poll!(ignore_backstop: false) ⇒ Integer
Poll Dispatch Science order status and persist any new events.
- #pollable? ⇒ Boolean
-
#status_rows(body, tracking_numbers = @tracking_numbers) ⇒ Array<Hash>
Map a GET /orders/
{id}/status body into ShipmentEvent attribute hashes — one per (STATUS_EVENTS timestamp present, shipment barcode).
Constructor Details
#initialize(delivery) ⇒ SpeedeeTracker
Returns a new instance of SpeedeeTracker.
57 58 59 60 61 62 63 |
# File 'app/services/shipping/speedee_tracker.rb', line 57 def initialize(delivery) @delivery = delivery @order_id = delivery.freight_order_number.to_s.strip # The events key on each shipment's carrier barcode; collect them so # delivered?/persist can resolve without re-querying per event. @tracking_numbers = delivery.shipments.where.not(tracking_number: [nil, '']).pluck(:tracking_number) end |
Class Method Details
.candidates ⇒ ActiveRecord::Relation<Delivery>
Returns Spee-Dee deliveries with a DS
orderId that are recent enough to still be moving.
49 50 51 52 53 54 |
# File 'app/services/shipping/speedee_tracker.rb', line 49 def self.candidates Delivery.joins(:shipping_option) .where(shipping_options: { carrier: CARRIER }) .where.not(freight_order_number: [nil, '']) .where(deliveries: { updated_at: POLL_MAX_AGE.ago.. }) end |
Instance Method Details
#delivered? ⇒ Boolean
Returns a delivered scan is already on file for every shipment.
75 76 77 78 79 |
# File 'app/services/shipping/speedee_tracker.rb', line 75 def delivered? return false if @tracking_numbers.empty? @tracking_numbers.all? { |tn| ShipmentEvent.for_tracking_number(tn).delivered.exists? } end |
#past_backstop? ⇒ Boolean
Returns past the age backstop (ship-labeled time, falling back to
last-updated when not recorded).
83 84 85 86 |
# File 'app/services/shipping/speedee_tracker.rb', line 83 def past_backstop? anchor = @delivery.ship_labeled_at&.to_date || @delivery.updated_at&.to_date anchor.present? && anchor < POLL_MAX_AGE.ago.to_date end |
#poll!(ignore_backstop: false) ⇒ Integer
Poll Dispatch Science order status and persist any new events.
92 93 94 95 96 97 98 99 100 101 |
# File 'app/services/shipping/speedee_tracker.rb', line 92 def poll!(ignore_backstop: false) return 0 if @order_id.blank? return 0 if delivered? return 0 if !ignore_backstop && past_backstop? result = WyShipping.speedee_tracking(@delivery) return 0 unless result.is_a?(Hash) && result[:status] == :ok persist_rows(status_rows(result[:order_status])) end |
#pollable? ⇒ Boolean
66 67 68 69 70 71 72 |
# File 'app/services/shipping/speedee_tracker.rb', line 66 def pollable? return false if @order_id.blank? || @tracking_numbers.empty? return false if delivered? return false if past_backstop? true end |
#status_rows(body, tracking_numbers = @tracking_numbers) ⇒ Array<Hash>
Map a GET /orders/{id}/status body into ShipmentEvent attribute hashes —
one per (STATUS_EVENTS timestamp present, shipment barcode).
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 |
# File 'app/services/shipping/speedee_tracker.rb', line 109 def status_rows(body, tracking_numbers = @tracking_numbers) return [] unless body.is_a?(Hash) # Spee-Dee stamps pickedUpDate AT the delivery scan (verified in # production 2026-08-03: delivered orders carry pickedUpDate == # deliveredDate to the minute), so an identical pair is ONE scan — a # Picked Up event would just duplicate the delivery. Emit AC only when # the pickup was observed separately: no delivery yet, or a genuinely # earlier pickup timestamp. picked_up_at = parse_occurred_at(body['pickedUpDate']) delivered_at = parse_occurred_at(body['deliveredDate']) picked_up_at = nil if delivered_at.present? && picked_up_at.present? && picked_up_at >= delivered_at = { 'pickedUpDate' => picked_up_at, 'deliveredDate' => delivered_at } STATUS_EVENTS.flat_map do |ev| occurred_at = [ev[:date_key]] next [] if occurred_at.blank? Array(tracking_numbers).map do |tracking_number| { tracking_number: tracking_number, carrier_code: SCAC, occurred_at: occurred_at, status_code: ev[:status_code], status_description: ShipmentEvent::STATUS_CODE_LABELS[ev[:status_code]], carrier_status_code: ev[:carrier_status], carrier_status_description: ev[:label], event_code: nil, payload: body } end end end |