Module: AmazonProductsHelper

Defined in:
app/helpers/amazon_products_helper.rb

Overview

View helpers for the CRM Amazon products dashboard: status icons, price
comparisons, Buy Box/competitor indicators, and retailer price dropdowns.

Constant Summary collapse

AMAZON_SELLER_IDS =

Amazon's seller IDs (US and Canada) - we don't compete with them on price

%w[A2R2RITDJNW1Q6 A3DWYIK6Y9EEQB].freeze
EXTERNAL_COMPETITOR_ID =

Special marker for external competitors (Amazon threshold-based)

'EXTERNAL_COMPETITOR'

Instance Method Summary collapse

Instance Method Details

#build_price_history_html(_catalog_item) ⇒ nil

Builds the price history popover HTML from PaperTrail versions.

PERFORMANCE: disabled for now — querying the versions table per row is
expensive. TODO: re-enable with batch preloading if price history is needed.

Parameters:

  • _catalog_item (CatalogItem)

    item (unused while disabled)

Returns:

  • (nil)

    always nil while disabled



527
528
529
530
531
# File 'app/helpers/amazon_products_helper.rb', line 527

def build_price_history_html(_catalog_item)
  # PERFORMANCE: Disabled for now - querying versions table per-row is expensive
  # TODO: Re-enable with batch preloading if price history is needed
  nil
end

#buy_box_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer

Buy Box trophy/pause/circle icon reflecting win state and repricing status.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    Buy Box status icon HTML



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'app/helpers/amazon_products_helper.rb', line 252

def buy_box_indicator(catalog_item)
  repricing_disabled = catalog_item.respond_to?(:reprice_rule) && catalog_item.reprice_rule == 'off'

  if catalog_item.is_amz_buy_box_winner
    # Check if it's FBA or MFN winning
    fulfillment_type = get_winning_fulfillment_type(catalog_item)

    if fulfillment_type == :fba
      tag.span(class: 'status-icon warning', title: 'FBA Buy Box Winner', data: { bs_toggle: 'tooltip' }) do
        fa_icon('trophy', family: :solid)
      end
    else
      # MFN/FBM winner (or unknown - default to standard)
      tag.span(class: 'status-icon success', title: 'Buy Box Winner', data: { bs_toggle: 'tooltip' }) do
        fa_icon('trophy', family: :solid)
      end
    end
  elsif repricing_disabled
    tag.span(class: 'status-icon warning', title: 'Not winning — repricing disabled (channel protection)', data: { bs_toggle: 'tooltip' }) do
      fa_icon('pause', family: :solid)
    end
  else
    tag.span(class: 'status-icon muted', title: 'Not Buy Box Winner', data: { bs_toggle: 'tooltip' }) do
      fa_icon('circle', family: :regular)
    end
  end
end

#cached_amazon_maximum_seller_allowed_price_with_tax(catalog_item) ⇒ BigDecimal, Float

Computes the maximum seller-allowed price including tax from cached MSRP
(avoids N+1).

Parameters:

  • catalog_item (CatalogItem)

    item to compute the price ceiling for

Returns:

  • (BigDecimal, Float)

    price ceiling including tax



55
56
57
58
59
60
61
62
# File 'app/helpers/amazon_products_helper.rb', line 55

def cached_amazon_maximum_seller_allowed_price_with_tax(catalog_item)
  msrp = cached_msrp(catalog_item)
  price = [msrp, catalog_item.amount].compact.max

  return price unless catalog_item.tax_rate

  ((catalog_item.tax_rate + 1) * (price || 0.0)).round(2)
end

#cached_competitor_name(seller_id) ⇒ String?

Resolves a competitor's display name from the controller-preloaded cache,
falling back to a database lookup.

Parameters:

  • seller_id (String, nil)

    Amazon seller ID to resolve

Returns:

  • (String, nil)

    display name, the raw seller ID when no competitor
    record exists, or nil when seller_id is blank



15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'app/helpers/amazon_products_helper.rb', line 15

def cached_competitor_name(seller_id)
  return nil if seller_id.blank?

  # Handle external competitor marker
  return 'External (Amazon Threshold)' if seller_id == EXTERNAL_COMPETITOR_ID

  if @amazon_competitors_cache.present?
    competitor = @amazon_competitors_cache[seller_id]
    return competitor&.display_name || seller_id
  end

  # Fallback to database lookup
  AmazonCompetitor.name_for(seller_id) || seller_id
end

#cached_edi_log(catalog_item) ⇒ EdiCommunicationLog?

Returns the cached processed buy_box_status EDI log for an item (preloaded in controller).

Parameters:

  • catalog_item (CatalogItem)

    item whose EDI log is needed

Returns:



284
285
286
287
288
289
290
291
292
293
294
295
# File 'app/helpers/amazon_products_helper.rb', line 284

def cached_edi_log(catalog_item)
  if @edi_logs_cache.present?
    # Try both integer and string keys (PostgreSQL may return either)
    return @edi_logs_cache[catalog_item.id] || @edi_logs_cache[catalog_item.id.to_s]
  end

  # Fallback to query if not preloaded
  catalog_item.edi_communication_logs
              .where(category: :buy_box_status, state: 'processed')
              .order(created_at: :desc)
              .first
end

#cached_merchant_id(catalog_item) ⇒ String?

Returns our merchant ID from the controller-preloaded value, falling back
to the catalog orchestrator.

Parameters:

  • catalog_item (CatalogItem)

    item whose catalog orchestrator provides the fallback

Returns:

  • (String, nil)

    merchant ID



302
303
304
305
306
307
# File 'app/helpers/amazon_products_helper.rb', line 302

def cached_merchant_id(catalog_item)
  return @merchant_id if @merchant_id.present?

  # Fallback to query if not preloaded
  catalog_item.catalog.load_orchestrator&.merchant_id
end

#cached_msrp(catalog_item) ⇒ BigDecimal?

