Class: Edi::Commercehub::InventoryMessageProcessor

Inherits:
BaseEdiService show all
Defined in:
app/services/edi/commercehub/inventory_message_processor.rb

Constant Summary

Constants included from AddressAbbreviator

AddressAbbreviator::MAX_LENGTH

Instance Attribute Summary

Attributes inherited from BaseEdiService

#orchestrator

Instance Method Summary collapse

Methods inherited from BaseEdiService

#duplicate_po_already_notified?, #initialize, #mark_duplicate_po_as_notified, #report_order_creation_issues, #safe_process_edi_communication_log

Methods included from AddressAbbreviator

#abbreviate_street, #collect_street_originals, #record_address_abbreviation_notes

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 Edi::BaseEdiService

Instance Method Details

#append_catalog_items(xml, catalog_items) ⇒ Object



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
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
# File 'app/services/edi/commercehub/inventory_message_processor.rb', line 58

def append_catalog_items(xml, catalog_items)
  catalog_items.each do |ci|
    ErrorReporting.scoped(catalog_item_id: ci.id, partner: orchestrator.partner) do
      discontinued = ci.discontinued? || ci.pending_discontinue? || ci.in_hide_from_feed_state?
      merchant_sku = ci.third_party_part_number
      merchant_sku ||= ci.reported_vendor_sku if %w[costco walmartca].include?(orchestrator.ch_partner_id) # report something if this is missing because Costco, Walmart require this
      next if merchant_sku.blank? && %w[thdca thehomedepot].include?(orchestrator.ch_partner_id) # If merchant sku is missing this will error out for these channels, this is a patch RB please review

      xml.send(:product) do
        xml.send(:vendor_SKU, ci.reported_vendor_sku)
        if discontinued
          if %w[thehomedepot thdca walmartca].include?(orchestrator.ch_partner_id)
            # these partners only support YES or NO for available types, so set available = 'No' and total_available = 0
            available = 'No'
            total_available = 0
            stocks = {}
          elsif %w[lowes rona costco].include?(orchestrator.ch_partner_id)
            # these partners support YES, NO, DISCONTINUED and DELETED for available types, so set available and total_available based on specific criteria
            # get actual stock, which we will pass on for NO and DISCONTINUED availibility
            stocks = ci.reported_stocks(use_alternate_warehouse: false)
            total_available = stocks.values.sum
            if ci.in_hide_from_feed_state?
              available = 'No'
            elsif ci.pending_discontinue?
              available = 'Discontinued'
            elsif ci.discontinued?
              available = 'Deleted'
              total_available = 0 # set stock 0 for DELETED items
            end
          end
          xml.send(:qtyonhand, total_available)
          xml.send(:available, available)
          xml.send(:discontinued_date, ci.discontinued_date&.strftime('%Y%m%d') || Date.current.strftime('%Y%m%d'))
        else
          stocks = ci.reported_stocks(use_alternate_warehouse: false)
          total_available = stocks.values.sum
          xml.send(:qtyonhand, total_available)
          xml.send(:available, total_available.positive? ? 'Yes' : 'No')
          future_stocks = {}
          if total_available < 10
            global_next_available_date = nil
            global_next_available_qty = nil
            begin
              # Use depth-limited version to prevent infinite recursion
              next_available_by_warehouse = ci.next_available_by_warehouse_with_depth_limit(use_alternate_warehouse: true, max_depth: 10)
              next_available_by_warehouse.each do |warehouse_name, on_order_data|
                next unless on_order_data && on_order_data.next_available_date

                future_stocks[warehouse_name] = {
                  next_available_date: on_order_data.next_available_date.strftime('%Y%m%d'),
                  next_available_qty: on_order_data.next_available_qty
                }
                global_next_available_date ||= on_order_data.next_available_date
                global_next_available_qty ||= on_order_data.next_available_qty
                xml.send(:next_available_date, global_next_available_date.strftime('%Y%m%d'))
                xml.send(:next_available_qty, global_next_available_qty)
              end
            rescue SystemStackError => e
              # Log the recursion error with detailed context
              ErrorReporting.error(e,
                catalog_item_id: ci.id,
                partner: orchestrator.partner,
                catalog_item_sku: ci.item&.sku,
                error_type: 'stack_level_too_deep',
                message: 'Infinite recursion detected in next_available_by_warehouse method')
              # Set fallback values to prevent the error from stopping the entire batch
              global_next_available_date = 90.days.from_now
              global_next_available_qty = 1
              xml.send(:next_available_date, global_next_available_date.strftime('%Y%m%d'))
              xml.send(:next_available_qty, global_next_available_qty)
            end
          end
        end
        xml.send(:description, ci.reported_name)
        xml.send(:unitOfMeasure, 'EA')
        xml.send(:merchantSKU, merchant_sku) if merchant_sku.present?
        xml.send(:UPC, ci.item.upc) if ci.item.upc.present?

        xml.send(:manufacturer_SKU, ci.reported_vendor_sku)
        if orchestrator.warehouse_id.present?
          xml.send(:warehouseBreakout) do
            orchestrator.warehouse_id.each do |ch_warehouse_name, wy_warehouse_name|
              xml.send(:warehouse, 'warehouse-id': ch_warehouse_name) do
                xml.send(:qtyonhand, stocks[wy_warehouse_name] || 0)
                if future_stocks.present? && (warehouse_stock_data = future_stocks[wy_warehouse_name]).present?
                  xml.send(:next_available_date, warehouse_stock_data[:next_available_date])
                  xml.send(:next_available_qty, warehouse_stock_data[:next_available_qty])
                end
              end
            end
          end
        end
      end
    rescue StandardError => e
      logger.error "Error building inventory for catalog item #{ci.id} partner #{orchestrator.partner}: #{e.message}"
      ErrorReporting.error(e, catalog_item_id: ci.id, partner: orchestrator.partner)
    end
  end
  xml
