Class: VariantGroup

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/variant_group.rb

Overview

Canonical, marketplace-agnostic product family — the single definition of
"these items are variants of one product" (Phase 1 of
doc/tasks/202607051428_VARIANT_GROUPING_UNIFICATION.md).

The variant AXES (Voltage, Size, …) live on the group (axis_tokens), and
every channel derives its grouping from this one model:

  • WebsiteItem#item_grouping_info resolves the picker from the
    family's membership; sort_keys/use_product_picture drive its display.
  • Amazon — an override whose catalog is a Seller Central catalog IS the
    per-marketplace parent listing (external_sku/ASIN/theme).
  • Wayfair#to_wayfair_variant_grouping feeds submitV2's
    variantGrouping::groupReferenceId with #slug.
  • Google / OpenAI feeds#slug is the itemGroupId.

Membership is explicit (VariantGroupMember) rather than path-derived, so
families that share a SKU prefix or product line but are different products
(Flex Rolls vs shower mats) stay separate.

Catalog inheritance: a group with catalog_id: nil is the DEFAULT
family. A catalog that groups differently gets an OVERRIDE — a child group
with catalog set and parent pointing at the default, AT MOST ONE ACTIVE
per (family, catalog). The override carries the catalog's grouping data
(parent SKU, variation ASIN, theme, schema); item-level marketplace data
(e.g. the Wayfair display SKU an item is listed under) lives on
catalog_items instead. Resolution (#resolved_for,
Item#variant_group_for) is override-first with fallback to the default,
and an override with no explicit members INHERITS the default's members
until members are added.

Defined Under Namespace

Classes: AmazonDefaults

Constant Summary collapse

DEFAULT_SORT_KEYS =

The sort order every consumer falls back to when a family has no explicit
sort_keys — the universal facet-era default (Www::ProductCatalogPresenter
applies the same pair).

['price asc', 'item_sku asc'].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#catalog_idObject (readonly)

ONE ACTIVE override per catalog per default family — the override IS the
catalog's grouping decision and (for marketplace catalogs) its parent
listing. Inactive rows are exempt so retired Amazon relist listings can
accumulate as ASIN history. Item-level marketplace data (e.g. the Wayfair
display SKU an item is listed under) lives on catalog_items, never here.
DB partial index idx_variant_groups_one_active_override_per_catalog.

Validations (if => -> { parent_id.present? && !inactive? } ):

  • Uniqueness ({ scope: :parent_id, conditions: -> { active } })


89
90
# File 'app/models/variant_group.rb', line 89

validates :catalog_id, uniqueness: { scope: :parent_id, conditions: -> { active } },
if: -> { parent_id.present? && !inactive? }

#external_idObject (readonly)

Marketplace listing id (Amazon ASIN) for an amazon-catalog override; unique per catalog.

Validations:



96
# File 'app/models/variant_group.rb', line 96

validates :external_id, uniqueness: { scope: :catalog_id }, allow_nil: true

#external_skuObject (readonly)

sku/asin are reused across NA marketplaces, so listing identity is unique
per catalog (DB partial indexes idx_variant_groups_external_{sku,id}_per_catalog).

Validations (if => #amazon_channel? ):

Validations:



93
# File 'app/models/variant_group.rb', line 93

validates :external_sku, presence: true, if: :amazon_channel?

#nameObject (readonly)

Human-readable family name (normalized); the display label across channels.

Validations:



79
# File 'app/models/variant_group.rb', line 79

validates :name, presence: true

#slugObject (readonly)

Stable, unique cross-channel identifier — Wayfair groupReferenceId,
Google/OpenAI itemGroupId. Derived once from #name, then frozen.

Validations:



82
# File 'app/models/variant_group.rb', line 82

validates :slug, presence: true, uniqueness: true

Class Method Details

.activeActiveRecord::Relation<VariantGroup>

A relation of VariantGroups that are active. Active Record Scope

Returns:

See Also:



111
# File 'app/models/variant_group.rb', line 111

scope :active, -> { where(inactive: false) }

.amazon_channelActiveRecord::Relation<VariantGroup>

A relation of VariantGroups that are amazon channel. Active Record Scope

Returns:

See Also:



110
# File 'app/models/variant_group.rb', line 110

scope :amazon_channel, -> { joins(:catalog).where.not(catalogs: { amazon_marketplace_id: nil }) }

.amazon_rows_for_selectArray<Array(String, Integer)>

["parent SKU - name", id] pairs of Amazon parent-listing rows for search
selects (replaces AmazonVariation.variations_for_select).

Returns:

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


143
144
145
# File 'app/models/variant_group.rb', line 143

def self.amazon_rows_for_select
  amazon_channel.where.not(external_sku: nil).order(:external_sku).map { |row| ["#{row.external_sku} - #{row.name}", row.id] }
end

.asin_searchActiveRecord::Relation<VariantGroup>

A relation of VariantGroups that are asin search. Active Record Scope

Returns:

See Also:



113
114
115
116
117
# File 'app/models/variant_group.rb', line 113

scope :asin_search, ->(asin) {
  where(external_id: asin).or(
    where("EXISTS (SELECT 1 FROM jsonb_each(COALESCE(channel_settings->'retailer_information', '{}'::jsonb)) AS info WHERE info.value->>'asin' = ?)", asin)
  )
}

.axis_token_optionsArray<String>

Every known spec token, for the axis_tokens picker.

Returns:

  • (Array<String>)


127
128
129
# File 'app/models/variant_group.rb', line 127

def self.axis_token_options
  ProductSpecification.token_select_options
end

.defaultsActiveRecord::Relation<VariantGroup>

A relation of VariantGroups that are defaults. Active Record Scope

Returns:

See Also:



108
# File 'app/models/variant_group.rb', line 108

scope :defaults, -> { where(parent_id: nil, catalog_id: nil) }

.select_optionsArray<Array(String, Integer)>

["name (slug)", id] pairs for selects (mirrors AmazonMarketplace.select_options).

Returns:

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


121
122
123
# File 'app/models/variant_group.rb', line 121

def self.select_options
  order(:name).map { |group| ["#{group.name} (#{group.slug})", group.id] }
end

.sort_key_optionsArray<String>

Sortable options for the sort_keys picker: catalog view columns plus
spec-token sorts (e.g. "spec:width:in asc").

Returns:

  • (Array<String>)


134
135
136
137
138
# File 'app/models/variant_group.rb', line 134

def self.sort_key_options
  column_options = ViewProductCatalog.columns.map(&:name).sort.flat_map { |n| ["#{n} asc", "#{n} desc"] }
  spec_options = ProductSpecification.token_select_options.flat_map { |t| ["spec:#{t} asc", "spec:#{t} desc"] }
  column_options + spec_options
end

Instance Method Details

#amazon_browse_nodesActiveRecord::Relation<AmazonBrowseNode>

Amazon browse nodes (recommended categories) for this parent listing.

Returns:

See Also:



66
67
# File 'app/models/variant_group.rb', line 66

has_and_belongs_to_many :amazon_browse_nodes,
join_table: 'amazon_browse_nodes_variant_groups'

#amazon_channel?Boolean

Returns whether this row is an Amazon parent listing (its
catalog is a Seller Central catalog).

Returns:

  • (Boolean)

    whether this row is an Amazon parent listing (its
    catalog is a Seller Central catalog)



266
267
268
# File 'app/models/variant_group.rb', line 266

def amazon_channel?
  catalog&.amazon_marketplace_id.present?
end

#amazon_delete_variation_listing(catalog) ⇒ Hash

Deletes this parent listing from Amazon via the catalog's orchestrator.

Parameters:

  • catalog (Catalog)

    the Seller Central catalog to delete from

Returns:

  • (Hash)

    a { status:, message: } result — :skipped when the
    orchestrator cannot delete (nil or order-only), :error on failure, otherwise :success



396
397
398
399
400
401
402
403
404
405
# File 'app/models/variant_group.rb', line 396

def amazon_delete_variation_listing(catalog)
  return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog&.load_orchestrator).respond_to?(:delete_listing_from_catalog_item)

  res = orchestrator.delete_listing_from_catalog_item(amazon_variation: self)
  if res.any?(false)
    { status: :error, message: "#{self}: could not send DELETE listing data to Amazon!" }
  else
    { status: :success, message: "#{self}: sent DELETE listing data to Amazon!" }
  end
end

#amazon_json_generator(marketplace_id:, attribute_actions: nil, fba: false, language_tag: nil, business_price_available: true) ⇒ Edi::Amazon::JsonListingGenerator::BaseGenerator

Builds the Amazon Listings JSON generator for this parent variation in the
given marketplace, bound to that marketplace's catalog_item.

Parameters:

  • marketplace_id (String)

    Amazon marketplace identifier

  • attribute_actions (Hash, nil) (defaults to: nil)

    per-attribute action overrides

  • fba (Boolean) (defaults to: false)

    accepted for interface parity; unused here

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

    BCP-47 language tag override

  • business_price_available (Boolean) (defaults to: true)

    whether business pricing is enabled

Returns:

Raises:

  • (ArgumentError)

    if no catalog_item exists for the marketplace



354
355
356
357
# File 'app/models/variant_group.rb', line 354

def amazon_json_generator(marketplace_id:, attribute_actions: nil, fba: false, language_tag: nil, business_price_available: true) # rubocop:disable Lint/UnusedMethodArgument
  catalog_item = catalog_items_for_amazon_seller_marketplace_identifier(marketplace_id).first
  Edi::Amazon::JsonListingGenerator::Factory.generator_for_variation(self, catalog_item:, marketplace_id:, attribute_actions:, language_tag:, business_price_available:)
end

#amazon_marketplaceAmazonMarketplace?

Returns the marketplace behind this row's catalog.

Returns:



271
272
273
# File 'app/models/variant_group.rb', line 271

def amazon_marketplace
  catalog&.amazon_marketplace
end

#amazon_product_type_in_effectString?

Submittable Listings product type: the explicit schema (e.g. TOWEL_HOLDER)
when set, otherwise the desired catalog type. The listing feed submits
this; never an unlaunched catalog PT (Listings rejects 4000003).

Returns:

  • (String, nil)


309
310
311
# File 'app/models/variant_group.rb', line 309

def amazon_product_type_in_effect
  amazon_product_schema.presence || amazon_desired_product_type
end

#amazon_pull_listing_information(catalog) ⇒ Hash

Pulls this parent listing's current data from Amazon via the catalog's orchestrator.

Parameters:

  • catalog (Catalog)

    the Seller Central catalog to pull from

Returns:

  • (Hash)

    a { status:, message: } result — :skipped when the catalog
    resolves an order-only (Vendor Central) orchestrator, :error on a failed
    pull, otherwise :success



364
365
366
367
368
369
370
371
372
373
374
375
# File 'app/models/variant_group.rb', line 364

def amazon_pull_listing_information(catalog)
  # A Vendor Central catalog can resolve to the order-only Edi::MftGateway::Orchestrator,
  # which lacks the listing methods — respond_to? guards both nil and wrong-orchestrator.
  return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog&.load_orchestrator).respond_to?(:pull_amazon_variation_listing_information)

  res = orchestrator.pull_amazon_variation_listing_information(self)
  if res.any? { |h| h.values.any?(false) }
    { status: :error, message: "#{self}: could not pull listing data from Amazon!" }
  else
    { status: :success, message: "#{self}: listing data pulled from Amazon!" }
  end
