Class: CatalogItem

Overview

== Schema Information

Table name: catalog_items
Database name: primary

id :integer not null, primary key
alternate_warehouse_stock_fraction :integer
alternate_warehouse_stock_reporting_max :integer
amazon_desired_product_type :string
amazon_fba_labeling :enum
amazon_fba_sku :string
amazon_fnsku :string
amazon_info_datetime :datetime
amazon_item_suppression_pull_datetime :datetime
amazon_listing_reported_product_type :string
amazon_product_schema :string(30)
amazon_reported_product_type :string
amount :decimal(8, 2)
amz_last_buy_box_winner_change :datetime
amz_min_seller_price_override :decimal(8, 2)
backup_tppn :string
business_discount_override :integer
clearance :boolean default(FALSE), not null
discontinued_date :date
discontinued_part_number :string
google_feed :boolean default(FALSE), not null
google_feed_title :string
google_report :jsonb
is_amz_buy_box_winner :boolean default(FALSE), not null
is_amz_featured_merchant :boolean default(FALSE), not null
item_suppressed_status :string
item_suppressed_status_message :string
last_inventory_advice_sent_json :jsonb
last_price_advice_sent_json :jsonb
legacy_fulfillment :integer default(0), not null
legacy_sale_price_effective_date :date
legacy_sale_price_expiration_date :date
margin_required :decimal(, )
max_discount :integer default(100)
min_stock_to_report :integer
new_price :decimal(8, 2)
new_price_effective_date :date
new_sale_price :decimal(8, 2)
old_amount :decimal(10, 2)
pack_at_kit_level :boolean default(FALSE), not null
parent_sku :string
pending_discontinue_date :date
position :integer default(100)
price_editable :boolean default(FALSE), not null
price_updated_at :datetime
quantity_1_lower_bound :integer
quantity_1_price_discount :decimal(6, 2)
quantity_1_price_type :enum
quantity_2_lower_bound :integer
quantity_2_price_discount :decimal(6, 2)
quantity_2_price_type :enum
quantity_3_lower_bound :integer
quantity_3_price_discount :decimal(6, 2)
quantity_3_price_type :enum
quantity_4_lower_bound :integer
quantity_4_price_discount :decimal(6, 2)
quantity_4_price_type :enum
quantity_5_lower_bound :integer
quantity_5_price_discount :decimal(6, 2)
quantity_5_price_type :enum
quantity_discount_price_type :enum
reserve_stock :integer
retail_price :decimal(8, 2)
retailer_information :jsonb
retailer_price_updated_at :datetime
retailer_requested_cost :decimal(10, 2)
sale_price :decimal(8, 2)
skip_url_checks :boolean default(FALSE), not null
state :string(30) default("active"), not null
third_party_description :text
third_party_name :string(255)
third_party_part_number :string(255)
third_party_product_type :string
third_party_promo_part_number :string
third_party_short_description :text
third_party_sku :string
translations :jsonb
url :string
url_last_checked :datetime
url_valid :boolean default(FALSE), not null
created_at :datetime
updated_at :datetime
catalog_id :integer
coupon_id :integer
new_coupon_id :integer
store_item_id :integer not null

Indexes

index_catalog_items_on_amazon_fnsku (amazon_fnsku) UNIQUE
index_catalog_items_on_catalog_id_and_store_item_id (catalog_id,store_item_id) UNIQUE
index_catalog_items_on_catalog_id_and_third_party_sku (catalog_id,third_party_sku) UNIQUE
index_catalog_items_on_catalog_id_and_tpn (catalog_id,third_party_promo_part_number) UNIQUE
index_catalog_items_on_coupon_id (coupon_id)
index_catalog_items_on_new_coupon_id (new_coupon_id)
index_catalog_items_on_parent_sku (parent_sku)
index_catalog_items_on_state_and_store_item_id_and_catalog_id (state,store_item_id,catalog_id)
index_catalog_items_on_state_and_url_valid (state,url_valid)
index_catalog_items_on_store_item_id_and_state (store_item_id,state)
index_catalog_items_on_third_party_part_number_and_catalog_id (third_party_part_number,catalog_id) UNIQUE WHERE (third_party_part_number IS NOT NULL)
index_catalog_items_on_third_party_sku_and_catalog_id (third_party_sku,catalog_id) UNIQUE WHERE (third_party_sku IS NOT NULL)

Foreign Keys

catalog_items_catalog_id_fkey (catalog_id => catalogs.id)
catalog_items_store_item_id_fk (store_item_id => store_items.id) ON DELETE => cascade
fk_rails_... (coupon_id => coupons.id)
fk_rails_... (new_coupon_id => coupons.id) ON DELETE => nullify

Defined Under Namespace

Classes: CouponAndSalePriceValidator, Onboarder, PriceSync, PriceUpdatedHandler, QuantityBoundsValidator, Remapper, ScheduledPriceChanger

Constant Summary collapse

HIDDEN_STATES =

Recognised hidden states.

%w[active_hidden discontinued pending_client_review invalid_catalog_item pending_onboarding].freeze
EDI_FEED_STATUSES =

Recognised edi feed statuses.

%w[active require_vendor_update pending_onboarding pending_vendor_update pending_discontinue].freeze
EDI_DELTA_FEED_CATEGORIES =

Omitting an item from a feed automatically will trigger a removal, therefore pending_discontinue should be excluded
from selection

%w[inventory_advice price_advice].freeze
ORCHESTRATOR_STATES =

Recognised orchestrator states — what the Mirakl product-data and price feeds
send (via for_orchestrator_keys).

pending_discontinue IS sent: the listing still has to be maintained while
remaining stock sells through. discontinued is NOT — we have stopped selling
it, so it must never be pushed to a marketplace again.

discontinued used to be in this list, which inverted the meaning of retiring
an item: it moved a catalog item INTO the feed rather than out of it, because
pending_onboarding is absent. Discontinuing the Ember accessories on ADEO
therefore fed an unmappable thermostat to the product presenter and took the
whole ADEO product feed down (AppSignal #6521).

%w[active require_vendor_update pending_vendor_update pending_discontinue].freeze
ORDER_MATCHABLE_STATES =

States an inbound EDI order is allowed to match against.

Wider than EDI_FEED_STATUSES on both ends: active_hidden items are not
fed but are still ordered off stale retailer listings, and
pending_onboarding is matched because the order itself proves the listing
is live — see #onboarded_by_order!. A state we don't match raises out of the
line-item build and rolls back the WHOLE order batch, so this list errs
toward accepting the order and warning
(#state_requires_edi_warning_on_order?) over refusing it.

%w[active active_hidden require_vendor_update pending_vendor_update pending_discontinue pending_onboarding].freeze
REPRICE_RULES_RAISING =

Rules that route through the raising / lowering services (SQL-usable).

%w[up_only up_only_preserve_buy_box auto_buy_box].freeze
REPRICE_RULES_LOWERING =

Reprice rules lowering.

%w[down_only_buy_box auto_buy_box].freeze
REPRICE_REVERT_WINDOW =

How long after a raise a Buy-Box loss still triggers an automatic revert.

7.days

Constants included from Models::CatalogItemWalmartHelper

Models::CatalogItemWalmartHelper::WALMART_ALREADY_RETIRED_ERROR_CODE

Constants included from Models::CatalogItemAmazonHelper

Models::CatalogItemAmazonHelper::AMAZON_DEFAULT_BUSINESS_DISCOUNT, Models::CatalogItemAmazonHelper::AMAZON_LISTING_LANGUAGES, Models::CatalogItemAmazonHelper::AMAZON_MARKETPLACE_LISTING_LOCALES, Models::CatalogItemAmazonHelper::AMAZON_MIN_PROFIT_MARGIN

Constants included from RouteUrlHelpers

RouteUrlHelpers::ROUTES

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Attributes included from Models::CatalogItemAmazonHelper

#amazon_fnsku

Attributes included from Models::Translatable

#do_not_compact_translation_container

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Has and belongs to many collapse

Delegated Instance Attributes collapse

Methods included from Models::CatalogItemAmazonHelper

#amazon_description, #amazon_feature_1, #amazon_feature_2, #amazon_feature_3, #amazon_feature_4, #amazon_feature_5, #amazon_feature_6, #amazon_feature_7, #amazon_feature_8, #amazon_feature_9, #amazon_generic_keyword, #amazon_target_keywords, #amazon_title, #title_for_amazon

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::EdgeCachePurgeable

#edge_cache_urls, #enqueue_edge_cache_purge

Methods included from Models::CatalogItemWayfairHelper

#wayfair_catalog_item?, #wayfair_class_diverged?, #wayfair_effective_taxonomy_category_id, #wayfair_primary_product_url, #wayfair_product_url, #wayfair_pull_taxonomy_schema, #wayfair_sync_age, #wayfair_synced?, #wayfair_taxonomy_available?, #wayfair_taxonomy_category_id_in_effect, wayfairs

Methods included from Models::CatalogItemWalmartHelper

#walmart_catalog_item?, #walmart_retire_item, walmarts

Methods included from Models::CatalogItemAmazonHelper

#amalytix_tags, #amazon_asin, #amazon_business_price, #amazon_business_price_factor, #amazon_business_price_fba, #amazon_business_price_with_tax, #amazon_business_price_with_tax_fba, #amazon_catalog_item?, #amazon_current_images, #amazon_delete_information, #amazon_effective_business_price_discount, #amazon_effective_desired_product_type, #amazon_effective_product_schema, #amazon_gated_locales, #amazon_item_cost, #amazon_json_generator, #amazon_label_requirements, #amazon_list_price, #amazon_locales, #amazon_lowest_quantity_discount_price, #amazon_lowest_quantity_discounted_price, #amazon_maximum_seller_allowed_price_with_tax, #amazon_maximum_seller_allowed_price_with_tax_fba, #amazon_minimum_profit_margin, #amazon_minimum_seller_allowed_price_with_tax, #amazon_minimum_seller_allowed_price_with_tax_fba, #amazon_patch_information, #amazon_price_with_tax, #amazon_price_with_tax_fba, #amazon_product_data, #amazon_product_type_in_effect, #amazon_product_type_incoherent?, #amazon_product_url, #amazon_pull_buy_box_status, #amazon_pull_catalog_information, #amazon_pull_listing_information, #amazon_pull_listing_schema, #amazon_put_information, #amazon_quantity_discount_price, #amazon_repricing_floor_with_tax, #amazon_sale_price_with_tax, #amazon_sale_price_with_tax_fba, #amazon_schema, #amazon_seller_item?, #amazon_send_patch_listing_information, #amazon_send_put_listing_information, #amazon_vendor_code, #amazon_vendor_item?, amazons, amazons_sellers, amazons_sellers_with_asins, amazons_with_asins, #amz_available_attributes, #api_ready_state?, #broadcast_amazon_dashboard_update, by_asins, #extract_amazon_procurement_cost_price, #extract_retailer_information, #fba_discount, #get_amz_all_patches, #get_amz_attribute, #get_amz_attributes, #has_amazon_fba?, #map_price_with_tax, #max_discount_allowed, #max_discount_allowed_dollars, #max_discount_allowed_percentage, #profit_margin_amazon_minimum_below_target, #profit_marging_amazon_maximum_seller_allowed_price, #profit_marging_amazon_minimum_seller_allowed_price, #ready_to_print_amazon_fba_labels?, #reprice_allows_lowering?, #reprice_allows_raising?, #reprice_revert_due?, #reprice_revert_last_raise!, #reprice_reverts_on_buy_box_loss?, #reprice_settings_amazon_only, #reprice_settings_customized?, #reset_reprice_raise_bookkeeping, #retailer_information_first_locale, with_asin

Methods included from Models::SaleDiscountable

#effective_price, #money_effective_price, #money_price, #money_sale_price

Methods included from Models::Translatable

#compact_translation_container, skip_compaction_for

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

#add_kit_components_to_catalogObject

Returns the value of attribute add_kit_components_to_catalog.



182
183
184
# File 'app/models/catalog_item.rb', line 182

def add_kit_components_to_catalog
  @add_kit_components_to_catalog
end

#alternate_warehouse_stock_fractionObject (readonly)

Validates alternate warehouse stock fraction.

Validations:

  • Numericality ({ only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100 })
  • Allow_nil


351
# File 'app/models/catalog_item.rb', line 351

validates :alternate_warehouse_stock_fraction, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true

#alternate_warehouse_stock_reporting_maxObject (readonly)

Validates alternate warehouse stock reporting max.

Validations:

  • Numericality ({ only_integer: true, greater_than: 0 })
  • Allow_nil


349
# File 'app/models/catalog_item.rb', line 349

validates :alternate_warehouse_stock_reporting_max, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true

#amountObject (readonly)

Validates amount.

Validations:



340
# File 'app/models/catalog_item.rb', line 340

validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0.0 }

#item_idObject

Item id.



835
836
837
# File 'app/models/catalog_item.rb', line 835

def item_id
  store_item&.item_id
end

#max_discountObject (readonly)

Validates max discount.

Validations:

  • Numericality ({ greater_than_or_equal_to: 0, less_than_or_equal_to: 100 })
  • Allow_nil


353
# File 'app/models/catalog_item.rb', line 353

validates :max_discount, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true

#new_priceObject (readonly)

Validates new price, new price effective date.

Validations:

  • Presence ({ if: :new_price_effective_date, message: 'Must be specified alongside new price effective date' })


336
# File 'app/models/catalog_item.rb', line 336

validates :new_price, presence: { if: :new_price_effective_date, message: 'Must be specified alongside new price effective date' }

#new_price_effective_dateObject (readonly)

Validates new price effective date, new price.

Validations:

  • Presence ({ if: :new_price, message: 'Must be specified alongside new price' })


338
# File 'app/models/catalog_item.rb', line 338

validates :new_price_effective_date, presence: { if: :new_price, message: 'Must be specified alongside new price' }

#reserve_stockObject (readonly)

Validates reserve stock.

Validations:

  • Numericality ({ only_integer: true, greater_than_or_equal_to: 0 })
  • Allow_nil


347
# File 'app/models/catalog_item.rb', line 347

validates :reserve_stock, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true

#skip_check_kit_componentsObject

Returns the value of attribute skip_check_kit_components.



182
183
184
# File 'app/models/catalog_item.rb', line 182

def skip_check_kit_components
  @skip_check_kit_components
end

#skip_create_kit_componentsObject

Returns the value of attribute skip_create_kit_components.



182
183
184
# File 'app/models/catalog_item.rb', line 182

def skip_create_kit_components
  @skip_create_kit_components
end

#store_idsObject

Store ids.



830
831
832
# File 'app/models/catalog_item.rb', line 830

def store_ids
  store_items.map(&:store_id)
end

#third_party_name_enObject (readonly)

Validates third party name en, third party name en required.

Validations:

  • Presence ({ if: :third_party_name_en_required? })


332
# File 'app/models/catalog_item.rb', line 332

validates :third_party_name_en, presence: { if: :third_party_name_en_required? }

#third_party_name_frObject (readonly)

Validates third party name fr, third party name fr required.

Validations:

  • Presence ({ if: :third_party_name_fr_required? })


334
# File 'app/models/catalog_item.rb', line 334

validates :third_party_name_fr, presence: { if: :third_party_name_fr_required? }

#third_party_part_numberObject (readonly)

validates :store_item_id, uniqueness: { scope: :catalog_id }

Validations:

  • Presence ({ if: :third_party_part_number_required? })


322
# File 'app/models/catalog_item.rb', line 322

validates :third_party_part_number, presence: { if: :third_party_part_number_required? }

#third_party_skuObject (readonly)

Validates third party sku, third party sku required.

Validations:

  • Presence ({ if: :third_party_sku_required? })


324
# File 'app/models/catalog_item.rb', line 324

validates :third_party_sku, presence: { if: :third_party_sku_required? }

#third_party_sku_variant_idObject (readonly)

Marketplace variant selector (e.g. Wayfair piid): a given variant must map to
at most one of our catalog items per catalog. allow_nil so the many single /
non-variant items (no selector) coexist — a blank selector is normalized to
nil (see the normalizes block below) so a form-submitted "" doesn't trip
this against another blank item. The partial DB index is the backstop.

Validations:



330
# File 'app/models/catalog_item.rb', line 330

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

Class Method Details

.accessoriesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are accessories. Active Record Scope

Returns:

See Also:



428
# File 'app/models/catalog_item.rb', line 428

scope :accessories, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_ACCESSORIES)) }

.activeActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are active. Active Record Scope

Returns:

See Also:



401
# File 'app/models/catalog_item.rb', line 401

scope :active, -> { where(state: %w[active active_hidden]) }

.available_for_edi_feedsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are available for edi feeds. Active Record Scope

Returns:

See Also:



450
# File 'app/models/catalog_item.rb', line 450

scope :available_for_edi_feeds, -> { available_for_edi_feeds_by_states(EDI_FEED_STATUSES) }

.available_for_edi_feeds_by_statesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are available for edi feeds by states. Active Record Scope

Returns:

See Also:



454
455
456
457
# File 'app/models/catalog_item.rb', line 454

scope :available_for_edi_feeds_by_states, ->(states) {
  states_to_use = states.presence || EDI_FEED_STATUSES
  where(state: states_to_use)
}

.by_skusActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are by skus. Active Record Scope

Returns:

See Also:



407
# File 'app/models/catalog_item.rb', line 407

scope :by_skus, ->(*skus) { with_item.where(items: { sku: [skus].flatten.uniq.compact }) }

.by_upcsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are by upcs. Active Record Scope

Returns:

See Also:



408
# File 'app/models/catalog_item.rb', line 408

scope :by_upcs, ->(*upcs) { with_item.where(items: { upc: [upcs].flatten.uniq.compact }) }

.calculate_discounted_price(price_before_discount, discount, max_discount = 1.0) ⇒ Object

Calculate discounted price.

Parameters:

  • price_before_discount (Object)

    the price before discount

  • discount (Object)

    the discount

  • max_discount (Object) (defaults to: 1.0)

    the max discount



597
598
599
600
601
# File 'app/models/catalog_item.rb', line 597

def self.calculate_discounted_price(price_before_discount, discount, max_discount = 1.0)
  price_before_discount ||= 0.0
  discount_factor = 1.0 - [max_discount, discount].min
  (price_before_discount * discount_factor).round(2)
end

.catalog_items_for_selectObject

Catalog items for select.



581
582
583
# File 'app/models/catalog_item.rb', line 581

def self.catalog_items_for_select
  CatalogItem.includes([:catalog, { store_item: :item }]).order('catalogs.name,items.sku').map { |ci| ["#{ci.sku} - #{ci.name.first(30)} - #{ci.catalog.name}", ci.id] }
end

.complimentary_ofActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are complimentary of. Active Record Scope

Returns:

See Also:



400
# File 'app/models/catalog_item.rb', line 400

scope :complimentary_of, ->(pl) { with_product_lines.where('product_lines.id' => pl.full_complimentary_product_line_ids) }

.controlsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are controls. Active Record Scope

Returns:

See Also:



422
# File 'app/models/catalog_item.rb', line 422

scope :controls, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_CONTROLS)) }

.coupons_for_future_promo_selectObject

Coupons for future promo select.



612
613
614
615
616
617
# File 'app/models/catalog_item.rb', line 612

def self.coupons_for_future_promo_select
  coupons = []
  coupons += Coupon.active_and_future.tier1.where(calculation_type_goods: 'MX')
  coupons += Coupon.active_and_future.tier3
  coupons.uniq.map { |c| [c.to_s, c.id] }.sort
end

.coupons_for_promo_selectObject

Coupons for promo select.



604
605
606
607
608
609
# File 'app/models/catalog_item.rb', line 604

def self.coupons_for_promo_select
  coupons = []
  coupons += Coupon.active.tier1.where(calculation_type_goods: 'MX')
  coupons += Coupon.active.tier3
  coupons.uniq.map { |c| [c.code, c.id] }.sort
end

.discontinuedActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are discontinued. Active Record Scope

Returns:

See Also:



404
# File 'app/models/catalog_item.rb', line 404

scope :discontinued, -> { where(state: %w[discontinued]) }

.edi_delta_feeds_for_partner_inventory_adviceActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are edi delta feeds for partner inventory advice. Active Record Scope

Returns:

See Also:



459
460
461
462
463
464
465
466
467
468
469
# File 'app/models/catalog_item.rb', line 459

scope :edi_delta_feeds_for_partner_inventory_advice, ->(partner) {
  # Here we want to a delta query based on store_items.last_inventory_updated_at
  where(<<~SQL.squish, partner: partner)
    store_items.last_inventory_updated_at IS NULL
    OR (catalog_items.last_inventory_advice_sent_json ->> :partner) IS NULL
    OR (
      store_items.last_inventory_updated_at::timestamptz >=
        (catalog_items.last_inventory_advice_sent_json ->> :partner)::timestamptz
    )
  SQL
}

.edi_delta_feeds_for_partner_price_adviceActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are edi delta feeds for partner price advice. Active Record Scope

Returns:

See Also:



470
471
472
473
474
475
476
477
478
479
480
# File 'app/models/catalog_item.rb', line 470

scope :edi_delta_feeds_for_partner_price_advice, ->(partner) {
  # Here we want to a delta query based on catalog_items.price_updated_at
  where(<<~SQL.squish, partner: partner)
    catalog_items.price_updated_at IS NULL
    OR (catalog_items.last_price_advice_sent_json ->> :partner) IS NULL
    OR (
      catalog_items.price_updated_at::timestamptz >=
        (catalog_items.last_price_advice_sent_json ->> :partner)::timestamptz
    )
  SQL
}

.for_edi_feedsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for edi feeds. Active Record Scope

Returns:

See Also:



451
# File 'app/models/catalog_item.rb', line 451

scope :for_edi_feeds, -> { available_for_edi_feeds.with_item.ordered_by_sku }

.for_edi_feeds_by_statesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for edi feeds by states. Active Record Scope

Returns:

See Also:



458
# File 'app/models/catalog_item.rb', line 458

scope :for_edi_feeds_by_states, ->(states) { available_for_edi_feeds_by_states(states).with_item.ordered_by_sku }

.for_google_feedActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for google feed. Active Record Scope

Returns:

See Also:



438
# File 'app/models/catalog_item.rb', line 438

scope :for_google_feed, -> { for_online_catalog }

.for_online_catalogActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for online catalog. Active Record Scope

Returns:

See Also:



434
435
436
437
# File 'app/models/catalog_item.rb', line 434

scope :for_online_catalog, -> {
  public_catalog_items
    .merge(Item.goods.non_publications)
}

.for_online_catalog_or_non_web_accessible_with_successor_itemActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for online catalog or non web accessible with successor item. Active Record Scope

Returns:

See Also:



442
443
444
445
446
447
448
# File 'app/models/catalog_item.rb', line 442

scope :for_online_catalog_or_non_web_accessible_with_successor_item, -> {
  ids = for_online_catalog.ids
  successor_item_ids = non_web_accessible.has_successor_item.pluck(Arel.sql('distinct items.successor_item_id'))
  successor_item_skus = Item.active.where(id: successor_item_ids).pluck(:sku)
  successor_item_cis = by_skus(successor_item_skus).where(state: 'active').ids
  where(catalog_items: { id: (ids + successor_item_cis).uniq })
}

.for_orchestrator_keysActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for orchestrator keys. Active Record Scope

Returns:

See Also:



452
# File 'app/models/catalog_item.rb', line 452

scope :for_orchestrator_keys, ->(orchestrator_keys) { where(state: ORCHESTRATOR_STATES).with_item.ordered_by_sku.joins(:catalog).merge(Catalog.where(orchestrator_key: orchestrator_keys)) }

.for_product_category_pathActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for product category path. Active Record Scope

Returns:

See Also:



417
418
419
# File 'app/models/catalog_item.rb', line 417

scope :for_product_category_path, ->(slug_path) {
  slug_path.blank? ? none : with_item.where(Item[:pc_path_slugs].ltree_descendant(slug_path))
}

.for_product_line_pathActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for product line path. Active Record Scope

Returns:

See Also:



414
415
416
# File 'app/models/catalog_item.rb', line 414

scope :for_product_line_path, ->(slug_path) {
  slug_path.blank? ? none : with_item.where(Item[:primary_pl_path_slugs].ltree_descendant(slug_path))
}

.for_where_to_buy_listActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are for where to buy list. Active Record Scope

Returns:

See Also:



495
496
497
# File 'app/models/catalog_item.rb', line 495

scope :for_where_to_buy_list, ->(parent_catalog_id) {
  joins(:catalog).where(state: 'active').where.not(catalog_id: [1, 2, 74]).where.not(url: nil).where(Catalog[:parent_catalog_id].eq(parent_catalog_id)).order(:position, Catalog[:name]).pluck(Arel.sql('COALESCE(catalogs.public_name, catalogs.name), catalog_items.url'))
}

.has_successor_itemActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are has successor item. Active Record Scope

Returns:

See Also:



441
# File 'app/models/catalog_item.rb', line 441

scope :has_successor_item, -> { with_item.where.not(items: { successor_item_id: nil }) }

.heating_elementsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are heating elements. Active Record Scope

Returns:

See Also:



421
# File 'app/models/catalog_item.rb', line 421

scope :heating_elements, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_HEATING_ELEMENTS)) }

.hidden_from_catalogActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are hidden from catalog. Active Record Scope

Returns:

See Also:



481
# File 'app/models/catalog_item.rb', line 481

scope :hidden_from_catalog, -> { where(state: HIDDEN_STATES) }

.in_stockActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are in stock. Active Record Scope

Returns:

See Also:



485
# File 'app/models/catalog_item.rb', line 485

scope :in_stock, -> { with_item.where('store_items.qty_available - COALESCE(catalog_items.reserve_stock, 0) > 0 OR items.always_available_online IS TRUE') }

.in_stock_with_alternate_warehouse_store_itemsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are in stock with alternate warehouse store items. Active Record Scope

Returns:

See Also:



486
487
488
489
490
491
# File 'app/models/catalog_item.rb', line 486

