Module: Models::ShipQuotable

Extended by:
ActiveSupport::Concern
Includes:
ShipMeasurable
Included in:
Order, Quote
Defined in:
app/concerns/models/ship_quotable.rb

Overview

ActiveSupport::Concern mixin for models that can be quoted for shipping.

Provides associations, calculations, and workflows for determining shipping
costs, delivery groupings, freight readiness, and pre-pack requirements.

See Also:

Belongs to collapse

Has many collapse

Instance Method Summary collapse

Methods included from ShipMeasurable

#cartons_total, #crates_total, #pallets_total, #ship_freight_class_from_shipments, #ship_volume_from_shipments, #ship_volume_from_shipments_in_cubic_feet, #ship_weight_from_shipments, #shipment_set, #shipments_for_measure

Instance Method Details

#carrierString?

Returns the carrier of the first delivery's shipping method.

Returns:

  • (String, nil)

    the carrier of the first delivery's shipping method



486
487
488
# File 'app/concerns/models/ship_quotable.rb', line 486

def carrier
  deliveries.first&.carrier
end

#chosen_shipping_methodShippingCost?

Returns the selected shipping cost of the first delivery.

Returns:

  • (ShippingCost, nil)

    the selected shipping cost of the first delivery



481
482
483
# File 'app/concerns/models/ship_quotable.rb', line 481

def chosen_shipping_method
  deliveries.first&.chosen_shipping_method
end

#days_commitment(use = :least) ⇒ Integer

Days commitment across deliveries.

Parameters:

  • use (Symbol) (defaults to: :least)

    :least for the shortest commitment, otherwise :greatest

Returns:

  • (Integer)

    the selected days commitment



457
458
459
460
461
462
463
# File 'app/concerns/models/ship_quotable.rb', line 457

def days_commitment(use = :least)
  if use == :least
    deliveries.map { |dq| dq.selected_shipping_cost&.days_commitment || 3 }.min
  else
    deliveries.map { |dq| dq.selected_shipping_cost&.days_commitment || 3 }.max
  end
end

#deliveriesActiveRecord::Relation<Delivery>

Delivery records grouped by fulfillment origin for this quotable.

Returns:

See Also:



20
# File 'app/concerns/models/ship_quotable.rb', line 20

has_many :deliveries, -> { order(:origin_address_id) }, inverse_of: name.tableize.singularize.to_sym, autosave: true, dependent: :destroy

#determine_origin_address(line_item) ⇒ Address

Determine the origin address to use for a given line item.

Parameters:

  • line_item (LineItem)

    the line item whose origin is being determined

Returns:

  • (Address)

    the address to ship the line item from



159
160
161
162
163
164
# File 'app/concerns/models/ship_quotable.rb', line 159

def determine_origin_address(line_item)
  # credit orders should always use the shipping address of the warehouse, in case one of the items was previously shipped from a different location, and STs should always use the shipping address of the store warehouse pending a better solution to multiple warehouse stores (like Amazon)
  return store.warehouse_address if is_a?(Order) && ((order_type == 'CO') || (order_type == 'ST'))

  shipping_address.try(:is_warehouse) || single_origin ? store.warehouse_address : line_item.fulfillment_origin_address
end

#everything_in_stock?Boolean

Returns true if all line items are in stock.

Returns:

  • (Boolean)

    true if all line items are in stock



466
467
468
# File 'app/concerns/models/ship_quotable.rb', line 466

def everything_in_stock?
  LineItem.inventory_check(self) == :ok
end

#friendly_shipping_method(show_customer_pays_info = false, for_www = false, with_delivery_commitment = false) ⇒ String

Human-readable description of the selected shipping method(s).

Parameters:

  • show_customer_pays_info (Boolean) (defaults to: false)

    whether to include customer payment info

  • for_www (Boolean) (defaults to: false)

    whether to format the description for the public site

  • with_delivery_commitment (Boolean) (defaults to: false)

    whether to include delivery commitment

Returns:

  • (String)

    comma-separated friendly shipping method descriptions



420
421
422
423
424
425
426
# File 'app/concerns/models/ship_quotable.rb', line 420

def friendly_shipping_method(show_customer_pays_info = false, for_www = false, with_delivery_commitment = false)
  if show_customer_pays_info
    @friendly_shipping_method_with_pay_info ||= retrieve_friendly_shipping_method(show_customer_pays_info, for_www, with_delivery_commitment)
  else
    @friendly_shipping_method ||= retrieve_friendly_shipping_method(show_customer_pays_info, for_www, with_delivery_commitment)
  end
end

#is_drop_ship?Boolean

Returns true if a one-time shipping address is present.

Returns:

  • (Boolean)

    true if a one-time shipping address is present



182
183
184
# File 'app/concerns/models/ship_quotable.rb', line 182

def is_drop_ship?
  one_time_shipping_address.present?
end

#is_override?Boolean

Returns true if any delivery has an overridden shipping method.

