Class: Edi::MftGateway::InventoryMessageProcessor

Inherits:
BaseEdiService show all
Defined in:
app/services/edi/mft_gateway/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(catalog_items, inventory_date, inventory_time) ⇒ Object



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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'app/services/edi/mft_gateway/inventory_message_processor.rb', line 83

def append_catalog_items(catalog_items, inventory_date, inventory_time)
  line_items_arr = []
  qty_schedules_locations_arr = []
  is_amazon = %i[mft_gateway_amazon_vc_edi_wasn4 mft_gateway_amazon_vc_edi_wat0f].include?(orchestrator.partner) || false

  catalog_items.each_with_index do |ci, index|
    ErrorReporting.scoped(
      catalog_item_id: ci.id,
      partner: orchestrator.partner,
      catalog_item_sku: ci.item&.sku,
      catalog_item_index: index,
      total_catalog_items: catalog_items.size
    ) do
      qty_schedules_locations_arr = []
      discontinued = ci.discontinued? || ci.pending_discontinue? || ci.in_hide_from_feed_state?
      if discontinued == true
        total_available = 0
      else
        stocks = ci.reported_stocks(use_alternate_warehouse: false)
        total_available = stocks.values.sum
      end
      qty_schedules_locations_arr << {
        QuantityQualifier: '33', # from: https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/ItemRegistries/QuantityQualifier: ... 33 Quantity of current stock that is on hand for sale or use ...
        TotalQty: total_available,
        TotalQtyUOM: 'EA',
        Dates: [
          {
            DateTimeQualifier: '018', # per Sydney at SPS Commerce
            Date: inventory_date, # e.g. 2022-12-24
            Time: inventory_time # e.g. 21:05:49-06:00
          }
        ]
      }
      if !is_amazon && (discontinued || (total_available <= ci.item.qty_warn_on_stock))
        # for discontinued tell them nothing's coming, otherwise, if low stock, tell them something's coming
        total_available = 0
        next_available_date = inventory_date # per Sydney at SPS COmmerce
        next_available_time = inventory_time # per Sydney at SPS COmmerce
        unless discontinued
          global_next_available_date = nil
          global_next_available_qty = nil
          begin
            # Add depth counter 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

              global_next_available_date ||= on_order_data.next_available_date.end_of_day
              global_next_available_qty ||= on_order_data.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.end_of_day
            global_next_available_qty = 1
          end
          # &^%&^% Canadian Tire *requires* you send them something, and since we are not allegedly not discontinued, send them 1 item is coming 90 days from now or some such thing
          global_next_available_date ||= 90.days.from_now.end_of_day
          global_next_available_qty ||= 1
          if global_next_available_date
            total_available = global_next_available_qty
            next_available_date, next_available_time = global_next_available_date.iso8601.split('T')
          end
        end
        if !is_amazon && (total_available > 0 || discontinued) # only send if discontinued or if we do have more items on order
          qty_schedules_locations_arr << {
            QuantitiesSchedulesLocations: {
              QuantityQualifier: '29', # from: https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/ItemRegistries/QuantityQualifier: ... 29 Projected Available Inventory ...
              TotalQty: total_available,
              TotalQtyUOM: 'EA',
              Dates: [
                {
                  DateTimeQualifier: '018', # per Sydney at SPS Commerce
                  Date: next_available_date, # e.g. 2022-12-24
                  Time: next_available_time # e.g. 21:05:49-06:00
                }
              ]
            }
          }
        end
      end
      line_items_hash = {
        InventoryLine: {
          BuyerPartNumber: ci.third_party_part_number.presence || ci.item.sku, #  documentation from SPS Commerce requires this
          VendorPartNumber: ci.item.sku,
          ConsumerPackageCode: ci.item.upc.presence #  documentation from SPS Commerce requires this)
        },
        ProductOrItemDescription: {
          ProductCharacteristicCode: '08', # from: https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/ItemRegistries/ProductCharacteristicCode: ... 08 Product Description ...
          ProductDescription: ci.item.name.to_s[0..79].tr('', "'").gsub(/[^0-9a-z.,\s]/i, '')[0..79]
        }
      }
      # handle differences between Amazon and other EDI partners
      if is_amazon
        line_items_hash[:ProductPricing] = {
          UnitPrice: ci.amount,
          CurrencyCode: ci.catalog.currency
        }
      end
      line_items_hash[:QuantitiesSchedulesLocations] = qty_schedules_locations_arr
      line_items_arr << line_items_hash
    end
  rescue StandardError => e
    # Log the error with detailed context but continue processing other items
    ErrorReporting.error(e,
      catalog_item_id: ci.id,
      partner: orchestrator.partner,
      catalog_item_sku: ci.item&.sku,
      catalog_item_index: index,
      total_catalog_items: catalog_items.size,
      error_type: 'catalog_item_processing_error',
      message: "Error processing catalog item #{ci.id} (#{ci.item&.sku}) for partner #{orchestrator.partner}")
    logger.error "Error processing catalog item #{ci.id} (#{ci.item&.sku}) for partner #{orchestrator.partner}: #{e.message}"
    # Continue with next item instead of failing the entire batch
    next
  end
  { LineItem: line_items_arr }
end

#build_json(catalog_items: nil) ⇒ Object



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

def build_json(catalog_items: nil)
  logger.info "#{catalog_items.size} items in inventory payload"
  inventory_date, inventory_time = Time.current.iso8601.split('T')
  report_type = 'MB'
  address_type = 'VN' # from: https://developercenter.spscommerce.com/#/rsx/docs/fields-qualifiers/7.7.6/ItemRegistries/AddressTypeCode: ... VN Vendor ...
  address_name = 'WarmlyYours'
  if %i[mft_gateway_amazon_vc_edi_wasn4 mft_gateway_amazon_vc_edi_wat0f].include?(orchestrator.partner)
    report_type = 'IP'
    address_type = 'WH'
    address_name = orchestrator.warehouse_code
  end
  {
    Header: {
      HeaderReport: {
        TradingPartnerId: orchestrator.partner_id,
        DocumentId: orchestrator.generate_unique_document_id('inventory_advice'),
        TsetPurposeCode: '00',
        ReportTypeCode: report_type,
        InventoryDate: inventory_date, # e.g. 2022-12-24
        InventoryTime: inventory_time, # e.g. 21:05:49-06:00
        Vendor: orchestrator.vendor_id
      },
      Contacts: [{
        ContactTypeCode: 'IC', # per Sydney at SPS Commerce
        ContactName: 'WarmlyYours',
        PrimaryPhone: '800-875-5285',
        PrimaryEmail: 'orders@warmlyyours.com'
      }],
      Address: {
        AddressTypeCode: address_type,
        AddressName: address_name
      }
    },
    Structure: append_catalog_items(catalog_items, inventory_date, inventory_time),
    Summary: {
      TotalLineItemNumber: catalog_items.count
    }
  }.to_json
end

#load_catalog_items(states: nil) ⇒ Object



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

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|
    catalog_items = customer.catalog.catalog_items.where(state: states).not_hidden_from_catalog
    if customer.catalog.third_party_part_number_required
      # 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)
    end
    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



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

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)
    return :empty_set unless catalog_items.present?

    data = build_json(catalog_items: [catalog_items].flatten)
    ecl = EdiCommunicationLog.create_outbound_file_from_data(
      data: data,
      file_extension: 'inv',
      partner: orchestrator.partner,
      data_type: 'json',
      category: 'inventory_advice',
      resources: catalog_items,
      file_info: {}
    )
  end
  ecl
end