Class: Shipping::ItemMd5Extractor
- Inherits:
-
BaseService
- Object
- BaseService
- Shipping::ItemMd5Extractor
- Defined in:
- app/services/shipping/item_md5_extractor.rb
Overview
Takes an item, looks at shipping dimensions, create a packing entry
Race Condition Note:
This service can be triggered by multiple sources simultaneously:
- Item.after_save callback when box dimensions change
- Delivery state transitions (picking -> pending_ship_labels, any -> shipped)
- Pre-pack workflow calling set_packaged_items_md5_hash
To prevent ActiveRecord::RecordNotUnique errors when two processes try to
create the same Packing (same md5 + service_type), we use upsert which is
atomic at the database level.
Defined Under Namespace
Classes: Result
Instance Attribute Summary collapse
-
#ignore_timestamp ⇒ Object
readonly
Returns the value of attribute ignore_timestamp.
Instance Method Summary collapse
-
#initialize(options = {}) ⇒ ItemMd5Extractor
constructor
A new instance of ItemMd5Extractor.
- #process(item) ⇒ Object
Methods inherited from BaseService
#log_debug, #log_error, #log_info, #log_warning, #logger, #options, #tagged_logger
Constructor Details
#initialize(options = {}) ⇒ ItemMd5Extractor
Returns a new instance of ItemMd5Extractor.
23 24 25 26 |
# File 'app/services/shipping/item_md5_extractor.rb', line 23 def initialize( = {}) @ignore_timestamp = .delete(:ignore_timestamp).to_b super end |
Instance Attribute Details
#ignore_timestamp ⇒ Object (readonly)
Returns the value of attribute ignore_timestamp.
21 22 23 |
# File 'app/services/shipping/item_md5_extractor.rb', line 21 def @ignore_timestamp end |
Instance Method Details
#process(item) ⇒ Object
28 29 30 31 32 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 |
# File 'app/services/shipping/item_md5_extractor.rb', line 28 def process(item) result = Result.new logger.tagged("Item:#{item.id}") do # We only create packaging for goods # We only create packaging if shipping dimensions are present? (just in case validation didn't catch it) unless item.is_goods? && item.box1_defined? result. = msg = "Item #{item.id} is not a good or has no shipping dimensions" logger.warn msg return result end md5_result = Shipping::Md5HashItem.process(item) logger.info "item md5: #{md5_result.md5}, item string: #{md5_result.items_string}" result.item_md5 = md5_result.md5 # Create a packing for this single item service_type = Packing.service_types[item.shipping_class] raise "Unable to match item shipping class #{item.shipping_class}" unless service_type # Check for existing packing with newer timestamp (skip update if so) existing_packing = Packing.find_by(md5: md5_result.md5, service_type:) if existing_packing && ! && existing_packing.created_at && existing_packing.created_at > item.updated_at result. = msg = "Newer Packing data already available. Existing packing #{existing_packing.id}/#{existing_packing.created_at} > item #{item.id}/#{item.updated_at}, skip saving" logger.warn msg return result end if existing_packing&.origin.in?(%w[from_delivery from_manual_entry]) result. = msg = "Packing #{existing_packing.id} is #{existing_packing.origin}; from_item must not overwrite" logger.info msg return result end # Build packing attributes contents = if item.ships_in_single_box? md5_result.full_item_id_hash else item.get_multi_box_item_contents_item_id_hash_array end packdims = item.to_packdims packdim_contents = Packing.format_packdim_contents(md5: md5_result.md5, contents: contents, packdims: packdims) = item.updated_at || item.created_at # Use upsert to atomically insert or update - prevents race condition when # multiple workers try to create the same packing simultaneously upsert_result = Packing.upsert( { md5: md5_result.md5, service_type:, origin: Packing.origins[:from_item], packdims: packdims, contents:, packdim_contents: packdim_contents.to_json, item_id: item.id, delivery_id: nil, shipment_id: nil, created_at: , updated_at: Time.current }, unique_by: :index_packings_on_md5_and_service_type ) # Reload the packing to get the record (upsert doesn't return the full object) packing = Packing.find_by(md5: md5_result.md5, service_type:) unless packing msg = "Packing upsert did not persist (weight audit or DB); item #{item.id} md5=#{md5_result.md5}" result. = msg logger.warn msg return result end # Link kit item IDs for searchability (use union to avoid duplicates) packing.item_ids |= md5_result.kit_item_ids result.packings << packing logger.info "Packing #{packing.id} upserted successfully for item #{item.id}" end result end |