Class: Shipping::SpeedeeTracker

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(delivery) ⇒ SpeedeeTracker

Returns a new instance of SpeedeeTracker.

Parameters:



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

.candidatesActiveRecord::Relation<Delivery>

Returns Spee-Dee deliveries with a DS
orderId that are recent enough to still be moving.

Returns:

  • (ActiveRecord::Relation<Delivery>)

    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.

Returns:

  • (Boolean)

    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).

Returns:

  • (Boolean)

    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.

Parameters:

  • ignore_backstop (Boolean) (defaults to: false)

    skip the age backstop (used by a backfill).

Returns:

  • (Integer)

    ShipmentEvent rows inserted (excludes dedup hits)



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

Returns:

  • (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).

Parameters:

  • body (Object)

    the parsed order-status response

  • tracking_numbers (Array<String>) (defaults to: @tracking_numbers)

    the delivery's shipment barcodes

Returns:

  • (Array<Hash>)


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
  timestamps = { 'pickedUpDate' => picked_up_at, 'deliveredDate' => delivered_at }

  STATUS_EVENTS.flat_map do |ev|
    occurred_at = timestamps[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