Class: Edi::BaseOrchestrator

Inherits:
Object
  • Object
show all
Extended by:
Memery
Defined in:
app/services/edi/base_orchestrator.rb

Constant Summary collapse

ORCHESTRATORS =
['Edi::Amazon::Orchestrator', 'Edi::Commercehub::Orchestrator', 'Edi::Houzz::Orchestrator', 'Edi::Wayfair::Orchestrator', 'Edi::AmazonVc::Orchestrator', 'Edi::MiraklSeller::Orchestrator',
'Edi::MftGateway::Orchestrator', 'Edi::Walmart::Orchestrator', 'Edi::Menard::Orchestrator'].freeze
[1, 2, 3, 4, 6, 8, 12, 24].freeze
DEFAULT_PENDING_DISCONTINUE_LIFETIME =
1.day

Instance Attribute Summary collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(partner, options = {}) ⇒ BaseOrchestrator

Returns a new instance of BaseOrchestrator.



310
311
312
313
314
315
316
317
318
319
320
# File 'app/services/edi/base_orchestrator.rb', line 310

def initialize(partner, options = {})
  @config = self.class.partners[partner.to_sym]
  raise 'Unrecognized Partner' unless @config

  @config.each do |name, val|
    singleton_class.send :attr_accessor, name.to_sym
    public_send :"#{name}=", val
  end
  @options = options
  @logger = options[:logger] || Rails.logger
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



5
6
7
# File 'app/services/edi/base_orchestrator.rb', line 5

def config
  @config
end

#loggerObject (readonly)

Returns the value of attribute logger.



5
6
7
# File 'app/services/edi/base_orchestrator.rb', line 5

def logger
  @logger
end

#optionsObject (readonly)

Returns the value of attribute options.



5
6
7
# File 'app/services/edi/base_orchestrator.rb', line 5

def options
  @options
end

Class Method Details

.all_orchestrators_classObject



16
17
18
# File 'app/services/edi/base_orchestrator.rb', line 16

def all_orchestrators_class
  ORCHESTRATORS.map(&:constantize)
end

.build(partner_config_key, options = {}) ⇒ Object



50
51
52
53
54
55
56
# File 'app/services/edi/base_orchestrator.rb', line 50

def build(partner_config_key, options = {})
  # Find the orchestrator for this partner key
  orchestrator_class = all_orchestrators_class.detect { |o| o.partners.key?(partner_config_key.to_sym) }
  return orchestrator_class.new(partner_config_key, options) if orchestrator_class

  raise "Cannot determine orchestrator class for partner #{partner_config_key}"
end

.cached_build(partner_config_key, options = {}) ⇒ Object

Returns a cached orchestrator instance for the given partner key
This avoids expensive repeated instantiation of orchestrators with dynamic accessors



71
72
73
74
# File 'app/services/edi/base_orchestrator.rb', line 71

def cached_build(partner_config_key, options = {})
  cache_key = [partner_config_key.to_sym, options.hash].join('_')
  cached_orchestrators[cache_key] ||= build(partner_config_key, options)
end

.cached_orchestratorsObject

Request/job-scoped cache of orchestrator instances by partner key.
Backed by CurrentScope so it is automatically reset between web
requests (Rails) and Sidekiq jobs (Sidekiq::CurrentAttributes
middleware). The previous implementation used a class-level instance
variable (@cached_orchestrators ||= {}) which is shared across
threads and never reset -- so it both leaked memory unboundedly and
held stale partner config across deploys/reloads.



65
66
67
# File 'app/services/edi/base_orchestrator.rb', line 65

def cached_orchestrators
  CurrentScope.edi_orchestrator_cache ||= {}
end

.catalog_id_to_pending_discontinue_lifetimeObject

Returns a hash of { catalog_id => ActiveSupport::Duration } for all active
orchestrators that define a custom pending_discontinue_lifetime. Used by
Maintenance::ItemMaintenance to apply per-partner wait times.



369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'app/services/edi/base_orchestrator.rb', line 369

def self.catalog_id_to_pending_discontinue_lifetime
  map = {}
  orchestrators.each do |o|
    next unless o.active

    lifetime = o.pending_discontinue_lifetime
    next if lifetime == DEFAULT_PENDING_DISCONTINUE_LIFETIME

    catalog_id = o.try(:catalog_id)
    map[catalog_id] = lifetime if catalog_id
  end
  map
end

.catalog_ids_edi_enabledObject



113
114
115
# File 'app/services/edi/base_orchestrator.rb', line 113

def catalog_ids_edi_enabled
  Customer.where(id: customer_ids_edi_enabled).pluck(:catalog_id).uniq.sort
end

.customer_id_to_partner_key_mapObject

Builds a lookup hash from customer_id to partner config key for fast lookups
Handles both single customer_id values and multi-customer partner configurations



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/services/edi/base_orchestrator.rb', line 28

