Class: VariantGroupsController

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

Overview

CRM management of canonical VariantGroup families — the single place where
variant membership + axes are defined and the per-marketplace projections
(Amazon parent listings, Wayfair grouping, Google itemGroupId) are surfaced.
Owns the variant axes directly (axis_tokens/sort_keys).

Amazon rows (overrides whose catalog is a Seller Central catalog) ARE the
per-marketplace parent listings, so the listing operations absorbed from
Crm::AmazonVariationsController live here as member actions guarded by
#require_amazon_row.

Constant Summary collapse

AMAZON_LISTING_ACTIONS =

Member actions that only make sense on an Amazon parent-listing row.

%i[refresh_specs import_images do_import_images pull_listing_data
send_listing_data send_delete_listing_data clone move_items].freeze
CORE_CATALOG_IDS =

The website catalogs — governed directly by the core (catalog-nil)
families, so selecting them shows the defaults. This is the index's
initial catalog selection.

[CatalogConstants::US_CATALOG_ID, CatalogConstants::CA_CATALOG_ID].freeze

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

#add_membervoid

This method returns an undefined value.

Adds an item to the family (fails politely when it belongs to another one).



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

def add_member
  member = @variant_group.variant_group_members.new(item_id: params[:item_id])
  if member.save
    flash[:info] = "#{member.item.sku} added to #{@variant_group.name}"
  else
    flash[:alert] = member.errors.full_messages.to_sentence
  end
  redirect_to variant_group_path(@variant_group)
end

#clonevoid

This method returns an undefined value.

Clones this Amazon parent listing for a relist (next -vN external SKU,
ASIN dropped so Amazon assigns a fresh one). The clone REPLACES this row
as the catalog's active override, so the original is retired in the same
transaction (only one ACTIVE override per family+catalog may exist).



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'app/controllers/variant_groups_controller.rb', line 235

def clone
  cloned_group = @variant_group.deep_dup
  VariantGroup.transaction do
    @variant_group.update!(inactive: true)
    cloned_group.save!
  end
  flash[:notice] = "Listing cloned as #{cloned_group.sku}; #{@variant_group.sku} retired"
  redirect_to variant_group_path(cloned_group)
rescue ActiveRecord::RecordInvalid => e
  flash[:error] = "Failed to clone listing: #{e.record.errors.full_messages.join(', ')}"
  redirect_to variant_group_path(@variant_group)
rescue ActiveRecord::RecordNotUnique
  # Concurrent clones can race past the validation into the partial unique
  # index (one active override per family+catalog).
  flash[:error] = 'Failed to clone listing: another active listing already exists for this catalog'
  redirect_to variant_group_path(@variant_group)
end

#createvoid

This method returns an undefined value.



99
100
101
102
103
104
105
# File 'app/controllers/variant_groups_controller.rb', line 99

def create
  if @variant_group.save
    redirect_to variant_group_path(@variant_group)
  else
    render :new, status: :unprocessable_content
  end
end

#destroyvoid

This method returns an undefined value.



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

def destroy
  if @variant_group.destroy
    redirect_to(variant_groups_path)
  else
    flash[:alert] = @variant_group.errors.full_messages.to_sentence.presence || 'Unable to delete variant group'
    redirect_to(variant_group_path(@variant_group))
  end
end

#do_import_imagesvoid

This method returns an undefined value.

Applies the picked image profiles onto the selected items.



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'app/controllers/variant_groups_controller.rb', line 172

def do_import_images
  # You shall receive
  # params[:import][:image_profile_id] => (key is the image_id, value is the image_type)
  # {"23219"=>"AMZ_MAIN", "9638"=>"AMZ_PT01", "4365"=>"AMZ_PT02", "14806"=>"AMZ_PT03", "3684"=>"AMZ_PT04", "1932"=>""}
  # params[:import][:replace] => "1"
  # params[:item_ids] => ["1882", "1883"]
  image_profile_actions = params.dig(:import, :image_profile_id) || {}
  item_ids = params[:item_ids]&.map(&:to_i) || []
  replace = params.dig(:import, :replace).to_b
  # Now we get to work
  operation_counter = 0
  failures = []
  image_profile_actions.each do |image_id, image_type|
    next if image_type.blank? || image_id.blank?

    item_ids.each do |item_id|
      # delete any image profile in this slot
      ImageProfile.destroy_by(item_id: item_id, image_id: image_id) if replace
      image_profile = ImageProfile.where(item_id: item_id, image_type: image_type).first_or_initialize
      next unless image_profile.new_record? || replace

      begin
        image_profile.update!(image_id: image_id)
        operation_counter += 1
      rescue ActiveRecord::RecordInvalid => e
        failures << "item #{item_id} / #{image_type}: #{e.record.errors.full_messages.to_sentence}"
      end
    end
  end
  flash[:info] = "Imported #{operation_counter} image profiles"
  flash[:alert] = "Skipped #{failures.size}: #{failures.first(5).join('; ')}" if failures.any?
  redirect_to variant_group_path(@variant_group)
