Class: Crm::RetailersController

Inherits:
CrmController show all
Includes:
Controllers::AmazonProductsGrid, Controllers::RequireTurboFrame
Defined in:
app/controllers/crm/retailers_controller.rb

Overview

Per-retailer operational dashboards. The index mirrors the daily
retailer-compliance email (Retailer::DailyComplianceReport) as an
at-a-glance grid; each :id (a Catalog) gets a dashboard with a health gauge,
trends (from CatalogDataPoint history), action items, a sales summary, and
probe health, plus quick links into the catalog, customers, listing issues,
EDI, and webhooks.

Constant Summary collapse

TAB_ACTIONS =

Lazy-tab actions guarded by require_in_pane_tab_frames (frame-only).

%i[tab_overview tab_trends tab_action_items tab_sales tab_probes tab_listings].freeze
INDEX_CACHE_KEY =

Short-TTL cache of the index's report rows (the compliance counts move at
most daily). Exposed so tests can clear it between runs.

'crm/retailers/dashboard_rows'

Constants included from Controllers::AmazonProductsGrid

Controllers::AmazonProductsGrid::AMAZON_SELLER_IDS, Controllers::AmazonProductsGrid::GRID_FILTER_PARAMS

Constants included from Controllers::ReferenceFindable

Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS

Constants included from Controllers::AnalyticsEvents

Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY

Constants included from Controllers::ErrorRendering

Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES

Instance Method Summary collapse

Methods included from Controllers::RequireTurboFrame

require_in_pane_tab_frames, require_turbo_frame

Methods inherited from CrmController

#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y

Methods included from Controllers::ReturnPathHandling

#check_for_return_path, #redirect_to_return_path_or_default

Methods included from Controllers::AnalyticsEvents

#consume_queued_analytics_events, #registration_lead_type, #track_event

Methods included from Controllers::DeviceDetection

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TurboSafeRedirect

#redirect_to

Methods included from Controllers::TrackingDetection

#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?

Methods included from Controllers::AcceleratedFileSending

#send_file_accelerated, #send_upload_accelerated

Methods included from Controllers::ErrorRendering

#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback

Methods included from Controllers::TurnstileVerification

#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!

Methods included from Controllers::CloudflareCaching

edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session

Methods included from Controllers::Webpackable

#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?

Methods included from Controllers::Localizable

#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale

Methods included from Controllers::Authenticable

#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Instance Method Details

#analyzevoid

This method returns an undefined value.

POST: kick off a Sunny AI analysis of this catalog (health, sales,
errors, onboarding, assortment gaps, advertising). One run at a time
per catalog; each run is a linked AssistantConversation so past
analyses stay browsable from the dashboard. The lookup-and-create runs
inside a catalog-keyed advisory lock so concurrent POSTs can't both
start a run; the worker re-verifies under the same lock before doing
the expensive chat call.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'app/controllers/crm/retailers_controller.rb', line 148

def analyze
  # The loser of a concurrent pair just renders the panel — the winner's
  # row is already visible as processing.
  AssistantConversation.with_advisory_lock_result("retailer_analysis:#{@catalog.id}", timeout_seconds: 0) do
    processing_analysis || start_analysis
  end

  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace(
        ActionView::RecordIdentifier.dom_id(@catalog, :analysis),
        partial: 'crm/retailers/analysis',
        locals: { catalog: @catalog, analyses: AssistantConversation.retailer_analyses_for(@catalog.id).limit(6).to_a }
      )
    end
    format.html { redirect_to retailer_path(@catalog) }
  end
end

#indexvoid

This method returns an undefined value.

At-a-glance grid of every retailer in the compliance scope, grouped by
region, with health and 30-day sales alongside the email's counts.



30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'app/controllers/crm/retailers_controller.rb', line 30

def index
  # The compliance counts change at most daily (probe/sync/report cadence),
  # so serve the recursive report SQL from a short-TTL cache rather than
  # re-running it on every page view.
  rows = Rails.cache.fetch(INDEX_CACHE_KEY, expires_in: 10.minutes) do
    Retailer::DailyComplianceReport.for_dashboard.rows
  end
  @report = Retailer::DailyComplianceReport.new(rows:)
  @rows_by_region = @report.rows_by_region
  @totals = @report.totals
  @flagged = @report.flagged_rows.sort_by { |r| -action_weight(r) }
  @sales = sales_by_catalog(@report.rows.pluck(:catalog_id), since: 30.days.ago)
end

#showvoid

This method returns an undefined value.

One retailer's dashboard: heading + tab nav; every section lazy-loads
through its tab_* frame (Overview preselected).



47
48
49
50
51
# File 'app/controllers/crm/retailers_controller.rb', line 47