def customer_id_to_partner_key_map
  map = {}
  # First pass: single customer_id partners (higher priority)
  partners.each do |key, config|
    next unless config[:active]
    next if config[:customer_id].is_a?(Hash) # Skip multi-customer partners in first pass

    map[config[:customer_id]] = key if config[:customer_id]
  end
  # Second pass: multi-customer partners (lower priority, only if not already mapped)
  partners.each do |key, config|
    next unless config[:active]
    next unless config[:customer_id].is_a?(Hash)

    config[:customer_id].values.each do |cid|
      map[cid] ||= key # Only set if not already mapped by single-customer partner
    end
  end
  map
end

.customer_ids_edi_enabledObject



96
97
98
# File 'app/services/edi/base_orchestrator.rb', line 96

def customer_ids_edi_enabled
  partners.values.select { |v| v[:customer_id].try(:values) || v[:customer_id] }.map { |v| v[:customer_id].try(:values) || v[:customer_id] }.flatten.uniq.sort
end

.customer_ids_with_invoice_message_enabledObject



101
102
103
104
105
106
107
108
109
110
# File 'app/services/edi/base_orchestrator.rb', line 101

def customer_ids_with_invoice_message_enabled
  Rails.cache.fetch('edi/customer_ids_with_invoice_message_enabled', expires_in: 1.hour) do
    partners.keys.filter_map do |key|
      o = cached_build(key)
      next unless o.respond_to?(:invoice_message_enabled?) && o.invoice_message_enabled?

      Array(o.config[:customer_id].is_a?(Hash) ? o.config[:customer_id].values : o.config[:customer_id])
    end.flatten.compact.uniq.sort
  end
end

.execute_discontinue_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object

Executes the discontinue flow for EDI orchestrators.
Picks up pending_discontinue catalog items and sends DELETE via SP-API.

orchestrator_name - The name of a specific orchestrator to run, optional.
partner - The partner key to run for, optional.
logger - The logger to use.
trial_run - If true, will not send real requests.



167
168
169
# File 'app/services/edi/base_orchestrator.rb', line 167

def self.execute_discontinue_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false)
  execute_flow(:execute_discontinue_flow, orchestrator_name:, partner:, logger:, trial_run:)
end

.execute_flow(flow, orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object

Executes the specified flow (inventory, order, or product data)
for the given orchestrator(s). Allows filtering by orchestrator name and partner.
Logs execution and returns results.



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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'app/services/edi/base_orchestrator.rb', line 174

def self.execute_flow(flow, orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false)
  valid_flows = %i[execute_inventory_flow execute_order_flow execute_product_data_flow execute_price_flow execute_listing_message_feed_flow execute_discontinue_flow]
  raise "Invalid flow option, must be one of #{valid_flows.join(', ')}" unless flow.in?(valid_flows)

  results = []
  all_orchestrators_class.each do |oc|
    next if orchestrator_name.present? && oc.name != orchestrator_name

    logger.tagged oc.name do
      oc.orchestrators.each do |orchestrator|
        next if partner.present? && orchestrator.partner.to_s != partner

        logger.tagged orchestrator.partner do
          logger.tagged flow do
            logger.info 'started'
            begin
              result = if orchestrator.should_execute_flow?(flow) && orchestrator.respond_to?(flow)
                         trial_run ? :trial_run : orchestrator.send(flow)
                       else
                         :scheduled_skip
                       end
              logger.info "Result: #{loggable_result(result)}"
              results << { orchestrator_class: oc.name, partner: orchestrator.partner, flow:, result: }
            rescue Exception => e
              msg = "#{oc.name} #{orchestrator.partner} #{flow} exception. #{e}"
              # Enhanced error logging with detailed context
              ErrorReporting.error(e, {
                orchestrator_class: oc.name,
                partner: orchestrator.partner,
                flow: flow,
                error_type: 'orchestrator_execution_error',
                orchestrator_name: orchestrator.class.name,
                flow_method: flow,
                exception_class: e.class.name,
                exception_message: e.message,
                backtrace: e.backtrace&.first(10),
                message: msg
              })
              logger.error msg
              # Add error result to results array instead of failing silently
              results << {
                orchestrator_class: oc.name,
                partner: orchestrator.partner,
                flow:,
                result: :error,
                error: e.message,
                error_class: e.class.name
              }
            end
            logger.info 'completed'
          end
        end
      end
    end
  end
  results
end

.execute_inventory_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object

Executes the inventory flow for EDI orchestrators.

orchestrator_name - The name of a specific orchestrator to run, optional.
partner - The partner key to run for, optional.
logger - The logger to use.
trial_run - If true, will not send real requests.



136
137
138
# File 'app/services/edi/base_orchestrator.rb', line 136

def self.execute_inventory_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false)
  execute_flow(:execute_inventory_flow, orchestrator_name:, partner:, logger:, trial_run:)