scope :in_stock_with_alternate_warehouse_store_items, -> {
  with_item.where("(store_items.qty_available +
                      (select round(coalesce(sum(si.qty_available),0),0)
                      from store_items si inner join stores s on si.store_id = s.id and s.owner = 'warmlyyours'
                      where si.store_id <> store_items.store_id and si.item_id = store_items.item_id and si.location = 'AVAILABLE')) - COALESCE(catalog_items.reserve_stock, 0) > 0 OR items.always_available_online is true")
}

.inactiveActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are inactive. Active Record Scope

Returns:

See Also:



402
# File 'app/models/catalog_item.rb', line 402

scope :inactive, -> { where.not(state: %w[active active_hidden]) }

.install_kitsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are install kits. Active Record Scope

Returns:

See Also:



431
# File 'app/models/catalog_item.rb', line 431

scope :install_kits, -> { for_product_line_path(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_INSTALLATION_KITS).merge(accessories) }

.insulationsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are insulations. Active Record Scope

Returns:

See Also:



429
# File 'app/models/catalog_item.rb', line 429

scope :insulations, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_ACCESSORIES_INSULATIONS)) }

.integration_kitsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are integration kits. Active Record Scope

Returns:

See Also:



427
# File 'app/models/catalog_item.rb', line 427

scope :integration_kits, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_POWER)).where(Item[:primary_pl_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING_CONTROL_INTEGRATION)) }

.last_custom_mat_catalog_item(catalog_id, volts) ⇒ Object

Last custom mat catalog item.

Parameters:

  • catalog_id (Integer)

    the catalog id

  • volts (Object)

    the volts



1339
1340
1341
1342
1343
1344
1345
1346
# File 'app/models/catalog_item.rb', line 1339

def self.last_custom_mat_catalog_item(catalog_id, volts)
  for_product_category_path(LtreePaths::PC_HEATING_ELEMENTS)
    .for_product_line_path(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CUSTOM_MAT)
    .where(catalog_id:)
    .where.not(items: { supplier_item_id: nil })
    .where('items.rendered_product_specifications @> ?', { voltage: { raw: volts } }.to_json)
    .order('items.created_at DESC').first
end

.main_catalogsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are main catalogs. Active Record Scope

Returns:

See Also:



449
# File 'app/models/catalog_item.rb', line 449

scope :main_catalogs, -> { where(catalog_id: Catalog.main_catalog_ids) }

.non_publicationsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are non publications. Active Record Scope

Returns:

See Also:



395
# File 'app/models/catalog_item.rb', line 395

scope :non_publications, -> { with_item.merge(Item.goods.non_publications) }

.non_web_accessibleActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are non web accessible. Active Record Scope

Returns:

See Also:



439
# File 'app/models/catalog_item.rb', line 439

scope :non_web_accessible, -> { main_catalogs.with_item.merge(Item.goods.non_publications).where('(items.is_discontinued IS TRUE) OR (store_items.is_discontinued IS TRUE) OR (catalog_items.state IN (?))', HIDDEN_STATES) }

.not_discontinuedActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are not discontinued. Active Record Scope

Returns:

See Also:



403
# File 'app/models/catalog_item.rb', line 403

scope :not_discontinued, -> { where.not(state: %w[discontinued]) }

.not_hidden_from_catalogActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are not hidden from catalog. Active Record Scope

Returns:

See Also:



482
# File 'app/models/catalog_item.rb', line 482

scope :not_hidden_from_catalog, -> { where.not(state: HIDDEN_STATES) }

.not_pending_onboardingActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are not pending onboarding. Active Record Scope

Returns:

See Also:



405
# File 'app/models/catalog_item.rb', line 405

scope :not_pending_onboarding, -> { where.not(state: %w[pending_onboarding]) }

.order_by_specActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are order by spec. Active Record Scope

Returns:

See Also:



494
# File 'app/models/catalog_item.rb', line 494

scope :order_by_spec, ->(spec, datatype = 'varchar', direction = 'asc') { joins(store_item: :item).merge(Item.order_by_spec(spec, datatype, direction)) }

.ordered_by_skuActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are ordered by sku. Active Record Scope

Returns:

See Also:



397
# File 'app/models/catalog_item.rb', line 397

scope :ordered_by_sku, -> { with_item.order(Item[:sku]) }

.out_of_stockActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are out of stock. Active Record Scope

Returns:

See Also:



492
# File 'app/models/catalog_item.rb', line 492

scope :out_of_stock, -> { with_item.where('store_items.qty_available - COALESCE(catalog_items.reserve_stock, 0) <= 0 AND items.always_available_online IS NOT TRUE') }

.pending_onboardingActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are pending onboarding. Active Record Scope

Returns:

See Also:



406
# File 'app/models/catalog_item.rb', line 406

scope :pending_onboarding, -> { where(state: %w[pending_onboarding]) }

.power_modulesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are power modules. Active Record Scope

Returns:

See Also:



426
# File 'app/models/catalog_item.rb', line 426

scope :power_modules, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_POWER_MODULES)) }

.primary_catalogs_firstActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are primary catalogs first. Active Record Scope

Returns:

See Also:



371
372
373
374
# File 'app/models/catalog_item.rb', line 371

scope :primary_catalogs_first, -> {
  in_order_of(:catalog_id, CatalogConstants::ALL_MAIN_CATALOG_IDS, filter: false)
    .order(Arel.sql('catalogs.name ASC'))
}

.primary_catalogs_first_then_by_state_and_nameActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are primary catalogs first then by state and name. Active Record Scope

Returns:

See Also:



375
376
377
378
379
# File 'app/models/catalog_item.rb', line 375

scope :primary_catalogs_first_then_by_state_and_name, -> {
  in_order_of(:catalog_id, CatalogConstants::ALL_MAIN_CATALOG_IDS, filter: false)
    .in_order_of(:state, %w[active pending_client_review pending_onboarding require_vendor_update pending_vendor_update pending_discontinue active_hidden discontinued invalid_catalog_item], filter: false)
    .order(Arel.sql('catalogs.name ASC'))
}

.probeableActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are probeable. Active Record Scope

Returns:

See Also:



388
389
390
391
# File 'app/models/catalog_item.rb', line 388

scope :probeable, lambda {
  where(skip_url_checks: false)
    .where(probe_backoff_until: [nil, ..Time.current])
}

.public_catalog_itemsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are public catalog items. Active Record Scope

Returns:

See Also:



409
# File 'app/models/catalog_item.rb', line 409

scope :public_catalog_items, -> { with_item.merge(StoreItem.active).active.not_hidden_from_catalog }

.relay_panelsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are relay panels. Active Record Scope

Returns:

See Also:



424
# File 'app/models/catalog_item.rb', line 424

scope :relay_panels, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_POWER_RELAY_PANELS)) }

.sensorsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are sensors. Active Record Scope

Returns:

See Also:



425
# File 'app/models/catalog_item.rb', line 425

scope :sensors, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_SENSORS)) }

.spare_partsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are spare parts. Active Record Scope

Returns:

See Also:



432
# File 'app/models/catalog_item.rb', line 432

scope :spare_parts, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_SPARE_PARTS)) }

.state_options_for_select(show_all_states = false) ⇒ Array<Array>

Returns [label, id] pairs for select dropdowns.

Parameters:

  • show_all_states (Object) (defaults to: false)

    the show all states

Returns:

  • (Array<Array>)

    [label, id] pairs for select dropdowns



587
588
589
590
591
# File 'app/models/catalog_item.rb', line 587

def self.state_options_for_select(show_all_states = false)
  state_syms = CatalogItem.state_machines[:state].states.map { |s| s.name.to_sym }
  state_syms -= [:discontinued] unless show_all_states
  state_syms.map { |s| [s.to_s.humanize, s.to_s] }.sort # discontinued is only for Heatwave bg job
end

.thermostatsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are thermostats. Active Record Scope

Returns:

See Also:



423
# File 'app/models/catalog_item.rb', line 423

scope :thermostats, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_CONTROLS_THERMOSTATS)) }

.towel_warmersActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are towel warmers. Active Record Scope

Returns:

See Also:



430
# File 'app/models/catalog_item.rb', line 430

scope :towel_warmers, -> { with_item.where(Item[:pc_path_slugs].ltree_descendant(LtreePaths::PC_TOWEL_WARMERS)) }

.web_accessibleActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are web accessible. Active Record Scope

Returns:

See Also:



440
# File 'app/models/catalog_item.rb', line 440

scope :web_accessible, -> { main_catalogs.with_item.merge(Item.goods.non_publications).where('(items.is_discontinued IS FALSE) AND (store_items.is_discontinued IS FALSE) AND NOT(catalog_items.state IN (?))', HIDDEN_STATES) }

.with_itemActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with item. Active Record Scope

Returns:

See Also:



393
# File 'app/models/catalog_item.rb', line 393

scope :with_item, -> { joins(store_item: :item) }

.with_item_and_classificationsActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with item and classifications. Active Record Scope

Returns:

See Also:



394
# File 'app/models/catalog_item.rb', line 394

scope :with_item_and_classifications, -> { joins(store_item: { item: %i[primary_product_line product_category] }).includes(store_item: { item: %i[primary_product_line product_category] }) }

.with_item_condition_newActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with item condition new. Active Record Scope

Returns:

See Also:



396
# File 'app/models/catalog_item.rb', line 396

scope :with_item_condition_new, -> { with_item.merge(Item.active.condition_new) }

.with_product_categoryActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with product category. Active Record Scope

Returns:

See Also:



399
# File 'app/models/catalog_item.rb', line 399

scope :with_product_category, -> { includes(store_item: { item: :product_category }).references(store_item: { item: :product_category }) }

.with_product_linesActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with product lines. Active Record Scope

Returns:

See Also:



398
# File 'app/models/catalog_item.rb', line 398

scope :with_product_lines, -> { includes(store_item: { item: :product_lines }).references(store_item: { item: :product_lines }) }

.with_product_specificationActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with product specification. Active Record Scope

Returns:

See Also:



493
# File 'app/models/catalog_item.rb', line 493

scope :with_product_specification, ->(token, value, grouping = nil) { with_item.merge(Item.with_product_specification(token, value, grouping)) }

.with_third_party_numberActiveRecord::Relation<CatalogItem>

A relation of CatalogItems that are with third party number. Active Record Scope

Returns:

See Also:



433
# File 'app/models/catalog_item.rb', line 433

scope :with_third_party_number, -> { where.not(third_party_part_number: [nil, '']) }

Instance Method Details

#all_amazon_image_profilesObject

Alias for
to: :item#all_amazon_image_profiles

Returns:

  • (Object)
           to: :item#all_amazon_image_profiles
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#alternate_warehouse_stock_fraction_overridden?Boolean

Whether this item overrides the catalog's alternate-warehouse share.

Returns:

  • (Boolean)


1100
1101
1102
# File 'app/models/catalog_item.rb', line 1100

def alternate_warehouse_stock_fraction_overridden?
  alternate_warehouse_stock_fraction.present?
end

#alternate_warehouse_store_itemsObject

Alternate warehouse store items.



1256
1257
1258
# File 'app/models/catalog_item.rb', line 1256

def alternate_warehouse_store_items
  StoreItem.where(item_id: store_item.item_id).where.not(store_id: store_item.store_id).available.joins(:store).merge(Store.warmlyyours_warehouses)
end

#amazon_browse_nodesActiveRecord::Relation<AmazonBrowseNode>

Returns the associated amazon browse nodes.

Returns:

  • (ActiveRecord::Relation<AmazonBrowseNode>)

    the associated amazon browse nodes



259
# File 'app/models/catalog_item.rb', line 259

has_and_belongs_to_many :amazon_browse_nodes

#amazon_catalog_item_flagsActiveRecord::Relation<AmazonCatalogItemFlag>

Amazon catalog item flags.

Returns:

See Also:



247
# File 'app/models/catalog_item.rb', line 247

has_many    :amazon_catalog_item_flags, dependent: :destroy

#amazon_marketplaceAmazonMarketplace

Amazon marketplace.



237
# File 'app/models/catalog_item.rb', line 237

has_one     :amazon_marketplace, through: :catalog

#amazon_sales_rank_data_pointsActiveRecord::Relation<AmazonSalesRankDataPoint>

Amazon sales rank data points.

Returns:

See Also:



249
# File 'app/models/catalog_item.rb', line 249

has_many    :amazon_sales_rank_data_points, dependent: :delete_all

#amazon_variant_catalog_itemsObject

Amazon variant catalog items.



1441
1442
1443
1444
1445
1446
1447
# File 'app/models/catalog_item.rb', line 1441

def amazon_variant_catalog_items
  # Siblings sharing THIS catalog_item's per-marketplace Amazon parent
  # listing (VariantGroup amazon row), within the same catalog.
  return CatalogItem.none unless variant_group_id

  catalog_items_in_same_catalog.where(variant_group_id:).includes(:item)
end

#amount_currency_symbolObject

Amount currency symbol.



939
940
941
# File 'app/models/catalog_item.rb', line 939

def amount_currency_symbol
  catalog.currency_symbol
end

#available_in_edi_feed?Boolean