end

#build_xml(catalog_items: nil, states: nil) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'app/services/edi/commercehub/inventory_message_processor.rb', line 44

def build_xml(catalog_items: nil, states: nil)
  logger.info "#{catalog_items.size} items in inventory payload"
  b = Nokogiri::XML::Builder.new do |xml|
    xml.send(:advice_file) do
      xml.send(:advice_file_control_number, 0)
      xml.send(:vendor, 'warmlyyours')
      xml.send(:vendorMerchID, orchestrator.ch_partner_id)
      append_catalog_items(xml, catalog_items)
      xml.send(:messageCount, catalog_items.size)
    end
  end
  b.to_xml
end

#load_catalog_items(states: nil) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'app/services/edi/commercehub/inventory_message_processor.rb', line 25

def load_catalog_items(states: nil)
  states ||= %w[active require_vendor_update pending_vendor_update pending_discontinue]
  catalog_item_ids = []
  orchestrator.customers.each do |customer|
    exclude_hide_from_feed_items = true
    exclude_hide_from_feed_items = false if ['lowes'].include?(orchestrator.ch_partner_id)
    catalog_items = customer.catalog.catalog_items.where(state: states)
    catalog_items = catalog_items.not_hidden_from_catalog if exclude_hide_from_feed_items
    # When our catalog requires third party part number, do not grab those catalog items without one
    catalog_items = catalog_items.where.not(third_party_part_number: nil) if customer.catalog.third_party_part_number_required
    catalog_items = catalog_items.where('third_party_sku ~ ?', customer.catalog.third_party_sku_filter_regex) if customer.catalog.is_active_third_party_sku_filter
    catalog_item_ids += catalog_items.pluck(:id)
  end
  CatalogItem.where(id: catalog_item_ids.uniq)
             .with_item
             .eager_load(:store_item, :item)
             .order(Item[:sku])
end

#process(catalog_items: nil, states: nil) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'app/services/edi/commercehub/inventory_message_processor.rb', line 6

def process(catalog_items: nil, states: nil)
  ecl = nil
  EdiCommunicationLog.transaction do
    logger.info "Creating inventory advice for partner #{orchestrator.partner}"
    catalog_items ||= load_catalog_items(states: states)
    data_xml = build_xml(catalog_items: [catalog_items].flatten, states: states)
    ecl = EdiCommunicationLog.create_outbound_file_from_data(
      data: data_xml,
      file_extension: 'inv',
      partner: orchestrator.partner,
      category: 'inventory_advice',
      resources: catalog_items,
      data_type: 'xml',
      file_info: {}
    )
  end
  ecl
end