Class: FreightEvent
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- FreightEvent
- Defined in:
- app/models/freight_event.rb
Overview
Typed projection of CHR/Freightquote (CH Robinson Navisphere Events product)
payloads into indexed columns for the freight events pipeline.
Populated by WebhookProcessors::FreightquoteProcessor from WebhookLog
records ingested at Webhooks::V1::FreightquoteController#create. Each row
is one Navisphere event (LOAD CREATED, LOAD BOOKED, LOAD CANCELLED,
ORDER REJECTED, APPOINTMENT UPDATED, PRO NUMBER ADDED, etc.). The raw
payload stays in webhook_logs.data; this table is the read-optimised
projection for cron sweeps (pickup-window safety net) and triage queries.
Idempotency: CHR does not provide an event ID, so we synthesise a natural
key from (event_type, event_time, load_number, order_number). Replay
safety relies on the unique index on idempotency_key.
Constant Summary collapse
- CARRIER_REJECTION_TYPES =
Event types that signal the carrier won't fulfill (or the load was
cancelled by ops). Triggers the failure-path side effects in the
processor. ['ORDER REJECTED', 'LOAD CANCELLED', 'ORDER CANCELED'].freeze
- PICKUP_MILESTONE_TYPES =
Stop-scope events confirming forward motion against the booked pickup.
['CARRIER ARRIVED', 'CARRIER DEPARTED', 'LOAD PICKED UP'].freeze
- DELIVERED_TYPES =
Terminal happy-path events; the pickup-window safety sweep stops alerting
for a delivery once we have any of these. ['LOAD DELIVERED'].freeze
- NAVISPHERE_NOISE_EVENT_TYPES =
CHR's Navisphere runs two parallel subsystems (load mgmt + order mgmt)
that emit independently. The load subsystem is the operationally
meaningful one — its events (LOAD CREATED/BOOKED/PICKED UP/DELIVERED/
CANCELLED, APPOINTMENT UPDATED, PRO NUMBER ADDED) drive every workflow
we care about. The order subsystem replays the same booking ~20 sec
later as a billing/reference wrapper; ORDER CREATED + ORDER UPDATED
carry no carrier, no items, no locations — justloadNumberspointing
back to the load events we already processed. They confuse the Freight
Events tab (ORDER CREATED appears AFTER LOAD BOOKED, which is CHR's
actual lifecycle but reads as nonsense) and trigger no side effect
anywhere in the codebase.The OTHER ORDER-* events are NOT noise: ORDER REJECTED + ORDER CANCELED
drive carrier-rejection alerts and void confirmation; ORDER COMPLETED
is a terminal-delivery signal in FreightEventStatusSummary. Only the
two below are pure replication chatter.Filtered at the FreightquoteProcessor entry point — the raw payload
still lives in webhook_logs.data, so if CHR ever attaches meaning to
these we can revisit without losing history. ['ORDER CREATED', 'ORDER UPDATED'].freeze
Constants included from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
- #event_time ⇒ Object readonly
- #event_type ⇒ Object readonly
- #idempotency_key ⇒ Object readonly
Belongs to collapse
Class Method Summary collapse
-
.carrier_rejections ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are carrier rejections.
-
.extract_load_number(event) ⇒ Object
LOAD-scope events expose
loadNumber(scalar); ORDER-scope events useloadNumbers(array). - .extract_navisphere_tracking_number(event) ⇒ Object
-
.extract_order_number(event) ⇒ Object
LOAD-scope events use
orderDetails(plural array); ORDER-scope events useorderDetail(singular array). -
.for_load ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are for load.
-
.for_order ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are for order.
-
.idempotency_key_for(event_type:, event_time:, load_number: nil, order_number: nil) ⇒ String
Build the natural-key string used for replay-safe upserts.
-
.of_type ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are of type.
-
.parse_payload(payload) ⇒ Hash
Parse one Navisphere event-callback payload into the attribute hash this model expects.
-
.pickup_milestones ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are pickup milestones.
-
.since ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are since.
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#event_time ⇒ Object (readonly)
58 |
# File 'app/models/freight_event.rb', line 58 validates :event_time, presence: true |
#event_type ⇒ Object (readonly)
57 |
# File 'app/models/freight_event.rb', line 57 validates :event_type, presence: true |
#idempotency_key ⇒ Object (readonly)
59 |
# File 'app/models/freight_event.rb', line 59 validates :idempotency_key, presence: true, uniqueness: true |
Class Method Details
.carrier_rejections ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are carrier rejections. Active Record Scope
65 |
# File 'app/models/freight_event.rb', line 65 scope :carrier_rejections, -> { where(event_type: CARRIER_REJECTION_TYPES) } |
.extract_load_number(event) ⇒ Object
LOAD-scope events expose loadNumber (scalar); ORDER-scope events use
loadNumbers (array). Same value when an order has one load.
111 112 113 114 115 116 |
# File 'app/models/freight_event.rb', line 111 def self.extract_load_number(event) scalar = event['loadNumber'] return scalar.to_i if scalar.present? Array(event['loadNumbers']).compact.first&.to_i end |
.extract_navisphere_tracking_number(event) ⇒ Object
126 127 128 129 |
# File 'app/models/freight_event.rb', line 126 def self.extract_navisphere_tracking_number(event) details = Array(event['orderDetails']).presence || Array(event['orderDetail']).presence details&.first&.dig('navisphereTrackingNumber').presence end |
.extract_order_number(event) ⇒ Object
LOAD-scope events use orderDetails (plural array); ORDER-scope events
use orderDetail (singular array). Both contain objects with
orderNumber.
121 122 123 124 |
# File 'app/models/freight_event.rb', line 121 def self.extract_order_number(event) details = Array(event['orderDetails']).presence || Array(event['orderDetail']).presence details&.first&.dig('orderNumber')&.to_i end |
.for_load ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are for load. Active Record Scope
61 |
# File 'app/models/freight_event.rb', line 61 scope :for_load, ->(load_number) { where(load_number: load_number) } |
.for_order ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are for order. Active Record Scope
62 |
# File 'app/models/freight_event.rb', line 62 scope :for_order, ->(order_number) { where(order_number: order_number) } |
.idempotency_key_for(event_type:, event_time:, load_number: nil, order_number: nil) ⇒ String
Build the natural-key string used for replay-safe upserts.
77 78 79 80 81 |
# File 'app/models/freight_event.rb', line 77 def self.idempotency_key_for(event_type:, event_time:, load_number: nil, order_number: nil) time = event_time.is_a?(String) ? Time.zone.parse(event_time) : event_time time_str = time.utc.iso8601(6) [event_type, time_str, load_number, order_number].map(&:to_s).join('|') end |
.of_type ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are of type. Active Record Scope
63 |
# File 'app/models/freight_event.rb', line 63 scope :of_type, ->(types) { where(event_type: types) } |
.parse_payload(payload) ⇒ Hash
Parse one Navisphere event-callback payload into the attribute hash this
model expects. CHR's shape varies by scope (LOAD vs ORDER vs APPOINTMENT
events use different singular/plural keys), so the helpers below
normalise across all variants seen on DE787078.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'app/models/freight_event.rb', line 91 def self.parse_payload(payload) event = payload['event'] || {} { event_type: event['eventType'].to_s, event_sub_type: event['eventSubType'], event_time: payload['eventTime'], emitted_at: payload['time'], load_number: extract_load_number(event), order_number: extract_order_number(event), carrier_scac: event.dig('carrier', 'scac'), carrier_name: event.dig('carrier', 'name'), pro_number: event.dig('carrier', 'proNumber'), customer_reference_number: payload['customerReferenceNumber'].presence, navisphere_tracking_number: extract_navisphere_tracking_number(event), payload: event } end |
.pickup_milestones ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are pickup milestones. Active Record Scope
66 |
# File 'app/models/freight_event.rb', line 66 scope :pickup_milestones, -> { where(event_type: PICKUP_MILESTONE_TYPES) } |
.since ⇒ ActiveRecord::Relation<FreightEvent>
A relation of FreightEvents that are since. Active Record Scope
64 |
# File 'app/models/freight_event.rb', line 64 scope :since, ->(time) { where('event_time >= ?', time) } |
Instance Method Details
#delivery ⇒ Delivery
54 |
# File 'app/models/freight_event.rb', line 54 belongs_to :delivery, inverse_of: :freight_events, optional: true |
#webhook_log ⇒ WebhookLog
55 |
# File 'app/models/freight_event.rb', line 55 belongs_to :webhook_log, optional: true |