Returns whether the record available in edi feed.

Returns:

  • (Boolean)

    whether the record available in edi feed



840
841
842
# File 'app/models/catalog_item.rb', line 840

def available_in_edi_feed?
  EDI_FEED_STATUSES.include?(state)
end

#available_storesObject

Available stores.



825
826
827
# File 'app/models/catalog_item.rb', line 825

def available_stores
  Store.where(company_id: catalog.company_id).order(:name)
end

#bom_priceObject

Bom price.



929
930
931
# File 'app/models/catalog_item.rb', line 929

def bom_price
  retail_price || amount
end

#calculate_minimum_price_for_margin(target_margin) ⇒ Object

Provide a target margin below a 100

Parameters:

  • target_margin (Object)

    the target margin

Raises:

  • (ArgumentError)


671
672
673
674
675
676
677
678
# File 'app/models/catalog_item.rb', line 671

def calculate_minimum_price_for_margin(target_margin)
  return unless unit_cogs&.positive? && target_margin # skip non-positive unit_cogs

  raise ArgumentError, 'Target margin must be less than 100%' if target_margin >= 100

  minimum_price = unit_cogs / (1 - (target_margin / 100.0))
  minimum_price.round(2) # Round to 2 decimal places for currency formatting
end

#calculate_profit_margin(price) ⇒ Object

Calculate profit margin.

Parameters:

  • price (Object)

    the price



662
663
664
665
666
667
# File 'app/models/catalog_item.rb', line 662

def calculate_profit_margin(price)
  return unless unit_cogs && price

  profit = (price - unit_cogs)
  ((profit / price) * 100).round(2)
end

#calculate_sale_price(percentage_off, include_vat: false, from_msrp: false) ⇒ Object

Calculates the sale price based on a percentage off (0-100), specify also if price with VAT should be used as the starting amount

Parameters:

  • percentage_off (Object)

    the percentage off

  • include_vat (Object) (defaults to: false)

    the include vat

  • from_msrp (Object) (defaults to: false)

    the from msrp



1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
# File 'app/models/catalog_item.rb', line 1489

def calculate_sale_price(percentage_off, include_vat: false, from_msrp: false)
  return nil unless percentage_off.present? && percentage_off.positive?
  raise 'sale price percentage off must be between 0 and 100' unless (1..100).cover?(percentage_off)

  starting_amount = if from_msrp
                      if include_vat
                        msrp_with_vat
                      else
                        msrp
                      end
                    elsif include_vat
                      price_with_vat
                    else
                      amount
                    end
  if percentage_off.nil? && starting_amount.nil?
    msg = "calculate_sale_price: Catalog Item ID #{id} cannot have sale price calculated, percentage off: #{percentage_off}, starting_amount: #{starting_amount}"
    ErrorReporting.warning(msg)
    logger.warn msg
    return nil
  end
  calculated_sale_price = (starting_amount * (1.0 - (percentage_off.to_f / 100))).round(2)
  calculated_sale_price /= (1.0 + tax_rate) if include_vat && tax_rate
  calculated_sale_price.round(2)
end

#calculated_updated_price_from_parentObject

Calculated updated price from parent.



1401
1402
1403
1404
1405
1406
1407
1408
1409
# File 'app/models/catalog_item.rb', line 1401

def calculated_updated_price_from_parent
  return unless (parent_amount = parent_catalog_item&.amount)

  # Determine discount to use
  discount = parent_catalog_discount || 0.0
  return unless discount.between?(0.0, 1.0)

  (parent_amount * (1.0 - discount)).round(2)
end

#catalogCatalog

Catalog.

Returns:

See Also:



220
# File 'app/models/catalog_item.rb', line 220

belongs_to  :catalog, touch: true, inverse_of: :catalog_items

#catalog_discount_humanObject

Catalog discount human.



1009
1010
1011
1012
1013
# File 'app/models/catalog_item.rb', line 1009

def catalog_discount_human
  return unless catalog.parent_catalog_discount

  "#{(catalog.parent_catalog_discount * 100).round(2)} %"
end

#catalog_items_in_same_catalog(starting_scope = nil) ⇒ Object

Catalog items in same catalog.

Parameters:

  • starting_scope (Object) (defaults to: nil)

    the starting scope



1430
1431
1432
1433
1434
1435
1436
1437
1438
# File 'app/models/catalog_item.rb', line 1430

def catalog_items_in_same_catalog(starting_scope = nil)
  starting_scope ||= CatalogItem.active
  CatalogItem.includes(:catalog)
             .where(catalog_id:)
             .with_item
             .with_product_lines
             .with_product_category
             .order(ProductCategory[:priority], CatalogItem[:amount])
end

#check_for_price_updateObject (protected)

Check for price update.



1665
1666
1667
1668
1669
1670
# File 'app/models/catalog_item.rb', line 1665

def check_for_price_update
  return unless amount_changed?

  self.price_updated_at = Time.current
  self.old_amount = amount_was
end

#check_kit_components_availableObject (protected)

Check kit components available.



1638
1639
1640
1641
1642
# File 'app/models/catalog_item.rb', line 1638

def check_kit_components_available
  return unless missing_kit_components?

  errors.add(:base, 'All kit components must be present in the target catalog')
end

#check_ok_to_destroyObject (protected)

Check ok to destroy.



1660
1661
1662
# File 'app/models/catalog_item.rb', line 1660

def check_ok_to_destroy
  errors.add(:base, 'Cannot destroy because there are dependent line_items, please discontinue instead') unless ok_to_destroy?
end

#check_store_item_is_availableObject (protected)

Check store item is available.



1633
1634
1635
# File 'app/models/catalog_item.rb', line 1633

def check_store_item_is_available
  errors.add(:store_item_id, 'Store Item must be part of an available location') if store_item && !store_item.available_location?
end

#check_upcObject (protected)

Check upc.



1704
1705
1706
1707
1708
1709
1710
# File 'app/models/catalog_item.rb', line 1704

def check_upc
  # An active public item needs a upc
  return unless upc_required? && item.upc.blank?

  item.update_upc = true
  item.save
end

#complete_stateObject

Complete state.



924
925
926
# File 'app/models/catalog_item.rb', line 924

def complete_state
  "#{human_state_name} - #{HIDDEN_STATES.include?(state) ? 'not public' : 'public'}"
end

#content_locales_to_renderObject

Content locales to render.



1354
1355
1356
# File 'app/models/catalog_item.rb', line 1354

def content_locales_to_render
  (Mobility.available_locales & ([I18n.default_locale] + (catalog.locales || []))).uniq.sort
end

#couponCoupon

Coupon.

Returns:

See Also:



222
# File 'app/models/catalog_item.rb', line 222

belongs_to  :coupon, optional: true

#create_component_catalog_itemsObject

Create component catalog items.



681
682
683
684
685
686
687
688
# File 'app/models/catalog_item.rb', line 681

def create_component_catalog_items
  store_id = store_item.store_id
  location = store_item.location
  item.kit_components.each do |component_item|
    create_single_component_in_catalog_item(store_id, location, component_item)
  end
  true
end

#create_single_component_in_catalog_item(store_id, location, component_item) ⇒ Object

Create single component in catalog item.

Parameters:

  • store_id (Integer)

    the store id

  • location (Object)

    the location

  • component_item (Object)

    the component item



694
695
696
697
698
699
700
701
702
703
704
# File 'app/models/catalog_item.rb', line 694

def create_single_component_in_catalog_item(store_id, location, component_item)
  si = StoreItem.where(item_id: component_item.id, store_id:, location:).first
  si = StoreItem.create!(item_id: component_item.id, store_id:, location:, qty_on_hand: 0, qty_committed: 0, handling_charge: 0, unit_cogs: 0) if si.nil?
  existing = CatalogItem.where(catalog_id:, store_item_id: si.id).first
  return existing if existing.present? # Catalog Item already exists

  parent_catalog_item = parent_catalog.nil? ? nil : CatalogItem.where(catalog: parent_catalog, store_item: si).first
  price = parent_catalog_item&.amount
  price *= (1 - parent_catalog_discount) if price && parent_catalog_discount
  CatalogItem.create!(catalog_id:, store_item_id: si.id, amount: price || 0, max_discount: 100, state: 'active_hidden')
end

#currencyObject

Alias for Catalog#currency

Returns:

  • (Object)

    Catalog#currency

See Also:



265
266
267
268
269
# File 'app/models/catalog_item.rb', line 265

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#currency_symbolObject

Alias for Catalog#currency_symbol

Returns:

  • (Object)

    Catalog#currency_symbol

See Also:



265
266
267
268
269
# File 'app/models/catalog_item.rb', line 265

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#deep_dupObject

Returns a deep-copied, unsaved duplicate of the record.

Returns:

  • (Object)

    a deep-copied, unsaved duplicate of the record



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'app/models/catalog_item.rb', line 162

def deep_dup
  deep_clone(except: %i[third_party_part_number
                        third_party_promo_part_number
                        third_party_sku
                        new_price
                        new_price_effective_date
                        coupon_id
                        sale_price
                        new_coupon_id
                        new_sale_price
                        price_updated_at
                        parent_sku
                        clearance]) do |_original, copy|
    # clearance is NOT NULL with a false default; reset the clone to false
    # rather than nil, which would violate the constraint on save.
    copy.clearance = false
  end
end

#dependent_catalog_items(filtered_by_catalog_id = nil) ⇒ Object

Find all similar catalog items in the child catalogs, optionally filter only
to one specific catalog

Parameters:

  • filtered_by_catalog_id (Integer) (defaults to: nil)

    the filtered by catalog id



1041
1042
1043
1044
1045
# File 'app/models/catalog_item.rb', line 1041

def dependent_catalog_items(filtered_by_catalog_id = nil)
  catalog.children.select { |c| filtered_by_catalog_id.nil? || c.id == filtered_by_catalog_id }.map do |child_cat|
    child_cat.catalog_items.where(store_item_id:)
  end.flatten.uniq
end

#discounted_price(customer = nil, target = nil) ⇒ Object

Discounted price.

Parameters:

  • customer (Customer) (defaults to: nil)

    the customer

  • target (Object) (defaults to: nil)

    the target



1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
# File 'app/models/catalog_item.rb', line 1323

def discounted_price(customer = nil, target = nil)
  discount = (begin
    target.pricing_program_discount_factor
  rescue StandardError
    nil
  end) || (begin
    customer.pricing_program_discount_factor
  rescue StandardError
    nil
  end)
  CatalogItem.calculate_discounted_price(amount, discount, 1.0)
end

#diverging_current_price_vs_proposed_price?Boolean

Returns whether the record diverging current price vs proposed price.

Returns:

  • (Boolean)

    whether the record diverging current price vs proposed price



620
621
622
623
624
625
# File 'app/models/catalog_item.rb', line 620

def diverging_current_price_vs_proposed_price?
  return false unless parent_catalog_discount
  return false unless (cup = calculated_updated_price_from_parent)

  amount != cup
end

#edi_communication_logsActiveRecord::Relation<EdiCommunicationLog>

Edi communication logs.

Returns:

See Also:



245
# File 'app/models/catalog_item.rb', line 245

has_many    :edi_communication_logs, through: :edi_documents

#edi_customer_ids_for_catalogObject

Returns the list of EDI customer IDs for this catalog, cached for reuse



860
861
862
# File 'app/models/catalog_item.rb', line 860

def edi_customer_ids_for_catalog
  @edi_customer_ids_for_catalog ||= catalog.customers.where(id: Edi::BaseOrchestrator.customer_ids_edi_enabled).ids
end

#edi_documentsActiveRecord::Relation<EdiDocument>

Edi documents.

Returns:

See Also:



243
# File 'app/models/catalog_item.rb', line 243

has_many    :edi_documents, dependent: :destroy

#edi_offer_new_sale_priceObject

Edi offer new sale price.



818
819
820
821
822
# File 'app/models/catalog_item.rb', line 818

def edi_offer_new_sale_price
  return new_sale_price_with_vat if tax_rate.present?

  new_sale_price.to_f.positive? ? new_sale_price : nil
end

#edi_offer_priceObject

EDI offer prices. European Mirakl partners carry a catalog tax_rate and
list VAT-inclusive, so the *_with_vat helpers apply. Tax-exclusive
markets (e.g. Best Buy Canada — CAD, no VAT) have a nil tax_rate, which
makes those helpers return nil; for those, fall back to the plain pre-tax
price/sale so the offer feed isn't sent priceless.



806
807
808
# File 'app/models/catalog_item.rb', line 806

def edi_offer_price
  tax_rate.present? ? price_with_vat : amount
end

#edi_offer_sale_priceObject

Edi offer sale price.



811
812
813
814
815
# File 'app/models/catalog_item.rb', line 811

def edi_offer_sale_price
  return sale_price_with_vat if tax_rate.present?

  sale_price.to_f.positive? ? sale_price : nil
end

#edi_orchestrators_for_catalogObject

Returns orchestrators for EDI customers in this catalog, cached for reuse
Uses the class-level orchestrator cache for fast lookups



866
867
868
869
870
# File 'app/models/catalog_item.rb', line 866