end

#amazon_send_put_listing_information(catalog) ⇒ Hash

Sends (PUT) this parent listing's data to Amazon via the catalog's orchestrator.

Parameters:

  • catalog (Catalog)

    the Seller Central catalog to publish to

Returns:

  • (Hash)

    a { status:, message: } result — :skipped when the
    orchestrator cannot push (nil or order-only), :error on failure, otherwise :success



381
382
383
384
385
386
387
388
389
390
# File 'app/models/variant_group.rb', line 381

def amazon_send_put_listing_information(catalog)
  return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog&.load_orchestrator).respond_to?(:push_listing_from_amazon_variation)

  res = orchestrator.push_listing_from_amazon_variation(self)
  if res.any?(false)
    { status: :error, message: "#{self}: could not send put listing data to Amazon!" }
  else
    { status: :success, message: "#{self}: sent put listing data to Amazon!" }
  end
end

#asinString?

Returns the ASIN Amazon assigned this parent listing.

Returns:

  • (String, nil)

    the ASIN Amazon assigned this parent listing



283
284
285
# File 'app/models/variant_group.rb', line 283

def asin
  external_id
end

#axis_tokensArray<String>

The ordered variant axes (spec tokens, e.g. ["voltage", "size"]). Owned by
the group; an override inherits its default's axes when it has none.