Returns:

  • (Boolean)

    true if any delivery has an overridden shipping method



187
188
189
# File 'app/concerns/models/ship_quotable.rb', line 187

def is_override?
  deliveries.any?(&:override_shipping_method?)
end

#is_warehouse_pickup?Boolean

Returns true if any delivery is a warehouse pickup.

Returns:

  • (Boolean)

    true if any delivery is a warehouse pickup



543
544
545
# File 'app/concerns/models/ship_quotable.rb', line 543

def is_warehouse_pickup?
  deliveries.any?(&:is_warehouse_pickup?)
end

#line_items_grouped_by_deliveries_quoting(group_by_item_category = true) ⇒ Hash{Delivery => Array<LineItem>}

Group non-shipping line items by their quoting delivery.

Parameters:

  • group_by_item_category (Boolean) (defaults to: true)

    whether to further group line items
    within each delivery by item category

Returns:



522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
# File 'app/concerns/models/ship_quotable.rb', line 522

def line_items_grouped_by_deliveries_quoting(group_by_item_category = true)
  items = {}
  # group by origin address (warehouse pickups all have the same origin/destination address)
  deliveries.quoting.to_a.sort_by(&:origin_address_id).each do |dq|
    items[dq] ||= [] # Required to set order
  end
  line_items.parents_only.non_shipping.includes([{ catalog_item: { store_item: :item } }, :item, :delivery, { children: :item }]).to_a.each do |li|
    dq = li.delivery
    items[dq] ||= []
    items[dq] << li
    if dq.nil? && Rails.env.production?
      logger.error "ship_quotable#line_items_grouped_by_deliveries_quoting had non shipping line item(s) with delivery nil #{self.class} ID: #{id}, line item ID: #{li.id}, Line Item:\n#{li.inspect}"
      # Mailer.admin_notification("ship_quotable#line_items_grouped_by_deliveries_quoting had non shipping line item(s) with delivery nil #{self.class} ID: #{self.id}, line item ID: #{li.id}","Line Item:\n#{li.inspect}").deliver_now
    end
  end

  items.each { |key, lines| items[key] = LineItem.group_by_category(lines) } if group_by_item_category
  items
end

#line_items_match_deliveries_if_anyBoolean

Verify that quoting deliveries cover the same line items as the quotable.

Returns:

  • (Boolean)

    true when line items match, false otherwise



559
560
561
562
563
564
565
566
567
568
569
# File 'app/concerns/models/ship_quotable.rb', line 559

def line_items_match_deliveries_if_any
  res = true
  quoting_deliveries = deliveries.select?(&:quoting?)
  quoting_deliveries_line_items = quoting_deliveries.map { |dq| dq.line_items.active_goods_lines }.flatten
  if quoting_deliveries.present? &&
     (md5_hash_items != md5_hash_items(quoting_deliveries_line_items))
    res = false
    errors.add(:base, "line items don't match all deliveries.select{|d| d.state == 'quoting'} line items")
  end
  res
end

#need_to_pre_pack_reasonsArray<String>

Reasons the order should stop for warehouse pre-packing verification.

Returns:

  • (Array<String>)

    human-readable reasons, empty when pre-pack is not
    required



581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'app/concerns/models/ship_quotable.rb', line 581

