Class: Shipping::SmallItemBundler

Inherits:
Object
  • Object
show all
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

Class Method Details

.bundle(calc_items) ⇒ Array(Array<Hash>, Hash{Integer=>Integer})

Bundles a list of calc-item hashes (the format PackingCalculator expects).

Parameters:

  • calc_items (Array<Hash>)

    entries with :dimensions, :weight, :item_id, :quantity

Returns:

  • (Array(Array<Hash>, Hash{Integer=>Integer}))

    [reduced_items, real_contents]

    • reduced_items — original list when nothing bundles, otherwise big items + 1 bundle entry
    • real_contents{ item_id => total_qty } for items folded into the bundle, or nil


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).

Parameters:

Returns:



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).

Parameters:

  • entry (Hash{Symbol=>Object})

    a calc-item hash with
    :dimensions (Array in inches), :weight, :item_id,
    :quantity

Returns:

  • (Boolean)

    false when dimensions are blank, zero-valued, or
    exceed either threshold; true otherwise



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