Class: FreightEvent

Inherits:
ApplicationRecord show all
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
['ORDER CREATED', 'ORDER UPDATED'].freeze

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Class Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#event_timeObject (readonly)



58
# File 'app/models/freight_event.rb', line 58

validates :event_time, presence: true

#event_typeObject (readonly)



57
# File 'app/models/freight_event.rb', line 57

validates :event_type, presence: true

#idempotency_keyObject (readonly)



59
# File 'app/models/freight_event.rb', line 59

validates :idempotency_key, presence: true, uniqueness: true

Class Method Details

.carrier_rejectionsActiveRecord::Relation<FreightEvent>

A relation of FreightEvents that are carrier rejections. Active Record Scope

Returns:

See Also:



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_loadActiveRecord::Relation<FreightEvent>

A relation of FreightEvents that are for load. Active Record Scope

Returns:

See Also:



61
# File 'app/models/freight_event.rb', line 61

scope :for_load, ->(load_number) { where(load_number: load_number) }

.for_orderActiveRecord::Relation<FreightEvent>

A relation of FreightEvents that are for order. Active Record Scope

Returns:

See Also:



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.

Parameters:

  • event_type (String)
  • event_time (Time, String)
  • load_number (Integer, nil) (defaults to: nil)
  • order_number (Integer, nil) (defaults to: nil)

Returns:

  • (String)


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_typeActiveRecord::Relation<FreightEvent>

A relation of FreightEvents that are of type. Active Record Scope

Returns:

See Also:



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.

Parameters:

  • payload (Hash)

    the full event-callback body

Returns:

  • (Hash)

    FreightEvent attribute hash; event_time stays a string
    for AR coercion, payload is the inner event object only.



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_milestonesActiveRecord::Relation<FreightEvent>

A relation of FreightEvents that are pickup milestones. Active Record Scope

Returns:

See Also:



66
# File 'app/models/freight_event.rb', line 66

scope :pickup_milestones, -> { where(event_type: PICKUP_MILESTONE_TYPES) }

.sinceActiveRecord::Relation<FreightEvent>

A relation of FreightEvents that are since. Active Record Scope

Returns:

See Also:



64
# File 'app/models/freight_event.rb', line 64

scope :since, ->(time) { where('event_time >= ?', time) }

Instance Method Details

#deliveryDelivery

Returns:

See Also:



54
# File 'app/models/freight_event.rb', line 54

belongs_to :delivery, inverse_of: :freight_events, optional: true

#webhook_logWebhookLog

Returns:

See Also:



55
# File 'app/models/freight_event.rb', line 55

belongs_to :webhook_log, optional: true