def need_to_pre_pack_reasons
  res = []
  weight_key = :package
  weight_key = :ltl if deliveries.any?(&:ships_ltl_freight?)
  # return [] if all shipments packed
  return res if deliveries.all? { |d| d.shipments.all?(&:packed?) }
  # return [] if we are dealing with a single pallet delivery of less than 400lbs, even a legacy suggested pallet is a pretty decent guess, and skips a lot of back and forth on quote revisions or tweaks of some accessories etc, no need to bother warehouse
  return res if deliveries.size == 1 && deliveries.first.shipments.size == 1 && deliveries.first.shipments.first.pallet? && (ship_weight <= PRE_PACK_SHIPPING_WEIGHT_THRESHOLD[weight_key])

  # Skip pre-pack only when EVERY delivery's packing comes from a TRUSTED source:
  # actual history — from_delivery / from_item / from_manual_entry — i.e. a human
  # packed this combination before.
  #
  # ⚠️ DO NOT add algorithmic sources (packing_calculator, item_group_packaging,
  # freight_calculator / legacy_packaging) to this list. They are ESTIMATES, and
  # the pre-pack threshold checks below are exactly the sanity net that catches
  # them when they go wrong. 52f9640462 (2026-02-24) added packing_calculator
  # here to fix small quotes wrongly stopping for pre-pack, which silently
  # disabled every threshold — quote 608296 (2026-08-04) then shipped a ~50-carton
  # calculator plan straight past the "too many suggested packages" stop.
  # Regression coverage: test/concerns/models/ship_quotable_pre_pack_package_count_test.rb.
  # Fix over-eager pre-pack stops by tuning the PRE_PACK_* thresholds in
  # config/initializers/shipping.rb, never by widening this trust list.
  packing_solutions = deliveries.map { |d| Shipping::DeterminePackaging.new.process(delivery: d, is_freight: d.ships_ltl_freight?) }
  # Even trusted history only holds while the box plan is sane — more than
  # PRE_PACK_NUM_PACKAGES_THRESHOLD containers in a delivery falls through
  # to the threshold checks below.
  return res if packing_solutions.all? { |packing_solution| packing_solution.present? && %w[from_delivery from_item from_manual_entry].include?(packing_solution.source_type.to_s) } &&
                deliveries.none? { |d| d.shipments.count > PRE_PACK_NUM_PACKAGES_THRESHOLD }

  # eliminate any custom or drop-ship items (e.g. custom LED mirrors or custom mats) since our warehouse won't know how to estimate packaging, better to use specialized suggested shipping
  if line_items.none?(&:dropship?) && !is_warehouse_pickup? && ((calculate_shipping_cost > PRE_PACK_SHIPPING_COST_THRESHOLD) || (ship_weight > PRE_PACK_SHIPPING_WEIGHT_THRESHOLD[weight_key]) || (days_commitment < PRE_PACK_DAYS_COMMITMENT_THRESHOLD) || # tweak to pair too many packages criteria with a high enough shipping cost
deliveries.any? do |d|
d.shipments.count > PRE_PACK_NUM_PACKAGES_THRESHOLD && calculate_shipping_cost > 0.5 * PRE_PACK_SHIPPING_COST_THRESHOLD
end)
    res << "Shipping cost of $#{calculate_shipping_cost.round(2)} is too high: exceeds current threshold of $#{PRE_PACK_SHIPPING_COST_THRESHOLD.round(2)} for suggested packaging" if calculate_shipping_cost > PRE_PACK_SHIPPING_COST_THRESHOLD
    res << "Shipping weight of #{ship_weight.round} lbs is too high:  exceeds current threshold of #{PRE_PACK_SHIPPING_WEIGHT_THRESHOLD[weight_key].round} lbs for suggested packaging" if ship_weight > PRE_PACK_SHIPPING_WEIGHT_THRESHOLD[weight_key]
    res << 'Expedited/Express shipping speed needs accurate non-suggested packaging' if days_commitment < PRE_PACK_DAYS_COMMITMENT_THRESHOLD && 2 * calculate_shipping_cost > PRE_PACK_SHIPPING_COST_THRESHOLD
    res << "Too many suggested packages: exceeds current threshold of #{PRE_PACK_NUM_PACKAGES_THRESHOLD}" if deliveries.any? do |d|
      d.shipments.count > PRE_PACK_NUM_PACKAGES_THRESHOLD && calculate_shipping_cost > 0.5 * PRE_PACK_SHIPPING_COST_THRESHOLD
    end
  end
  res
end

#need_to_recalculate_shippingBoolean

Mark shipping for recalculation and re-enable shipping detection.

Returns:

  • (Boolean)

    the value assigned to recalculate_shipping



441
442
443
444
# File 'app/concerns/models/ship_quotable.rb', line 441

def need_to_recalculate_shipping
  self.recalculate_shipping = true
  self.do_not_detect_shipping = false
end

#one_time_shipping_addressAddress?

The shipping address when it is flagged as one-time-only.

Returns:

  • (Address, nil)

    the one-time shipping address, if present and one-time-only



177
178
179
# File 'app/concerns/models/ship_quotable.rb', line 177

def one_time_shipping_address
  shipping_address if shipping_address&.one_time_only
end

#one_time_shipping_address=(val) ⇒ Address?

Assign a one-time shipping address.

Parameters:

  • val (Address, nil)

    the address to assign

Returns:

  • (Address, nil)

    the assigned shipping address



170
171
172
# File 'app/concerns/models/ship_quotable.rb', line 170

def one_time_shipping_address=(val)
  self.shipping_address = val
end

#qualifies_for_cod?Boolean

Returns true if the order qualifies for cash-on-delivery.

Returns:

  • (Boolean)

    true if the order qualifies for cash-on-delivery



471
472
473
474
475
476
477
478
# File 'app/concerns/models/ship_quotable.rb', line 471

def qualifies_for_cod?
  # we limit COD orders to single delivery origin domestic orders from one of our warehouses
  true if (begin
    deliveries.quoting.one? && deliveries.quoting.first.origin_address.is_warehouse? && (deliveries.quoting.first.origin_address.country_iso3 == deliveries.quoting.first.destination_address.country_iso3)
  rescue StandardError
    false
  end) && line_items.goods.any?
end

#refresh_deliveries_quoting(include_shipping_lines = false) ⇒ ActiveRecord::Relation<Delivery>

Rebuild the quoting deliveries for the current line items and shipping address.

Used mostly during quote-to-order conversion so shipping lines are preserved.

Parameters:

  • include_shipping_lines (Boolean) (defaults to: false)

    whether to include existing shipping line items