Returns the MSRP amount from the cached root catalog item (avoids N+1 on root_catalog_item).

Parameters:

  • catalog_item (CatalogItem)

    item whose MSRP is needed

Returns:

  • (BigDecimal, nil)

    MSRP amount, or nil when unavailable



46
47
48
# File 'app/helpers/amazon_products_helper.rb', line 46

def cached_msrp(catalog_item)
  cached_root_catalog_item(catalog_item).try(:amount)
end

#cached_root_catalog_item(catalog_item) ⇒ CatalogItem

Resolves the root catalog item from the controller-preloaded cache,
avoiding N+1 lookups.

Parameters:

  • catalog_item (CatalogItem)

    item whose root catalog item is needed

Returns:

  • (CatalogItem)

    the cached root catalog item, or the given item when
    the cache is absent or has no entry for it



36
37
38
39
40
# File 'app/helpers/amazon_products_helper.rb', line 36

def cached_root_catalog_item(catalog_item)
  return catalog_item if @root_catalog_items_cache.nil?

  @root_catalog_items_cache[catalog_item.store_item_id] || catalog_item
end

#cached_sibling_retailer_prices(catalog_item) ⇒ Array<CatalogItem>

Returns sibling catalog items with retail prices from the controller-preloaded cache.

These are prices from other retailers (Home Depot, Wayfair, Costco, etc.)
for the same item. Falls back to a direct query when the cache is not
available (e.g., Turbo Stream updates).

Parameters:

Returns:

  • (Array<CatalogItem>)

    sibling catalog items with retail price fields



861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
# File 'app/helpers/amazon_products_helper.rb', line 861

def cached_sibling_retailer_prices(catalog_item)
  item_id = catalog_item.respond_to?(:item_id) ? catalog_item.item_id : catalog_item.store_item&.item_id
  return [] unless item_id

  if @sibling_retailer_prices_cache.present?
    siblings = @sibling_retailer_prices_cache[item_id] || []
    store_id = resolve_store_id_for_sibling_filter(catalog_item)
    return siblings.select { |s| s.store_id == store_id } if store_id

    return siblings
  end

  # Fallback: Direct query for Turbo Stream updates
  # This is less efficient but only happens for single-item updates
  fetch_sibling_retailer_prices_for_item(item_id, catalog_item)
end

#competitive_price_column(catalog_item) ⇒ ActiveSupport::SafeBuffer

Renders the dedicated competitive price column with a popover comparing our
price to the best competitor.

Logic Details

Green when we beat the competitor by 10%+, orange when we beat by less (or
tie), red when the competitor is cheaper and we're not winning the Buy Box,
blue for Amazon (we don't auto-compete with them). External thresholds show
as an orange warning for Buy Box winners.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    competitive price span HTML



572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'app/helpers/amazon_products_helper.rb', line 572

def competitive_price_column(catalog_item)
  # Get the best competitor offer from EDI data (any seller that isn't us)
  edi_data = get_best_competitor_from_edi(catalog_item)

  return tag.span('', class: 'text-body-secondary') if edi_data.blank?

  comp_price = edi_data[:price]
  competitor_seller_id = edi_data[:seller_id]
  is_external = edi_data[:is_external] == true
  is_warning = edi_data[:is_warning] == true # Buy Box winner with external threshold below our price
  is_amazon_competitor = edi_data[:is_amazon] == true # Amazon is the Buy Box winner

  return tag.span('', class: 'text-body-secondary') if comp_price.blank?

  our_price = catalog_item.amazon_price_with_tax
  return tag.span('', class: 'text-body-secondary') if our_price.blank? || our_price.zero?

  price_diff = our_price - comp_price # Positive means we're higher, negative means we're lower
  price_diff_pct = (price_diff / comp_price * 100).round(1)

  # Color coding based on price comparison
  # Special case: External threshold warning for Buy Box winners → always orange
  # Green: We beat competitor by 10%+ (our price is 10%+ lower)
  # Orange: We beat competitor by less than 10% (including same price), OR warning for BB winners
  # Red: Competitor beats us (their price is lower) and we're NOT the Buy Box winner
  # Blue: Amazon competitors (we don't compete with them)
  diff_class = if is_warning
                 'text-warning'  # Buy Box winner but external threshold below our price - warning
               elsif price_diff.negative? && price_diff_pct <= -10
                 'text-success'  # We're 10%+ cheaper - excellent
               elsif price_diff.negative? || price_diff.zero?
                 'text-warning'  # We're cheaper by less than 10% OR same price
               else
                 'text-danger'   # Competitor is cheaper - we need to lower price
               end

  # Build popover with competitor info (no links - they don't work in popovers)
  competitor_name = if is_external
                      'External'
                    elsif competitor_seller_id.present?
                      cached_competitor_name(competitor_seller_id)
                    else
                      'Unknown'
                    end

  popover_html = "<strong>Competitor:</strong> #{h(competitor_name)}"
  if is_external && is_warning
    popover_html += " <span class='badge bg-warning text-dark'>Price Ceiling</span>"
  elsif is_external
    popover_html += " <span class='badge bg-danger'>Amazon Threshold</span>"
  elsif is_amazon_competitor
    popover_html += " <span class='badge' style='background-color: #0d6efd;'>Amazon</span>"
  end
  popover_html += "<br><strong>Their price:</strong> #{number_to_currency(comp_price)}"
  popover_html += "<br><strong>Our price:</strong> #{number_to_currency(our_price)}"

  # Use inline style for Amazon to ensure blue color renders in popover
  if is_external
    popover_html += "<br><strong>Difference:</strong> <span class='#{diff_class}'>#{'+' unless price_diff.negative?}#{number_to_currency(price_diff)} (#{'+' unless price_diff.negative?}#{price_diff_pct}%)</span>"
    popover_html += if is_warning
                      "<br><em class='text-warning small'>#{fa_icon('circle-exclamation', family: :solid, class: 'me-1')}Price ceiling - don't raise above this</em>"
                    elsif price_diff.positive?
                      "<br><em class='text-danger small'>#{fa_icon('triangle-exclamation', family: :solid, class: 'me-1')}External competitor beating us</em>"
                    else
                      "<br><em class='text-body-secondary small'>External competitor (Amazon threshold)</em>"
                    end
  elsif is_amazon_competitor
    popover_html += "<br><strong>Difference:</strong> <span style='color: #0d6efd;'>#{'+' unless price_diff.negative?}#{number_to_currency(price_diff)} (#{'+' unless price_diff.negative?}#{price_diff_pct}%)</span>"
    popover_html += "<br><em style='color: #0d6efd;' class='small'>#{fa_icon('amazon', family: :brands, class: 'me-1')}We don't auto-compete with Amazon</em>"
  else
    popover_html += "<br><strong>Difference:</strong> <span class='#{diff_class}'>#{'+' unless price_diff.negative?}#{number_to_currency(price_diff)} (#{'+' unless price_diff.negative?}#{price_diff_pct}%)</span>"
  end

  # Display with optional indicator
  # External competitor warning (Buy Box winner): show ceiling icon in orange
  # External competitor danger (not winning): show warning icon in red
  price_display = if is_external && is_warning
                    safe_join([fa_icon('arrow-up', family: 'solid', class: 'me-1'), number_to_currency(comp_price)])
                  elsif is_external && price_diff.positive?
                    safe_join([fa_icon('triangle-exclamation', family: 'solid', class: 'me-1'), number_to_currency(comp_price)])
                  elsif is_external
                    safe_join([tag.span('EXT', class: 'badge bg-secondary me-1', style: 'font-size: 0.6rem;'), number_to_currency(comp_price)])
                  elsif is_amazon_competitor
                    safe_join([fa_icon('amazon', family: 'brands', class: 'me-1'), number_to_currency(comp_price)])
                  else
                    number_to_currency(comp_price)
                  end

  # Use blue for Amazon competitors, red for external
  display_style = if is_amazon_competitor
                    'color: #0d6efd !important;'
                  elsif is_external
                    nil # Use text-danger class
                  end

  tag.span(
    class: "#{diff_class} cursor-pointer",
    style: display_style,
    data: {
      bs_toggle: 'popover',
      bs_trigger: 'hover focus',
      bs_html: 'true',
      bs_content: popover_html,
      bs_placement: 'left'
    }
  ) do
    price_display
  end
end

#competitive_price_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer, String

Inline competitive-price hint for non-Buy-Box winners, from the most recent
flag carrying competitive price data.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer, String)

    competitor price HTML or ''



