Class: Packing
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Packing
- 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 estimatefrom_shipmentis 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
- #md5 ⇒ Object readonly
- #origin ⇒ Object readonly
- #service_type ⇒ Object readonly
Belongs to collapse
Methods included from Models::Auditable
Has and belongs to many collapse
Has many collapse
Class Method Summary collapse
- .format_packdim_contents(md5:, contents:, packdims:) ⇒ Object
-
.from_calculator_containers(containers, md5:, relevant_md5:, service_type:) ⇒ void
Persist PackingCalculator containers as a Packing record so future identical item sets are served from history rather than running the calculator again.
- .item_id_hash_to_items(item_id_hash) ⇒ Object
-
.md5_nested_find ⇒ ActiveRecord::Relation<Packing>
A relation of Packings that are md5 nested find.
-
.normalize_packdim_rows(packdims) ⇒ Object
Normalizes packdims to an array of per-container rows [L,W,H,Wt,(ct)].
- .packing_for_item_id(item_id) ⇒ Object
- .ransackable_scopes(_auth_object = nil) ⇒ Object
-
.sum_items_shipping_weight_lbs(item_id_qty_hash) ⇒ Object
Sums expected lbs from an { item_id => qty } hash (exploded kit components).
-
.upsert(attributes, on_duplicate: :update, returning: nil, unique_by: nil, record_timestamps: nil) ⇒ Object
Upsert skips ActiveRecord validations; enforce the same weight audit here.
Instance Method Summary collapse
-
#authoritative_over?(incoming_origin) ⇒ Boolean
Should this existing Packing block an incoming write tagged with the given origin?.
- #contents_to_items ⇒ Object
-
#fresh? ⇒ Boolean
Whether this Packing has been touched within FRESHNESS_WINDOW.
- #link_items ⇒ Object
-
#measured_weight? ⇒ Boolean
from_delivery weights are measured at the shipping station — they are the source of truth.
-
#origin_rank ⇒ Integer
The authority rank for this packing's origin.
-
#to_packages(md5 = nil) ⇒ Object
Represent this packing as boxes, if you pass an md5, only that package will be used.
-
#to_partial_path ⇒ Object
Partial lives under
app/views/crm/packings/, not the conventionalapp/views/packings/. -
#weight_audit_failure? ⇒ Boolean
True when recorded weights are implausible vs catalog item weights (used by data cleanup).
- #weight_audit_messages ⇒ Object
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
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#md5 ⇒ Object (readonly)
64 |
# File 'app/models/packing.rb', line 64 validates :md5, :service_type, :origin, presence: true |
#origin ⇒ Object (readonly)
64 |
# File 'app/models/packing.rb', line 64 validates :md5, :service_type, :origin, presence: true |
#service_type ⇒ Object (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.
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.}" 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_find ⇒ ActiveRecord::Relation<Packing>
A relation of Packings that are md5 nested find. Active Record Scope
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} — " \ "#{(attrs).join('; ')}" ) return nil end super(attrs, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by, record_timestamps: ) end |
Instance Method Details
#authoritative_over?(incoming_origin) ⇒ Boolean
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:
- #fresh? — this row is recent enough to still be trusted, AND
- #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.
355 356 357 358 359 |
# File 'app/models/packing.rb', line 355 def (incoming_origin) return false unless fresh? origin_rank > (ORIGIN_AUTHORITY[incoming_origin.to_s] || 0) end |
#contents_to_items ⇒ Object
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 |
#delivery ⇒ Delivery
55 |
# File 'app/models/packing.rb', line 55 belongs_to :delivery, optional: true |
#fresh? ⇒ Boolean
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.
333 334 335 |
# File 'app/models/packing.rb', line 333 def fresh? updated_at.present? && updated_at > FRESHNESS_WINDOW.ago end |
#items ⇒ ActiveRecord::Relation<Item>
57 |
# File 'app/models/packing.rb', line 57 has_and_belongs_to_many :items, inverse_of: :packings |
#link_items ⇒ Object
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.
280 281 282 |
# File 'app/models/packing.rb', line 280 def measured_weight? from_delivery? end |
#origin_rank ⇒ Integer
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.
322 323 324 |
# File 'app/models/packing.rb', line 322 def origin_rank ORIGIN_AUTHORITY[origin.to_s] || 0 end |
#packaging_discrepancy_items ⇒ ActiveRecord::Relation<Item>
58 |
# File 'app/models/packing.rb', line 58 has_many :packaging_discrepancy_items, class_name: 'Item', foreign_key: 'packaging_discrepancy_packing_id', dependent: :nullify |
#shipment ⇒ Shipment
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_path ⇒ Object
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).
362 363 364 |
# File 'app/models/packing.rb', line 362 def weight_audit_failure? .any? end |
#weight_audit_messages ⇒ Object
366 367 368 369 370 371 |
# File 'app/models/packing.rb', line 366 def msgs = [] msgs.concat() msgs.concat() msgs end |