Returns:

  • (ActiveRecord::Relation<Delivery>)

    the rebuilt quoting deliveries



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'app/concerns/models/ship_quotable.rb', line 222

def refresh_deliveries_quoting(include_shipping_lines = false)
  logger.tagged "#{self.class.name}/#{id}: ShipQuotable#refresh_deliveries_quoting" do
    logger.debug('refresh_deliveries_quoting',
                 include_shipping_lines: include_shipping_lines,
                 shipping_address_id: shipping_address_id,
                 quoting_delivery_count: deliveries.count { |d| d.state == 'quoting' })
    return [] unless shipping_address.present? || installation_postal_code.present? # just skip if no shipping address or installation postal code

    cod_collection_type_to_use = nil
    if respond_to?(:cod_collection_type)
      self.cod_collection_type = nil unless qualifies_for_cod?
      cod_collection_type_to_use = cod_collection_type
    end
    # Reload deliveries from DB so we see any deliveries created by a concurrent process.
    # Without this, a stale in-memory association can miss deliveries, leaving orphan
    # duplicates that each carry their own shipping charge (see quote 601367).
    deliveries.reload unless new_record?

    # Keep a reference to the old delivery quotes now we will delete them later
    # Keep track of the old delivery quotes attributes, they will be used to merge into the new one.
    # IMPORTANT: ensure we skip the "id" attribute or it will run afoul of the unique delivery_id_idx constraint!
    quoting_deliveries = deliveries.select { |d| (d.quoting? || d.pre_pack?) && !d.marked_for_destruction? && d.persisted? }
    # Remapped object arrays
    remapped_pre_payment_ids = []
    remapped_shipment_ids = []
    old_override_attributes = []
    # Payment ids grouped by their old delivery's origin address. The
    # rebuilt deliveries below reclaim only the payments authorized against
    # their own origin; a blanket remap would instead dump every payment
    # onto whichever delivery is built last, leaving the others with a
    # phantom balance and the order stuck in pending_payment (SO728798).
    payments_by_origin = Hash.new { |h, k| h[k] = [] }
    old_deliveries_quoting_attributes = quoting_deliveries.map do |dq|
      remapped_pre_payment_ids += dq.payments.ids
      payments_by_origin[dq.origin_address_id] += dq.payments.ids
      remapped_shipment_ids += dq.shipments.packed.ids unless dq.is_smart_service?
      if dq.selected_shipping_cost.present? && dq.selected_shipping_cost.is_override? && !dq.is_warehouse_pickup? # Here we want to preserve a manually selected override, but not warehouse pickups
        old_override_attributes << {
                  origin_address_id: dq.origin_address_id,
                  cost: dq.selected_shipping_cost.cost,
                  description_override: dq.selected_shipping_cost.description_override
                }
      end
      # Dupe removes the id which is what you want
      if dq.order&.shipment_reference_number.blank? # Here we want to copy the shipment reference (or FBA ID) to carrier_bol when present
        dq.dup.attributes.merge(carrier_bol: dq.order&.shipment_reference_number)
      else
        dq.dup.attributes
      end
    end
    if remapped_pre_payment_ids.empty? && respond_to?(:payment_ids) && payment_ids.present?
      # Let's just assume each payment gets mapped
      payment_ids.each { |pid| remapped_pre_payment_ids << pid }
    end
    remapped_pre_payment_ids.uniq!
    remapped_shipment_ids.uniq!
    # Unmap those pre payments and shipments from their original deliveries from a db point of view
    Payment.where(id: remapped_pre_payment_ids).update_all(delivery_id: nil) if remapped_pre_payment_ids.present?
    Shipment.where(id: remapped_shipment_ids).update_all(delivery_id: nil) if remapped_shipment_ids.present?

    # Kill the quoting deliveries! cascading foreign key might not do things in the right order, so manually
    # do it
    quoting_deliveries.each do |qd|
      logger.debug('Removing quoting delivery', delivery_id: qd.id)
      next unless qd.id && (qd = Delivery.where(id: qd.id).first).present? # Fresh associations if found

      qd.shipments.destroy_all # We disassociated earlier the one that matters; destroy_all runs Shipment#destroy so shipment_events nullify (FK is RESTRICT)
      qd.shipping_costs.each do |sc|
        sc.line_items.delete_all
        sc.delete
      end
      qd.delete
    end

    logger.debug('Processing deliveries', old_delivery_count: old_deliveries_quoting_attributes.size)

    if shipping_address || installation_postal_code
      if include_shipping_lines
        line_items_to_use = line_items.active_lines
      else
        line_items_to_use = line_items.active_non_shipping_lines # IMPORTANT, if you use scope you will lose any line items that have not yet been saved
        line_items.active_shipping_lines.each(&:destroy)
      end

      # IMPORTANT: Only process line items that are NOT already assigned to a non-quoting delivery.
      # This prevents orphaning deliveries that have already transitioned to awaiting_po_fulfillment
      # or other non-quoting states (which may have POs or other commitments).
      non_quoting_delivery_ids = deliveries.reject { |d| d.quoting? || d.pre_pack? || d.new_record? }.map(&:id)
      if non_quoting_delivery_ids.any?
        line_items_to_use = line_items_to_use.reject { |li| non_quoting_delivery_ids.include?(li.delivery_id) }
        logger.debug('Filtered out line items already in non-quoting deliveries',
                     non_quoting_delivery_ids: non_quoting_delivery_ids,
                     remaining_count: line_items_to_use.size)
      end

      logger.debug('Line items to process', count: line_items_to_use.size)
      # Track which remapped payments have been re-homed and every delivery
      # we rebuild, so the origin-matched remap below hands each payment to
      # its own delivery and the safety net can catch any that no longer map
      # to a rebuilt origin.
      assigned_payment_ids = []
      rebuilt_deliveries = []
      # Group our line items by delivery origin address
      line_items_to_use.to_a.group_by { |li| determine_origin_address(li) }.each do |fulfillment_origin_address, grouped_line_items|
        logger.debug('Processing origin address group',
                     origin_address_id: fulfillment_origin_address.id,
                     line_item_count: grouped_line_items.size)
        # here we try to preserve the same attributes if the delivery quotes are from the same origin
        matched_old_delivery_quoting_attributes = old_deliveries_quoting_attributes.find do |dqa|
          (dqa['origin_address_id'].to_i == fulfillment_origin_address.id) ||
            ((dqa['origin_address_id'].to_i == 1) && fulfillment_origin_address.is_warehouse?)
        end
        matched_old_delivery_quoting_attributes ||= {}

        logger.debug('Matched old delivery attributes', matched: matched_old_delivery_quoting_attributes.present?)
        dq_attributes = matched_old_delivery_quoting_attributes.merge(
          state: 'quoting', # here we force state quoting, overrides pre_pack
          supplier_id: fulfillment_origin_address.party_id,
          origin_address_id: fulfillment_origin_address.id,
          bill_shipping_to_customer: customer.bill_shipping_to_customer,
          do_not_recalculate: false,
          cod_collection_type: cod_collection_type_to_use
        )

        # handle signature_confirmation and saturday_delivery options, assume nil means default ie unselected manually as false or true
        dq_attributes[:signature_confirmation] = signature_confirmation if dq_attributes[:signature_confirmation].nil?
        dq_attributes[:saturday_delivery] = saturday_delivery if dq_attributes[:saturday_delivery].nil?

        delivery = deliveries.build(dq_attributes)
        unless shipping_address.nil? || (installation_postal_code && try(:is_www_ship_by_zip))
          if shipping_address.new_record?
            delivery.destination_address = shipping_address # A new one so we have to assign this way and not rely on shipping_address_id
          else
            delivery.destination_address_id = shipping_address.id # Not a new one so we assign using shipping_address_id
          end
        end
        grouped_line_items.each { |li| li.delivery = delivery }

        delivery.line_items = grouped_line_items
        delivery.save!
        # here we try to preserve the same override shipping cost attributes if the delivery quotes are from the same origin
        matched_override_attributes = old_override_attributes.find do |oa|
          (oa[:origin_address_id] == fulfillment_origin_address.id) ||
            ((oa[:origin_address_id] == 1) && fulfillment_origin_address.is_warehouse?)
        end
        if matched_override_attributes
          # we have something, so create a shipping cost with these attributes
          so = ShippingOption.where(name: 'override', country: delivery.resource.store.country.iso).first
          shpcst = ShippingCost.new(shipping_option: so, cost: matched_override_attributes[:cost], description_override: matched_override_attributes[:description_override])
          delivery.shipping_costs << shpcst
          # Update both selected_shipping_cost_id AND shipping_option_id to preserve the override
          # This ensures the override is fully restored after the delivery is recreated
          delivery.update_columns(selected_shipping_cost_id: shpcst.id, selected_shipping_cost_description: shpcst.description_override, shipping_option_id: so.id)
        end
        rebuilt_deliveries << delivery
        # Remap our unmapped payments and shipments! doing it this way so we get an audit trail.
        # Reclaim only the payments whose original delivery shared this
        # delivery's origin address (mirroring the override-attribute match
        # above), so each payment follows the funds it was authorized
        # against instead of every payment piling onto the last delivery.
        payment_ids_for_origin = payments_by_origin.select do |origin_id, _pids|
          (origin_id.to_i == fulfillment_origin_address.id) ||
            ((origin_id.to_i == 1) && fulfillment_origin_address.is_warehouse?)
        end.values.flatten - assigned_payment_ids
        if payment_ids_for_origin.present?
          Payment.where(id: payment_ids_for_origin).find_each { |pp| pp.update_attribute!(:delivery_id, delivery.id) }
          assigned_payment_ids.concat(payment_ids_for_origin)
        end
        Shipment.where(id: remapped_shipment_ids).find_each { |shp| shp.update_attribute!(:delivery_id, delivery.id) } if remapped_shipment_ids.present? && !delivery.is_smart_service?
        if remapped_shipment_ids.present? && delivery.quantities_remaining_to_allocate > 0 && delivery.delta_weight_factor_remaining_to_allocate > DELTA_WEIGHT_FACTOR_THRESHOLD_TO_REPACK_LTL # && delivery.delta_volume_factor_remaining_to_allocate > DELTA_VOLUME_FACTOR_THRESHOLD_TO_REPACK_LTL)
          # We want to make these unpacked since delivery items have changed and enough are unallocated
          # Here we do not unpack LTL freight if the delta_weight_remaining_to_allocate is less than DELTA_WEIGHT_FACTOR_THRESHOLD_TO_REPACK, because pallets do not change even if we change a lot items, and all weights are measured before ship labeling.
          Shipment.where(id: remapped_shipment_ids).find_each(&:unpack)
        end
        logger.debug('Created delivery', delivery_id: delivery.id, line_item_count: delivery.line_items.size)
      end

      # Safety net: any remapped payment whose origin no longer maps to a
      # rebuilt delivery (delivery topology changed, or order-level payments
      # carrying no per-delivery origin) must still be re-homed — never
      # leave a captured payment stranded with a null delivery_id.
      leftover_payment_ids = remapped_pre_payment_ids - assigned_payment_ids
      if leftover_payment_ids.present? && (fallback_delivery = rebuilt_deliveries.first)
        logger.warn('Remapping payments with no matching origin to first rebuilt delivery',
                    payment_ids: leftover_payment_ids, delivery_id: fallback_delivery.id)
        Payment.where(id: leftover_payment_ids).find_each { |pp| pp.update_attribute!(:delivery_id, fallback_delivery.id) }
      end
    end
  end
  deliveries.quoting