def edi_orchestrators_for_catalog
  @edi_orchestrators_for_catalog ||= edi_customer_ids_for_catalog.filter_map do |customer_id|
    Edi::BaseOrchestrator.orchestrator_for_customer_id(customer_id)
  end.uniq(&:partner)
end

#edi_partner_skuObject

Edi partner sku.



714
715
716
# File 'app/models/catalog_item.rb', line 714

def edi_partner_sku
  third_party_part_number.presence || third_party_sku.presence || sku
end

#effective_alternate_warehouse_stock_fractionFloat

Resolves the alternate-warehouse share reported externally, as a decimal.

Logic Details

For EXTERNAL reporting only (feeds, website, EDI) — never internal CRM
figures, which use #real_stock. Resolution is most-specific-first: this
item's nullable integer-percent override, then the catalog's channel-wide
percentage. New catalogs explicitly persist 0% when it is not configured.

Returns:

  • (Float)

    resolved reporting share, such as 0.25



1094
1095
1096
# File 'app/models/catalog_item.rb', line 1094

def effective_alternate_warehouse_stock_fraction
  (alternate_warehouse_stock_fraction || catalog.alternate_warehouse_stock_fraction) / 100.0
end

#effective_seo_description(char_limit: nil) ⇒ Object

Effective seo description.

Parameters:

  • char_limit (Object) (defaults to: nil)

    the char limit



917
918
919
920
921
# File 'app/models/catalog_item.rb', line 917

def effective_seo_description(char_limit: nil)
  d = seo_description.presence || item.effective_seo_description(char_limit:) || +''
  d << " (#{item.sku})" unless Regexp.new(item.sku).match(d)
  d
end

#european?Boolean

Returns whether the record european.

Returns:

  • (Boolean)

    whether the record european



1080
1081
1082
# File 'app/models/catalog_item.rb', line 1080

def european?
  catalog.country&.eu_country? || false
end

#exported_catalog_itemExportedCatalogItem

Exported catalog item.



233
# File 'app/models/catalog_item.rb', line 233

has_one     :exported_catalog_item, dependent: :delete

#get_successor_online_catalog_itemObject

Get successor online catalog item.



1573
1574
1575
1576
# File 'app/models/catalog_item.rb', line 1573

def get_successor_online_catalog_item
  res = catalog.catalog_items.for_online_catalog.by_skus(item.successor_item.sku).first if !item_is_web_accessible? && item.successor_item.present?
  res
end

#in_google_feed?Boolean

Returns whether the record in google feed.

Returns:

  • (Boolean)

    whether the record in google feed



1619
1620
1621
# File 'app/models/catalog_item.rb', line 1619

def in_google_feed?
  self.class.for_google_feed.exists?(id: id)
end

#in_hide_from_feed_state?Boolean

Returns whether the record in hide from feed state.

Returns:

  • (Boolean)

    whether the record in hide from feed state



1455
1456
1457
# File 'app/models/catalog_item.rb', line 1455

def in_hide_from_feed_state?
  HIDDEN_STATES.include?(state)
end

#in_main_catalog?Boolean (protected)

Returns whether the record in main catalog.

Returns:

  • (Boolean)

    whether the record in main catalog



1713
1714
1715
# File 'app/models/catalog_item.rb', line 1713

def in_main_catalog?
  Catalog.main_catalog_ids.include?(catalog_id)
end

#inventory_message_enabled?Boolean

Returns whether the record inventory message enabled.

Returns:

  • (Boolean)

    whether the record inventory message enabled



845
846
847
# File 'app/models/catalog_item.rb', line 845

def inventory_message_enabled?
  inventory_message_processors.present?
end

#inventory_message_processorsObject

Retrieves all inventory message processors for edi customers carrying this item
This can be used to push inventory at once to all channels for one item



874
875
876
877
878
# File 'app/models/catalog_item.rb', line 874

def inventory_message_processors
  edi_orchestrators_for_catalog.filter_map do |o|
    o.inventory_message_processor if o.inventory_message_enabled? && o.respond_to?(:inventory_message_processor)
  end
end

#is_available_to_publicObject

Alias for
to: :item#is_available_to_public

Returns:

  • (Object)
           to: :item#is_available_to_public
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_cable_accessory?Object

Alias for
to: :item#is_cable_accessory?

Returns:

  • (Object)
           to: :item#is_cable_accessory?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_cable_fit_guide?Object

Alias for
to: :item#is_cable_fit_guide?

Returns:

  • (Object)
           to: :item#is_cable_fit_guide?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_circuit_check?Object

Alias for
to: :item#is_circuit_check?

Returns:

  • (Object)
           to: :item#is_circuit_check?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_control?Object

Alias for
to: :item#is_control?

Returns:

  • (Object)
           to: :item#is_control?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_discontinued?Boolean Also known as: is_discontinued

Returns whether the record is discontinued.

Returns:

  • (Boolean)

    whether the record is discontinued



1579
1580
1581
# File 'app/models/catalog_item.rb', line 1579

def is_discontinued?
  discontinued? || pending_discontinue?
end

#is_heating_element?Object

Alias for
to: :item#is_heating_element?

Returns:

  • (Object)
           to: :item#is_heating_element?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_membrane?Object

Alias for
to: :item#is_membrane?

Returns:

  • (Object)
           to: :item#is_membrane?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_publication?Object

Alias for
to: :item#is_publication?

Returns:

  • (Object)
           to: :item#is_publication?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_roughin_kit?Object

Alias for
to: :item#is_roughin_kit?

Returns:

  • (Object)
           to: :item#is_roughin_kit?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_snow_melt_plaque?Object

Alias for
to: :item#is_snow_melt_plaque?

Returns:

  • (Object)
           to: :item#is_snow_melt_plaque?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#is_underlayment?Object

Alias for
to: :item#is_underlayment?

Returns:

  • (Object)
           to: :item#is_underlayment?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#itemItem

Item.

Returns:

See Also:



231
# File 'app/models/catalog_item.rb', line 231

has_one     :item, through: :store_item, inverse_of: :catalog_items

#item_is_web_accessible?Boolean

Returns whether the record item is web accessible.

Returns:

  • (Boolean)

    whether the record item is web accessible



1566
1567
1568
1569
1570
# File 'app/models/catalog_item.rb', line 1566

def item_is_web_accessible?
  !in_hide_from_feed_state? &&
    !item&.is_discontinued &&
    !store_item&.is_discontinued
end

#line_itemsActiveRecord::Relation<LineItem>

Line items.

Returns:

See Also:



239
# File 'app/models/catalog_item.rb', line 239

has_many    :line_items, dependent: :restrict_with_error

#listing_issuesActiveRecord::Relation<ListingIssue>

Listing issues.

Returns:

See Also:



251
# File 'app/models/catalog_item.rb', line 251

has_many    :listing_issues, dependent: :destroy

#listing_message_enabled?Boolean

Returns whether the record listing message enabled.

Returns:

  • (Boolean)

    whether the record listing message enabled



855
856
857
# File 'app/models/catalog_item.rb', line 855

def listing_message_enabled?
  listing_message_processors.present?
end

#listing_message_processorsObject

Retrieves all listing message processors for edi customers carrying this item
This can be used to patch listing data at once to all channels for one item



892
893
894
895
896
# File 'app/models/catalog_item.rb', line 892

def listing_message_processors
  edi_orchestrators_for_catalog.filter_map do |o|
    o.listing_message_processor if o.respond_to?(:listing_message_processor) && o.listing_message_enabled?
  end
end

#map_differenceObject

Difference between retail price and MAP (negative = violation)



1032
1033
1034
1035
1036
# File 'app/models/catalog_item.rb', line 1032

def map_difference
  return unless retail_price.present? && map_price.present?

  (retail_price - map_price).round(2)
end

#map_priceObject

MAP (Minimum Advertised Price) based on catalog's map_percentage setting
Only meaningful for vendor catalogs where retailer controls pricing



1017
1018
1019
1020
1021
# File 'app/models/catalog_item.rb', line 1017

def map_price
  return if msrp.blank?

  (msrp * catalog.map_percentage).round(2)
end

#map_to_stores(store_ids) ⇒ Object

This method creates a link between a catalog item and additional stores
It is useful for amazon FBA where we keep track of the stock in amazon's warehouses
Simply pass an array an store id to map the catalog item to and it will do the rest
Note that this method never removes the primary store item for a catalog item

Parameters:

  • store_ids (Object)

    the store ids



747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
# File 'app/models/catalog_item.rb', line 747

def map_to_stores(store_ids)
  return if store_ids.blank?

  store_ids.uniq.each do |sid|
    s = Store.find(sid)
    # If store id already exist, nothing to do
    next if store_items.find { |si| si.store_id == s.id }

    # if not quick build
    if catalog.company_id != s.company_id
      errors.add(:base, "Store #{s.id} #{s.short_name} does not belong to same company as catalog item #{id}")
    elsif (si = StoreItem.where(store_id: s.id, item_id: item.id, location: 'AVAILABLE').first) # See if we already have this si
      # Then link it
      store_items << si
    else # Create a new one (includes implicit linking)
      store_items.create(store_id: s.id, item_id: item.id, qty_on_hand: 0, qty_committed: 0, unit_cogs: amount, location: 'AVAILABLE')
    end
  end
  # Any stores to remove?
  store_items.available.each do |si|
    next if store_item.store_id == si.store_id # The primary store is never removable
    next if si.store_id.in?(store_ids)

    # This store is not in the selected list, try to delete it
    errors.add(:base, "Store Item #{si.id} cannot be deleted due to the presence of stock") unless si.ok_to_destroy? && si.destroy
  end
  store_items
end

#map_violation?Boolean

Check if the retailer's current price is below MAP

Returns:

  • (Boolean)


1024
1025
1026
1027
1028
1029
# File 'app/models/catalog_item.rb', line 1024

def map_violation?
  return false unless catalog.retailer_type_vendor?
  return false unless retail_price.present? && map_price.present?

  retail_price < map_price
end

#missing_kit_components?Boolean

Returns whether the record missing kit components.

Returns:

  • (Boolean)

    whether the record missing kit components



1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
# File 'app/models/catalog_item.rb', line 1542

def missing_kit_components?
  return false unless item&.is_kit?

  kit_component_item_skus = item.kit_components.pluck(:sku)
  kit_components_count = kit_component_item_skus.size
  catalog_items = CatalogItem.joins(store_item: :item).where(catalog_id:, items: { sku: kit_component_item_skus }, store_items: { location: store_item.location })
  catalog_item_skus = catalog_items.pluck(:sku)
  catalog_items_count = catalog_item_skus.size
  if kit_components_count == catalog_items_count
    false
  else
    logger.error "Missing kit components in catalog #{catalog_id} for #{item.sku}: #{kit_component_item_skus - catalog_item_skus}"
    true
  end
end

#msrpObject

Msrp.



990
991
992
# File 'app/models/catalog_item.rb', line 990

def msrp
  root_catalog_item.try(:amount)
end

#msrp_with_vatObject

Msrp with vat.



995
996
997
998
999
# File 'app/models/catalog_item.rb', line 995

def msrp_with_vat
  return if tax_rate.blank?

  root_catalog_item.try(:price_with_vat)
end

#name(locale: nil) ⇒ String

Returns the name of the record.

Parameters:

  • locale (Object) (defaults to: nil)

    the locale

Returns:

  • (String)

    the name of the record



1361
1362
1363
1364
1365
1366
1367
1368
# File 'app/models/catalog_item.rb', line 1361

def name(locale: nil)
  I18n.with_locale(locale || I18n.locale) do
    n = third_party_name.presence
    n ||= item.amazon_title if amazon_catalog_item?
    n ||= item&.public_name
    n
  end
end

#new_couponCoupon

New coupon.

Returns:

See Also:



224
# File 'app/models/catalog_item.rb', line 224

belongs_to  :new_coupon, optional: true, class_name: 'Coupon'

#new_sale_price_with_vatObject

New sale price with vat.



793
794
795
796
797
798
799
# File 'app/models/catalog_item.rb', line 793

def new_sale_price_with_vat
  return if tax_rate.blank?

  res = ((tax_rate + 1) * (new_sale_price || 0.0)).round(2)
  res = nil if res == 0.0
  res
end

#next_available(use_store_item: nil, use_alternate_warehouse: false) ⇒ Object

Returns next available for internal CRM use (100% real stock)
discontinued and pending discontinue item will report nil for next available

Parameters:

  • use_store_item (Object) (defaults to: nil)

    the use store item

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse



1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
# File 'app/models/catalog_item.rb', line 1200

def next_available(use_store_item: nil, use_alternate_warehouse: false)
  return nil if discontinued? || pending_discontinue?

  use_store_item ||= store_item
  next_available_hsh_arr = [use_store_item.next_available]
  if use_alternate_warehouse
    # What's next available in the other warehouse? Include 100% (real stock) with 1 week delay
    alternate_warehouse_store_items.each do |alternate_si|
      alt_next_available_hsh = alternate_si.next_available
      if alt_next_available_hsh.present?
        alt_next_available_hsh = OpenStruct.new(next_available_qty: alt_next_available_hsh[:next_available_qty],
                                                next_available_date: (alt_next_available_hsh[:next_available_date] + 1.week)).freeze
      end
      next_available_hsh_arr << alt_next_available_hsh
    end
  end
  # here we discard all the nil entries and sort by next available date, returning the first entry, i.e. the next available
  next_available_entry_to_use = next_available_hsh_arr.compact.min_by { |h| h[:next_available_date] }
  return nil if next_available_entry_to_use.blank?

  next_available_entry_to_use