538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'app/helpers/amazon_products_helper.rb', line 538

def competitive_price_indicator(catalog_item)
  return '' if catalog_item.is_amz_buy_box_winner

  # Check for recent flags with competitive price data
  flag = catalog_item.amazon_catalog_item_flags
                     .where.not(competitive_price: nil)
                     .order(created_at: :desc)
                     .first

  return '' if flag&.competitive_price.blank?

  price_diff = flag.competitive_price - catalog_item.amazon_price_with_tax
  diff_class = price_diff.negative? ? 'text-success' : 'text-danger'

  tag.small(class: "#{diff_class} d-block") do
    safe_join([
                'Comp: ',
                number_to_currency(flag.competitive_price),
                " (#{'+' if price_diff.positive?}#{number_to_currency(price_diff)})"
              ])
  end
end

#competitor_action_data(catalog_item) ⇒ Hash?

Returns competitor data for the Actions column link.

Parameters:

Returns:

  • (Hash, nil)

    { seller_id:, name:, url:, is_unknown:, is_external: },
    or nil when there is no competitor



687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
# File 'app/helpers/amazon_products_helper.rb', line 687

def competitor_action_data(catalog_item)
  edi_data = get_best_competitor_from_edi(catalog_item)
  return nil if edi_data.blank? || edi_data[:seller_id].blank?

  seller_id = edi_data[:seller_id]

  # Handle external competitors (Amazon threshold-based)
  if seller_id == EXTERNAL_COMPETITOR_ID
    return {
      seller_id: seller_id,
      name: 'External (Amazon Threshold)',
      url: nil, # No storefront URL for external competitors
      is_unknown: false,
      is_external: true
    }
  end

  competitor = AmazonCompetitor.find_by(seller_id: seller_id) if @amazon_competitors_cache.blank?
  competitor ||= @amazon_competitors_cache&.[](seller_id)

  {
    seller_id: seller_id,
    name: competitor&.display_name || seller_id,
    url: competitor_storefront_url_for(catalog_item, seller_id),
    is_unknown: competitor.nil? || competitor.name.blank?,
    is_external: false
  }
end

#competitor_storefront_url_for(catalog_item, seller_id) ⇒ String

Builds the competitor storefront URL for the catalog's Amazon marketplace.

Parameters:

  • catalog_item (CatalogItem, ViewAmazonCatalogItem)

    item carrying the
    marketplace identifier (directly or via its catalog)

  • seller_id (String)

    competitor Amazon seller ID

Returns:

  • (String)

    storefront URL (defaults to amazon.com for unknown marketplaces)



722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
# File 'app/helpers/amazon_products_helper.rb', line 722

def competitor_storefront_url_for(catalog_item, seller_id)
  # Use marketplace_identifier directly if available (from ViewAmazonCatalogItem)
  # Otherwise fall back to chaining through associations (CatalogItem)
  identifier = if catalog_item.respond_to?(:marketplace_identifier)
                 catalog_item.marketplace_identifier
               else
                 catalog_item.catalog&.amazon_marketplace&.marketplace_identifier
               end

  marketplace_domain = case identifier
                       when 'ATVPDKIKX0DER' then 'amazon.com'
                       when 'A2EUQ1WTGCTBG2' then 'amazon.ca'
                       when 'A1F83G8C2ARO7P' then 'amazon.co.uk'
                       when 'A1PA6795UKMFR9' then 'amazon.de'
                       when 'A13V1IB3VIYZZH' then 'amazon.fr'
                       when 'APJ6JRA9NG5V4' then 'amazon.it'
                       when 'A1RKKUPIHCS9HS' then 'amazon.es'
                       else 'amazon.com'
                       end
  "https://www.#{marketplace_domain}/sp?seller=#{seller_id}"
