Class: Shipping::PackagingImporter

Inherits:
BaseService show all
Defined in:
app/services/shipping/packaging_importer.rb

Overview

Converts Packaging records (warehouse-specific "N items fit in box X" rules)
into Packing records so they can be found by the MD5 history lookup in
Shipping::DeterminePackaging.

Background

Packaging is the legacy warehouse-assignment model: it records that up to N
units of a StoreItem fit into a specific WarehousePackage. Shipping::DeterminePackaging
used to fall back to Packaging when no Packing history existed. Now that the
fallback is the 3D-aware Shipping::PackingCalculator, Packaging is no longer
consulted at run-time — but its stacking knowledge (e.g. "25 thermal sheets
fit in a 36×24×3 box") cannot be derived from individual shipping_dimensions
alone, so it must be seeded into the Packing table.

Only records with number_items > 1 are processed. Single-item records are
already covered by Shipping::ItemMd5Extractor which fires on item save.

Quantity coverage

For each Packaging(store_item, warehouse_package, N=number_items):

  • Single-box tier: qty = 1 … N (all fit in one warehouse box)
    • When N > 50, only a representative subset is generated (1, steps of N/5)
  • Multi-box tier: qty = 2N, 3N, 4N, 5N (2–5 full boxes)

Idempotency

Records are inserted but NOT updated if a Packing with the same md5 already
exists. Packings derived from real deliveries (from_delivery, from_shipment)
are authoritative and must not be overwritten.

Examples:

Run once against the database

result = Shipping::PackagingImporter.process
puts "Imported #{result[:imported]}, skipped #{result[:skipped]}"

Constant Summary collapse

SINGLE_BOX_MAX_STEPS =

Maximum number of individual single-box qty steps to generate.

50
MAX_MULTI_BOX_TIERS =

Maximum number of additional multi-box tiers to generate (2×N … MAX_MULTI×N).

5

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #options, #tagged_logger

Constructor Details

This class inherits a constructor from BaseService

Class Method Details

.processObject



43
44
45
# File 'app/services/shipping/packaging_importer.rb', line 43

def self.process
  new.process
end

.process_one(packaging) ⇒ Object

Seed/refresh Packing records for a single Packaging row.
Called by PackagingImportWorker after Packaging saves.



49
50
51
# File 'app/services/shipping/packaging_importer.rb', line 49

def self.process_one(packaging)
  new.process_one(packaging)
end

Instance Method Details

#processObject



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/services/shipping/packaging_importer.rb', line 53

def process

  imported = 0
  skipped  = 0

  # Only multi-item stacking records are missing from Packing.
  scope = Packaging
            .includes(:warehouse_package, store_item: :item)
            .where('number_items > 1')
            .where.not(warehouse_package_id: nil)

  scope.find_each(batch_size: 200) do |packaging|
    item = packaging.store_item&.item
    next unless item&.is_goods?

    wp             = packaging.warehouse_package
    n              = packaging.number_items
    service_type   = Packing.service_types[item.shipping_class]
    next unless service_type

    ct_int = Shipment.container_types[wp.container_type]
    dims   = [wp.length, wp.width, wp.height].sort.reverse.map { |d| d.to_f.ceil(1) }

    quantities_for(n).each do |qty|
      md5_result = Shipping::Md5HashItem.process(item, qty_override: qty)
      next if md5_result.md5.blank?

      packdims, contents = build_packing(item, dims, ct_int, qty, n)
      pdc = Packing.format_packdim_contents(md5: md5_result.md5, contents: contents, packdims: packdims)

      attrs = {
        md5:              md5_result.md5,
        service_type:     service_type,
        origin:           Packing.origins[:from_item],
        packdims:         packdims,
        contents:         contents,
        packdim_contents: pdc.to_json,
        item_id:          item.id,
        delivery_id:      nil,
        shipment_id:      nil,
        created_at:       Time.current,
        updated_at:       Time.current
      }

      existing = Packing.find_by(md5: attrs[:md5], service_type:)
      if existing&.origin.in?(%w[from_delivery from_manual_entry])
        skipped += 1
        next
      end

      # Only count as "imported" if we are creating a brand-new record.
      # Re-runs over already-imported records are silent updates, not new imports,
      # which makes the importer idempotent from a counting perspective.
      was_new = existing.nil?
      upsert_ok = Packing.upsert(attrs, unique_by: :index_packings_on_md5_and_service_type)
      imported += 1 if was_new && upsert_ok
    end
  end

  logger.info "Shipping::PackagingImporter: #{imported} imported, #{skipped} skipped (already existed)"
  { imported:, skipped: }
end

#process_one(packaging) ⇒ Object

Seed/refresh Packing records for a single Packaging row.
Returns the number of Packing records created/updated.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'app/services/shipping/packaging_importer.rb', line 118

def process_one(packaging)
  return 0 unless packaging.number_items.to_i > 1

  item = packaging.store_item&.item
  return 0 unless item&.is_goods?

  wp           = packaging.warehouse_package
  return 0 unless wp

  n            = packaging.number_items
  service_type = Packing.service_types[item.shipping_class]
  return 0 unless service_type

  ct_int = Shipment.container_types[wp.container_type]
  dims   = [wp.length, wp.width, wp.height].sort.reverse.map { |d| d.to_f.ceil(1) }
  count  = 0

  quantities_for(n).each do |qty|
    md5_result = Shipping::Md5HashItem.process(item, qty_override: qty)
    next if md5_result.md5.blank?

    packdims, contents = build_packing(item, dims, ct_int, qty, n)
    pdc = Packing.format_packdim_contents(md5: md5_result.md5, contents: contents, packdims: packdims)
    attrs = {
      md5:              md5_result.md5,
      service_type:     service_type,
      origin:           Packing.origins[:from_item],
      packdims:         packdims,
      contents:         contents,
      packdim_contents: pdc.to_json,
      item_id:          item.id,
      delivery_id:      nil,
      shipment_id:      nil,
      created_at:       Time.current,
      updated_at:       Time.current
    }

    existing = Packing.find_by(md5: attrs[:md5], service_type:)
    next if existing&.origin.in?(%w[from_delivery from_manual_entry])

    count += 1 if Packing.upsert(attrs, unique_by: :index_packings_on_md5_and_service_type)
  end

  logger.info "Shipping::PackagingImporter#process_one: #{count} record(s) for Packaging##{packaging.id}"
  count
end