end

#next_available_by_warehouse(use_alternate_warehouse: false) ⇒ Object

Next available by warehouse.

Parameters:

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse



1284
1285
1286
1287
1288
# File 'app/models/catalog_item.rb', line 1284

def next_available_by_warehouse(use_alternate_warehouse: false)
  store_items_by_warehouse.to_h do |si|
    [si.store.name, next_available(use_store_item: si, use_alternate_warehouse:)]
  end
end

#next_available_by_warehouse_with_depth_limit(use_alternate_warehouse: false, max_depth: 10, current_depth: 0) ⇒ Object

Version with depth limit to prevent infinite recursion

Parameters:

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse

  • max_depth (Object) (defaults to: 10)

    the max depth

  • current_depth (Object) (defaults to: 0)

    the current depth



1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
# File 'app/models/catalog_item.rb', line 1294

def next_available_by_warehouse_with_depth_limit(use_alternate_warehouse: false, max_depth: 10, current_depth: 0)
  # Prevent infinite recursion
  if current_depth >= max_depth
    Rails.logger.warn "Maximum depth limit (#{max_depth}) reached for catalog item #{id} (#{item&.sku}). Possible circular reference in kit structure."
    return {}
  end

  store_items_by_warehouse.to_h do |si|
    [si.store.name, next_available_with_depth_limit(use_store_item: si, use_alternate_warehouse:, max_depth:, current_depth: current_depth + 1)]
  end
end

#next_available_with_depth_limit(use_store_item: nil, use_alternate_warehouse: false, max_depth: 10, current_depth: 0) ⇒ Object

Version with depth limit to prevent infinite recursion (100% real stock for internal use)

Parameters:

  • use_store_item (Object) (defaults to: nil)

    the use store item

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse

  • max_depth (Object) (defaults to: 10)

    the max depth

  • current_depth (Object) (defaults to: 0)

    the current depth



1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
# File 'app/models/catalog_item.rb', line 1228

def next_available_with_depth_limit(use_store_item: nil, use_alternate_warehouse: false, max_depth: 10, current_depth: 0)
  # Prevent infinite recursion
  if current_depth >= max_depth
    Rails.logger.warn "Maximum depth limit (#{max_depth}) reached for catalog item #{id} (#{item&.sku}). Possible circular reference in kit structure."
    return nil
  end

  use_store_item ||= store_item
  next_available_hsh_arr = [use_store_item.next_available]
  if use_alternate_warehouse
    # What's next available in the other warehouse? Include 100% (real stock) with 1 week delay
    alternate_warehouse_store_items.each do |alternate_si|
      alt_next_available_hsh = alternate_si.next_available_with_depth_limit(max_depth: max_depth, current_depth: current_depth + 1)
      if alt_next_available_hsh.present?
        alt_next_available_hsh = OpenStruct.new(next_available_qty: alt_next_available_hsh[:next_available_qty],
                                                next_available_date: (alt_next_available_hsh[:next_available_date] + 1.week)).freeze
      end
      next_available_hsh_arr << alt_next_available_hsh
    end
  end
  # here we discard all the nil entries and sort by next available date, returning the first entry, i.e. the next available
  next_available_entry_to_use = next_available_hsh_arr.compact.min_by { |h| h[:next_available_date] }
  return nil if next_available_entry_to_use.blank?

  next_available_entry_to_use
end

#notify_of_price_updateObject (protected)

Notify of price update.



1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
# File 'app/models/catalog_item.rb', line 1673

def notify_of_price_update
  Rails.configuration.event_store.publish(
    Events::PriceUpdated.new(data: {
      catalog_item_id: id,
      price_was: old_amount,
      price_now: amount
    }),
    stream_name: "CatalogItem-#{id}"
  )
end

#ok_to_destroy?Boolean

Returns whether the record ok to destroy.

Returns:

  • (Boolean)

    whether the record ok to destroy



1048
1049
1050
# File 'app/models/catalog_item.rb', line 1048

def ok_to_destroy?
  !line_items.exists?
end

#on_order(use_alternate_warehouse: false) ⇒ Object

On order.

Parameters:

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse



1276
1277
1278
1279
1280
# File 'app/models/catalog_item.rb', line 1276

def on_order(use_alternate_warehouse: false)
  store_items_by_warehouse.to_h do |si|
    [si.store.name, on_order_for_store_item(use_store_item: si, use_alternate_warehouse:)]
  end
end

#on_order_for_store_item(use_store_item: nil, use_alternate_warehouse: false) ⇒ Object

Returns on order quantities for internal CRM use (100% real stock)
discontinued and pending discontinue item will report 0 for on order

Parameters:

  • use_store_item (Object) (defaults to: nil)

    the use store item

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse



1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
# File 'app/models/catalog_item.rb', line 1180

def on_order_for_store_item(use_store_item: nil, use_alternate_warehouse: false)
  return 0 if discontinued? || pending_discontinue?

  use_store_item ||= store_item
  on_order_arr = use_store_item.on_order
  if use_alternate_warehouse
    # What's on order for the other warehouses? Include 100% (real stock) with 1 week delay
    alternate_warehouse_store_items.each do |alternate_si|
      alt_on_order_arr = alternate_si.on_order
      alt_on_order_arr.each { |h| h[:promised_delivery_date] = (h[:promised_delivery_date] + 1.week) }
      on_order_arr.concat(alt_on_order_arr)
    end
  end
  on_order_arr
end

#onboarded_by_order!Object

A retailer only transmits an order for a SKU it is actually selling, so an
inbound EDI order is the strongest evidence we ever get that a listing is
live — stronger than a price probe or a sitemap scan. Promote the item
rather than leaving it parked in onboarding, where it is invisible to the
inventory feed.

Returns false (no raise) when the transition can't run: any other state is
already order-legitimate, and a validation failure must not take down an
order batch we have otherwise accepted.

Origin: 22 Rona catalog items were moved to pending_onboarding on
2026-06-22 because their product ids were absent from Rona's sitemap. Rona
sold TRT120-KIT-OP-3.0x10 on 2026-08-10 and the order batch failed on an
"Unknown vendor sku" raise.



1604
1605
1606
# File 'app/models/catalog_item.rb', line 1604

def onboarded_by_order!
  pending_onboarding? && items_are_onboarded
end

#out_of_stock(use_threshhold = nil) ⇒ Object

Out of stock.

Parameters:

  • use_threshhold (Object) (defaults to: nil)

    the use threshhold



1054
1055
1056
1057
1058
1059
1060
# File 'app/models/catalog_item.rb', line 1054

def out_of_stock(use_threshhold = nil)
  # Use catalog_item.reserve_stock instead of legacy item threshold
  stock_reserved = use_threshhold || reserve_stock.to_i
  return true if store_item && (store_item.qty_available - stock_reserved) <= 0

  false
end

#oversize?Object

Alias for
to: :item#oversize?

Returns:

  • (Object)
           to: :item#oversize?
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#parent_catalogObject

Alias for Catalog#parent_catalog

Returns:

  • (Object)

    Catalog#parent_catalog

See Also:



265
266
267
268
269
# File 'app/models/catalog_item.rb', line 265

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#parent_catalog_currency_symbolObject

Parent catalog currency symbol.



960
961
962
963
964
# File 'app/models/catalog_item.rb', line 960

def parent_catalog_currency_symbol
  return unless parent_catalog

  parent_catalog.currency_symbol
end

#parent_catalog_discountObject

Parent catalog discount.



628
629
630
631
632
633
# File 'app/models/catalog_item.rb', line 628

def parent_catalog_discount
  return 0.0 unless catalog.parent_catalog
  return catalog.parent_catalog_discount_refurb if item.condition_refurbished?

  catalog.parent_catalog_discount
end

#parent_catalog_itemObject

If a parent_catalog exists as defined in the catalog of this catalog item
then find the matching catalog item by store item id which should be similar



968
969
970
971
972
# File 'app/models/catalog_item.rb', line 968

def parent_catalog_item
  return unless parent_catalog

  parent_catalog.catalog_items.where(store_item_id:).first
end

#parent_catalog_item_amountObject

Parent catalog item amount.



975
976
977
978
979
# File 'app/models/catalog_item.rb', line 975

def parent_catalog_item_amount
  return unless (pci = parent_catalog_item)

  pci.amount
end

#percentage_off_from_msrpObject

Percentage off from msrp.



1002
1003
1004
1005
1006
# File 'app/models/catalog_item.rb', line 1002

def percentage_off_from_msrp
  return unless msrp && effective_price

  ((1.0 - (effective_price / msrp)) * 100).round(2)
end

#prefix: :new_sale_price_effective_dateObject

Alias for New_coupon#effective_date

Returns:

  • (Object)

    New_coupon#prefix: :new_sale_price_effective_date

See Also:



315
316
317
318
319
# File 'app/models/catalog_item.rb', line 315

delegate :effective_date,
:expiration_date,
to: :new_coupon,
prefix: :new_sale_price,
allow_nil: true

#prefix: :new_sale_price_expiration_dateObject

Alias for New_coupon#expiration_date

Returns:

  • (Object)

    New_coupon#prefix: :new_sale_price_expiration_date

See Also:



315
316
317
318
319
# File 'app/models/catalog_item.rb', line 315

delegate :effective_date,
:expiration_date,
to: :new_coupon,
prefix: :new_sale_price,
allow_nil: true

#prefix: :sale_price_effective_dateObject

Alias for Coupon#effective_date

Returns:

  • (Object)

    Coupon#prefix: :sale_price_effective_date

See Also:



309
310
311
312
313
# File 'app/models/catalog_item.rb', line 309

delegate :effective_date,
:expiration_date,
to: :coupon,
prefix: :sale_price,
allow_nil: true

#prefix: :sale_price_expiration_dateObject

Alias for Coupon#expiration_date

Returns:

  • (Object)

    Coupon#prefix: :sale_price_expiration_date

See Also:



309
310
311
312
313
# File 'app/models/catalog_item.rb', line 309

delegate :effective_date,
:expiration_date,
to: :coupon,
prefix: :sale_price,
allow_nil: true

#price_message_enabled?Boolean

Returns whether the record price message enabled.

Returns:

  • (Boolean)

    whether the record price message enabled



850
851
852
# File 'app/models/catalog_item.rb', line 850

def price_message_enabled?
  price_message_processors.present?
end

#price_message_processorsObject

Retrieves all price message processors for edi customers carrying this item
This can be used to push price at once to all channels for one item



883
884
885
886
887
# File 'app/models/catalog_item.rb', line 883

def price_message_processors
  edi_orchestrators_for_catalog.filter_map do |o|
    o.price_message_processor if o.price_message_enabled? && o.respond_to?(:price_message_processor)
  end
end

#price_out_of_date?Boolean

This method determines if the catalog item is out of date compared to its parent catalog item, return true/false or nil if not applicable.

Returns:

  • (Boolean)


949
950
951
952
953
954
955
956
957
# File 'app/models/catalog_item.rb', line 949

def price_out_of_date?
  return false unless (pci = parent_catalog_item) # Not applicable if we don't have a parent catalog item
  return false unless (parent_price_updated_at = pci.price_updated_at) # Not applicable unless the parent catalog item has a price update
  return false unless catalog.price_sync_timed? # Don't care if not a timed catalog
  return true unless price_updated_at # Nil value will need to assume an out of date price if parent has one

  price_age_in_days = parent_price_updated_at - price_updated_at # should be an integer
  price_age_in_days >= catalog.price_sync_delay # Price is out of date when the catalog price delay is met or exceeded
end

#price_updated?Boolean (protected)

Returns whether the record price updated.

Returns:

  • (Boolean)

    whether the record price updated



1685
1686
1687
# File 'app/models/catalog_item.rb', line 1685

def price_updated?
  saved_changes.key?('amount')
end

#price_with_vatObject

Price with vat.



777
778
779
780
781
# File 'app/models/catalog_item.rb', line 777

def price_with_vat
  return if tax_rate.blank?

  ((tax_rate + 1) * (amount || 0.0)).round(2)
end

#primary_catalogObject

Primary catalog.



738
739
740
# File 'app/models/catalog_item.rb', line 738

def primary_catalog
  catalog_id.in?(CatalogConstants::ALL_MAIN_CATALOG_IDS)
end

#primary_imageObject

Alias for
to: :item#primary_image

Returns:

  • (Object)
           to: :item#primary_image
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#primary_product_lineObject

Alias for
to: :item#primary_product_line

Returns:

  • (Object)
           to: :item#primary_product_line
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#primary_product_line_idObject

Alias for
to: :item#primary_product_line_id

Returns:

  • (Object)
           to: :item#primary_product_line_id
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#probeable?Boolean

