Class: Catalog::AmazonPriceRaisingService

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

Overview

Service responsible for gradually raising Amazon prices when we're the stable Buy Box winner.
This service handles all price increase logic:

  • Checking if we're a stable Buy Box winner (winning for N days)
  • Refreshing external retailer prices before deciding to raise (to avoid competing with stale data)
  • Increasing prices within safe thresholds
  • Respecting price ceilings (parent price, external thresholds, sibling retailers)

Used by:

  • AmazonPricingAutomationService (nightly full run)

Defined Under Namespace

Classes: Result

Constant Summary collapse

BUY_BOX_WINNER_STABLE_DAYS =

Number of days we must hold Buy Box before raising prices

5
PRICE_INCREASE_PERCENT =

Price increase amounts (whichever is larger)

0.005
PRICE_INCREASE_DOLLAR =

0.5%

1.0
MAX_PRICE_INCREASE_PERCENT =

Maximum automatic price change percentage (5%)

0.05

Instance Method Summary collapse

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 BaseService

Instance Method Details

#buy_box_winner_stable?(catalog_item) ⇒ Boolean

Check if the catalog item has been a stable Buy Box winner
A catalog item is considered stable if:

  1. We have tracked when the Buy Box status last changed, and it was > N days ago, OR
  2. We don't have a tracked change date, but recent EDI logs show we've been winning consistently

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/services/catalog/amazon_price_raising_service.rb', line 75

def buy_box_winner_stable?(catalog_item)
  return false unless catalog_item.is_amz_buy_box_winner

  if catalog_item.amz_last_buy_box_winner_change.present?
    days_since_change = (Time.current - catalog_item.amz_last_buy_box_winner_change) / 1.day
    return days_since_change >= BUY_BOX_WINNER_STABLE_DAYS
  end

  # Fallback: Check recent EDI Buy Box status logs for stability
  # This handles cases where amz_last_buy_box_winner_change is NULL (e.g., historical items)
  check_edi_logs_for_stability(catalog_item)
end

#check_edi_logs_for_stability(catalog_item) ⇒ Object

Check recent EDI Buy Box status logs to determine if we've been a stable winner
Returns true only if we have consistent Buy Box wins over the required period



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'app/services/catalog/amazon_price_raising_service.rb', line 90

def check_edi_logs_for_stability(catalog_item)
  recent_logs = catalog_item.edi_communication_logs
                            .where(category: :buy_box_status)
                            .where(state: 'processed')
                            .where('edi_communication_logs.created_at >= ?', BUY_BOX_WINNER_STABLE_DAYS.days.ago)
                            .order('edi_communication_logs.created_at DESC')
                            .limit(10)

  # Must have at least 2 recent logs to verify stability
  return false if recent_logs.count < 2

  # Must span at least the required number of days
  oldest_log = recent_logs.last
  days_covered = (Time.current - oldest_log.created_at) / 1.day
  return false if days_covered < BUY_BOX_WINNER_STABLE_DAYS

  # Check that all logs show us as the Buy Box winner
  recent_logs.all? do |ecl|
    is_buy_box_winner_in_log?(ecl, catalog_item)
  end
rescue StandardError => e
  logger.warn "Error checking EDI logs for stability: #{e.message}"
  false
end

#is_buy_box_winner_in_log?(ecl, catalog_item) ⇒ Boolean

Parse an EDI log to determine if we were the Buy Box winner

Returns:

  • (Boolean)


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'app/services/catalog/amazon_price_raising_service.rb', line 116

def is_buy_box_winner_in_log?(ecl, catalog_item)
  return false unless ecl.data.present?

  json_hash = JSON.parse(ecl.data).with_indifferent_access
  status = json_hash.dig(:payload, :status)

  # If no buyable offers, we're not winning
  return false if status&.downcase == 'nobuyableoffers'

  offers = json_hash.dig(:payload, :Offers) || []
  return false if offers.empty?

  merchant_id = catalog_item.catalog.merchant_id
  our_offer = offers.find { |o| o[:SellerId] == merchant_id }

  # Only consider Buy Box winner status, not Featured Merchant alone
  our_offer.present? && our_offer[:IsBuyBoxWinner] == true
rescue JSON::ParserError, NoMethodError
  false
end

#process(catalog_item, skip_sibling_refresh: false) ⇒ Object

Process a single catalog item and attempt to raise price if appropriate
Returns a Result with :action (:price_increased, :no_action) and details

Parameters:

  • catalog_item (CatalogItem)

    The Amazon catalog item to process

  • skip_sibling_refresh (Boolean) (defaults to: false)

    Skip refreshing sibling prices (for testing/dry-run)



34
35
36
37
38
39
40
41
42
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
# File 'app/services/catalog/amazon_price_raising_service.rb', line 34

def process(catalog_item, skip_sibling_refresh: false)
  return Result.new(action: :no_action, reason: :repricing_disabled, new_price: nil, price_ceiling: nil) if catalog_item.disable_amz_repricing

  # Must be Buy Box winner to consider raising price (Featured Merchant alone is not sufficient)
  return Result.new(action: :no_action, reason: :not_buy_box_winner, new_price: nil, price_ceiling: nil) unless catalog_item.is_amz_buy_box_winner

  # Must be stable winner (holding Buy Box for required days)
  return Result.new(action: :no_action, reason: :buy_box_winner_not_stable, new_price: nil, price_ceiling: nil) unless buy_box_winner_stable?(catalog_item)

  # IMPORTANT: Before deciding to raise price, refresh sibling retailer probes
  # This ensures we have fresh external price data to avoid raising above competitors
  sibling_refresh_result = nil
  unless skip_sibling_refresh
    sibling_refresh_result = refresh_sibling_prices(catalog_item)
    if sibling_refresh_result && sibling_refresh_result.lowest_price.present?
      our_price = catalog_item.amazon_price_with_tax
      if sibling_refresh_result.lowest_price <= our_price
        logger.info "Catalog Item #{catalog_item.id}: Blocked after sibling refresh - external price $#{sibling_refresh_result.lowest_price} <= our price $#{our_price}"
        return Result.new(
          action: :no_action,
          reason: :blocked_by_refreshed_sibling_price,
          new_price: nil,
          price_ceiling: sibling_refresh_result.lowest_price,
          sibling_refresh_result: sibling_refresh_result
        )
      end
    end
  end

  # Check all price ceilings before raising
  ceiling_check = check_price_ceilings(catalog_item)
  return Result.new(action: :no_action, reason: ceiling_check[:reason], new_price: nil, price_ceiling: ceiling_check[:ceiling], sibling_refresh_result: sibling_refresh_result) unless ceiling_check[:can_raise]

  # Calculate and apply price increase
  increase_price(catalog_item, ceiling_check[:ceiling], sibling_refresh_result)
end