Module: Models::ShipQuotable
- Extended by:
- ActiveSupport::Concern
- Includes:
- ShipMeasurable
- 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.
Belongs to collapse
-
#shipping_address ⇒ Address
Customer-facing address where the order should be shipped.
Has many collapse
-
#deliveries ⇒ ActiveRecord::Relation<Delivery>
Delivery records grouped by fulfillment origin for this quotable.
-
#shipments ⇒ ActiveRecord::Relation<Shipment>
Shipments created from the delivery records.
Instance Method Summary collapse
-
#carrier ⇒ String?
The carrier of the first delivery's shipping method.
-
#chosen_shipping_method ⇒ ShippingCost?
The selected shipping cost of the first delivery.
-
#days_commitment(use = :least) ⇒ Integer
Days commitment across deliveries.
-
#determine_origin_address(line_item) ⇒ Address
Determine the origin address to use for a given line item.
-
#everything_in_stock? ⇒ Boolean
True if all line items are in stock.
-
#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).
-
#is_drop_ship? ⇒ Boolean
True if a one-time shipping address is present.
-
#is_override? ⇒ Boolean
True if any delivery has an overridden shipping method.
-
#is_warehouse_pickup? ⇒ Boolean
True if any delivery is a warehouse pickup.
-
#line_items_grouped_by_deliveries_quoting(group_by_item_category = true) ⇒ Hash{Delivery => Array<LineItem>}
Group non-shipping line items by their quoting delivery.
-
#line_items_match_deliveries_if_any ⇒ Boolean
Verify that quoting deliveries cover the same line items as the quotable.
-
#need_to_pre_pack_reasons ⇒ Array<String>
Reasons the order should stop for warehouse pre-packing verification.
-
#need_to_recalculate_shipping ⇒ Boolean
Mark shipping for recalculation and re-enable shipping detection.
-
#one_time_shipping_address ⇒ Address?
The shipping address when it is flagged as one-time-only.
-
#one_time_shipping_address=(val) ⇒ Address?
Assign a one-time shipping address.
-
#qualifies_for_cod? ⇒ Boolean
True if the order qualifies for cash-on-delivery.
-
#refresh_deliveries_quoting(include_shipping_lines = false) ⇒ ActiveRecord::Relation<Delivery>
Rebuild the quoting deliveries for the current line items and shipping address.
-
#reset_deliveries_shipping_option ⇒ Array<Delivery>
Clear the shipping option on every delivery so recalculation falls back to the customer's default instead of inheriting a previous selection.
-
#reset_shipping(message = nil, autosave = true) ⇒ Boolean?
Reset shipping state, destroy in-flight delivery/shipping line items, and optionally persist the change.
-
#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.
-
#retrieve_shipping_costs(rate_ship_date: nil) ⇒ Array<Hash>
Retrieve shipping costs for all deliveries.
-
#ship_quoted ⇒ Boolean
True if deliveries are quoting and shipping recalculation is prevented.
-
#ship_weight ⇒ Float
Total shipping weight for non-shipping line items.
-
#shipping_non_db_default_but_db_customer? ⇒ Boolean
True if the customer is direct-buy but the chosen shipping account number differs from the DB parent customer's default.
-
#shipping_signature_confirmation_non_db_default_but_db_customer? ⇒ Boolean
True if a direct-buy customer has a chosen shipping method but signature confirmation is not selected.
-
#shipping_via_wy_but_has_shipping_account? ⇒ Boolean
True if the chosen shipping method uses a WY shipping account number that is not in the customer's list of authorized accounts.
-
#ships_economy_package? ⇒ Boolean
True if every delivery ships via economy package.
-
#ships_freight? ⇒ Boolean
True if any delivery ships LTL freight.
-
#ships_freight_but_address_not_freight_ready? ⇒ Boolean
True if any delivery ships freight and the address is not freight-ready.
-
#should_ship_freight? ⇒ Boolean
True if any delivery should ship LTL freight.
-
#should_ship_freight_but_address_not_freight_ready? ⇒ Boolean
True if any delivery should ship freight and the address is not freight-ready.
-
#validate_deliveries ⇒ void
Add validation errors for quoting deliveries that have no shipping costs.
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
#carrier ⇒ String?
Returns 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_method ⇒ ShippingCost?
Returns 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.
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 |
#deliveries ⇒ ActiveRecord::Relation<Delivery>
Delivery records grouped by fulfillment origin for this quotable.
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.
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.
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).
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.
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.
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.
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.
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_any ⇒ Boolean
Verify that quoting deliveries cover the same line items as the quotable.
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_reasons ⇒ Array<String>
Reasons the order should stop for warehouse pre-packing verification.
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_shipping ⇒ Boolean
Mark shipping for recalculation and re-enable shipping detection.
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_address ⇒ Address?
The shipping address when it is flagged as 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.
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.
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.
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_option ⇒ Array<Delivery>
Clear the shipping option on every delivery so recalculation falls back to
the customer's default instead of inheriting a previous selection.
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.
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( = nil, autosave = true) ||= '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: }] 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.
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.
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_quoted ⇒ Boolean
Returns 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_weight ⇒ Float
Total shipping weight for non-shipping line items.
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 |
#shipments ⇒ ActiveRecord::Relation<Shipment>
Shipments created from the delivery records.
22 |
# File 'app/concerns/models/ship_quotable.rb', line 22 has_many :shipments, through: :deliveries |
#shipping_address ⇒ Address
Customer-facing address where the order should be shipped.
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.
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).shipping_account_number(carrier, billing_address, shipping_address).nil? || (chosen_shipping_method.shipping_account_number != Customer.find(Customer::DB_PARENT_ID).shipping_account_number(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.
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.
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&.shipping_account_number && customer.shipping_account_numbers(carrier, billing_address, shipping_address).index(chosen_shipping_method.shipping_account_number).nil? rescue StandardError false end) end |
#ships_economy_package? ⇒ Boolean
Returns 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.
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.
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.
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.
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_deliveries ⇒ void
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 |