Whether the retailer-probe pipeline may call this listing right now. Row
equivalent of the probeable scope — see it for why the two gates stay
separate.

Returns:

  • (Boolean)


723
724
725
# File 'app/models/catalog_item.rb', line 723

def probeable?
  !skip_url_checks? && !probe_backoff_until&.future?
end

#product_categoryObject

Alias for
to: :item#product_category

Returns:

  • (Object)
           to: :item#product_category
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#product_category_visible_in_feed?Boolean

Returns whether the record product category visible in feed.

Returns:

  • (Boolean)

    whether the record product category visible in feed



1465
1466
1467
# File 'app/models/catalog_item.rb', line 1465

def product_category_visible_in_feed?
  product_category&.available_to_public
end

#product_linesObject

Alias for
to: :item#product_lines

Returns:

  • (Object)
           to: :item#product_lines
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#product_stock_statusObject

Product stock status.



1307
1308
1309
1310
1311
1312
1313
# File 'app/models/catalog_item.rb', line 1307

def product_stock_status
  if item.always_available_online? || !out_of_stock
    'InStock'
  else
    'OutOfStock'
  end
end

#profit_marginObject

Profit margin.



636
637
638
# File 'app/models/catalog_item.rb', line 636

def profit_margin
  calculate_profit_margin amount
end

#profit_margin_new_priceObject

Profit margin new price.



656
657
658
# File 'app/models/catalog_item.rb', line 656

def profit_margin_new_price
  calculate_profit_margin new_price
end

#profit_margin_new_sale_priceObject

Profit margin new sale price.



651
652
653
# File 'app/models/catalog_item.rb', line 651

def profit_margin_new_sale_price
  calculate_profit_margin new_sale_price
end

#profit_margin_retailer_requested_costObject

Profit margin retailer requested cost.



641
642
643
# File 'app/models/catalog_item.rb', line 641

def profit_margin_retailer_requested_cost
  calculate_profit_margin retailer_requested_cost
end

#profit_margin_sale_priceObject

Profit margin sale price.



646
647
648
# File 'app/models/catalog_item.rb', line 646

def profit_margin_sale_price
  calculate_profit_margin sale_price
end

#public_description_htmlObject

Alias for
to: :item#public_description_html

Returns:

  • (Object)
           to: :item#public_description_html
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#public_nameObject

Alias for
to: :item#public_name

Returns:

  • (Object)
           to: :item#public_name
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#publish_buy_box_changeObject (protected)

Announce a Buy Box win/loss flip, whoever wrote it (orchestrator pulls
today; an ANY_OFFER_CHANGED SQS handler once that subscription lands) —
see Events::AmazonBuyBoxChanged.



1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
# File 'app/models/catalog_item.rb', line 1692

def publish_buy_box_change
  publish_event(
    Events::AmazonBuyBoxChanged,
    data: {
      catalog_item_id: id,
      won: is_amz_buy_box_winner,
      previous_won: saved_change_to_is_amz_buy_box_winner.first
    }
  )
end

#purge_edge_cacheObject (protected)

Purge edge cache.



1754
1755
1756
1757
1758
1759
1760
# File 'app/models/catalog_item.rb', line 1754

def purge_edge_cache
  return unless saved_changes?
  # only need to do this for main catalogs
  return unless Catalog.main_catalog_ids.include?(catalog_id)

  site_maps.each(&:purge_edge_cache)
end

#push_inventory_messageObject

Performs an integration push where applicable for stock to partner carrying this catalog item



900
901
902
# File 'app/models/catalog_item.rb', line 900

def push_inventory_message
  inventory_message_processors.map { |imp| imp.process(catalog_items: CatalogItem.where(id:)) }.flatten
end

#push_price_messageObject

Push price message.



905
906
907
908
# File 'app/models/catalog_item.rb', line 905

def push_price_message
  logger.warn "No price message processors found for catalog item #{id}" if price_message_processors.empty?
  price_message_processors.map { |imp| imp.process(catalog_items: CatalogItem.where(id:)) }.flatten
end

#qty_availableObject

Alias for Store_item#qty_available

Returns:

  • (Object)

    Store_item#qty_available

See Also:



304
305
306
307
# File 'app/models/catalog_item.rb', line 304

delegate :qty_available,
:qty_available_outside_order,
:unit_cogs,
to: :store_item

#qty_available_outside_orderObject

Alias for Store_item#qty_available_outside_order

Returns:

  • (Object)

    Store_item#qty_available_outside_order

See Also:



304
305
306
307
# File 'app/models/catalog_item.rb', line 304

delegate :qty_available,
:qty_available_outside_order,
:unit_cogs,
to: :store_item

#real_stock(use_store_item: nil, use_alternate_warehouse: false) ⇒ Object

Returns REAL stock available (100% of all warehouses) for internal CRM use
Use this for cycle counts, pick items, order management, quotes, etc.

Parameters:

  • use_store_item (Object) (defaults to: nil)

    the use store item

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse



1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
# File 'app/models/catalog_item.rb', line 1108

def real_stock(use_store_item: nil, use_alternate_warehouse: false)
  use_store_item ||= store_item
  qty_available = use_store_item.qty_available.to_i
  if use_alternate_warehouse
    alternate_warehouse_store_items.each do |alternate_si|
      qty_available += alternate_si.qty_available.to_i
    end
  end
  qty_available
end

#refurbishedObject

Refurbished.



1559
1560
1561
1562
1563
# File 'app/models/catalog_item.rb', line 1559

def refurbished
  return unless (irv = item.refurbished_version)

  irv.catalog_items.where(catalog_id:).not_hidden_from_catalog.first
end

#reported_nameString?

Product name reported to external retailers and product feeds (Wayfair,
CommerceHub, generic feed), cleaned through Heatwave::Normalizers.feed_safe
so trademark glyphs and smart punctuation never reach a marketplace.

Returns:

  • (String, nil)


1380
1381
1382
# File 'app/models/catalog_item.rb', line 1380

def reported_name
  Heatwave::Normalizers.feed_safe(name)
end

#reported_name_for_googleString?

Google Shopping feed title: the per-item +google_feed_title+ override
(cleaned) when present, otherwise the already-cleaned #reported_name.

Returns:

  • (String, nil)


1387
1388
1389
# File 'app/models/catalog_item.rb', line 1387

def reported_name_for_google
  Heatwave::Normalizers.feed_safe(google_feed_title.presence) || reported_name
end

#reported_stock(use_store_item: nil, use_alternate_warehouse: false, safety_stock: nil) ⇒ Integer

Calculates stock available for external reporting.

Logic Details

Discontinued items report zero. When alternate-warehouse reporting is
enabled, the configured share of alternate stock is added. The primary
warehouse reserve is withheld only when the alternate warehouse contributes
no reportable stock; contributing alternate stock acts as the safety buffer.

Parameters:

  • use_store_item (StoreItem, nil) (defaults to: nil)

    primary warehouse stock record

  • use_alternate_warehouse (Boolean) (defaults to: false)

    whether to include alternate stock

  • safety_stock (Integer, nil) (defaults to: nil)

    reserve override; defaults to #reserve_stock

Returns:

  • (Integer)

    quantity available to report externally



1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
# File 'app/models/catalog_item.rb', line 1132

def reported_stock(use_store_item: nil, use_alternate_warehouse: false, safety_stock: nil)
  return 0 if discontinued? || pending_discontinue?

  safety_stock ||= reserve_stock.to_i
  use_store_item ||= store_item
  qty_available = use_store_item.qty_available.to_i
  alternate_stock_reported = use_alternate_warehouse ? reported_alternate_warehouse_stock : 0
  qty_available += alternate_stock_reported
  # The reserve buffer exists to stop us overselling the primary warehouse. If
  # the alternate warehouse is actually contributing stock, that backup IS the
  # buffer — so report what we genuinely have rather than withholding twice.
  # Only skipped when the alternate contributed something; an empty alternate
  # warehouse leaves the reserve fully in force.
  #
  # Without this, an item with 1 on hand and a reserve of 1 reports 0 even
  # when the other warehouse is holding 12 (real case: WSHC-120-00057 on
  # Wayfair-CA, 2026-07-30).
  qty_available -= safety_stock unless alternate_stock_reported.positive?
  qty_available = [qty_available, 0].max
  # If our always available stock is set we will use that value if the qty available is below
  if use_store_item.permanent_qty_available&.zero?
    0
  else
    [qty_available, min_stock_to_report, use_store_item.permanent_qty_available].compact.max
  end
end

#reported_stocks(use_alternate_warehouse: false) ⇒ Object

Returns a hash of store ids and reported stock available in each
{ 'WarmlyYours-CA': 14, 'WarmlyYours-US': 20 }

Parameters:

  • use_alternate_warehouse (Object) (defaults to: false)

    the use alternate warehouse



1268
1269
1270
1271
1272
# File 'app/models/catalog_item.rb', line 1268

def reported_stocks(use_alternate_warehouse: false)
  store_items_by_warehouse.to_h do |si|
    [si.store.name, reported_stock(use_store_item: si, use_alternate_warehouse:)]
  end
end

#reported_vendor_sku(_orchestrator_partner = nil) ⇒ Object

Reported vendor sku.

Parameters:

  • _orchestrator_partner (Object) (defaults to: nil)

    the orchestrator partner



1372
1373
1374
# File 'app/models/catalog_item.rb', line 1372

def reported_vendor_sku(_orchestrator_partner = nil)
  third_party_sku.presence || sku
end

#reprice_reference_catalogsActiveRecord::Relation<RepriceReferenceCatalog>

Reprice reference catalogs.

Returns:

  • (ActiveRecord::Relation<RepriceReferenceCatalog>)

See Also:



257
# File 'app/models/catalog_item.rb', line 257

has_many    :reprice_reference_catalogs, through: :reprice_references, source: :catalog

#reprice_referencesActiveRecord::Relation<CatalogItemRepriceReference>

Reprice references.

Returns:

See Also:



255
# File 'app/models/catalog_item.rb', line 255

has_many    :reprice_references, class_name: 'CatalogItemRepriceReference', dependent: :destroy

#reset_edi_delta_feed_fieldsObject

Reset edi delta feed fields.



1609
1610
1611
1612
1613
1614
1615
1616
# File 'app/models/catalog_item.rb', line 1609

def reset_edi_delta_feed_fields
  EDI_DELTA_FEED_CATEGORIES.each do |category|
    message_sent_json_column_name = :"last_#{category}_sent_json"
    if respond_to?(message_sent_json_column_name) && try(message_sent_json_column_name).present? # if this column exists in CatalogItem, then let's reset it, which ensures it will get sent in the next schedule EDI feed for that category of EDI message
      update_column(message_sent_json_column_name, {})
    end
  end
end

#retail_price_currency_symbolObject

Retail price currency symbol.



944
945
946
# File 'app/models/catalog_item.rb', line 944

def retail_price_currency_symbol
  catalog.currency_symbol
end

#retailer_probesActiveRecord::Relation<CatalogItemRetailerProbe>

Retailer probes.

Returns:

See Also:



253
# File 'app/models/catalog_item.rb', line 253

has_many    :retailer_probes, class_name: 'CatalogItemRetailerProbe', dependent: :destroy

#root_catalog_itemObject

Root catalog item.



982
983
984
985
986
987
# File 'app/models/catalog_item.rb', line 982

def root_catalog_item
  root_catalog = catalog.root
  return self unless root_catalog != catalog

  root_catalog.catalog_items.find_by(store_item_id:)
end

#sale_price_in_effect?(exclude_effective_date: false, date_to_check: nil) ⇒ Boolean

Returns whether the record sale price in effect.

Parameters:

  • exclude_effective_date (Date) (defaults to: false)

    the exclude effective date

  • date_to_check (Object) (defaults to: nil)

    the date to check

Returns:

  • (Boolean)

    whether the record sale price in effect



1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
# File 'app/models/catalog_item.rb', line 1472

def sale_price_in_effect?(exclude_effective_date: false, date_to_check: nil)
  return false if sale_price.blank?

  unless exclude_effective_date
    date_to_check ||= Date.current
    # If today is before the sale start, don't send
    return false if sale_price_effective_date && date_to_check < sale_price_effective_date
    # If today is past the sale expiration, don't send
    return false if sale_price_expiration_date && date_to_check > sale_price_expiration_date
  end
  true
end

#sale_price_percentage_offObject

Returns the percentage off (0-100) of an item comparing sale price to catalog price



1522
1523
1524
1525
1526
# File 'app/models/catalog_item.rb', line 1522

def sale_price_percentage_off
  return 0.0 unless sale_price.present? && amount.present? && amount.positive?

  ((1.0 - (sale_price / amount)) * 100).round(2)
end

#sale_price_percentage_off=(percentage_off) ⇒ Object

Assign a percentage off (0-100) and the sale price will be calculated based on the catalog item price without VAT

Parameters:

  • percentage_off (Object)

    the percentage off



1517
1518
1519
# File 'app/models/catalog_item.rb', line 1517

def sale_price_percentage_off=(percentage_off)
  calculate_sale_price(percentage_off)
end

#sale_price_reset_if_no_couponObject (protected)