end

#editvoid

This method returns an undefined value.



96
# File 'app/controllers/variant_groups_controller.rb', line 96

def edit; end

#import_imagesvoid

This method returns an undefined value.

Step 1 of the image import flow: pick the source item whose image profiles
will be copied onto the listing's other items.



165
166
167
168
# File 'app/controllers/variant_groups_controller.rb', line 165

def import_images
  item_id = params.dig(:item_filter, :item_id)
  @item = Item.find_by(id: item_id) if item_id
end

#indexvoid

This method returns an undefined value.

The scoped, filtered index (catalog multi-select + status + search).



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
# File 'app/controllers/variant_groups_controller.rb', line 27

def index
  @search_query = params[:search_query].to_s.strip
  @sku_query = params[:sku_query].to_s.strip
  # ONE scoping control: the selected catalogs (multi-select). Core catalogs
  # contribute the default families (their grouping IS the core family via
  # inheritance) plus any explicit website overrides; a marketplace catalog
  # contributes its per-catalog variant groups. A bare page load defaults to
  # core; an explicit search with the selection cleared means ALL catalogs
  # (the form always submits `status`, distinguishing the two).
  # (`catalog_id` accepted for old links.)
  @selected_catalog_ids = (Array(params[:catalog_ids]) + [params[:catalog_id]]).compact_blank.map(&:to_i).uniq
  @selected_catalog_ids = CORE_CATALOG_IDS.dup if @selected_catalog_ids.empty? && params[:status].blank?
  # Only catalogs that actually have variant groups are offered (core always is).
  @catalog_options = Catalog.where(id: VariantGroup.select(:catalog_id))
                            .or(Catalog.where(id: CORE_CATALOG_IDS)).order(:name)
  # Build on the CanCanCan-loaded collection so ability scoping survives if
  # the VariantGroup grant is ever narrowed.
  @status_filter = %w[active discontinued any].include?(params[:status]) ? params[:status] : 'active'
  scope =
    if @selected_catalog_ids.empty?
      @variant_groups
    elsif @selected_catalog_ids.intersect?(CORE_CATALOG_IDS)
      @variant_groups.where(catalog_id: @selected_catalog_ids).or(@variant_groups.where(catalog_id: nil))
    else
      @variant_groups.where(catalog_id: @selected_catalog_ids)
    end
  scope = scope.active if @status_filter == 'active'
  scope = scope.where(inactive: true) if @status_filter == 'discontinued'
  @q = scope.ransack(params[:q])
  @q.sorts = 'name asc' if @q.sorts.blank?

  base_scope = @q.result(distinct: true).includes(:primary_item, :variant_group_members, :catalog_items,
                                                  { overrides: :catalog }, { catalog: :amazon_marketplace })
  base_scope = base_scope.where('variant_groups.name ILIKE :q OR variant_groups.slug ILIKE :q', q: "%#{@search_query}%") if @search_query.present?
  base_scope = base_scope.merge(sku_filtered_groups(@sku_query)) if @sku_query.present?

  @pagy, @variant_groups = pagy(base_scope, limit: 25)
end

#move_itemsvoid

This method returns an undefined value.

Re-parents the selected items' catalog_items onto another Amazon parent
listing in the same marketplace (the relist flow after a clone).



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'app/controllers/variant_groups_controller.rb', line 256

def move_items
  if params[:target_variant_group_id].blank?
    flash[:error] = 'Please select a target listing'
    return redirect_to variant_group_path(@variant_group)
  end

  target_group = VariantGroup.amazon_channel.find_by(id: params[:target_variant_group_id])
  unless target_group
    flash[:error] = "Target listing not found (ID: #{params[:target_variant_group_id]})"
    return redirect_to variant_group_path(@variant_group)
  end

  # Listings are per-catalog; never move catalog_items across catalogs.
  if target_group.catalog_id != @variant_group.catalog_id
    flash[:error] = 'Target listing must be in the same catalog'
    return redirect_to variant_group_path(@variant_group)
  end

  item_ids = params[:item_ids]&.map(&:to_i) || []
  if item_ids.empty?
    flash[:error] = 'No items selected to move'
    return redirect_to variant_group_path(@variant_group)
  end

  # Move the per-marketplace catalog_items belonging to this listing for the
  # selected items — membership of an Amazon parent resolves through
  # catalog_items.variant_group_id.
  catalog_items_to_move =
    @variant_group.catalog_items.joins(:store_item).where(store_items: { item_id: item_ids })
  moved = catalog_items_to_move.update_all(variant_group_id: target_group.id)

  if moved.zero?
    flash[:error] = 'No matching items found to move'
    return redirect_to variant_group_path(@variant_group)
  end

  flash[:notice] = "Successfully moved #{moved} catalog item(s) to #{target_group.sku}"
  redirect_to variant_group_path(@variant_group)
