Class: Edi::BaseOrchestrator
- Inherits:
-
Object
- Object
- Edi::BaseOrchestrator
- Extended by:
- Memery
- Defined in:
- app/services/edi/base_orchestrator.rb
Direct Known Subclasses
Amazon::Orchestrator, AmazonVc::Orchestrator, Commercehub::Orchestrator, Houzz::Orchestrator, Menard::Orchestrator, MftGateway::Orchestrator, MiraklSeller::Orchestrator, Walmart::Orchestrator, Wayfair::Orchestrator
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
- RECOMMENDED_EXECUTE_FLOW_EVERY_X_HOUR =
[1, 2, 3, 4, 6, 8, 12, 24].freeze
- DEFAULT_PENDING_DISCONTINUE_LIFETIME =
1.day
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
Delegated Instance Attributes collapse
-
#customer_catalog ⇒ Object
Alias for Customer#catalog.
Class Method Summary collapse
- .all_orchestrators_class ⇒ Object
- .build(partner_config_key, options = {}) ⇒ Object
-
.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.
-
.cached_orchestrators ⇒ Object
Request/job-scoped cache of orchestrator instances by partner key.
-
.catalog_id_to_pending_discontinue_lifetime ⇒ Object
Returns a hash of { catalog_id => ActiveSupport::Duration } for all active orchestrators that define a custom pending_discontinue_lifetime.
- .catalog_ids_edi_enabled ⇒ Object
-
.customer_id_to_partner_key_map ⇒ Object
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.
- .customer_ids_edi_enabled ⇒ Object
- .customer_ids_with_invoice_message_enabled ⇒ Object
-
.execute_discontinue_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object
Executes the discontinue flow for EDI orchestrators.
-
.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).
-
.execute_inventory_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object
Executes the inventory flow for EDI orchestrators.
-
.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.
- .execute_order_flow(options = {}) ⇒ Object
-
.execute_price_flow(orchestrator_name: nil, partner: nil, logger: Rails.logger, trial_run: false) ⇒ Object
Executes the price flow for EDI orchestrators.
- .execute_product_data_flow(options = {}) ⇒ Object
- .orchestrator_for_customer_id(customer_id, use_cache: true) ⇒ Object
- .orchestrators(options = {}) ⇒ Object
- .partners ⇒ Object
Instance Method Summary collapse
-
#confirm_outbound_processing? ⇒ Boolean
By default we don't require a two stage processing (ready -> processing -> complete).
- #customer(segment = nil) ⇒ Object
- #customer_ids ⇒ Object
-
#customers ⇒ Object
Returns customers (or single customer) associated with an orchestrator as an active relation.
- #execute_inventory_flow ⇒ Object
- #execute_order_flow ⇒ Object
- #execute_price_flow ⇒ Object
- #execute_product_data_flow ⇒ Object
-
#ignore_back_orders ⇒ Object
By default back orders are not ignored.
-
#initialize(partner, options = {}) ⇒ BaseOrchestrator
constructor
A new instance of BaseOrchestrator.
- #inventory_message_enabled? ⇒ Boolean
- #pending_discontinue_lifetime ⇒ Object
- #price_message_enabled? ⇒ Boolean
- #product_data_enabled? ⇒ Boolean
-
#should_execute_flow?(flow) ⇒ Boolean
Determines if the inventory flow should run based on the stored frequency.
-
#should_execute_order_flow? ⇒ Boolean
Determines if the order flow should run.
-
#should_execute_product_data_flow? ⇒ Boolean
Determines if the product data flow should run.
- #test_mode? ⇒ Boolean
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, = {}) @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 = @logger = [:logger] || Rails.logger end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
5 6 7 |
# File 'app/services/edi/base_orchestrator.rb', line 5 def config @config end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
5 6 7 |
# File 'app/services/edi/base_orchestrator.rb', line 5 def logger @logger end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
5 6 7 |
# File 'app/services/edi/base_orchestrator.rb', line 5 def @options end |
Class Method Details
.all_orchestrators_class ⇒ Object
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, = {}) # 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, ) 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, = {}) cache_key = [partner_config_key.to_sym, .hash].join('_') cached_orchestrators[cache_key] ||= build(partner_config_key, ) end |
.cached_orchestrators ⇒ Object
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_lifetime ⇒ Object
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_enabled ⇒ Object
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_map ⇒ Object
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_enabled ⇒ Object
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_enabled ⇒ Object
101 102 103 104 105 106 107 108 109 110 |
# File 'app/services/edi/base_orchestrator.rb', line 101 def 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. 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., 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., 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.(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( = {}) # Queue for Ship confirm what can be confirmed right away # Edi::ShipConfirm.new.process # NO MORE AUTO SHIP CONFIRM orchestrators().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( = {}) orchestrators().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( = {}) partners.keys.map { |partner| new(partner, ) } end |
.partners ⇒ Object
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)
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_catalog ⇒ Object
Alias for Customer#catalog
11 |
# File 'app/services/edi/base_orchestrator.rb', line 11 delegate :catalog, to: :customer, prefix: true |
#customer_ids ⇒ Object
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 |
#customers ⇒ Object
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_flow ⇒ Object
298 299 300 |
# File 'app/services/edi/base_orchestrator.rb', line 298 def execute_inventory_flow # Implement me in subclass end |
#execute_order_flow ⇒ Object
294 295 296 |
# File 'app/services/edi/base_orchestrator.rb', line 294 def execute_order_flow # Implement me in subclass end |
#execute_price_flow ⇒ Object
302 303 304 |
# File 'app/services/edi/base_orchestrator.rb', line 302 def execute_price_flow # Implement me in subclass end |
#execute_product_data_flow ⇒ Object
306 307 308 |
# File 'app/services/edi/base_orchestrator.rb', line 306 def execute_product_data_flow # Implement me in subclass end |
#ignore_back_orders ⇒ Object
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
256 257 258 |
# File 'app/services/edi/base_orchestrator.rb', line 256 def try(:inventory_message_enabled).to_b end |
#pending_discontinue_lifetime ⇒ Object
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
248 249 250 |
# File 'app/services/edi/base_orchestrator.rb', line 248 def try(:price_message_enabled).to_b end |
#product_data_enabled? ⇒ 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
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
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
244 245 246 |
# File 'app/services/edi/base_orchestrator.rb', line 244 def should_execute_product_data_flow? true end |
#test_mode? ⇒ Boolean
322 323 324 |
# File 'app/services/edi/base_orchestrator.rb', line 322 def test_mode? Rails.env.development? end |