Class: Crm::ReviewsIoController

Inherits:
CrmController show all
Defined in:
app/controllers/crm/reviews_io_controller.rb

Overview

CRM controller for viewing Reviews.io data
This is a read-only interface - reviews cannot be created, edited, or deleted from CRM
All review management must be done through the Reviews.io dashboard

Constant Summary

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 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

#generate_headlineObject

POST /reviews/:id/generate_headline
Calls the LLM to suggest a headline; returns JSON — no page reload.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'app/controllers/crm/reviews_io_controller.rb', line 140

def generate_headline
  @review = ReviewsIo.find(params[:id])

  if @review.comments.blank?
    return render json: { error: 'This review has no comment body to generate a headline from.' },
                  status: :unprocessable_content
  end

  headline = ReviewHeadlineGenerator.call(@review.comments)
  render json: { headline: headline }
rescue RubyLLM::RateLimitError
  render json: { error: 'AI service is rate-limited. Please try again in a moment.' },
         status: :too_many_requests
rescue RubyLLM::Error => e
  render json: { error: "AI service error: #{e.message}" }, status: :service_unavailable
end

#importvoid

This method returns an undefined value.

Enqueues a background import of reviews from Reviews.io and redirects to the index.



86
87
88
89
# File 'app/controllers/crm/reviews_io_controller.rb', line 86

def import
  ReviewsIoImportWorker.perform_async
  redirect_to reviews_io_index_path, notice: 'Review import has been queued. It may take a few minutes to complete.'
end

#import_imagesvoid

This method returns an undefined value.

Imports the review's photos into the image library and redirects back with the result.



127
128
129
130
131
132
133
134
135
136
# File 'app/controllers/crm/reviews_io_controller.rb', line 127

def import_images
  @review = ReviewsIo.find(params[:id])
  result = ReviewsIoImageImporter.new(@review, force: params[:force].present?).call

  if result.success?
    redirect_to reviews_io_path(@review), notice: "Imported #{result.imported_count} photo(s) into the image library."
  else
    redirect_to reviews_io_path(@review), alert: "Import completed with errors: #{result.errors.join('; ')}"
  end
end

#indexvoid

This method returns an undefined value.

Lists Reviews.io reviews with ransack filters, pagination, preloaded orders/items/images,
and summary stats (counts and average ratings).



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
# File 'app/controllers/crm/reviews_io_controller.rb', line 10

def index
  params[:q] ||= {}

  # Wildcard search across multiple fields
  search_query = params[:search_query].to_s.strip.presence
  params[:q][:comments_or_title_or_sku_or_author_name_or_order_id_cont] = search_query if search_query

  @q = ReviewsIo.all.ransack(params[:q])
  @q.sorts = 'review_date desc' if @q.sorts.blank?

  @reviews = @q.result

  # Filter by customer (through orders)
  if params.dig(:q, :by_customer_id).present?
    customer_order_ids = Order.where(customer_id: params[:q][:by_customer_id]).pluck(:id, :reference_number).flatten.map(&:to_s)
    @reviews = @reviews.where(order_id: customer_order_ids) if customer_order_ids.any?
  end

  @pagy, @reviews = pagy(@reviews, items: 25)

  # Preload orders by ID and by reference_number to avoid N+1 queries
  order_ids = @reviews.filter_map(&:order_id).uniq
  if order_ids.any?
    orders = Order.includes(:customer).where(id: order_ids).or(Order.includes(:customer).where(reference_number: order_ids))
    @orders_by_id = orders.index_by { |o| o.id.to_s }
    @orders_by_reference = orders.index_by(&:reference_number)
  else
    @orders_by_id = {}
    @orders_by_reference = {}
  end

  # Preload items by SKU to avoid N+1 queries
  skus = @reviews.filter_map(&:sku).uniq
  @items_by_sku = skus.any? ? Item.where(sku: skus).index_by(&:sku) : {}

  # Preload which reviews have imported images
  review_ids = @reviews.map(&:id)
  @reviews_with_images = review_ids.any? ? ReviewsIoImage.where(reviews_io_id: review_ids).distinct.pluck(:reviews_io_id).to_set : Set.new

  # Fan out the 4 summary aggregates concurrently. Rails 7.1 async_count
  # returns an ActiveRecord::Promise; async_average likewise. Materialize
  # them all in one transform_values pass.
  promises = {
    product_review_count: ReviewsIo.active.product_reviews.async_count,
    store_review_count:   ReviewsIo.active.store_reviews.async_count,
    product_avg_rating:   ReviewsIo.active.product_reviews.async_average(:rating),
    store_avg_rating:     ReviewsIo.active.store_reviews.async_average(:rating)
  }
  stats = promises.transform_values(&:value)
  @product_review_count = stats[:product_review_count]
  @store_review_count   = stats[:store_review_count]
  @product_avg_rating   = stats[:product_avg_rating]&.round(2)
  @store_avg_rating     = stats[:store_avg_rating]&.round(2)

  # Tags for the filter dropdown
  @available_tags = ReviewsIo.all_tags