end

#reset_deliveries_shipping_optionArray<Delivery>

Clear the shipping option on every delivery so recalculation falls back to
the customer's default instead of inheriting a previous selection.

Returns:

  • (Array<Delivery>)

    the deliveries collection



147
148
149
150
151
152
153
# File 'app/concerns/models/ship_quotable.rb', line 147

def reset_deliveries_shipping_option
  # this could be called on a before_save callback so don't assume arive_record associations or save, just reset all deliveries shipping_option field to nil. This is so that on say a change of shipping address, we don't inherit the previous delivery's shipping option when recalculating shipping, like we might on a change of items, but fall back to customer's default.
  Rails.logger.debug { "reset_deliveries_shipping_option called for shipquotable: #{self.class} #{id}, #{reference_number}" }
  deliveries.each do |d|
    d.shipping_option = nil
  end
end

#reset_shipping(message = nil, autosave = true) ⇒ Boolean?

Reset shipping state, destroy in-flight delivery/shipping line items, and
optionally persist the change.

Parameters:

  • message (String, nil) (defaults to: nil)

    message to record in the shipping reset result

  • autosave (Boolean) (defaults to: true)

    whether to save and reload the record

Returns:

  • (Boolean, nil)

    result of save when autosave is true, otherwise nil



129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'app/concerns/models/ship_quotable.rb', line 129