Returns:

  • (Array<String>)


211
212
213
214
215
216
# File 'app/models/variant_group.rb', line 211

def axis_tokens
  own = Array(self[:axis_tokens]).compact_blank
  return own if own.any?

  override? ? parent.axis_tokens : []
end

#axis_values_for(item) ⇒ Hash{String=>Object}

Per-member axis values, for the UI matrix and channel adapters.

Parameters:

Returns:

  • (Hash{String=>Object})

    token => spec value (nil values omitted)



236
237
238
# File 'app/models/variant_group.rb', line 236

def axis_values_for(item)
  axis_tokens.index_with { |token| item.spec_value(token) }.compact
end

#catalogCatalog

The catalog an override targets — catalogs ARE the channel+region
(Amazon US/CA Seller Central, Wayfair US/CA, website catalogs 1/2). A
default family carries no catalog. An amazon-catalog override doubles as
the per-marketplace parent listing: external_sku = parent SKU,
external_id = ASIN, channel_settings = Amazon-specific definition.

Returns:

See Also:



53
# File 'app/models/variant_group.rb', line 53

belongs_to :catalog, optional: true

#catalog_itemsActiveRecord::Relation<CatalogItem>

Per-catalog listing rows that name this group as their variation parent.

Returns:

See Also:



60
# File 'app/models/variant_group.rb', line 60

