Class: Shipping::SmallItemBundler
- Inherits:
-
Object
- Object
- Shipping::SmallItemBundler
- Defined in:
- app/services/shipping/small_item_bundler.rb
Overview
Bundles "small accessory" items into a single virtual entry before they reach
Shipping::PackingCalculator. Replaces N tiny items (thermostats, sensors,
screws, ...) that would each open their own carton with one bundle carton
sized to the largest accessory.
Port of Ramie Blatt's small-item detection from the legacy
Packaging.use_shipping_dimensions? / get_packaging_for_line_items pipeline
(https://app.shortcut.com/warmlyyours/story/N — see Packaging#195-187). That
code is dead (no live callers) since the cascade moved to
Shipping::DeterminePackaging → PackingCalculator. This service revives the
bucketing for the modern pipeline.
Heuristic
An item is a "small accessory" when its shipping volume is ≤ SMALL_VOLUME_CUIN
AND no single shipping dimension exceeds SMALL_MAX_DIM_INCHES. Mirrors the
legacy predicate, just inverted (legacy returned false for these).
Bundling
When the summed weight of all small items fits in one PackingCalculator
carton (PER_BOX_WEIGHT_LIMIT), they collapse into one synthetic calc entry
whose dimensions are the largest individual small accessory's dims (by
volume). The real item-id → quantity map is returned alongside so the
caller can rehydrate the resulting Shipping::Container's contents after the
calculator runs.
When the summed weight exceeds the per-box limit, we don't bundle — the
original entries are returned unchanged so the calculator can split by
weight without losing per-item attribution.
Constant Summary collapse
- SMALL_VOLUME_CUIN =
Volume threshold below which an item is a candidate for bundling.
Mirrors Packaging.use_shipping_dimensions? (vol > 250 cu in OR dim > 20"). 250- SMALL_MAX_DIM_INCHES =
Max single dimension before an item is considered "big" regardless of volume.
20- BUNDLE_SENTINEL_ITEM_ID =
Sentinel item_id used on the synthetic bundle Unit. Negative so it can
never collide with a real Item#id (PK is positive int). Callers identify
the bundle container by scanning contents for this id. -1
Class Method Summary collapse
-
.bundle(calc_items) ⇒ Array(Array<Hash>, Hash{Integer=>Integer})
Bundles a list of calc-item hashes (the format PackingCalculator expects).
-
.rehydrate_contents(packages, real_contents) ⇒ Array<Shipping::Container>
Replaces the sentinel content on whichever Shipping::Container the bundle landed in with the actual small-item contents.
-
.small_accessory?(entry) ⇒ Boolean
Whether a calc-item hash qualifies as a "small accessory" — eligible for collapse into the bundle carton.
Class Method Details
.bundle(calc_items) ⇒ Array(Array<Hash>, Hash{Integer=>Integer})
Bundles a list of calc-item hashes (the format PackingCalculator expects).
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'app/services/shipping/small_item_bundler.rb', line 57 def self.bundle(calc_items) smalls, bigs = partition(calc_items) return [calc_items, nil] if smalls.empty? total_weight = smalls.sum { |e| e[:weight].to_f * e[:quantity].to_i } return [calc_items, nil] if total_weight > Shipping::PackingCalculator::PER_BOX_WEIGHT_LIMIT bundle_dims = smalls.max_by { |e| e[:dimensions].reduce(:*) }[:dimensions] bundle_entry = { dimensions: bundle_dims, weight: total_weight, item_id: BUNDLE_SENTINEL_ITEM_ID, quantity: 1 } real_contents = smalls.each_with_object(Hash.new(0)) do |e, h| h[e[:item_id]] += e[:quantity].to_i end [bigs + [bundle_entry], real_contents] end |
.rehydrate_contents(packages, real_contents) ⇒ Array<Shipping::Container>
Replaces the sentinel content on whichever Shipping::Container the bundle
landed in with the actual small-item contents. Mutates and returns
packages (idempotent — runs only on containers carrying the sentinel).
86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'app/services/shipping/small_item_bundler.rb', line 86 def self.rehydrate_contents(packages, real_contents) return packages if real_contents.blank? packages.each do |pkg| sentinel = pkg.contents.find { |c| c.item_id == BUNDLE_SENTINEL_ITEM_ID } next unless sentinel pkg.contents.delete(sentinel) real_contents.each { |item_id, qty| pkg.add_content(item_id, qty) } end packages end |
.small_accessory?(entry) ⇒ Boolean
Whether a calc-item hash qualifies as a "small accessory" — eligible
for collapse into the bundle carton.
Logic Details
An entry is small when its shipping volume is <= SMALL_VOLUME_CUIN
(250 cu in) AND every individual dimension is <= SMALL_MAX_DIM_INCHES
(20 in). Mirrors the legacy Packaging.use_shipping_dimensions?
predicate, just inverted (legacy returned false for these).
113 114 115 116 117 118 119 120 121 |
# File 'app/services/shipping/small_item_bundler.rb', line 113 def self.small_accessory?(entry) dims = entry[:dimensions] return false if dims.blank? vol = dims.reduce(:*).to_f return false unless vol.positive? vol <= SMALL_VOLUME_CUIN && dims.all? { |d| d.to_f <= SMALL_MAX_DIM_INCHES } end |