end

#data_freshness_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer

Shows how fresh the Amazon data is, color-coded by age.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    freshness span HTML



432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'app/helpers/amazon_products_helper.rb', line 432

def data_freshness_indicator(catalog_item)
  return tag.span('Never', class: 'text-body-secondary') if catalog_item.amazon_info_datetime.blank?

  time_ago = time_ago_in_words(catalog_item.amazon_info_datetime)
  freshness_class = if catalog_item.amazon_info_datetime > 24.hours.ago
                      'text-success'
                    elsif catalog_item.amazon_info_datetime > 7.days.ago
                      'text-warning'
                    else
                      'text-danger'
                    end

  tag.span(time_ago, class: freshness_class, title: catalog_item.amazon_info_datetime.strftime('%Y-%m-%d %H:%M'), data: { bs_toggle: 'tooltip' })
end

Star icon shown when we are the featured merchant for the item.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    featured merchant icon HTML



337
338
339
340
341
342
343
344
345
346
347
# File 'app/helpers/amazon_products_helper.rb', line 337

def featured_merchant_indicator(catalog_item)
  if catalog_item.is_amz_featured_merchant
    tag.span(class: 'status-icon warning', title: 'Featured Merchant', data: { bs_toggle: 'tooltip' }) do
      fa_icon('star', family: :solid)
    end
  else
    tag.span(class: 'status-icon muted', title: 'Not Featured', data: { bs_toggle: 'tooltip' }) do
      fa_icon('circle', family: :regular)
    end
  end
end

#fetch_sibling_retailer_prices_for_item(item_id, catalog_item) ⇒ Array<CatalogItem>

Fetches sibling retailer prices for a single item when the cache is not available.

Parameters:

Returns:

  • (Array<CatalogItem>)

    sibling catalog items with retail price fields



895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
# File 'app/helpers/amazon_products_helper.rb', line 895

def fetch_sibling_retailer_prices_for_item(item_id, catalog_item)
  store_id = catalog_item.respond_to?(:store_id) ? catalog_item.store_id : catalog_item.catalog&.store_id
  return [] unless store_id

  CatalogItem
    .joins(:catalog, :store_item)
    .where(catalogs: { store_id: store_id, external_price_check_enabled: true })
    .where(store_items: { item_id: item_id })
    .where(state: 'active')
    .where.not(retail_price: nil)
    .where('catalog_items.retail_price > 0')
    .select(
      'catalog_items.id as catalog_item_id',
      'catalog_items.retail_price',
      'catalog_items.retailer_price_updated_at as retail_price_updated_at',
      'catalog_items.url',
      'catalogs.name as catalog_name',
      'catalogs.currency as retailer_currency'
    )
    .to_a
end

#get_best_competitor_from_edi(catalog_item) ⇒ Hash?

Returns the best (lowest price) competitor offer from EDI buy box status logs.

Considers offers from sellers that are NOT us (matched by SellerId) and
Amazon's competitive benchmark (CompetitivePriceThreshold; the
SuggestedLowerPricePlusShipping nudge is deliberately excluded).

Logic Details

  • If Amazon is Buy Box winner → their price in blue (is_amazon: true)
  • If we're Buy Box winner and external threshold < our price → warning (orange)
  • If we're NOT Buy Box winner and external/seller competitor < our price → danger (red)
  • Seller competitors take priority over external threshold (unless external is lower)

Parameters:

Returns:

  • (Hash, nil)

    { price: Float, seller_id: String, is_external: Boolean, is_warning: Boolean, is_amazon: Boolean } or nil



761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
# File 'app/helpers/amazon_products_helper.rb', line 761

def get_best_competitor_from_edi(catalog_item)
  ecl = cached_edi_log(catalog_item)
  return nil if ecl&.data.blank?

  json_hash = JSON.parse(ecl.data).with_indifferent_access
  our_price = catalog_item.amazon_price_with_tax
  is_buy_box_winner = catalog_item.is_amz_buy_box_winner

  # Extract external competitor threshold from Summary
  summary = json_hash.dig(:payload, :Summary)
  external_threshold = nil
  if summary.present?
    # Amazon's suggestion to charge less is not a competitor price — see
    # Catalog::AmazonCompetitiveSignal.
    external_threshold = Catalog::AmazonCompetitiveSignal.binding_threshold(summary)
  end

  # Get offers from EDI data
  offers = json_hash.dig(:payload, :Offers) || []
  return nil if offers.empty?

  offers = offers.map(&:with_indifferent_access)
  merchant_id = cached_merchant_id(catalog_item)

  # Check if Amazon is the Buy Box winner - if so, show their price in blue
  amazon_winner = offers.find { |o| AMAZON_SELLER_IDS.include?(o[:SellerId]) && o[:IsBuyBoxWinner] }
  if amazon_winner.present?
    price = amazon_winner.dig(:ListingPrice, :Amount) || amazon_winner.dig(:Price, :Amount)
    return { price: price&.to_f, seller_id: amazon_winner[:SellerId], is_external: false, is_warning: false, is_amazon: true }
  end

  # Get all competitor offers (not from us, not from Amazon)
  competitor_offers = offers.reject do |o|
    o[:SellerId] == merchant_id || AMAZON_SELLER_IDS.include?(o[:SellerId])
  end

  seller_competitor = nil
  if competitor_offers.any?
    # Get the lowest priced competitor
    lowest_offer = competitor_offers.min_by do |offer|
      offer.dig(:ListingPrice, :Amount) || offer.dig(:Price, :Amount) || Float::INFINITY
    end

    if lowest_offer
      price = lowest_offer.dig(:ListingPrice, :Amount) || lowest_offer.dig(:Price, :Amount)
      seller_competitor = { price: price&.to_f, seller_id: lowest_offer[:SellerId], is_external: false, is_warning: false, is_amazon: false }
    end
  end

  # Case 1: We ARE the Buy Box winner
  # Show external threshold as WARNING (orange) if it's below our price
  if is_buy_box_winner
    return { price: external_threshold, seller_id: EXTERNAL_COMPETITOR_ID, is_external: true, is_warning: true, is_amazon: false } if external_threshold.present? && our_price.present? && external_threshold < our_price
    # We're winning and no threatening external threshold - show seller competitor if any
    return seller_competitor if seller_competitor.present?

    return nil
  end

  # Case 2: We are NOT the Buy Box winner
  # Show the lowest threat (seller or external) - this is a danger situation
  if seller_competitor.present?
    # We have a seller competitor - only consider external if it's lower than BOTH
    if external_threshold.present? &&
       external_threshold < seller_competitor[:price] &&
       our_price.present? && external_threshold < our_price
      return { price: external_threshold, seller_id: EXTERNAL_COMPETITOR_ID, is_external: true, is_warning: false, is_amazon: false }
    else
      return seller_competitor
    end
  elsif external_threshold.present? && our_price.present? && external_threshold < our_price
    # No seller competitor, but external threshold is below our price - show it
    return { price: external_threshold, seller_id: EXTERNAL_COMPETITOR_ID, is_external: true, is_warning: false, is_amazon: false }
  end

  nil