def show
  @open_issue_count = ListingIssue.open.joins(:catalog_item)
                                  .where(catalog_items: { catalog_id: @catalog.id }).count
  @amazon_tiles = amazon_tiles(@catalog) if @catalog.id.in?(CatalogConstants::AMAZON_SELLER_IDS)
end

#tab_action_itemsvoid

This method returns an undefined value.

Lazy frame: open listing issues bucketed by code (buy_box_lost split into
out-of-stock-driven vs genuine competition losses) + top MAP offenders.
Counts come from SQL; only the handful of sample rows per bucket is
materialized.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'app/controllers/crm/retailers_controller.rb', line 76

def tab_action_items
  scoped = ListingIssue.open.joins(:catalog_item).where(catalog_items: { catalog_id: @catalog.id })
  counts = scoped.group(:code).count
  severities = scoped.group(:code).minimum(:severity)
  titles = scoped.group(:code).minimum(:title)
  oos_ci_ids = AmazonCatalogItemFlag.active.where(flag_type: 'out_of_stock')
                                    .joins(:catalog_item)
                                    .where(catalog_items: { catalog_id: @catalog.id })
                                    .select(:catalog_item_id)

  @issue_buckets = counts.sort_by { |_, count| -count }.map do |code, count|
    samples = scoped.where(code:).order(last_seen_at: :desc)
                    .includes(catalog_item: [:catalog, { store_item: :item }]).limit(5)
    bucket = { code:, severity: severities[code], title: titles[code], count:, issues: samples }
    if code == 'buy_box_lost'
      oos_count = ListingIssue.open.where(catalog_item_id: oos_ci_ids, code: 'buy_box_lost').count
      bucket[:out_of_stock_count] = oos_count
      bucket[:competition_count] = count - oos_count
    end
    bucket
  end
  @map_offenders = map_offenders(@catalog)
  render_tab
end

#tab_listingsvoid

This method returns an undefined value.

Lazy frame: the Amazon per-item operations grid (buy box, issues, price,
competitive price, freshness) scoped to this catalog — the same engine
the standalone /amazon_catalog_items page ran, consolidated onto the
retailer dashboard. Only meaningful for Amazon seller catalogs (the
dashboard only links the frame for those).



131
132
133
134
135
136
137
138
# File 'app/controllers/crm/retailers_controller.rb', line 131

def tab_listings
  return head :not_found unless @catalog.id.in?(CatalogConstants::AMAZON_SELLER_IDS)

  # NB: setup_amazon_products_grid re-derives @catalog from the selected
  # catalogs — the same record set_catalog loaded.
  setup_amazon_products_grid([@catalog.id])
  render_tab
end

#tab_overviewvoid

This method returns an undefined value.

Lazy frame (default tab): health gauge, compliance metrics, Amazon ops
tiles with deep-filter links into the Listings frame, AI analysis, and
the pre-made catalog queries.



57
58
59
60
61
62
# File 'app/controllers/crm/retailers_controller.rb', line 57

def tab_overview
  @customers = @catalog.customers.to_a.sort_by { |c| c.name.to_s }
  @analyses = AssistantConversation.retailer_analyses_for(@catalog.id).limit(6).to_a
  @amazon_tiles = amazon_tiles(@catalog) if @catalog.id.in?(CatalogConstants::AMAZON_SELLER_IDS)
  render_tab
end

#tab_probesvoid

This method returns an undefined value.

Lazy frame: probe status breakdown over the compliance lookback window.



117
118
119
120
121
122
123
# File 'app/controllers/crm/retailers_controller.rb', line 117

def tab_probes
  @probe_counts = probe_status_breakdown(@catalog)
  @last_probe_at = CatalogItemRetailerProbe.joins(:catalog_item)
                                           .where(catalog_items: { catalog_id: @catalog.id })
                                           .maximum(:created_at)
  render_tab
end

#tab_salesvoid

This method returns an undefined value.

Lazy frame: sales_orders count + revenue trend for this retailer.



103
104
105
106
107
108
109
110
111
112
113
# File 'app/controllers/crm/retailers_controller.rb', line 103

def tab_sales
  @monthly = @catalog.orders.sales_orders
                     .where(created_at: 12.months.ago.beginning_of_month..)
                     .reorder(nil)
                     .group_by_month('orders.created_at', last: 12, format: '%b %Y')
  @orders_by_month = @monthly.count
  @revenue_by_month = @monthly.sum(:total)
  @recent_orders = @catalog.orders.sales_orders.order(created_at: :desc)
                           .includes(:customer).limit(15)
  render_tab
end

This method returns an undefined value.

Lazy frame: compliance-metric trends from CatalogDataPoint history.



66
67
68
69
# File 'app/controllers/crm/retailers_controller.rb', line 66

def tab_trends
  @series = trend_series(@catalog)
  render_tab
end