Class: Shipping::RlCarriersTracker

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

Overview

Polls R&L Carriers' Shipment Tracing SOAP service for a direct-R&L freight
delivery's PRO and projects the result into ShipmentEvent rows — the SAME
per-scan table parcel + ShipEngine LTL tracking write to. Reusing it means
the existing parcel status icon and Tracking Events tab light up with no new
UI: for direct R&L the PRO (WebProNumber) is stored on the delivery's
master_tracking_number AND on each shipment's tracking_number, so the
icon/tab (which key off shipment tracking numbers) already find these rows.

R&L has no tracking webhook, so this is poll-based — a near-clone of
ShipengineLtlTracker. Differences:

  • keyed on master_tracking_number (= WebProNumber), not ltl_pro_number
    (which is only set for the ShipEngine LTL carriers);
  • R&L returns a free-text status Description (no status code), so we map
    it via ShipmentEvent.infer_status_code — the same description→code
    inference built for Canpar.

Stop conditions (see #pollable?): a delivered scan (DE/SP) on file, or the
POLL_MAX_AGE age backstop past pickup.

Constant Summary collapse

CARRIER =
'RlCarriers'
SCAC =

R&L's SCAC (direct), stored on the event as carrier_code for parity with
the ShipEngine LTL path (informational only).

'RLCA'
POLL_MAX_AGE =
30.days
TRACKING_TZ =

R&L returns Date + Time-of-day split, without an offset — parse in the
server's canonical display zone.

'America/Chicago'
DATE_TIME_FORMATS =

R&L dates are US-format MM/DD/YYYY and times are 12-hour with AM/PM
(e.g. "05/28/2026" / "05:59:02 PM"). Time.zone.parse misreads MM/DD/YYYY
as DD/MM/YYYY (verified live: "06/01/2026" → Jan 6, "05/28/2026" → error),
so parse with explicit strptime formats. Ordered most- to least-specific;
first match wins, nil if none (drop the scan rather than store a wrong date).

['%m/%d/%Y %I:%M:%S %p', '%m/%d/%Y %I:%M %p',
'%m/%d/%Y %H:%M:%S', '%m/%d/%Y'].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(delivery) ⇒ RlCarriersTracker

Returns a new instance of RlCarriersTracker.

Parameters:



53
54
55
56
# File 'app/services/shipping/rl_carriers_tracker.rb', line 53

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

Class Method Details

.candidatesActiveRecord::Relation<Delivery>

Returns:



45
46
47
48
49
50
# File 'app/services/shipping/rl_carriers_tracker.rb', line 45

def self.candidates
  Delivery.invoiced
          .where.not(master_tracking_number: [nil, ''])
          .joins(:shipping_option)
          .where(shipping_options: { carrier: CARRIER })
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.



68
69
70
71
72
# File 'app/services/shipping/rl_carriers_tracker.rb', line 68

def delivered?
  return false if @pro.blank?

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

#event_rows(result) ⇒ Array<Hash>

Map an R&L trace result into ShipmentEvent attribute hashes — one per
StatusHistory entry. Status code is inferred from the free-text
description (R&L emits no code), reusing ShipmentEvent's inference.

Parameters:

Returns:

  • (Array<Hash>)


104
105
106
107
# File 'app/services/shipping/rl_carriers_tracker.rb', line 104

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

#past_backstop?Boolean

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

Returns:

  • (Boolean)

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



76
77
78
79
# File 'app/services/shipping/rl_carriers_tracker.rb', line 76

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

#poll!(ignore_backstop: false) ⇒ Integer

Poll R&L and persist any new events.

Parameters:

  • ignore_backstop (Boolean) (defaults to: false)

    when true, skip the 30-day age backstop
    (used by the one-time backfill to reach older deliveries the hourly
    sweep deliberately excludes). Still skips blank-PRO and already-delivered.

Returns:

  • (Integer)

    ShipmentEvent rows inserted (excludes dedup hits)



87
88
89
90
91
92
93
94
95
96
# File 'app/services/shipping/rl_carriers_tracker.rb', line 87

def poll!(ignore_backstop: false)
  return 0 if @pro.blank?
  return 0 if delivered?
  return 0 if !ignore_backstop && past_backstop?

  result = WyShipping.rl_carriers_tracking(@delivery)
  return 0 if result.blank? || !result[:success]

  persist_events(result)
end

#pollable?Boolean

Returns:

  • (Boolean)


59
60
61
62
63
64
65
# File 'app/services/shipping/rl_carriers_tracker.rb', line 59

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

  true
end