end

#newvoid

This method returns an undefined value.

New default family, or a prefilled override when parent_id is given.



88
89
90
91
92
93
# File 'app/controllers/variant_groups_controller.rb', line 88

def new
  # Creating a marketplace override from the default group's page.
  if params[:parent_id].present? && (parent = VariantGroup.defaults.find_by(id: params[:parent_id]))
    @variant_group = parent.new_override(Catalog.find_by(id: params[:catalog_id]))
  end
end

#pull_listing_datavoid

This method returns an undefined value.



207
208
209
210
211
212
# File 'app/controllers/variant_groups_controller.rb', line 207

def pull_listing_data
  catalog = Catalog.find(params.expect(:catalog_id))
  res = @variant_group.amazon_pull_listing_information(catalog)
  flash[:info] = "Listing Message Sent for item. #{res.inspect}"
  redirect_to variant_group_path(@variant_group)
end

#refresh_specsvoid

This method returns an undefined value.

Queues an ItemAttributeWorker spec refresh for every item listed under
this Amazon parent.



156
157
158
159
160
# File 'app/controllers/variant_groups_controller.rb', line 156

def refresh_specs
  item_ids = @variant_group.items_via_catalog.ids
  item_ids.each { |item_id| ItemAttributeWorker.perform_async(item_id) }
  redirect_to variant_group_path(@variant_group), notice: "Spec refresh has been queued for #{item_ids.size} listed items"
end

#remove_membervoid

This method returns an undefined value.



139
140
141
142
143
144
145
146
147
148
149
# File 'app/controllers/variant_groups_controller.rb', line 139

def remove_member
  member = @variant_group.variant_group_members.find_by(item_id: params[:item_id])
  if member.nil?
    flash[:alert] = 'Item was not a member of this variant group'
  elsif member.destroy
    flash[:info] = 'Member removed'
  else
    flash[:alert] = member.errors.full_messages.to_sentence.presence || 'Unable to remove member'
  end
  redirect_to variant_group_path(@variant_group)
end

#send_delete_listing_datavoid

This method returns an undefined value.



223
224
225
226
227
228
# File 'app/controllers/variant_groups_controller.rb', line 223

def send_delete_listing_data
  catalog = Catalog.find(params.expect(:catalog_id))
  res = @variant_group.amazon_delete_variation_listing(catalog)
  flash[:info] = "Delete Listing Message Sent for listing. #{res.inspect}"
  redirect_to variant_group_path(@variant_group)
end

#send_listing_datavoid

This method returns an undefined value.



215
216
217
218
219
220
# File 'app/controllers/variant_groups_controller.rb', line 215

def send_listing_data
  catalog = Catalog.find(params.expect(:catalog_id))
  res = @variant_group.amazon_send_put_listing_information(catalog)
  flash[:info] = "Listing Message Sent for item. #{res.inspect}"
  redirect_to variant_group_path(@variant_group)
end

#showvoid

This method returns an undefined value.

A core family page or a per-catalog group page.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'app/controllers/variant_groups_controller.rb', line 68

def show
  # Channel detail renders only on the channel's own group page; the core
  # page enumerates its channels in the Catalog overrides table.
  @wayfair_catalog_items = @variant_group.wayfair_catalog_items.where(catalog_id: @variant_group.catalog_id) if @variant_group.override? && @variant_group.catalog_id.in?(CatalogConstants::WAYFAIR_CATALOGS)
  # The members table's per-catalog state columns: the catalogs THIS group
  # maps to — its own catalog for an override, the core website catalogs
  # for a default family (inherited marketplace listings live on the
  # override pages).
  column_ids = @variant_group.override? ? [@variant_group.catalog_id] : CORE_CATALOG_IDS
  @member_catalog_columns = Catalog.where(id: column_ids).order(:name).to_a
  @member_catalog_items = CatalogItem.joins(:store_item)
                                     .where(catalog_id: column_ids,
                                            store_items: { item_id: @variant_group.effective_members.select(:item_id) })
                                     .includes(:store_item, :site_maps, catalog: :amazon_marketplace)
                                     .index_by { |ci| [ci.store_item.item_id, ci.catalog_id] }
  @sorted_members = sorted_members
end

#updatevoid

This method returns an undefined value.



108
109
110
111
112
113
114
# File 'app/controllers/variant_groups_controller.rb', line 108

def update
  if @variant_group.update(variant_group_params)
    redirect_to variant_group_path(@variant_group)
  else
    render :edit, status: :unprocessable_content
  end
end