end

#showvoid

This method returns an undefined value.

Shows a single review with its images, tags, and the related order/customer when resolvable.



70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'app/controllers/crm/reviews_io_controller.rb', line 70

def show
  @review = ReviewsIo.find(params[:id])
  @review_images = @review.images.order(created_at: :desc)
  @available_tags = ReviewsIo.all_tags

  return if @review.order_id.blank?

  @order = Order.find_by(id: @review.order_id) ||
           Order.find_by(reference_number: @review.order_id)
  customer = @order&.customer
  @customer_full_name    = customer&.full_name.presence
  @customer_profile_name = customer&.profile&.friendly_name.presence
end

#toggle_hiddenvoid

This method returns an undefined value.

Toggles the review's public visibility (hidden flag) and redirects to the review.



93
94
95
96
97
98
# File 'app/controllers/crm/reviews_io_controller.rb', line 93

def toggle_hidden
  @review = ReviewsIo.find(params[:id])
  @review.update!(hidden: !@review.hidden)
  status = @review.hidden? ? 'hidden from public' : 'visible to public'
  redirect_to reviews_io_path(@review), notice: "Review is now #{status}."
end

#update_avatarvoid

This method returns an undefined value.

Sets (or clears) the review's avatar image, validating the image exists first.



111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'app/controllers/crm/reviews_io_controller.rb', line 111

def update_avatar
  @review = ReviewsIo.find(params[:id])
  image_id = review_avatar_params[:avatar_image_id].presence&.to_i

  if image_id && !Image.exists?(image_id)
    redirect_to reviews_io_path(@review), alert: "Image ##{image_id} not found in the library."
    return
  end

  @review.update!(avatar_image_id: image_id)
  notice = image_id ? "Avatar set to image ##{image_id}." : 'Avatar cleared.'
  redirect_to reviews_io_path(@review), notice: notice
end

#update_headlineObject

PATCH /reviews/:id/update_headline
Persists (or clears) the custom_headline. Returns JSON.



159
160
161
162
163
# File 'app/controllers/crm/reviews_io_controller.rb', line 159

def update_headline
  @review = ReviewsIo.find(params[:id])
  @review.update!(custom_headline: params[:custom_headline].presence)
  render json: { custom_headline: @review.custom_headline }
end

#update_tagsvoid

This method returns an undefined value.

Replaces the review's tags with the submitted list and redirects to the review.



102
103
104
105
106
107
# File 'app/controllers/crm/reviews_io_controller.rb', line 102

def update_tags
  @review = ReviewsIo.find(params[:id])
  new_tags = review_tags_params[:tags]&.reject(&:blank?) || []
  @review.update!(tags: new_tags)
  redirect_to reviews_io_path(@review), notice: 'Tags updated successfully.'
end