def reset_shipping(message = nil, autosave = true)
  message ||= 'Shipping was reset'
  self.shipping_cost = 0.0
  self.recalculate_shipping = false
  self.do_not_detect_shipping = true
  self.last_shipping_rate_request_result = [{ code: :shipping_reset, message: message }]
  line_items.shipping_only.destroy_all
  deliveries.select { |d| %w[quoting pre_pack].include?(d.state) }.each(&:destroy!)
  return unless autosave

  save
  reload
end

#retrieve_friendly_shipping_method(show_customer_pays_info = false, for_www = false, with_delivery_commitment = false) ⇒ String

Compose the friendly shipping method description without memoization.

Parameters:

  • show_customer_pays_info (Boolean) (defaults to: false)

    whether to include customer payment info

  • for_www (Boolean) (defaults to: false)

    whether to format the description for the public site

  • with_delivery_commitment (Boolean) (defaults to: false)

    whether to include delivery commitment

Returns:

  • (String)

    comma-separated friendly shipping method descriptions



434
435
436
# File 'app/concerns/models/ship_quotable.rb', line 434

def retrieve_friendly_shipping_method(show_customer_pays_info = false, for_www = false, with_delivery_commitment = false)
  deliveries.map { |dq| dq.friendly_shipping_method(show_customer_pays_info, for_www, with_delivery_commitment) }.join(', ')
end

#retrieve_shipping_costs(rate_ship_date: nil) ⇒ Array<Hash>

Retrieve shipping costs for all deliveries.

Parameters:

  • rate_ship_date (Date, nil) (defaults to: nil)

    Optional date to use for rate calculation (user can override)

Returns:

  • (Array<Hash>)

    last shipping rate request result for each quoting delivery



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
115
116
117
118
119
120
121
# File 'app/concerns/models/ship_quotable.rb', line 33

