Class: Catalog::AmazonPricingAutomationService

Inherits:
BaseService
  • Object
show all
Defined in:
app/services/catalog/amazon_pricing_automation_service.rb

Overview

Nightly pricing automation service for Amazon US and Canada catalogs.
Orchestrates both price lowering (to win Buy Box) and price raising (when winning stably).

This service delegates to specialized services:

  • AmazonPriceLoweringService: Handles all price competition logic
  • AmazonPriceRaisingService: Handles gradual price increases for stable winners

Runs nightly after fresh Amazon data is pulled (scheduled for 6:15am CT).

Defined Under Namespace

Classes: Result

Constant Summary collapse

DB_RETRY_EXCEPTIONS =

Exceptions that trigger retries in db.

[ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad].freeze
DB_RETRY_TRIES =

Db retry tries.

3
BUY_BOX_WINNER_STABLE_DAYS =

Delegate constants to specialized services for external reference

Catalog::AmazonPriceRaisingService::BUY_BOX_WINNER_STABLE_DAYS
COMPETITIVE_PRICE_THRESHOLD_PERCENT =

Competitive price threshold percent.

Catalog::AmazonPriceLoweringService::COMPETITIVE_PRICE_THRESHOLD_PERCENT
AMAZON_SELLER_IDS =

Amazon seller ids.

Catalog::AmazonPriceLoweringService::AMAZON_SELLER_IDS
EXTERNAL_BLOCK_REASONS =

Raise-attempt outcomes where an external retailer price is the binding
constraint - these create/refresh a reprice_blocked_by_external flag.

%i[
  blocked_by_refreshed_sibling_price
  blocked_by_cached_sibling_price
  at_or_above_sibling_retailer
  at_or_above_external_threshold
].freeze
RAISE_GATE_REASONS =

Raise-attempt gates that exit before external prices are evaluated.
They carry no information about whether an earlier external block has
cleared, so any active reprice_blocked_by_external flag is left alone.

%i[
  repricing_disabled
  not_buy_box_winner
  buy_box_winner_not_stable
].freeze

Instance Attribute Summary

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger

Constructor Details

#initializeAmazonPricingAutomationService

Returns a new instance of AmazonPricingAutomationService.



49
50
51
52
53
54
# File 'app/services/catalog/amazon_pricing_automation_service.rb', line 49

def initialize
  super
  @lowering_service = Catalog::AmazonPriceLoweringService.new
  @raising_service = Catalog::AmazonPriceRaisingService.new
  @buy_box_service = Catalog::AmazonBuyBoxService.new(lowering_service: @lowering_service, raising_service: @raising_service)
end

Instance Method Details

#process(options = {}) ⇒ Result

Returns the automation result.

Parameters:

  • options (Hash) (defaults to: {})

    processing options

Options Hash (options):

  • catalog_ids (Array<Integer>)

    catalog ids to process (defaults to the Amazon US and Canada catalog ids)

  • limit (Integer, String)

    maximum number of catalog items to process

  • skip_fresh_data_check (Boolean)

    process all items with Amazon data, not just freshly pulled ones (default: false)

  • send_email_report (Boolean)

    email the repricing report when significant actions occurred (default: true)

Returns:

  • (Result)

    the automation result



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
# File 'app/services/catalog/amazon_pricing_automation_service.rb', line 62

def process(options = {})
  options = options.symbolize_keys if options.is_a?(Hash)
  catalog_ids = options[:catalog_ids] || [CatalogConstants::AMAZON_SC_US_CATALOG_ID, CatalogConstants::AMAZON_SC_CA_CATALOG_ID]
  limit = options[:limit]&.to_i
  skip_fresh_data_check = options.fetch(:skip_fresh_data_check, false)
  send_email_report = options.fetch(:send_email_report, true)

  messages = []
  processed_count = 0
  price_increased_count = 0
  price_lowered_count = 0
  price_reverted_count = 0
  flagged_count = 0
  blocked_by_external = []

  catalog_items = build_catalog_items_query(catalog_ids, limit, skip_fresh_data_check)
  logger.info "Found #{catalog_items.count} catalog items to process"

  catalog_items.find_each do |catalog_item|
    yield(catalog_item: catalog_item) if block_given?

    result = with_connection_retry(catalog_item) { process_catalog_item(catalog_item) }
    processed_count += 1

    case result[:action]
    when :price_increased
      price_increased_count += 1
      messages << "Catalog Item #{catalog_item.id} (#{catalog_item.sku}): Price increased to #{catalog_item.reload.amount}"
    when :price_lowered
      price_lowered_count += 1
      messages << "Catalog Item #{catalog_item.id} (#{catalog_item.sku}): Price lowered to #{catalog_item.reload.amount}"
    when :price_reverted
      price_reverted_count += 1
      messages << "Catalog Item #{catalog_item.id} (#{catalog_item.sku}): Raise reverted to #{catalog_item.reload.amount} after Buy Box loss"
    when :flagged
      flagged_count += 1
      messages << "Catalog Item #{catalog_item.id} (#{catalog_item.sku}): Flagged - #{result[:reason]}"
    when :blocked_by_external
      # Track items blocked by external retailer prices
      blocked_by_external << result[:blocked_item_data]
      messages << "Catalog Item #{catalog_item.id} (#{catalog_item.sku}): Blocked by external price $#{result[:external_price]}"
    when :no_action
      logger.debug "Catalog Item #{catalog_item.id}: No action - #{result[:reason]}"
    end

    logger.info "Processed catalog item #{catalog_item.id}: #{result[:action]}"
  end

  # Send email report if there were significant actions
  if send_email_report && (price_increased_count.positive? || price_lowered_count.positive? || price_reverted_count.positive? || blocked_by_external.any?)
    send_repricing_report(
      processed_count: processed_count,
      price_increased_count: price_increased_count,
      price_lowered_count: price_lowered_count,
      price_reverted_count: price_reverted_count,
      flagged_count: flagged_count,
      blocked_by_external: blocked_by_external,
      messages: messages
    )
  end

  Result.new(
    processed_count: processed_count,
    price_increased_count: price_increased_count,
    price_lowered_count: price_lowered_count,
    price_reverted_count: price_reverted_count,
    flagged_count: flagged_count,
    blocked_by_external: blocked_by_external,
    messages: messages
  )
end

#refresh_listing_issues(catalog_item) ⇒ void

This method returns an undefined value.

Re-derive the has_issues flag for a single catalog item from its
(freshly-pulled) Amazon listing data — resolving a flag Amazon no longer
reports and (re)creating one it still does. Public entry point for the
Listing Issues "Recheck" refresh; reconciles issues only, never reprices.

Parameters:



140
141
142
# File 'app/services/catalog/amazon_pricing_automation_service.rb', line 140

def refresh_listing_issues(catalog_item)
  sync_listing_issues(catalog_item)
end