Class: VariantGroupsController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- VariantGroupsController
- 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
-
#add_member ⇒ void
Adds an item to the family (fails politely when it belongs to another one).
-
#clone ⇒ void
Clones this Amazon parent listing for a relist (next -vN external SKU, ASIN dropped so Amazon assigns a fresh one).
- #create ⇒ void
- #destroy ⇒ void
-
#do_import_images ⇒ void
Applies the picked image profiles onto the selected items.
- #edit ⇒ void
-
#import_images ⇒ void
Step 1 of the image import flow: pick the source item whose image profiles will be copied onto the listing's other items.
-
#index ⇒ void
The scoped, filtered index (catalog multi-select + status + search).
-
#move_items ⇒ void
Re-parents the selected items' catalog_items onto another Amazon parent listing in the same marketplace (the relist flow after a clone).
-
#new ⇒ void
New default family, or a prefilled override when parent_id is given.
- #pull_listing_data ⇒ void
-
#refresh_specs ⇒ void
Queues an ItemAttributeWorker spec refresh for every item listed under this Amazon parent.
- #remove_member ⇒ void
- #send_delete_listing_data ⇒ void
- #send_listing_data ⇒ void
-
#show ⇒ void
A core family page or a per-catalog group page.
- #update ⇒ void
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
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
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_member ⇒ void
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..to_sentence end redirect_to variant_group_path(@variant_group) end |
#clone ⇒ void
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..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 |
#create ⇒ void
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 |
#destroy ⇒ void
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..to_sentence.presence || 'Unable to delete variant group' redirect_to(variant_group_path(@variant_group)) end end |
#do_import_images ⇒ void
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..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 |
#edit ⇒ void
This method returns an undefined value.
96 |
# File 'app/controllers/variant_groups_controller.rb', line 96 def edit; end |
#import_images ⇒ void
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 |
#index ⇒ void
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_items ⇒ void
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 |
#new ⇒ void
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_data ⇒ void
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_specs ⇒ void
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_member ⇒ void
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..to_sentence.presence || 'Unable to remove member' end redirect_to variant_group_path(@variant_group) end |
#send_delete_listing_data ⇒ void
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_data ⇒ void
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 |
#show ⇒ void
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 |
#update ⇒ void
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 |