def retrieve_shipping_costs(rate_ship_date: nil)
  # Guard against same-thread re-entry for the SAME record — thread-local
  # and keyed by id, NOT an instance ivar: delivery.save! callbacks (e.g.
  # set_proper_shipping_cost → order save → shipping detection) re-invoke
  # this through a DIFFERENT instance of the same order, which an ivar can't
  # see — and the advisory lock below is session-re-entrant, so it happily
  # re-acquires. The nested run's refresh_deliveries_quoting then destroys
  # the outer loop's deliveries and the outer `delivery.reload` raises
  # RecordNotFound (rolled back order splits, 2026-07-09).
  guard_key = "#{self.class.name.downcase}:#{id || object_id}:retrieve_shipping_costs"
  # thread_variable_* (true thread-local), NOT Thread.current[] (fiber-local):
  # the re-entry is synchronous today, but a future fiber hop must not drop
  # the guard and reopen the race.
  requote_registry = Thread.current.thread_variable_get(:ship_quotable_requote_guard)
  unless requote_registry
    requote_registry = Set.new
    Thread.current.thread_variable_set(:ship_quotable_requote_guard, requote_registry)
  end
  # NOTE: the early return also runs the method-level `ensure`, so only the
  # invocation that ADDED the key may delete it — else a nested call would
  # strip the outer invocation's guard on its way out.
  return [{ code: :already_processing, message: 'Shipping costs are already being retrieved' }] if requote_registry.include?(guard_key)

  requote_registry << guard_key
  owns_requote_guard = true

  lock_key = "#{self.class.name.downcase}|#{id || object_id}|retrieve_shipping_costs".freeze
  result = self.class.with_advisory_lock_result(lock_key, timeout_seconds: 0) do
    logger.tagged lock_key do
      if new_record?
        self.retrieving_shipping_costs = true
      else
        update_column(:retrieving_shipping_costs, true)
      end
      logger.debug('retrieve_shipping_costs starting', shipping_address_id: shipping_address_id, rate_ship_date: rate_ship_date)
      retrieve = try(:editing_locked?) ||
                 shipping_address.present? ||
                 (installation_postal_code.present? && try(:draft?))

      if retrieve
        unless begin
          editing_locked?
        rescue StandardError
          false
        end
          if shipping_address_id_changed? && shipping_address_id_was && WAREHOUSE_ADDRESS_IDS.include?(shipping_address_id_was)
            # we reset selected shipping options because this is like a new shipping calculation and we want sensible defaults, not override
            deliveries.each { |d| d.update_columns(selected_shipping_cost_id: nil, selected_shipping_cost_description: nil, shipping_option_id: nil) }
          end

          deliveries_to_process = refresh_deliveries_quoting.reject(&:do_not_recalculate)

          deliveries_to_process.each do |delivery|
            delivery.retrieve_shipping_costs(rate_ship_date: rate_ship_date)
            # Trigger set_proper_shipping_cost callback now that shipping costs are available
            delivery.reload # Ensure shipping_costs association is fresh
            delivery.updated_at = Time.current # Mark as changed to trigger before_save
            delivery.save! # Trigger before_save callback which calls set_proper_shipping_cost
          end
        end
        # Reload the association so deleted deliveries (removed by refresh_deliveries_quoting)
        # are not included — stale in-memory records would carry ghost error messages.
        self.last_shipping_rate_request_result = deliveries.reload.select { |d| d.state == 'quoting' }.map(&:last_shipping_rate_request_result)
        # Persist the newly built messaging_log so the next request (after redirect) can read it.
        messaging_logs.select(&:new_record?).each(&:save!) unless new_record?
      else
        reset_shipping 'No shipping address provided - shipping will not be calculated', false
      end

      self.recalculate_shipping = false
      self.shipping_issue_alerted = false
      logger.debug('retrieve_shipping_costs complete')
      if new_record?
        self.retrieving_shipping_costs = false
      else
        update_columns(recalculate_shipping: false, retrieving_shipping_costs: false)
      end
    end
    last_shipping_rate_request_result
  end

  # If lock was not acquired, return already processing message
  return [{ code: :already_processing, message: 'Shipping costs are already being retrieved' }] unless result.lock_was_acquired?

  # Return the result from the block (last_shipping_rate_request_result)
  result.result
ensure
  requote_registry.delete(guard_key) if owns_requote_guard
end

#ship_quotedBoolean

Returns true if deliveries are quoting and shipping recalculation
is prevented.

Returns:

  • (Boolean)

    true if deliveries are quoting and shipping recalculation
    is prevented



573
574
575
# File 'app/concerns/models/ship_quotable.rb', line 573

def ship_quoted
  deliveries.any?(&:quoting?) && prevent_recalculate_shipping?
end

#ship_weightFloat

Total shipping weight for non-shipping line items.

Returns:

  • (Float)

    the computed shipping weight, floored at 0.1