rescue JSON::ParserError, NoMethodError
  nil
end

#get_competitive_data_from_edi(catalog_item) ⇒ Hash?

Legacy wrapper returning competitive data only when we're NOT winning the Buy Box.

Parameters:

Returns:

  • (Hash, nil)

    competitive data hash, or nil when we're the Buy Box winner

See Also:



846
847
848
849
850
# File 'app/helpers/amazon_products_helper.rb', line 846

def get_competitive_data_from_edi(catalog_item)
  return nil if catalog_item.is_amz_buy_box_winner

  get_best_competitor_from_edi(catalog_item)
end

#get_winning_fulfillment_type(catalog_item) ⇒ Symbol

Determines whether our winning Buy Box offer is FBA or MFN.

Parameters:

Returns:

  • (Symbol)

    :fba, :mfn, or :unknown



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'app/helpers/amazon_products_helper.rb', line 313

def get_winning_fulfillment_type(catalog_item)
  ecl = cached_edi_log(catalog_item)
  return :unknown if ecl&.data.blank?

  json_hash = JSON.parse(ecl.data).with_indifferent_access
  offers = json_hash.dig(:payload, :Offers) || []
  return :unknown if offers.empty?

  offers = offers.map(&:with_indifferent_access)
  merchant_id = cached_merchant_id(catalog_item)

  # Find our winning offer
  our_winning_offer = offers.find { |o| o[:SellerId] == merchant_id && o[:IsBuyBoxWinner] }
  return :unknown unless our_winning_offer

  our_winning_offer[:IsFulfilledByAmazon] ? :fba : :mfn
rescue JSON::ParserError, NoMethodError
  :unknown
end

#issues_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer

Popover badge listing unresolved flags (and repricing-disabled state) for an item.

Logic Details

Red (danger) when catalog issues, no buyable offers, or an external
competitor flag is present; orange (warning) for out-of-stock/threshold
issues or disabled repricing. The badge count includes a
repricing-disabled pseudo-issue.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    popover trigger icon HTML



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'app/helpers/amazon_products_helper.rb', line 359

