Class: Packing

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/packing.rb

Overview

== Schema Information

Table name: packings
Database name: primary

id :integer not null, primary key
contents :jsonb
description :text
md5 :string(32) not null
origin :enum default("from_delivery"), not null
packdim_contents :jsonb
packdims :decimal(7, 2) is an Array
relevant_md5 :string
service_type :enum default("parcel_service"), not null
created_at :datetime not null
updated_at :datetime not null
delivery_id :integer
item_id :integer
shipment_id :integer

Indexes

idx_service_type (service_type)
index_packings_on_delivery_id (delivery_id)
index_packings_on_item_id (item_id)
index_packings_on_md5_and_service_type (md5,service_type) UNIQUE
index_packings_on_packdim_contents (packdim_contents) USING gin
index_packings_on_shipment_id (shipment_id)

Foreign Keys

fk_rails_... (delivery_id => deliveries.id) ON DELETE => nullify
fk_rails_... (item_id => items.id) ON DELETE => cascade
fk_rails_... (shipment_id => shipments.id) ON DELETE => nullify

Constant Summary collapse

WEIGHT_AUDIT_MAX_SHORTFALL_FRACTION =

Maximum allowable shortfall vs catalog item weight: only flag (or delete in cleanup)
when recorded weight is more than this fraction below expected — avoids
discarding borderline packings (dunnage, rounding, catalog drift). E.g. 0.20 =>
keep when recorded >= 80% of expected.

0.20
WEIGHT_AUDIT_MIN_RATIO =

Minimum ratio: recorded weight must be at least (1 - WEIGHT_AUDIT_MAX_SHORTFALL_FRACTION)
of the sum of item shipping weights to pass.

1.0 - WEIGHT_AUDIT_MAX_SHORTFALL_FRACTION
WEIGHT_AUDIT_MIN_EXPECTED_LBS =

Absolute floor (lbs) below which we do not flag under-weight (tiny parts / missing dims).

0.75
ORIGIN_AUTHORITY =

Origin authority used by the extractors to decide whether incoming write
data should overwrite an existing row. Higher is more authoritative:

from_shipment per-shipment measurement (most granular; legacy
enum value, unused in production — see
Delivery#authoritative_packing_for_shipment_contents?)
from_delivery shipping-station weights (source of truth)
from_manual_entry pre-pack measurements (staff-entered)
from_item catalog dims (single-item history)
from_calculator PackingCalculator estimate

from_shipment is included so existing rows with that origin cannot be
silently overwritten by lower-authority writes — even though no current
code path writes it, the enum value still exists.

{
  'from_shipment'     => 5,
  'from_delivery'     => 4,
  'from_manual_entry' => 3,
  'from_item'         => 2,
  'from_calculator'   => 1
}.freeze
FRESHNESS_WINDOW =

How recently a Packing must have been updated to count as "fresh". A row
past this window can be overwritten by ANY origin — the assumption is
that the underlying item dimensions may have drifted enough that stale
high-authority data is no longer trustworthy.

90.days

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has and belongs to many collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, 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

#md5Object (readonly)



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

validates :md5, :service_type, :origin, presence: true

#originObject (readonly)



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

validates :md5, :service_type, :origin, presence: true

#service_typeObject (readonly)



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

validates :md5, :service_type, :origin, presence: true

Class Method Details

.format_packdim_contents(md5:, contents:, packdims:) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'app/models/packing.rb', line 232

def self.format_packdim_contents(md5:, contents:, packdims:)
  rows = normalize_packdim_rows(packdims)
  return [] if rows.empty?

  if contents.is_a?(Array)
    pdc = []
    rows.each_with_index do |packdim, i|
      c = contents[i]
      pdc << { md5: Shipping::Md5HashItem.process(item_id_hash_to_items(c)).md5, p: packdim.flatten.map { |d| d.is_a?(String) ? d : d.to_f }, c: c }
    end
    pdc
  elsif rows.size > 1
    # Multi-package delivery with a single combined line-item hash: we cannot map
    # items to boxes without per-shipment contents. Emit one row per physical
    # container with correct dims/weight only — do not attach the full item set
    # to one box (that produced bogus 9×5×5 / 2 lb "history" for an ~19 lb order).
    rows.map do |packdim|
      { md5: md5, p: packdim.flatten.map { |d| d.is_a?(String) ? d : d.to_f }, c: {} }
    end
  else
    [{ md5: md5, p: rows.first.flatten.map { |d| d.is_a?(String) ? d : d.to_f }, c: contents }]
  end
end

.from_calculator_containers(containers, md5:, relevant_md5:, service_type:) ⇒ void

This method returns an undefined value.

Persist PackingCalculator containers as a Packing record so future identical
item sets are served from history rather than running the calculator again.

Only overwrites existing from_calculator rows. All other origins
(from_delivery, from_manual_entry, from_item) are considered more
authoritative and are never overwritten by the calculator.

Parameters:

  • containers (Array<Shipping::Container>)
  • md5 (String)

    32-char hex

  • relevant_md5 (String)

    32-char hex

  • service_type (Symbol)

    :parcel_service or :freight_service



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'app/models/packing.rb', line 151

def self.from_calculator_containers(containers, md5:, relevant_md5:, service_type:)
  return if containers.blank? || md5.blank?

  service_type_label = service_types[service_type.to_s]
  return unless service_type_label

  existing = find_by(md5: md5, service_type: service_type_label)
  return if existing && !existing.from_calculator?

  packdims = containers.map do |c|
    # PACKDIM_CONTAINER_INTS preserves the integer wire format used by
    # historic packdim rows for MD5-based dedup — see Shipment#to_packdims.
    ct_int = Shipment::PACKDIM_CONTAINER_INTS[c.container_type.to_s] || 0
    [c.length.to_f, c.width.to_f, c.height.to_f, c.weight.to_f, ct_int]
  end

  raw_contents = containers.map do |c|
    c.contents.to_h { |pc| [pc.item_id, pc.quantity] }
  end
  contents = raw_contents.size == 1 ? raw_contents.first : raw_contents

  pdc = format_packdim_contents(md5: md5, contents: contents, packdims: packdims)

  attrs = {
    md5:              md5,
    relevant_md5:     relevant_md5,
    service_type:     service_type_label,
    origin:           origins[:from_calculator],
    packdims:         packdims,
    contents:         contents,
    packdim_contents: pdc.to_json,
    created_at:       Time.current,
    updated_at:       Time.current
  }

  upsert(attrs, unique_by: :index_packings_on_md5_and_service_type)
rescue StandardError => e
  Rails.logger.warn "Packing.from_calculator_containers: #{e.message}"
end

.item_id_hash_to_items(item_id_hash) ⇒ Object



199
200
201
202
203
# File 'app/models/packing.rb', line 199

def self.item_id_hash_to_items(item_id_hash)
  item_id_hash.each_with_object({}) do |(k, v), h|
    h[Item.find(k)] = v
  end
end

.md5_nested_findActiveRecord::Relation<Packing>

A relation of Packings that are md5 nested find. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Packing>)