You cannot have a sale price without a coupon, so before anything, we remove these



1626
1627
1628
1629
1630
# File 'app/models/catalog_item.rb', line 1626

def sale_price_reset_if_no_coupon
  return if coupon.present?

  self.sale_price = nil
end

#sale_price_with_vatObject

Sale price with vat.



784
785
786
787
788
789
790
# File 'app/models/catalog_item.rb', line 784

def sale_price_with_vat
  return if tax_rate.blank?

  res = ((tax_rate + 1) * (sale_price || 0.0)).round(2)
  res = nil if res == 0.0
  res
end

#sale_price_with_vat_percentage_offObject

Returns the percentage off (0-100) of an item comparing sale price with VAT to catalog price with VAT



1535
1536
1537
1538
1539
# File 'app/models/catalog_item.rb', line 1535

def sale_price_with_vat_percentage_off
  return 0.0 if sale_price_with_vat.blank?

  ((1.0 - (sale_price_with_vat / price_with_vat)) * 100).round(2)
end

#sale_price_with_vat_percentage_off=(percentage_off) ⇒ Object

Assign a percentage off (0-100) and the sale price will be calculated based on the catalog item price with VAT included

Parameters:

  • percentage_off (Object)

    the percentage off



1530
1531
1532
# File 'app/models/catalog_item.rb', line 1530

def sale_price_with_vat_percentage_off=(percentage_off)
  calculate_sale_price(percentage_off, include_vat: true)
end

#sellable_online_qty(use_store_item: nil) ⇒ Integer

Units actually offerable to shoppers for THIS catalog: the primary
warehouse's available stock minus the reserve buffer, never negative.
This is the reserve math behind #out_of_stock and therefore
#product_stock_status — the In / Out of Stock status shoppers see (which
additionally honors the item's always_available_online override).
Alternate-warehouse stock is deliberately NOT counted here — it only
contributes to #reported_stock for external feeds.

Parameters:

  • use_store_item (StoreItem, nil) (defaults to: nil)

    override the primary store item

Returns:

  • (Integer)

    sellable quantity, clamped at 0



1072
1073
1074
1075
1076
1077
# File 'app/models/catalog_item.rb', line 1072

def sellable_online_qty(use_store_item: nil)
  use_store_item ||= store_item
  return 0 unless use_store_item

  [use_store_item.qty_available.to_i - reserve_stock.to_i, 0].max
end

#seo_descriptionObject

Alias for
to: :item#seo_description

Returns:

  • (Object)
           to: :item#seo_description
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#seo_keywordsObject

Alias for
to: :item#seo_keywords

Returns:

  • (Object)
           to: :item#seo_keywords
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#seo_titleObject

Seo title.



911
912
913
# File 'app/models/catalog_item.rb', line 911

def seo_title
  item.effective_seo_title
end

#set_or_clear_discontinued_dateObject (protected)

Set or clear discontinued date.



1743
1744
1745
1746
1747
1748
1749
1750
1751
# File 'app/models/catalog_item.rb', line 1743

def set_or_clear_discontinued_date
  return unless state_changed?

  if %w[pending_discontinue discontinued].include?(state) && %w[active active_hidden pending_onboarding pending_vendor_update invalid_catalog_item].include?(state_was)
    self.discontinued_date = Date.current
  elsif %w[pending_discontinue discontinued].include?(state_was) && %w[active active_hidden pending_onboarding pending_vendor_update invalid_catalog_item].include?(state)
    self.discontinued_date = nil
  end
end

#should_appear_in_feed?Boolean

Returns whether the record should appear in feed.

Returns:

  • (Boolean)

    whether the record should appear in feed



1460
1461
1462
# File 'app/models/catalog_item.rb', line 1460

def should_appear_in_feed?
  !in_hide_from_feed_state? && product_category_visible_in_feed?
end

#siblingsObject

Siblings.



1413
1414
1415
# File 'app/models/catalog_item.rb', line 1413

def siblings
  CatalogItem.where(catalog_id:)
end

#single_sku?Boolean

Returns whether the record single sku.

Returns:

  • (Boolean)

    whether the record single sku



1450
1451
1452
# File 'app/models/catalog_item.rb', line 1450

def single_sku?
  variants(include_self: true).size <= 1
end

#site_mapsActiveRecord::Relation<SiteMap>

Site maps.

Returns:

  • (ActiveRecord::Relation<SiteMap>)

See Also:



241
# File 'app/models/catalog_item.rb', line 241

has_many    :site_maps, as: :resource, dependent: :destroy

#skuObject

Alias for
to: :item#sku

Returns:

  • (Object)
           to: :item#sku
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#sku_and_nameObject

Sku and name.



1316
1317
1318
# File 'app/models/catalog_item.rb', line 1316

def sku_and_name
  "#{item.sku} - #{item.name}"
end

#specObject

Alias for
to: :item#spec

Returns:

  • (Object)
           to: :item#spec
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#spec_outputObject

Alias for
to: :item#spec_output

Returns:

  • (Object)
           to: :item#spec_output
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#specificationsObject

Alias for
to: :item#specifications

Returns:

  • (Object)
           to: :item#specifications
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#specifications_groupedObject

Alias for
to: :item#specifications_grouped

Returns:

  • (Object)
           to: :item#specifications_grouped
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#state_requires_edi_warning_on_order?Boolean

Returns whether the record state requires edi warning on order.

Returns:

  • (Boolean)

    whether the record state requires edi warning on order



1585
1586
1587
1588
# File 'app/models/catalog_item.rb', line 1585

def state_requires_edi_warning_on_order?
  # here we warn when this item is in a state that should not be ordered via EDI, but allow order to go through
  %w[active require_vendor_update].include?(state) != true
end

#storeStore

Store.

Returns:

See Also:



235
# File 'app/models/catalog_item.rb', line 235

has_one     :store, through: :store_item

#store_itemStoreItem

Store item.

Returns:

See Also:



218
# File 'app/models/catalog_item.rb', line 218

belongs_to  :store_item, inverse_of: :catalog_items

#store_itemsActiveRecord::Relation<StoreItem>

Returns the associated store items.

Returns:

  • (ActiveRecord::Relation<StoreItem>)

    the associated store items



261
# File 'app/models/catalog_item.rb', line 261

has_and_belongs_to_many :store_items, inverse_of: :catalog_items

#store_items_by_warehouseObject

Store items by warehouse.



1261
1262
1263
# File 'app/models/catalog_item.rb', line 1261

def store_items_by_warehouse
  store_items.joins(:store).includes(:store).merge(Store.warmlyyours_warehouses)
end

#tax_classObject

Alias for
to: :item#tax_class

Returns:

  • (Object)
           to: :item#tax_class
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#tax_rateObject

Alias for Catalog#tax_rate

Returns:

  • (Object)

    Catalog#tax_rate

See Also:



265
266
267
268
269
# File 'app/models/catalog_item.rb', line 265

delegate :parent_catalog,
:currency,
:currency_symbol,
:tax_rate,
to: :catalog

#third_party_name_en_required?Boolean (protected)

Returns whether the record third party name en required.

Returns:

  • (Boolean)

    whether the record third party name en required



1733
1734
1735
# File 'app/models/catalog_item.rb', line 1733

def third_party_name_en_required?
  active? && catalog.third_party_name_en_required
end

#third_party_name_fr_required?Boolean (protected)

Returns whether the record third party name fr required.

Returns:

  • (Boolean)

    whether the record third party name fr required



1738
1739
1740
# File 'app/models/catalog_item.rb', line 1738

def third_party_name_fr_required?
  active? && catalog.third_party_name_fr_required
end

#third_party_part_number_labelObject

Third party part number label.



1349
1350
1351
# File 'app/models/catalog_item.rb', line 1349

def third_party_part_number_label
  catalog.determine_retailer_part_number_label
end

#third_party_part_number_required?Boolean (protected)

Returns whether the record third party part number required.

Returns:

  • (Boolean)

    whether the record third party part number required



1723
1724
1725
# File 'app/models/catalog_item.rb', line 1723

def third_party_part_number_required?
  active? && catalog.third_party_part_number_required
end

#third_party_sku_required?Boolean (protected)

Returns whether the record third party sku required.

Returns:

  • (Boolean)

    whether the record third party sku required



1728
1729
1730
# File 'app/models/catalog_item.rb', line 1728

def third_party_sku_required?
  active? && catalog.third_party_sku_required
end

#to_sString

Returns string representation of the record.

Returns:

  • (String)

    string representation of the record



934
935
936
# File 'app/models/catalog_item.rb', line 934

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

#tweak_ebay_skuObject

Tweak ebay sku.



707
708
709
710
711
# File 'app/models/catalog_item.rb', line 707

def tweak_ebay_sku
  return unless catalog.orchestrator_key == 'ebay_us'

  update(third_party_sku: item.sku.tr('.', '*'))
end

#unit_cogsObject

Alias for Store_item#unit_cogs

Returns:

  • (Object)

    Store_item#unit_cogs

See Also:



304
305
306
307
# File 'app/models/catalog_item.rb', line 304

delegate :qty_available,
:qty_available_outside_order,
:unit_cogs,
to: :store_item

#unprobeable_reasonString?

Why #probeable? said no, for logs.

Returns:

  • (String, nil)

    nil when the item IS probeable



730
731
732
733
734
735
# File 'app/models/catalog_item.rb', line 730

def unprobeable_reason
  return 'skip_url_checks set by a human' if skip_url_checks?
  return "probe backoff until #{probe_backoff_until.iso8601}" if probe_backoff_until&.future?

  nil
end

#upcObject

Alias for
to: :item#upc

Returns:

  • (Object)
           to: :item#upc
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#upc_required?Boolean (protected)

Returns whether the record upc required.

Returns:

  • (Boolean)

    whether the record upc required



1718
1719
1720
# File 'app/models/catalog_item.rb', line 1718

def upc_required?
  available_in_edi_feed? && catalog.require_upc?
end

#update_price_from_parentObject

When the catalog item state moves from pending client update to active, the price is updated



1392
1393
1394
1395
1396
1397
1398
# File 'app/models/catalog_item.rb', line 1392

def update_price_from_parent
  return unless parent_catalog_item

  push_price_service = Catalog::PushCatalogItemPrice.new
  result = push_price_service.process(parent_catalog_item, target_catalog_id: catalog_id)
  result.all_price_pushed?
end

#variant_axis_tokensObject

Alias for
to: :item#variant_axis_tokens

Returns:

  • (Object)
           to: :item#variant_axis_tokens
    

See Also:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/catalog_item.rb', line 271

delegate :sku,
:upc,
:public_name,
:primary_product_line,
:primary_product_line_id,
:product_category,
:product_lines,
:is_available_to_public,
:oversize?,
:is_circuit_check?,
:is_underlayment?,
:is_snow_melt_plaque?,
:is_roughin_kit?,
:is_cable_fit_guide?,
:is_cable_accessory?,
:is_membrane?,
:is_heating_element?,
:is_publication?,
:is_control?,
:primary_image,
:seo_keywords,
:seo_description,
:public_description_html,
:variant_axis_tokens,
:spec,
:spec_output,
:specifications,
:specifications_grouped,
:tax_class,
:all_amazon_image_profiles,
allow_nil: true,
to: :item

#variant_groupVariantGroup

The channel row (VariantGroup amazon override / Wayfair listing row) this
catalog_item lists under. Replaces the legacy amazon_variation FK, whose
data survives as the legacy_amazon_variation_id backup column (no
association — Maintenance::LegacyAmazonVariation reads it).



229
# File 'app/models/catalog_item.rb', line 229

belongs_to  :variant_group, optional: true, inverse_of: :catalog_items

#variant_group_in_catalog_marketplaceObject (protected)

Amazon channel rows are per-marketplace: the linked VariantGroup amazon
row must target this catalog's marketplace. Non-amazon rows (default
families, Wayfair listing rows) may link freely.



1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
# File 'app/models/catalog_item.rb', line 1647

def variant_group_in_catalog_marketplace
  return if variant_group_id.blank?
  return errors.add(:variant_group, 'must exist') unless variant_group
  return unless variant_group.amazon_channel?

  catalog_marketplace_id = catalog&.amazon_marketplace_id
  return if catalog_marketplace_id.blank?
  return if variant_group.catalog&.amazon_marketplace_id == catalog_marketplace_id

  errors.add(:variant_group, 'must belong to the same Amazon marketplace as this catalog')
end

#variants(_catalog_items_scope: nil, include_self: false, axis_filters: {}) ⇒ Object

Variants.

Parameters:

  • _catalog_items_scope (Object) (defaults to: nil)

    the catalog items scope

  • include_self (Object) (defaults to: false)

    the include self

  • axis_filters (Object) (defaults to: {})

    the axis filters



1421
1422
1423
1424
1425
1426
# File 'app/models/catalog_item.rb', line 1421

def variants(_catalog_items_scope: nil, include_self: false, axis_filters: {})
  item_variants = item.item_grouping_info(include_self:, axis_filters:)&.variants
  return CatalogItem.none if item_variants.blank?

  catalog_items_in_same_catalog(CatalogItem.for_online_catalog).merge(item_variants).includes(:item)
end