has_many :catalog_items, dependent: :nullify, inverse_of: :variant_group

#catalog_items_for_amazon_seller_marketplace_identifier(marketplace_id) ⇒ ActiveRecord::Relation<CatalogItem>, Array

This group's catalog_items in the Seller Central catalog for the given
Amazon marketplace.

Parameters:

  • marketplace_id (String)

    Amazon marketplace identifier (e.g. "ATVPDKIKX0DER")

Returns:

  • (ActiveRecord::Relation<CatalogItem>, Array)

    the marketplace's
    catalog_items, or [] when no catalog matches the marketplace



338
339
340
341
342
343
# File 'app/models/variant_group.rb', line 338

def catalog_items_for_amazon_seller_marketplace_identifier(marketplace_id)
  catalog = Catalog.for_amazon_seller_marketplace_identifier(marketplace_id)
  return [] unless catalog

  catalog_items.where(catalog_id: catalog.id)
end

#cloned_fromVariantGroup

The row this Amazon relist was version-cloned from (see #deep_dup); nil unless cloned.



55
# File 'app/models/variant_group.rb', line 55

belongs_to :cloned_from, class_name: 'VariantGroup', optional: true

#clonesActiveRecord::Relation<VariantGroup>

Relist copies made from this row via #deep_dup.

Returns:

See Also:



57
58
# File 'app/models/variant_group.rb', line 57

has_many :clones, class_name: 'VariantGroup', foreign_key: :cloned_from_id,
dependent: :nullify, inverse_of: :cloned_from

#deep_dupVariantGroup

Clone this Amazon parent listing for a relist: next -vN external SKU,
ASIN dropped (Amazon assigns a fresh one), browse nodes carried over.
The clone REPLACES this row as the catalog's active override — the caller
must retire the original (see VariantGroupsController#clone), or saving
the copy trips the one-active-override-per-catalog uniqueness.

Returns:

Raises:

  • (ArgumentError)


422
423
424
425
426
427
428
429
430
431
432
433
# File 'app/models/variant_group.rb', line 422

def deep_dup
  raise ArgumentError, 'only Amazon parent listings (external_sku set) can be version-cloned' if external_sku.blank?

  deep_clone(include: :amazon_browse_nodes, except: %i[external_id slug]) do |original, copy|
    if copy.is_a?(VariantGroup)
      copy.external_sku = VariantGroup::AmazonDefaults.next_version_sku(original.external_sku)
      copy.channel_settings = original.channel_settings.except('retailer_information')
      copy.cloned_from = original
      copy.name = "#{original.name} (v#{copy.external_sku[/v(\d+)$/, 1]})"
    end
  end
end

#edi_communication_logsActiveRecord::Relation<EdiCommunicationLog>

Transport logs for this group's #edi_documents.

Returns:

See Also:



64
# File 'app/models/variant_group.rb', line 64

has_many :edi_communication_logs, through: :edi_documents

#edi_documentsActiveRecord::Relation<EdiDocument>

EDI documents exchanged for this parent listing.

Returns:

See Also:



62
# File 'app/models/variant_group.rb', line 62

has_many :edi_documents, dependent: :destroy

#effective_itemsActiveRecord::Relation<Item>

Effective items (see #effective_members for the inheritance rule).

Returns:

  • (ActiveRecord::Relation<Item>)


204
205
206
# File 'app/models/variant_group.rb', line 204

def effective_items
  Item.where(id: effective_members.select(:item_id))
end

#effective_membersActiveRecord::Relation<VariantGroupMember>

Effective members for this group: an override with no explicit members
inherits the default family's members.

Returns:



198
199
200
# File 'app/models/variant_group.rb', line 198

def effective_members
  inherits_members? ? parent.variant_group_members : variant_group_members
end

#effective_sort_keysArray<String>

The variant-picker sort order. Same inheritance rules as #axis_tokens;
empty means the caller's default applies.

Returns:

  • (Array<String>)


226
227
228
229
230
231
# File 'app/models/variant_group.rb', line 226

def effective_sort_keys
  own = Array(self[:sort_keys]).compact_blank
  return own if own.any?

  override? ? parent.effective_sort_keys : []
end

#inherits_members?Boolean

Returns whether this override has no explicit members and is
currently serving its default family's members.

Returns:

  • (Boolean)

    whether this override has no explicit members and is
    currently serving its default family's members



191
192
193
# File 'app/models/variant_group.rb', line 191

def inherits_members?
  override? && variant_group_members.load.none?
end

#itemsActiveRecord::Relation<Item>

The member items, through #variant_group_members.

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



46
# File 'app/models/variant_group.rb', line 46

has_many :items, through: :variant_group_members

#items_via_catalogActiveRecord::Relation<Item>

Items listed under this Amazon parent, via their catalog_items — the
per-marketplace membership the feeds act on (distinct from the family's
variant_group_members, which is channel-agnostic).

Returns:

  • (ActiveRecord::Relation<Item>)


327
328
329
330
331
# File 'app/models/variant_group.rb', line 327

def items_via_catalog
  Item.joins(store_items: :catalog_items)
      .where(catalog_items: { variant_group_id: id })
      .distinct
end

#listing_catalogCatalog?

The catalog this row lists into (an override's catalog IS its listing
target). Drives the pull / push / delete listing actions.

Returns:



290
291
292
# File 'app/models/variant_group.rb', line 290

def listing_catalog
  catalog
end

#new_override(catalog) ⇒ VariantGroup

A prefilled, unsaved override of this default family for the given
catalog — derives a conventional name. Used by the
"Create override" flow; the override starts empty (serving the default's
members).

Parameters:

Returns:



158
159
160
161
# File 'app/models/variant_group.rb', line 158

def new_override(catalog)
  overrides.new(catalog: catalog,
                name: catalog ? "#{name} (#{catalog.name} override)" : nil)
end

#override?Boolean

Returns whether this group is a per-catalog override of a default family.

Returns:

  • (Boolean)

    whether this group is a per-catalog override of a default family



148
149
150
# File 'app/models/variant_group.rb', line 148

def override?
  parent_id.present?
end

#override_for(catalog) ⇒ VariantGroup?

The override governing the given catalog's grouping — at most one ACTIVE
row exists per (family, catalog). Reads the loaded association when
present so collection renders can preload :overrides and stay N+1-free.

Parameters:

  • catalog (Catalog, Integer)

    catalog or id

Returns:



180
181
182
183
184
185
186
187
# File 'app/models/variant_group.rb', line 180

def override_for(catalog)
  target_catalog_id = catalog.respond_to?(:id) ? catalog.id : catalog.to_i
  if overrides.loaded?
    overrides.detect { |override| override.catalog_id == target_catalog_id && !override.inactive? }
  else
    overrides.active.find_by(catalog_id: target_catalog_id)
  end
end

#overridesActiveRecord::Relation<VariantGroup>

Per-catalog override children of this default family (at most one ACTIVE per catalog).

Returns:

See Also:



41
42
# File 'app/models/variant_group.rb', line 41

has_many :overrides, class_name: 'VariantGroup', foreign_key: :parent_id,
dependent: :destroy, inverse_of: :parent

#parentVariantGroup

The default family this override belongs to; nil on a default group.



39
# File 'app/models/variant_group.rb', line 39

belongs_to :parent, class_name: 'VariantGroup', optional: true, inverse_of: :overrides

#possible_product_typesArray<String>

Distinct Amazon product types across this parent's member catalog_items —
the candidate values for the listing's product-type picker.

Returns:

  • (Array<String>)

    sorted amazon_desired_product_type values



316
317
318
319
320
321
# File 'app/models/variant_group.rb', line 316

def possible_product_types
  CatalogItem.joins(store_item: :item)
             .merge(items_via_catalog).where.not(amazon_desired_product_type: nil)
             .distinct.order(:amazon_desired_product_type)
             .pluck(:amazon_desired_product_type)
end

#primary_itemItem

The member flagged Primary for Wayfair (the family's lead child); optional.
Validated to be an actual member — see #primary_item_must_be_member.

Returns:

See Also:



37
# File 'app/models/variant_group.rb', line 37

belongs_to :primary_item, class_name: 'Item', optional: true

#reported_vendor_sku(_orchestrator_partner) ⇒ String

The vendor SKU reported to the marketplace API — the parent listing's
external SKU, regardless of partner.

Parameters:

Returns:

  • (String)

    the SKU reported to the marketplace API



412
413
414
# File 'app/models/variant_group.rb', line 412

def reported_vendor_sku(_orchestrator_partner)
  external_sku
end

#resolved_for(catalog) ⇒ VariantGroup

The group that governs the given catalog: this family's override for the
catalog when one exists, otherwise the default itself (inheritance).
Calling this on an override delegates through its default first.

Parameters:

  • catalog (Catalog, Integer, nil)

    catalog or id; nil resolves the default

Returns:



168
169
170
171
172
173
# File 'app/models/variant_group.rb', line 168

def resolved_for(catalog)
  base = override? ? parent : self
  return base if catalog.blank?

  base.override_for(catalog) || base
end

#skuString?

The listing SKU this row represents on its channel (Amazon parent SKU).
Canonical reader for the listing generators.

Returns:

  • (String, nil)


278
279
280
# File 'app/models/variant_group.rb', line 278

def sku
  external_sku
end

#to_sString

Returns "name [id]" for selects and logs.

Returns:

  • (String)

    "name [id]" for selects and logs



436
437
438
# File 'app/models/variant_group.rb', line 436

def to_s
  "#{name} [#{id}]"
end

#to_wayfair_variant_grouping(item) ⇒ Edi::Wayfair::VariantGrouping

The Wayfair submitV2 grouping directive for a member of this family.

Parameters:

  • item (Item)

    the member being submitted

Returns:



243
244
245
246
247
248
249
# File 'app/models/variant_group.rb', line 243

def to_wayfair_variant_grouping(item)
  Edi::Wayfair::VariantGrouping.new(
    group_reference_id: slug,
    categories: axis_values_for(item).transform_keys { |t| Edi::Wayfair::VariantGrouping::TOKEN_CATEGORY_MAP[t.to_s.downcase] || t.to_s.titleize },
    primary: item.id == primary_item_id
  )
end

#variant_group_membersActiveRecord::Relation<VariantGroupMember>

Explicit, ordered membership rows (channel-agnostic).

Returns:

See Also:



44
# File 'app/models/variant_group.rb', line 44

has_many :variant_group_members, -> { order(:position, :id) }, dependent: :destroy, inverse_of: :variant_group

#variation_theme_attributesArray<Symbol>

Array of attribute symbols for the current variation theme,
e.g. "SIZE_NAME/COLOR_NAME" => [:size, :color].

Returns:

  • (Array<Symbol>)


297
298
299
300
301
302
303
# File 'app/models/variant_group.rb', line 297

def variation_theme_attributes
  return [] if variation_theme_name.blank?

  variation_theme_name.split('/').filter_map do |component|
    VariantGroup::AmazonDefaults::VARIATION_THEME_ATTRIBUTE_MAP[component]
  end.uniq
end

#wayfair_catalog_itemsActiveRecord::Relation<CatalogItem>

The family's Wayfair catalog rows (US + CA), for the per-catalog panel.
Membership resolves per Wayfair catalog (each may carry its own override).

Returns:



254
255
256
257
258
259
260
# File 'app/models/variant_group.rb', line 254

def wayfair_catalog_items
  scopes = CatalogConstants::WAYFAIR_CATALOGS.map do |catalog_id|
    CatalogItem.where(catalog_id: catalog_id,
                      store_item: StoreItem.where(item_id: resolved_for(catalog_id).effective_members.select(:item_id)))
  end
  scopes.reduce(:or).includes(:item, :catalog)
end