See Also:



84
# File 'app/models/packing.rb', line 84

scope :md5_nested_find, ->(md5) { where("packdim_contents @> '[{\"md5\": \"#{md5}\"}]'") }

.normalize_packdim_rows(packdims) ⇒ Object

Normalizes packdims to an array of per-container rows [L,W,H,Wt,(ct)].
Callers may pass either [[L,W,H,Wt], ...] (delivery) or a single flat row
from Shipment#to_packdims (one box).



208
209
210
211
212
213
214
215
216
# File 'app/models/packing.rb', line 208

def self.normalize_packdim_rows(packdims)
  return [] if packdims.blank?

  if packdims.first.is_a?(Array)
    packdims
  else
    [packdims]
  end
end

.packing_for_item_id(item_id) ⇒ Object



90
91
92
# File 'app/models/packing.rb', line 90

def self.packing_for_item_id(item_id)
  Packing.order(id: :desc).find_by(item_id: item_id)
end

.ransackable_scopes(_auth_object = nil) ⇒ Object



86
87
88
# File 'app/models/packing.rb', line 86

def self.ransackable_scopes(_auth_object = nil)
  [:md5_nested_find]
end

.sum_items_shipping_weight_lbs(item_id_qty_hash) ⇒ Object

Sums expected lbs from an { item_id => qty } hash (exploded kit components).



219
220
221
222
223
224
225
226
227
228
229
230
# File 'app/models/packing.rb', line 219

def self.sum_items_shipping_weight_lbs(item_id_qty_hash)
  return 0.0 if item_id_qty_hash.blank?

  ids = item_id_qty_hash.keys.map(&:to_i)
  items_by_id = Item.where(id: ids).index_by(&:id)
  item_id_qty_hash.sum do |item_id, qty|
    item = items_by_id[item_id.to_i]
    next 0.0 unless item

    item.total_shipping_weight.to_f * qty.to_i
  end
end

.upsert(attributes, on_duplicate: :update, returning: nil, unique_by: nil, record_timestamps: nil) ⇒ Object

Upsert skips ActiveRecord validations; enforce the same weight audit here.
Pass skip_weight_audit: true only for tests or exceptional cases.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/models/packing.rb', line 96

def self.upsert(attributes, on_duplicate: :update, returning: nil, unique_by: nil, record_timestamps: nil)
  attrs = attributes.symbolize_keys
  skip = attrs.delete(:skip_weight_audit) == true

  if !skip && weight_audit_violation_for_upsert_attrs?(attrs)
    Rails.logger.warn(
      "Packing upsert skipped (weight audit): md5=#{attrs[:md5].inspect}" \
      "#{weight_audit_messages_for_upsert_attrs(attrs).join('; ')}"
    )
    return nil
  end

  super(attrs, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by, record_timestamps: record_timestamps)
end

Instance Method Details

#authoritative_over?(incoming_origin) ⇒ Boolean

Note:

An unknown incoming_origin is treated as rank 0, so any fresh
known-origin Packing will block it.

Should this existing Packing block an incoming write tagged with the
given origin?

Logic Details

Returns true only when both:

  1. #fresh? — this row is recent enough to still be trusted, AND
  2. #origin_rank strictly exceeds the incoming origin's rank

Peer-level overwrites (equal rank) are allowed: the newest measurement
of the same authority tier wins. Replaces the legacy
"newer-source-delivery wins" check on created_at, which broke when
re-quotes brought older deliveries back into pre-pack with better
measurements.

Parameters:

  • incoming_origin (String, Symbol)

    a origins key

Returns:

  • (Boolean)

    true to skip the write, false to allow it



355
356
357
358
359
# File 'app/models/packing.rb', line 355

def authoritative_over?(incoming_origin)
  return false unless fresh?

  origin_rank > (ORIGIN_AUTHORITY[incoming_origin.to_s] || 0)
end

#contents_to_itemsObject



195
196
197
# File 'app/models/packing.rb', line 195

def contents_to_items
  self.class.item_id_hash_to_items((contents.is_a?(Array) ? contents.reduce(&:merge) : contents))
end

#deliveryDelivery

Returns:

See Also:



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

belongs_to :delivery, optional: true

#fresh?Boolean

Note:

Returns false when updated_at is nil (new, unsaved records).

Whether this Packing has been touched within FRESHNESS_WINDOW.

Stale rows cannot block a lower-authority incoming write; see
#authoritative_over? for how this is used.

Returns:

  • (Boolean)


333
334
335
# File 'app/models/packing.rb', line 333

def fresh?
  updated_at.present? && updated_at > FRESHNESS_WINDOW.ago
end

#itemItem

Returns:

See Also:



56
# File 'app/models/packing.rb', line 56

belongs_to :item, optional: true

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

has_and_belongs_to_many :items, inverse_of: :packings


191
192
193
# File 'app/models/packing.rb', line 191

def link_items
  self.item_ids = (contents.is_a?(Array) ? contents.map(&:keys).flatten.uniq : contents.keys.uniq)
end

#measured_weight?Boolean

from_delivery weights are measured at the shipping station — they are the
source of truth. Other origins (calculator, item catalog, pre-pack) use
estimates or staff-entered values that may not reflect actual box weight.

Returns:

  • (Boolean)


280
281
282
# File 'app/models/packing.rb', line 280

def measured_weight?
  from_delivery?
end

#origin_rankInteger

The authority rank for this packing's origin.

Logic Details

Looks up origin (a Packing.origins enum string) in ORIGIN_AUTHORITY.
Unknown origins return 0 so any caller using > comparisons treats
them as the least-authoritative bucket.

Returns:

  • (Integer)

    0..5



322
323
324
# File 'app/models/packing.rb', line 322

def origin_rank
  ORIGIN_AUTHORITY[origin.to_s] || 0
end

#packaging_discrepancy_itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



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

has_many :packaging_discrepancy_items, class_name: 'Item', foreign_key: 'packaging_discrepancy_packing_id', dependent: :nullify

#shipmentShipment

Returns:

See Also:



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

belongs_to :shipment, optional: true

#to_packages(md5 = nil) ⇒ Object

Represent this packing as boxes, if you pass an md5, only that package will be used



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'app/models/packing.rb', line 257

def to_packages(md5 = nil)
  packages = []
  if packdim_contents.present?
    packdim_contents.each do |pc|
      # Skip this one if the md5 doesn't match, only in case when we do individual package matching
      next unless md5.nil? || pc['md5'] == md5

      le, wi, he, we, ct = pc['p']
      package = Shipping::Container.new(length: le, width: wi, height: he, weight: we, container_type: Shipment.container_type_from_packdim_int(ct))
      (pc['c'] || {}).each do |item_id, quantity|
        package.add_content(item_id, quantity)
      end
      packages << package
    end
  else
    packages = packdims.map { |(le, wi, he, we, ct)| Shipping::Container.new(length: le, width: wi, height: he, weight: we, container_type: Shipment.container_type_from_packdim_int(ct)) }
  end
  packages
end

#to_partial_pathObject

Partial lives under app/views/crm/packings/, not the conventional
app/views/packings/. Override so render @packings resolves correctly
from any controller, not just Crm::PackingsController.



43
44
45
# File 'app/models/packing.rb', line 43

def to_partial_path
  'crm/packings/packing'
end

#weight_audit_failure?Boolean

True when recorded weights are implausible vs catalog item weights (used by data cleanup).

Returns:

  • (Boolean)


362
363
364
# File 'app/models/packing.rb', line 362

def weight_audit_failure?
  weight_audit_messages.any?
end

#weight_audit_messagesObject



366
367
368
369
370
371
# File 'app/models/packing.rb', line 366

def weight_audit_messages
  msgs = []
  msgs.concat(weight_audit_messages_for_packdims)
  msgs.concat(weight_audit_messages_for_packdim_contents)
  msgs
end