Class: ShipmentTrackingRegistrationWorker
- Inherits:
-
Object
- Object
- ShipmentTrackingRegistrationWorker
- 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
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.} — 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., 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 |