def issues_indicator(catalog_item)
  active_flags = catalog_item.amazon_catalog_item_flags.reject(&:resolved?)
  repricing_disabled = catalog_item.respond_to?(:reprice_rule) && catalog_item.reprice_rule == 'off'

  # No issues - show dash (unless repricing is disabled)
  return tag.span('', class: 'text-body-secondary') if active_flags.empty? && !repricing_disabled

  # Separate flags by type - severity order: red (critical) > orange (warning)
  # Red flags: has_issues, no_buyable_offers, external_competitor (unexpected, needs attention)
  # Orange flags: out_of_stock, cannot_automatically_win_buy_box, repricing_disabled (expected/informational)
  catalog_issues = active_flags.select { |f| f.flag_type == 'has_issues' }
  no_offers_issues = active_flags.select { |f| f.flag_type == 'no_buyable_offers' }
  external_competitor_issues = active_flags.select { |f| f.flag_type == 'external_competitor' }

  issue_parts = []
  issue_parts << "<strong>Repricing disabled:</strong><br>Automatic Buy Box repricing paused to protect channel pricing (e.g. Costco)" if repricing_disabled

  active_flags.each do |flag|
    content = "<strong>#{flag.flag_type.humanize}:</strong><br>#{h(flag.issues&.truncate(200))}"
    # Add competitive price info if available
    if flag.competitive_price.present?
      content += "<br><small class='text-body-secondary'>Competitive: #{number_to_currency(flag.competitive_price)}</small>"
      content += "<br><small class='text-body-secondary'>Our price: #{number_to_currency(flag.our_price_at_flagging)}</small>" if flag.our_price_at_flagging.present?
    end
    # Add competitor info with link if available (except for external competitors)
    if flag.competitor_seller_id.present? && flag.competitor_seller_id != EXTERNAL_COMPETITOR_ID
      competitor_name = flag.competitor_name
      competitor_url = flag.competitor_storefront_url
      content += "<br><small class='text-body-secondary'>Competitor: "
      safe_competitor_url = competitor_url.to_s.match?(%r{\Ahttps?://}i) ? competitor_url : '#'
      content += "<a href=\"#{ERB::Util.html_escape(safe_competitor_url)}\" target=\"_blank\" class=\"text-decoration-none\" rel=\"noopener noreferrer\">#{h(competitor_name)} <i class=\"fa-solid fa-external-link-alt fa-xs\"></i></a>"
      content += '</small>'
    elsif flag.competitor_seller_id == EXTERNAL_COMPETITOR_ID
      content += "<br><small class='text-body-secondary'>Competitor: External (Amazon Threshold)</small>"
    end
    issue_parts << content
  end

  issues_html = issue_parts.join('<hr class="my-1">')

  # Determine icon color based on severity:
  # - Red (danger): catalog issues, no buyable offers, or external competitor (unexpected, needs attention)
  # - Orange (warning): out of stock, threshold issues, or repricing disabled (expected/informational)
  icon_class = if catalog_issues.any? || no_offers_issues.any? || external_competitor_issues.any?
                 'status-icon danger'   # Red for catalog issues, no buyable offers, or external competitor
               else
                 'status-icon warning'  # Orange for out of stock, threshold issues, or repricing disabled
               end

  badge_count = active_flags.count + (repricing_disabled ? 1 : 0)

  tag.span(
    class: icon_class,
    title: '',
    data: {
      bs_toggle: 'popover',
      bs_trigger: 'hover focus',
      bs_html: 'true',
      bs_content: issues_html,
      bs_placement: 'left'
    }
  ) do
    safe_join([
      fa_icon('triangle-exclamation', family: :solid),
      (tag.span(badge_count, class: 'ms-1 small fw-bold') if badge_count > 1)
    ].compact)
  end
end

#market_flag_for(catalog) ⇒ ActiveSupport::SafeBuffer

Returns a flag emoji span for the catalog's Amazon marketplace.

Parameters:

  • catalog (Catalog)

    Amazon Seller Central catalog

Returns:

  • (ActiveSupport::SafeBuffer)

    span with the market flag (🌐 for
    unrecognized catalogs)



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
# File 'app/helpers/amazon_products_helper.rb', line 95

def market_flag_for(catalog)
  case catalog.id
  when CatalogConstants::AMAZON_SC_US_CATALOG_ID
    tag.span('🇺🇸', title: 'US', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_CA_CATALOG_ID
    tag.span('🇨🇦', title: 'CA', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_UK_CATALOG_ID
    tag.span('🇬🇧', title: 'UK', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_DE_CATALOG_ID
    tag.span('🇩🇪', title: 'DE', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_FR_CATALOG_ID
    tag.span('🇫🇷', title: 'FR', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_IT_CATALOG_ID
    tag.span('🇮🇹', title: 'IT', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_ES_CATALOG_ID
    tag.span('🇪🇸', title: 'ES', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_NL_CATALOG_ID
    tag.span('🇳🇱', title: 'NL', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_PL_CATALOG_ID
    tag.span('🇵🇱', title: 'PL', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_SE_CATALOG_ID
    tag.span('🇸🇪', title: 'SE', class: 'fs-5')
  when CatalogConstants::AMAZON_SC_BE_CATALOG_ID
    tag.span('🇧🇪', title: 'BE', class: 'fs-5')
  else
    tag.span('🌐', title: catalog.name, class: 'fs-5')
  end
end

#price_change_class(catalog_item) ⇒ String

CSS class coloring a price cell based on the recent change direction.

Parameters:

Returns:

  • (String)

    'price-up fw-medium', 'price-down fw-medium', or ''



235
236
237
238
239
240
241
242
243
244
245
246
# File 'app/helpers/amazon_products_helper.rb', line 235

def price_change_class(catalog_item)
  return '' unless catalog_item.price_updated_at.present? && catalog_item.price_updated_at > 7.days.ago
  return '' unless catalog_item.old_amount.present? && catalog_item.amount.present?

  if catalog_item.amount > catalog_item.old_amount
    'price-up fw-medium'
  elsif catalog_item.amount < catalog_item.old_amount
    'price-down fw-medium'
  else
    ''
  end
end

#price_change_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer, ...

Arrow icon shown when the price changed within the last 7 days.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer, String, nil)

    arrow icon HTML, or blank
    when there is no recent change



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'app/helpers/amazon_products_helper.rb', line 188

def price_change_indicator(catalog_item)
  return '' unless catalog_item.price_updated_at.present? && catalog_item.price_updated_at > 7.days.ago

  if catalog_item.old_amount.present? && catalog_item.amount.present?
    if catalog_item.amount > catalog_item.old_amount
      tag.span(class: 'status-icon success', title: "Price raised on #{catalog_item.price_updated_at.strftime('%b %d')}", data: { bs_toggle: 'tooltip' }) do
        fa_icon('arrow-up', family: :solid)
      end
    elsif catalog_item.amount < catalog_item.old_amount
      tag.span(class: 'status-icon danger', title: "Price lowered on #{catalog_item.price_updated_at.strftime('%b %d')}", data: { bs_toggle: 'tooltip' }) do
        fa_icon('arrow-down', family: :solid)
      end
    end
  else
    ''
  end
end

#price_updated_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer

Price-updated cell with trend arrow and recency-based text color.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    price updated span HTML



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'app/helpers/amazon_products_helper.rb', line 451

def price_updated_indicator(catalog_item)
  return tag.span('', class: 'text-body-secondary') if catalog_item.price_updated_at.blank?

  time_str = if catalog_item.price_updated_at > 24.hours.ago
               time_ago_in_words(catalog_item.price_updated_at)
             else
               catalog_item.price_updated_at.strftime('%b %d')
             end

  arrow = if catalog_item.old_amount.present? && catalog_item.amount.present?
            if catalog_item.amount > catalog_item.old_amount
              fa_icon('arrow-trend-up', family: :solid, class: 'text-success me-1')
            elsif catalog_item.amount < catalog_item.old_amount
              fa_icon('arrow-trend-down', family: :solid, class: 'text-danger me-1')
            else
              ''
            end
          else
            ''
          end

  # Determine text color based on recency
  text_class = if catalog_item.price_updated_at > 7.days.ago
                 'text-dark'
               elsif catalog_item.price_updated_at > 30.days.ago
                 'text-body-secondary'
               else
                 'text-body-secondary opacity-50'
               end

  tooltip = "Updated: #{catalog_item.price_updated_at.strftime('%Y-%m-%d %H:%M')}"
  tooltip += "\nOld: #{number_to_currency(catalog_item.old_amount)}" if catalog_item.old_amount.present?
  tooltip += "\nNew: #{number_to_currency(catalog_item.amount)}" if catalog_item.amount.present?

  tag.span(class: text_class, title: tooltip, data: { bs_toggle: 'tooltip' }) do
    safe_join([arrow, time_str].compact)
  end
end

#price_with_history(catalog_item) ⇒ ActiveSupport::SafeBuffer

Price cell with a hover popover of price history (history itself is
currently disabled — see build_price_history_html).

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    price span HTML



495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'app/helpers/amazon_products_helper.rb', line 495

def price_with_history(catalog_item)
  current_price = number_to_currency(catalog_item.amount)

  # Build price history from PaperTrail versions if available
  history_html = build_price_history_html(catalog_item)

  if history_html.present?
    tag.span(
      current_price,
      class: price_change_class(catalog_item),
      style: 'cursor: pointer;',
      data: {
        bs_toggle: 'popover',
        bs_trigger: 'hover focus',
        bs_html: 'true',
        bs_title: 'Price History',
        bs_content: history_html,
        bs_placement: 'left'
      }
    )
  else
    tag.span(current_price, class: price_change_class(catalog_item))
  end
end

#render_amazon_status_icons(catalog_item) ⇒ ActiveSupport::SafeBuffer

Renders the state, price-change, and suppression status icons for an item.

Parameters:

  • catalog_item (CatalogItem)

    item to render icons for

Returns:

  • (ActiveSupport::SafeBuffer)

    space-joined status icon HTML



139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/helpers/amazon_products_helper.rb', line 139

def render_amazon_status_icons(catalog_item)
  icons = []

  # State indicator
  icons << state_indicator(catalog_item)

  # Price change indicator (if recently changed)
  icons << price_change_indicator(catalog_item)

  # Suppression indicator
  icons << suppression_indicator(catalog_item) if catalog_item.item_suppressed_status.present?

  safe_join(icons, ' ')
end

#render_stat_card(label, value, icon, color, url: nil) ⇒ ActiveSupport::SafeBuffer

Renders a stat card with icon, value, and label, optionally wrapped in a link.

Parameters:

  • label (String)

    card label text

  • value (Numeric)

    stat value to display

  • icon (String)

    Font Awesome icon name

  • color (String)

    Bootstrap text color, interpolated as text-{color}

  • url (String, nil) (defaults to: nil)

    optional link target; renders a plain div when absent

Returns:

  • (ActiveSupport::SafeBuffer)

    rendered card HTML



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'app/helpers/amazon_products_helper.rb', line 72

def render_stat_card(label, value, icon, color, url: nil)
  card_content = safe_join([
                             tag.div(class: "stat-value text-#{color}") do
                               safe_join([
                                           fa_icon(icon, class: 'me-2', style: 'font-size: 1rem;'),
                                           number_with_delimiter(value)
                                         ])
                             end,
                             tag.div(label, class: 'stat-label')
                           ])

  if url.present?
    link_to(card_content, url, class: 'stat-card stat-card-link text-decoration-none')
  else
    tag.div(card_content, class: 'stat-card')
  end
end

#reprice_reference_catalog_collection(catalog_item) ⇒ Array<Array(String, Integer)>

Sibling retailer catalogs eligible as repricing references for this item:
probe-enabled, same store, non-Amazon (the same set the raise ceiling reads).

Parameters:

Returns:

  • (Array<Array(String, Integer)>)

    name/id pairs for a select



221
222
223
224
225
226
227
228
229
# File 'app/helpers/amazon_products_helper.rb', line 221

def reprice_reference_catalog_collection(catalog_item)
  store_id = catalog_item.catalog&.store_id
  return [] unless store_id

  Catalog.where(store_id: store_id, external_price_check_enabled: true)
         .where.not(id: CatalogConstants::AMAZON_CATALOG_IDS)
         .order(:name)
         .pluck(:name, :id)
end

#resolve_store_id_for_sibling_filter(catalog_item) ⇒ Integer?

Resolves the store_id scoping sibling prices to the same country.

Uses @catalogs_selected (preloaded in controller) to avoid N+1 queries.

Parameters:

Returns:

  • (Integer, nil)

    store ID



884
885
886
887
888
# File 'app/helpers/amazon_products_helper.rb', line 884

def resolve_store_id_for_sibling_filter(catalog_item)
  return catalog_item.store_id if catalog_item.respond_to?(:store_id) && catalog_item.store_id.present?

  @catalogs_selected&.find { |c| c.id == catalog_item.catalog_id }&.store_id if catalog_item.respond_to?(:catalog_id) && catalog_item.catalog_id
end

#retailer_prices_dropdown(catalog_item) ⇒ ActiveSupport::SafeBuffer?

Renders a dropdown showing external retailer prices for the same product.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer, nil)

    dropdown HTML, or nil when no
    sibling prices are available



922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
# File 'app/helpers/amazon_products_helper.rb', line 922

def retailer_prices_dropdown(catalog_item)
  sibling_prices = cached_sibling_retailer_prices(catalog_item)
  return nil if sibling_prices.blank?

  our_price = catalog_item.amazon_price_with_tax

  # Build dropdown items
  dropdown_items = sibling_prices.map do |sibling|
    price = sibling.retail_price
    catalog_name = sibling.catalog_name
    currency = sibling.retailer_currency || 'USD'
    url = sibling.url
    updated_at = sibling.retail_price_updated_at
    catalog_item_id = sibling.catalog_item_id

    # Calculate price difference if we have our price
    diff_html = if our_price.present? && our_price.positive? && price.present?
                  price_diff = our_price - price
                  diff_class = if price_diff.positive?
                                 'text-danger'   # We're more expensive
                               elsif price_diff.negative?
                                 'text-success'  # We're cheaper
                               else
                                 'text-body-secondary' # Same price
                               end
                  tag.span(class: "#{diff_class} small ms-1") do
                    "(#{'+' unless price_diff.negative?}#{number_to_currency(price_diff, unit: currency == 'CAD' ? 'C$' : '$')})"
                  end
                end

    # Format date indicator - show warning color if stale (>7 days old)
    date_html = if updated_at.present?
                  days_old = (Time.current - updated_at).to_i / 1.day
                  date_class = days_old > 7 ? 'text-warning' : 'text-body-secondary'
                  date_text = if days_old == 0
                                'today'
                              elsif days_old == 1
                                '1d ago'
                              elsif days_old < 30
                                "#{days_old}d ago"
                              else
                                updated_at.strftime('%-m/%-d')
                              end
                  tag.span(class: "#{date_class} small ms-1", title: updated_at.strftime('%Y-%m-%d %H:%M')) do
                    "(#{date_text})"
                  end
                end

    # External link icon (opens listing URL in new tab)
    external_link_html = if url.present?
                           link_to(url, target: '_blank', rel: 'noopener', class: 'text-body-secondary ms-2 d-inline', title: 'Open listing') do
                             fa_icon('up-right-from-square', family: :solid, class: 'fa-xs')
                           end
                         end

    # Format the dropdown item - retailer name links to CRM catalog item
    content = tag.div(class: 'd-flex justify-content-between align-items-center px-2 py-1') do
      # Left side: retailer name (links to CRM catalog item) + external link icon
      left = tag.span(class: 'me-3 text-nowrap') do
        retailer_link = link_to(catalog_name, catalog_item_path(catalog_item_id), class: 'text-decoration-none d-inline')
        safe_join([retailer_link, external_link_html].compact)
      end

      # Right side: price, difference, and date
      right = tag.span(class: 'fw-medium text-nowrap') do
        safe_join([
          number_to_currency(price, unit: currency == 'CAD' ? 'C$' : '$'),
          diff_html,
          date_html
        ].compact)
      end

      safe_join([left, right])
    end

    tag.span(content, class: 'dropdown-item small')
  end

  # Return the dropdown structure
  tag.div(class: 'dropdown d-inline-block ms-1') do
    safe_join([
                tag.button(
                  class: 'btn btn-link btn-sm p-0 text-decoration-none',
                  type: 'button',
                  data: { bs_toggle: 'dropdown', bs_auto_close: 'true' },
                  title: 'View retailer prices',
                  'aria-expanded': 'false'
                ) do
                  tag.span(class: 'badge bg-info text-white', style: 'font-size: 0.6rem;') do
                    "+#{sibling_prices.count}"
                  end
                end,
                tag.ul(class: 'dropdown-menu dropdown-menu-end', style: 'min-width: 300px;') do
                  safe_join([
                              tag.li(tag.span('Other Retailer Prices', class: 'dropdown-header')),
                              tag.li(tag.hr(class: 'dropdown-divider my-1')),
                              *dropdown_items.map { |item| tag.li(item) }
                            ])
                end
              ])
  end
end

#row_class_for(catalog_item) ⇒ String

CSS row class for a catalog item based on unresolved flags and suppression state.

Parameters:

Returns:

  • (String)

    'has-issues', 'table-warning', or ''



128
129
130
131
132
133
# File 'app/helpers/amazon_products_helper.rb', line 128

def row_class_for(catalog_item)
  return 'has-issues' if catalog_item.amazon_catalog_item_flags.any? { |f| !f.resolved? }
  return 'table-warning' if catalog_item.item_suppressed_status.present?

  ''
end

#state_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer

Status icon reflecting the item's onboarding state.

Parameters:

  • catalog_item (CatalogItem)

    item whose state is displayed

Returns:

  • (ActiveSupport::SafeBuffer)

    tooltip-wrapped state icon HTML



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'app/helpers/amazon_products_helper.rb', line 158

def state_indicator(catalog_item)
  case catalog_item.state
  when 'active'
    tag.span(class: 'status-icon success', title: 'Active', data: { bs_toggle: 'tooltip' }) do
      fa_icon('check', family: :solid)
    end
  when 'active_hidden'
    tag.span(class: 'status-icon warning', title: 'Active (Hidden)', data: { bs_toggle: 'tooltip' }) do
      fa_icon('eye-slash', family: :solid)
    end
  when 'pending_onboarding'
    tag.span(class: 'status-icon info', title: 'Pending Onboarding', data: { bs_toggle: 'tooltip' }) do
      fa_icon('clock', family: :solid)
    end
  when 'inactive'
    tag.span(class: 'status-icon muted', title: 'Inactive', data: { bs_toggle: 'tooltip' }) do
      fa_icon('pause', family: :solid)
    end
  else
    tag.span(class: 'status-icon muted', title: catalog_item.state.humanize, data: { bs_toggle: 'tooltip' }) do
      fa_icon('question', family: :solid)
    end
  end
end

#suppression_indicator(catalog_item) ⇒ ActiveSupport::SafeBuffer

Ban icon shown for suppressed items, with the suppression reason in the tooltip.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    suppression icon HTML



210
211
212
213
214
215
# File 'app/helpers/amazon_products_helper.rb', line 210

def suppression_indicator(catalog_item)
  message = catalog_item.item_suppressed_status_message.presence || catalog_item.item_suppressed_status
  tag.span(class: 'status-icon danger', title: "Suppressed: #{message}", data: { bs_toggle: 'tooltip' }) do
    fa_icon('ban', family: :solid)
  end
end