end

.execute_listing_message_feed_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object

Executes the listing message flow for EDI orchestrators.

orchestrator_name - The name of a specific orchestrator to run, optional.
partner - The partner key to run for, optional.
logger - The logger to use.
trial_run - If true, will not send real requests.



156
157
158
# File 'app/services/edi/base_orchestrator.rb', line 156

def self.execute_listing_message_feed_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false)
  execute_flow(:execute_listing_message_feed_flow, orchestrator_name:, partner:, logger:, trial_run:)
end

.execute_order_flow(options = {}) ⇒ Object



232
233
234
235
236
# File 'app/services/edi/base_orchestrator.rb', line 232

def self.execute_order_flow(options = {})
  # Queue for Ship confirm what can be confirmed right away
  # Edi::ShipConfirm.new.process # NO MORE AUTO SHIP CONFIRM
  orchestrators(options).each(&:execute_order_flow)
end

.execute_price_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object

Executes the price flow for EDI orchestrators.

orchestrator_name - The name of a specific orchestrator to run, optional.
partner - The partner key to run for, optional.
logger - The logger to use.
trial_run - If true, will not send real requests.



146
147
148
# File 'app/services/edi/base_orchestrator.rb', line 146

def self.execute_price_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false)
  execute_flow(:execute_price_flow, orchestrator_name:, partner:, logger:, trial_run:)
end

.execute_product_data_flow(options = {}) ⇒ Object



290
291
292
# File 'app/services/edi/base_orchestrator.rb', line 290

def self.execute_product_data_flow(options = {})
  orchestrators(options).each(&:execute_product_data_flow)
end

.orchestrator_for_customer_id(customer_id, use_cache: true) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'app/services/edi/base_orchestrator.rb', line 76

def orchestrator_for_customer_id(customer_id, use_cache: true)
  # 041619 Ramie: match first on single customer id partners, then dig into multi customer partners
  # this is necessary to properly match Amazon Vendor Central vendor partners: there is a single EDI entity partner:
  # :amazon_vendor_central_direct_fulfillment (for both WAX7V and WAT4D)
  # but two single partners for vendors WAX7V and WAT4D:
  # :amazon_vendor_central_direct_fulfillment_us_WAX7V and
  # :amazon_vendor_central_direct_fulfillment_us_WAT4D
  # and we want to drill down to one of the correct single customer id partners above, not the multi customer :amazon_vendor_central_direct_fulfillment partner (which is really only set up for inventory)

  # Use the cached lookup map for O(1) partner key lookups instead of O(n) detect operations
  partner_config_key = customer_id_to_partner_key_map[customer_id]
  return unless partner_config_key

  use_cache ? cached_build(partner_config_key) : build(partner_config_key)
end

.orchestrators(options = {}) ⇒ Object



92
93
94
# File 'app/services/edi/base_orchestrator.rb', line 92

def orchestrators(options = {})
  partners.keys.map { |partner| new(partner, options) }
end

.partnersObject



21
22
23
# File 'app/services/edi/base_orchestrator.rb', line 21

def partners
  all_orchestrators_class.map(&:partners).reduce({}, :merge)
end

Instance Method Details

#confirm_outbound_processing?Boolean

By default we don't require a two stage processing (ready -> processing -> complete)

Returns:

  • (Boolean)


384
385
386
# File 'app/services/edi/base_orchestrator.rb', line 384

def confirm_outbound_processing?
  false
end

#customer(segment = nil) ⇒ Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'app/services/edi/base_orchestrator.rb', line 326

def customer(segment = nil)
  raise 'Orchestrator requires a segment for this partner to determine customer' if customer_id.is_a?(Hash) && segment.nil?

  if segment.present? && customer_id.is_a?(Hash)
    segment_cust_id = customer_id[segment.to_s.downcase.to_sym]
    segment_cust_id = customer_id[segment.to_s.to_sym] if segment_cust_id.nil? # In case we use uppercase keys
    cust = Customer.where(id: segment_cust_id).first
  elsif customer_id
    cust = Customer.where(id: customer_id).first
  end
  return cust if cust

  raise 'Orchestrator is unable to find a customer'
end

#customer_catalogObject

Alias for Customer#catalog

Returns:

  • (Object)

    Customer#customer_catalog

See Also:



11
# File 'app/services/edi/base_orchestrator.rb', line 11

delegate :catalog, to: :customer, prefix: true

#customer_idsObject



347
348
349
350
351
352
353
354
355
356
357
# File 'app/services/edi/base_orchestrator.rb', line 347

def customer_ids
  if respond_to?(:customer_id)
    if customer_id.respond_to?(:values)
      customer_id.values.uniq
    else
      [customer_id]
    end
  else
    []
  end