449
450
451
# File 'app/concerns/models/ship_quotable.rb', line 449

def ship_weight
  @ship_weight ||= [0.1, line_items.includes(:item).non_shipping.without_children.sum(&:total_shipping_weight).round(1)].max
end

#shipmentsActiveRecord::Relation<Shipment>

Shipments created from the delivery records.

Returns:

See Also:



22
# File 'app/concerns/models/ship_quotable.rb', line 22

has_many :shipments, through: :deliveries

#shipping_addressAddress

Customer-facing address where the order should be shipped.

Returns:

See Also:



17
# File 'app/concerns/models/ship_quotable.rb', line 17

belongs_to :shipping_address, class_name: 'Address', validate: true, optional: true

#shipping_non_db_default_but_db_customer?Boolean

Returns true if the customer is direct-buy but the chosen shipping
account number differs from the DB parent customer's default.

Returns:

  • (Boolean)

    true if the customer is direct-buy but the chosen shipping
    account number differs from the DB parent customer's default



502
503
504
505
506
507
508
509
# File 'app/concerns/models/ship_quotable.rb', line 502

def shipping_non_db_default_but_db_customer?
  (begin
    customer.is_direct_buy? && chosen_shipping_method && (Customer.find(Customer::DB_PARENT_ID).(carrier, billing_address,
shipping_address).nil? || (chosen_shipping_method. != Customer.find(Customer::DB_PARENT_ID).(carrier, billing_address, shipping_address)))
  rescue StandardError
    false
  end)
end

#shipping_signature_confirmation_non_db_default_but_db_customer?Boolean

Returns true if a direct-buy customer has a chosen shipping method
but signature confirmation is not selected.

Returns:

  • (Boolean)

    true if a direct-buy customer has a chosen shipping method
    but signature confirmation is not selected



513
514
515
# File 'app/concerns/models/ship_quotable.rb', line 513

def shipping_signature_confirmation_non_db_default_but_db_customer?
  customer&.is_direct_buy? && chosen_shipping_method && !signature_confirmation
end

#shipping_via_wy_but_has_shipping_account?Boolean

Returns true if the chosen shipping method uses a WY shipping account
number that is not in the customer's list of authorized accounts.

Returns:

  • (Boolean)

    true if the chosen shipping method uses a WY shipping account
    number that is not in the customer's list of authorized accounts



492
493
494
495
496
497
498
# File 'app/concerns/models/ship_quotable.rb', line 492

def shipping_via_wy_but_has_shipping_account?
  (begin
    chosen_shipping_method&. && customer.(carrier, billing_address, shipping_address).index(chosen_shipping_method.).nil?
  rescue StandardError
    false
  end)
end

#ships_economy_package?Boolean

Returns true if every delivery ships via economy package.

Returns:

  • (Boolean)

    true if every delivery ships via economy package



207
208
209
# File 'app/concerns/models/ship_quotable.rb', line 207

def ships_economy_package?
  deliveries.all?(&:ships_economy_package?)
end

#ships_freight?Boolean

Returns true if any delivery ships LTL freight.

Returns:

  • (Boolean)

    true if any delivery ships LTL freight



202
203
204
# File 'app/concerns/models/ship_quotable.rb', line 202

def ships_freight?
  deliveries.any?(&:ships_ltl_freight?)
end

#ships_freight_but_address_not_freight_ready?Boolean

Returns true if any delivery ships freight and the address is not freight-ready.

Returns:

  • (Boolean)

    true if any delivery ships freight and the address is not freight-ready



192
193
194
# File 'app/concerns/models/ship_quotable.rb', line 192

def ships_freight_but_address_not_freight_ready?
  ships_freight? && shipping_address && !shipping_address.is_valid_for_freight?
end

#should_ship_freight?Boolean

Returns true if any delivery should ship LTL freight.

Returns:

  • (Boolean)

    true if any delivery should ship LTL freight



212
213
214
# File 'app/concerns/models/ship_quotable.rb', line 212

def should_ship_freight?
  deliveries.any?(&:should_ship_ltl_freight?)
end

#should_ship_freight_but_address_not_freight_ready?Boolean

Returns true if any delivery should ship freight and the address is not freight-ready.

Returns:

  • (Boolean)

    true if any delivery should ship freight and the address is not freight-ready



197
198
199
# File 'app/concerns/models/ship_quotable.rb', line 197

def should_ship_freight_but_address_not_freight_ready?
  should_ship_freight? && shipping_address && !shipping_address.is_valid_for_freight?
end

#validate_deliveriesvoid

This method returns an undefined value.

Add validation errors for quoting deliveries that have no shipping costs.



550
551
552
553
554
# File 'app/concerns/models/ship_quotable.rb', line 550

def validate_deliveries
  deliveries.quoting.each do |dq|
    errors.add(:base, {}.merge(eval(dq.last_shipping_rate_request_result.to_s))[:message]) if dq.shipping_costs.empty? # rubocop:disable Security/Eval
  end
end