Class: ShipmentTrackingRegistrationWorker

Inherits:
Object
  • Object
show all
Includes:
Sidekiq::Job
Defined in:
app/workers/shipment_tracking_registration_worker.rb

Overview

Registers a Shipment's tracking number with ShipEngine's tracking-webhook
subscription (POST /v1/tracking/start). On success, stamps
shipments.tracking_registered_at so we don't re-register the same
(shipment, tracking_number) pair on every save.

Enqueued by the Shipment after_commit hook whenever tracking_number
transitions to a new value AND the shipment's carrier maps to a non-nil
ShipEngine carrier_code in PARCEL_CARRIER_INTERNAL_TO_SHIPENGINE_CODE.
Same worker is reusable for a one-time backfill over historical
shipments that pre-date this code path.

Why this matters: prior to this worker we only registered for tracking
webhooks on shipments WE labeled via Shipping::ShipengineBase. Customer-
supplied RMA return labels already had their own path
(Shipping::ReturnShippingInsurance), but marketplace shipments and any
other source of (carrier, tracking_number) pairs were invisible — we
never saw a delivery event, so the warehouse and the customer-service
desk lost telemetry on whether a package made it. This closes that gap
for every parcel carrier connected to our ShipEngine account.

Instance Method Summary collapse

Instance Method Details

#perform(shipment_id) ⇒ Object

Parameters:

  • shipment_id (Integer)


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'app/workers/shipment_tracking_registration_worker.rb', line 52

def perform(shipment_id)
  shipment = Shipment.find_by(id: shipment_id)
  unless shipment
    Rails.logger.info "[ShipmentTrackingRegistrationWorker] Shipment##{shipment_id} not found, skipping"
    return
  end

  if shipment.tracking_number.blank?
    Rails.logger.info "[ShipmentTrackingRegistrationWorker] Shipment##{shipment_id} has no tracking_number, skipping"
    return
  end

  if shipment.tracking_registered_at.present?
    Rails.logger.info "[ShipmentTrackingRegistrationWorker] Shipment##{shipment_id} already registered " \
                      "at #{shipment.tracking_registered_at}, skipping"
    return
  end

  # Resolve a canonical carrier from the free-form `shipments.carrier`
  # column, falling back to the tracking number's format pattern when
  # the carrier string is a placeholder ("Override", "Standard",
  # "Shipping override, please confirm") or unrecognised service-level
  # name. Same chain as WyShipping.class_for_carrier and the
  # Shipment after_commit hook so all three resolution sites agree.
  normalized_carrier = Heatwave::Normalizers.resolve_shipping_carrier(
    shipment.carrier,
    tracking_number: shipment.tracking_number
  )
  shipengine_code = PARCEL_CARRIER_INTERNAL_TO_SHIPENGINE_CODE[normalized_carrier]
  unless shipengine_code
    # Surface silent skips to AppSignal so the registration coverage
    # rate is measurable from prod telemetry — without this we can
    # only learn we're missing carriers by log-grepping after the
    # fact. Warning-level (not error) so it doesn't pollute the
    # incident feed; aggregate-by-carrier in AppSignal answers
    # "what's our coverage shape today?".
    msg = "[ShipmentTrackingRegistrationWorker] Shipment##{shipment_id} carrier=#{shipment.carrier.inspect} " \
          "(normalized=#{normalized_carrier.inspect}) has no ShipEngine carrier_code — skipping registration"
    Rails.logger.info msg
    ErrorReporting.warning(
      'ShipmentTrackingRegistrationWorker skipped — carrier has no ShipEngine code',
      shipment_id: shipment_id,
      carrier: shipment.carrier,
      normalized_carrier: normalized_carrier,
      tracking_number: shipment.tracking_number,
      tracking_number_prefix: shipment.tracking_number.to_s.first(4),
      source: :background
    )
    return
  end

  Rails.logger.info "[ShipmentTrackingRegistrationWorker] Registering Shipment##{shipment_id} " \
                    "carrier=#{shipment.carrier} (normalized=#{normalized_carrier}) " \
                    "tracking_number=#{shipment.tracking_number} se_code=#{shipengine_code}"

  begin
    WyShipping.start_tracking_for_carrier(shipment.tracking_number, normalized_carrier)
  rescue ShipEngineRb::Exceptions::BusinessRulesError => e
    # ShipEngine business-rule failures (e.g. "Invalid tracking_number",
    # "tracking_number not found in carrier system", expired/voided
    # tracking IDs, marketplace tracking numbers SE doesn't recognise)
    # are PERMANENT — retrying buys nothing except wasted SE API quota
    # and a queue-stall behind doomed jobs. Real-world incident on the
    # initial backfill: ~30% of shipments returned "Invalid
    # tracking_number" and went into the retry storm. Log and swallow.
    #
    # tracking_registered_at stays nil. The after_commit hook on
    # Shipment only re-fires on `saved_change_to_tracking_number?` so
    # this shipment won't get re-enqueued on routine saves — only if
    # the tracking number genuinely changes (e.g. a label is voided
    # and regenerated, in which case the new number is worth retrying).
    msg = "[ShipmentTrackingRegistrationWorker] Shipment##{shipment_id} " \
          "carrier=#{shipment.carrier} tracking_number=#{shipment.tracking_number} " \
          "rejected by ShipEngine — #{e.message} — not retrying"
    Rails.logger.warn msg
    # Surface to AppSignal so the rejection pattern is queryable post-hoc
    # (group by carrier, tracking-number prefix, etc.) without grepping
    # production logs. Warning-level keeps it out of the error feed.
    ErrorReporting.warning(
      'ShipmentTrackingRegistrationWorker rejected by ShipEngine',
      shipment_id: shipment_id,
      carrier: shipment.carrier,
      tracking_number: shipment.tracking_number,
      tracking_number_prefix: shipment.tracking_number.to_s.first(4),
      shipengine_code: shipengine_code,
      shipengine_error_class: e.class.name,
      shipengine_error_message: e.message,
      source: :background
    )
    return
  end

  shipment.update_columns(tracking_registered_at: Time.current)

  # Backfill historical scan events. `tracking.start` only subscribes
  # to FUTURE webhook callbacks — SE does NOT push historical scans
  # that already happened before registration. For a package that's
  # already in transit (or worse, already delivered) the Tracking
  # Events tab would stay empty forever without this. The polling
  # endpoint (`tracking.track`) returns the full events[] history,
  # which we feed through ShipengineProcessor exactly like a real
  # webhook so the existing per-scan upsert/idempotency-key path
  # handles dedup vs any concurrently-arriving live webhook.
  backfill_history(shipment, shipengine_code)
end