end

#customersObject

Returns customers (or single customer) associated with an orchestrator
as an active relation



343
344
345
# File 'app/services/edi/base_orchestrator.rb', line 343

def customers
  Customer.where(id: customer_ids)
end

#execute_inventory_flowObject



298
299
300
# File 'app/services/edi/base_orchestrator.rb', line 298

def execute_inventory_flow
  # Implement me in subclass
end

#execute_order_flowObject



294
295
296
# File 'app/services/edi/base_orchestrator.rb', line 294

def execute_order_flow
  # Implement me in subclass
end

#execute_price_flowObject



302
303
304
# File 'app/services/edi/base_orchestrator.rb', line 302

def execute_price_flow
  # Implement me in subclass
end

#execute_product_data_flowObject



306
307
308
# File 'app/services/edi/base_orchestrator.rb', line 306

def execute_product_data_flow
  # Implement me in subclass
end

#ignore_back_ordersObject

By default back orders are not ignored



389
390
391
# File 'app/services/edi/base_orchestrator.rb', line 389

def ignore_back_orders
  false
end

#inventory_message_enabled?Boolean

Returns:

  • (Boolean)


256
257
258
# File 'app/services/edi/base_orchestrator.rb', line 256

def inventory_message_enabled?
  try(:inventory_message_enabled).to_b
end

#pending_discontinue_lifetimeObject



361
362
363
364
# File 'app/services/edi/base_orchestrator.rb', line 361

def pending_discontinue_lifetime
  val = try(:pending_discontinue_lifetime_duration)
  val.is_a?(ActiveSupport::Duration) ? val : DEFAULT_PENDING_DISCONTINUE_LIFETIME
end

#price_message_enabled?Boolean

Returns:

  • (Boolean)


248
249
250
# File 'app/services/edi/base_orchestrator.rb', line 248

def price_message_enabled?
  try(:price_message_enabled).to_b
end

#product_data_enabled?Boolean

Returns:

  • (Boolean)


252
253
254
# File 'app/services/edi/base_orchestrator.rb', line 252

def product_data_enabled?
  try(:product_data_enabled).to_b
end

#should_execute_flow?(flow) ⇒ Boolean

Determines if the inventory flow should run based on the stored frequency

Returns:

  • (Boolean)


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
# File 'app/services/edi/base_orchestrator.rb', line 261

def should_execute_flow?(flow) # flow is in the format of execute_inventory_flow or execute_price_flow
  flow_every_x_hour_sym = :"#{flow}_every_x_hour"

  return true unless respond_to? flow_every_x_hour_sym

  every_x_hour = send(flow_every_x_hour_sym).to_i
  current_hour = Time.current.hour

  if every_x_hour > 0 && every_x_hour <= 24
    # valid integral value, let's use it
    # warn if it's not exactly one of the recommended value, i.e. integral factors of 24
    unless RECOMMENDED_EXECUTE_FLOW_EVERY_X_HOUR.include?(every_x_hour)
      msg = "EDI #{self.class} partner: #{partner} has a #{flow_every_x_hour_sym} value of #{every_x_hour} which is not one of the recommended values: #{RECOMMENDED_EXECUTE_FLOW_EVERY_X_HOUR}, inventory may not be sent at exactly the desired frequency"
      ErrorReporting.warning(msg)
      Rails.logger.info(msg)
    end
    # but do go ahead and send per the every_x_hour integral value
    return true if current_hour.modulo(every_x_hour) == 0
  else
    # invalid value, error but do send it at least once a day as a fall back
    msg = "EDI #{self.class} partner: #{partner} has an invalid #{flow_every_x_hour_sym} value of #{every_x_hour}. Valid values are between #{RECOMMENDED_EXECUTE_FLOW_EVERY_X_HOUR.min} and #{RECOMMENDED_EXECUTE_FLOW_EVERY_X_HOUR.max}, as a failsafe fallback, inventory will only be sent once per day!"
    ErrorReporting.error(msg)
    Rails.logger.error(msg)
    return true if current_hour.modulo(24) == 0
  end

  false
end

#should_execute_order_flow?Boolean

Determines if the order flow should run

Returns:

  • (Boolean)


239
240
241
# File 'app/services/edi/base_orchestrator.rb', line 239

def should_execute_order_flow?
  true
end

#should_execute_product_data_flow?Boolean

Determines if the product data flow should run

Returns:

  • (Boolean)


244
245
246
# File 'app/services/edi/base_orchestrator.rb', line 244

def should_execute_product_data_flow?
  true
end

#test_mode?Boolean

Returns:

  • (Boolean)


322
323
324
# File 'app/services/edi/base_orchestrator.rb', line 322

def test_mode?
  Rails.env.development?
end