Module: Models::CatalogItemAmazonHelper
- Extended by:
- ActiveSupport::Concern
- Includes:
- ActionView::Helpers::UrlHelper, Memery, RouteUrlHelpers
- Included in:
- CatalogItem
- Defined in:
- app/concerns/models/catalog_item_amazon_helper.rb
Overview
ActiveSupport::Concern mixin: catalog item amazon helper.
Constant Summary collapse
- AMAZON_DEFAULT_BUSINESS_DISCOUNT =
%
5- AMAZON_MIN_PROFIT_MARGIN =
Amazon min profit margin.
40- AMAZON_MARKETPLACE_LISTING_LOCALES =
Marketplaces that accept FEWER listing languages than their country
speaks. Keyed by marketplace identifier; absent means "trust the catalog".Belgium is trilingual and catalog 157 lists :fr, :nl and :de, but
Amazon.be rejects a de_BE value on listing attributes with error 90244
("Nous ne pouvons pas accepter la valeur de_BE ... pour item_name").
Verified 2026-08-07: the same PATCH is ACCEPTED with issues [] once de is
dropped. This only began to bite when #1864 started submitting EVERY
catalog locale instead of just the first, which made all 20 Belgian
item_name PATCHes fail.
Values are frozen too: Hash#freeze does not freeze the nested arrays, and
this allowlist decides what we send outbound for every seller. { 'AMEN7PMS3EDWL' => %i[fr nl].freeze, # Amazon.be — 90244 on a de_BE value 'A2EUQ1WTGCTBG2' => %i[en-CA].freeze # Amazon.ca — see below }.freeze
- AMAZON_LISTING_LANGUAGES =
Every language Amazon listing content may be written in across our
marketplaces. Intersected with a catalog's own locales by both
#amazon_locales and #amazon_gated_locales, which must agree on the
universe or the second would report a language as gated that we never
submit anywhere. %i[en-US en-CA fr de es nl it pl pt sv].freeze
Constants included from RouteUrlHelpers
Instance Attribute Summary collapse
-
#amazon_fnsku ⇒ String?
The Amazon Fulfillment Network SKU.
Delegated Instance Attributes collapse
-
#amazon_description ⇒ Object
Alias for to: :item#amazon_description.
-
#amazon_feature_1 ⇒ Object
Alias for to: :item#amazon_feature_1.
-
#amazon_feature_2 ⇒ Object
Alias for to: :item#amazon_feature_2.
-
#amazon_feature_3 ⇒ Object
Alias for to: :item#amazon_feature_3.
-
#amazon_feature_4 ⇒ Object
Alias for to: :item#amazon_feature_4.
-
#amazon_feature_5 ⇒ Object
Alias for to: :item#amazon_feature_5.
-
#amazon_feature_6 ⇒ Object
Alias for to: :item#amazon_feature_6.
-
#amazon_feature_7 ⇒ Object
Alias for to: :item#amazon_feature_7.
-
#amazon_feature_8 ⇒ Object
Alias for to: :item#amazon_feature_8.
-
#amazon_feature_9 ⇒ Object
Alias for to: :item#amazon_feature_9.
-
#amazon_generic_keyword ⇒ Object
Alias for to: :item#amazon_generic_keyword.
-
#amazon_target_keywords ⇒ Object
Alias for to: :item#amazon_target_keywords.
-
#amazon_title ⇒ Object
Alias for to: :item#amazon_title.
-
#title_for_amazon ⇒ Object
Alias for to: :item#title_for_amazon.
Class Method Summary collapse
-
.amazons ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons.
-
.amazons_sellers ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons sellers.
-
.amazons_sellers_with_asins ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons sellers with asins.
-
.amazons_with_asins ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons with asins.
-
.by_asins ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are by asins.
-
.with_asin ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are with asin.
Instance Method Summary collapse
-
#amalytix_tags ⇒ Array<String>
Tags to send to Amalytix for this item.
-
#amazon_asin ⇒ String?
If catalog item represents an amazon asin we return that value.
-
#amazon_business_price ⇒ BigDecimal, Float
The Amazon business price for this item before tax and FBA discount.
-
#amazon_business_price_factor ⇒ Float
Returns a ratio to apply to Amazon Catalog price to get the business price.
-
#amazon_business_price_fba ⇒ BigDecimal, ...
The Amazon business price with the FBA discount applied.
-
#amazon_business_price_with_tax ⇒ BigDecimal, ...
Returns the computer amazon business price.
-
#amazon_business_price_with_tax_fba ⇒ BigDecimal, ...
The tax-inclusive Amazon business price with the FBA discount applied.
-
#amazon_catalog_item? ⇒ Boolean
Whether this item belongs to an Amazon catalog.
-
#amazon_current_images ⇒ Hash{String => String}
Extracts the current image URLs from Amazon retailer information.
-
#amazon_delete_information(attribute_actions: nil) ⇒ Hash{Symbol => Symbol, String, Array<EdiCommunicationLog>}
Deletes Amazon listing information for this catalog item.
-
#amazon_effective_business_price_discount ⇒ Integer
What is the effective business discount for amazon.
-
#amazon_effective_desired_product_type ⇒ String?
The catalog classification we want the ASIN to carry (e.g. TOWEL_WARMER).
-
#amazon_effective_product_schema ⇒ String?
Submittable Listings product type (e.g. TOWEL_HOLDER), falling back to the variation's when the catalog item itself has none — mirrors #amazon_effective_desired_product_type.
-
#amazon_gated_locales ⇒ Array<Symbol>
The complement of #amazon_locales: languages this catalog carries that its marketplace REFUSES on listing attributes.
-
#amazon_item_cost ⇒ BigDecimal, Float
This can be refined later to add referral fee and shipping cogs.
-
#amazon_json_generator(marketplace_id: nil, attribute_actions: nil, fba: false, language_tag: nil, business_price_available: true) ⇒ Edi::Amazon::JsonListingGenerator::BaseGenerator
Builds the JSON listing generator for this catalog item.
-
#amazon_label_requirements ⇒ Array<String>
Lists missing fields that prevent printing Amazon FBA labels.
-
#amazon_list_price ⇒ BigDecimal, ...
The list price (MSRP) for Amazon, preferring the tax-inclusive value.
-
#amazon_locales ⇒ Array<Symbol>
Locales we may submit listing content in for this catalog's marketplace.
-
#amazon_lowest_quantity_discount_price ⇒ BigDecimal, ...
The lowest active quantity-discount price for Amazon.
-
#amazon_lowest_quantity_discounted_price ⇒ nil
Placeholder for the lowest quantity-discounted price calculation.
-
#amazon_maximum_seller_allowed_price_with_tax ⇒ BigDecimal, Float
The maximum price Amazon is allowed to offer for this item, with tax.
-
#amazon_maximum_seller_allowed_price_with_tax_fba ⇒ BigDecimal, ...
The maximum allowed price with tax and the FBA discount applied.
-
#amazon_minimum_profit_margin ⇒ Integer
The configured minimum profit margin percentage for Amazon.
-
#amazon_minimum_seller_allowed_price_with_tax ⇒ BigDecimal, Float
Creates the lower threshold for minimum price (in case of automated rule) Take the smallest of - the price yielding AMAZON_MIN_PROFIT_MARGIN on unit_cogs - the lowest quantity-discount price - catalog item amount (our price) - Amazon business price - Effective price (in case a sale is in effect) - amz_min_seller_price_override (manual, lowers only — it is a
.mincandidate, not an authoritative floor). -
#amazon_minimum_seller_allowed_price_with_tax_fba ⇒ BigDecimal, ...
The minimum allowed price with tax and the FBA discount applied.
-
#amazon_patch_information(attribute_actions: nil, use_fba_sku: false) ⇒ Hash{Symbol => Symbol, String, Array<EdiCommunicationLog>}
Patches Amazon listing attributes for this catalog item.
-
#amazon_price_with_tax ⇒ BigDecimal, Float
The regular Amazon price, preferring the tax-inclusive value.
-
#amazon_price_with_tax_fba ⇒ BigDecimal, ...
The regular Amazon price with tax and the FBA discount applied.
-
#amazon_product_data(fba: false) ⇒ Hash
Generates the Amazon product data payload for this catalog item.
-
#amazon_product_type_in_effect ⇒ String?
The Amazon Listings product type actually submitted (patch/put productType) and used to load the attribute schema.
-
#amazon_product_type_incoherent? ⇒ Boolean
Whether Amazon's reported product type differs from our desired type.
-
#amazon_product_url ⇒ String?
Builds the product detail URL on Amazon for this item's ASIN.
-
#amazon_pull_buy_box_status ⇒ Hash{Symbol => Symbol, String}
Pulls the current Buy Box status from Amazon for this item.
-
#amazon_pull_catalog_information ⇒ Hash{Symbol => Symbol, String}
Pulls catalog information from Amazon for this item's ASIN.
-
#amazon_pull_listing_information ⇒ Hash{Symbol => Symbol, String}
Pulls the current listing information from Amazon for this item.
-
#amazon_pull_listing_schema(product_type = nil) ⇒ Hash{Symbol => Symbol, String}
Pulls the JSON attribute schema for an Amazon listing product type.
-
#amazon_put_information(attribute_actions: nil, use_fba_sku: false) ⇒ Hash{Symbol => Symbol, String, Array<EdiCommunicationLog>}
Puts (fully replaces) Amazon listing attributes for this catalog item.
-
#amazon_quantity_discount_price(threshold_index) ⇒ BigDecimal, ...
Calculates the quantity-discount price for a given threshold index.
-
#amazon_repricing_floor_with_tax ⇒ BigDecimal, ...
The floor the REPRICER may not cross — the declared Amazon minimum, raised to MAP where MAP is knowable.
-
#amazon_sale_price_with_tax ⇒ BigDecimal, ...
The Amazon sale price, preferring the tax-inclusive value.
-
#amazon_sale_price_with_tax_fba ⇒ BigDecimal, ...
The Amazon sale price with tax and the FBA discount applied.
-
#amazon_schema ⇒ AmazonSchema?
Loads the cached AmazonSchema row for the effective product type and marketplace.
-
#amazon_seller_item? ⇒ Boolean
Whether this item is sold through an Amazon Seller (SC) catalog.
-
#amazon_send_patch_listing_information(attribute_actions: nil, fbm_vs_fba: 'Both') ⇒ Hash{Symbol => Symbol, ActiveSupport::SafeBuffer, Array<EdiCommunicationLog>}
Sends a PATCH listing update to Amazon for FBM and/or FBA SKUs.
-
#amazon_send_put_listing_information(attribute_actions: nil, fbm_vs_fba: 'Both') ⇒ Hash{Symbol => Symbol, ActiveSupport::SafeBuffer, Array<EdiCommunicationLog>}
Sends a PUT listing update to Amazon for FBM and/or FBA SKUs.
-
#amazon_vendor_code ⇒ String?
The vendor code for this item's catalog, if any.
-
#amazon_vendor_item? ⇒ Boolean
Whether this item is sold through an Amazon Vendor Central catalog.
-
#amz_available_attributes ⇒ Array<String>
List all available Amazon attribute names that can be used with get_amz_attribute.
-
#api_ready_state? ⇒ Boolean
Whether the item is in a state that can receive Amazon API pushes/pulls.
-
#broadcast_amazon_dashboard_update(skip_amazon_check: false) ⇒ void
Broadcast updates to the Amazon Catalog Items dashboard via Turbo Streams Called after Amazon operations complete (pull buy box, pull catalog info, etc.).
-
#extract_amazon_procurement_cost_price ⇒ Float?
Extracts the procurement cost price from retailer_information.
-
#extract_retailer_information(amz_attribute_sym) ⇒ Object?
Extracts a single attribute value from retailer_information.
-
#fba_discount ⇒ Float
This allows you to control the discount applied to the FBA price However I noticed that if you have no FBA stock, the FBM offer doesn't automatically take over and detects the FBA as a competitive price, so it is better to match them and let amazon decides which to present.
-
#get_amz_all_patches(fba: false, language_tag: nil) ⇒ Array<Hash>
Returns all Amazon attribute patches for this catalog item Useful for debugging to see what would be sent to Amazon.
-
#get_amz_attribute(attribute_name, fba: false, language_tag: nil, format: :patch, operation: :replace) ⇒ Hash, ...
Retrieves a single Amazon attribute's PATCH JSON through the full listing generator stack.
-
#get_amz_attributes(attribute_names, fba: false, language_tag: nil, format: :patch) ⇒ Hash
Retrieves multiple Amazon attributes at once.
-
#has_amazon_fba? ⇒ Boolean
Whether this catalog item has Amazon FBA configured with a labeling choice.
-
#map_price_with_tax ⇒ BigDecimal, ...
MAP in the same tax-inclusive units as the rest of this surface.
-
#max_discount_allowed ⇒ Hash{Symbol => Float}
Computes the maximum discount that still meets the minimum profit margin.
-
#max_discount_allowed_dollars ⇒ Float
The dollar component of #max_discount_allowed.
-
#max_discount_allowed_percentage ⇒ Float
The percentage component of #max_discount_allowed.
-
#profit_margin_amazon_minimum_below_target ⇒ Boolean
Whether the minimum allowed price yields a margin below the target.
-
#profit_marging_amazon_maximum_seller_allowed_price ⇒ BigDecimal, ...
The profit margin implied by the maximum allowed Amazon price.
-
#profit_marging_amazon_minimum_seller_allowed_price ⇒ BigDecimal, ...
The profit margin implied by the minimum allowed Amazon price.
-
#ready_to_print_amazon_fba_labels? ⇒ Boolean
Whether this item is ready to print Amazon FBA labels.
-
#reprice_allows_lowering? ⇒ Boolean
Whether the rule routes through the lowering service.
-
#reprice_allows_raising? ⇒ Boolean
Whether the rule routes through the raising service.
-
#reprice_revert_due? ⇒ Boolean
A raise from the automation is still "on probation": we lost the Buy Box within the revert window, so the raise is deemed the cause and gets rolled back rather than chased back down by the lowering service.
-
#reprice_revert_last_raise!(whodunnit:) ⇒ BigDecimal
Roll back the last automated raise and clear the bookkeeping.
-
#reprice_reverts_on_buy_box_loss? ⇒ Boolean
Whether losing the Buy Box after a raise rolls the price back.
-
#reprice_settings_amazon_only ⇒ void
Validation: reprice settings may only move off their defaults on Amazon catalog items.
-
#reprice_settings_customized? ⇒ Boolean
Any reprice setting moved off its column default?.
-
#reset_reprice_raise_bookkeeping ⇒ void
Clear the raise-streak bookkeeping (see the before_save above).
-
#retailer_information_first_locale ⇒ Hash
The first locale hash from retailer_information, or an empty hash.
Instance Attribute Details
#amazon_fnsku ⇒ String?
Returns the Amazon Fulfillment Network SKU.
48 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 48 validates :amazon_fnsku, amazon_fnsku: true, uniqueness: true, if: :amazon_fba_labeling_fnsku? |
Class Method Details
.amazons ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons. Active Record Scope
67 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 67 scope :amazons, -> { where(catalog_id: CatalogConstants::AMAZON_CATALOG_IDS) } |
.amazons_sellers ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons sellers. Active Record Scope
68 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 68 scope :amazons_sellers, -> { where(catalog_id: CatalogConstants::AMAZON_SELLER_IDS) } |
.amazons_sellers_with_asins ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons sellers with asins. Active Record Scope
70 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 70 scope :amazons_sellers_with_asins, -> { amazons.with_asin } |
.amazons_with_asins ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are amazons with asins. Active Record Scope
69 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 69 scope :amazons_with_asins, -> { amazons.with_asin } |
.by_asins ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are by asins. Active Record Scope
66 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 66 scope :by_asins, ->(*asins) { amazons_with_asins.where(items: { amazon_asin: [asins].flatten.uniq.compact }) } |
.with_asin ⇒ ActiveRecord::Relation<Models::CatalogItemAmazonHelper>
A relation of Models::CatalogItemAmazonHelpers that are with asin. Active Record Scope
71 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 71 scope :with_asin, -> { amazons.joins(store_item: :item).where.not(items: { amazon_asin: [nil, ''] }) } |
Instance Method Details
#amalytix_tags ⇒ Array<String>
Tags to send to Amalytix for this item.
1003 1004 1005 1006 1007 1008 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 1003 def = [] += item.primary_product_line.self_and_ancestors.select { |pl| pl.available_to_public || pl.show_in_sales_portal }.first(3).map { |pl| pl.slug_ltree.to_s } << variant_group.sku if variant_group.present? end |
#amazon_asin ⇒ String?
If catalog item represents an amazon asin we return that value
697 698 699 700 701 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 697 def amazon_asin return unless amazon_catalog_item? third_party_part_number.presence || item.amazon_asin end |
#amazon_business_price ⇒ BigDecimal, Float
The Amazon business price for this item before tax and FBA discount.
765 766 767 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 765 def amazon_business_price (amount.to_f * amazon_business_price_factor).round(2) end |
#amazon_business_price_factor ⇒ Float
Returns a ratio to apply to Amazon Catalog price to get the business price
759 760 761 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 759 def amazon_business_price_factor (100.0 - amazon_effective_business_price_discount.to_f) / 100.0 end |
#amazon_business_price_fba ⇒ BigDecimal, ...
The Amazon business price with the FBA discount applied.
771 772 773 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 771 def amazon_business_price_fba amazon_business_price&.-fba_discount end |
#amazon_business_price_with_tax ⇒ BigDecimal, ...
Returns the computer amazon business price
777 778 779 780 781 782 783 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 777 def amazon_business_price_with_tax price = amazon_business_price return price unless tax_rate ((tax_rate + 1) * (price || 0.0)).round(2) end |
#amazon_business_price_with_tax_fba ⇒ BigDecimal, ...
The tax-inclusive Amazon business price with the FBA discount applied.
787 788 789 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 787 def amazon_business_price_with_tax_fba amazon_business_price_with_tax&.-fba_discount end |
#amazon_catalog_item? ⇒ Boolean
Whether this item belongs to an Amazon catalog.
168 169 170 171 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 168 def amazon_catalog_item? # nil-safe: validations may run before the catalog association is set. catalog&.amazon_catalog? end |
#amazon_current_images ⇒ Hash{String => String}
Extracts the current image URLs from Amazon retailer information.
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 667 def amazon_current_images # This differs if you're vendor or seller current_images = {} if amazon_vendor_item? current_images['MAIN'] = retailer_information_first_locale.dig(:catalog, :payload, :summaries, 0, :mainImage, :link) (1..9).to_a.each do |i| # Note that vendor does not give us the image type use, just a positional number so we guess and assign it PT0x media_url = retailer_information_first_locale.dig('attributes', "other_product_image_locator_#{i}", 0, 'media_location') current_images["PT0#{i}"] = media_url if media_url.present? end elsif amazon_seller_item? images_in_marketplace = retailer_information_first_locale.dig(:catalog, :payload, :images, 0, :images) || [] largest_images = {} # On seller, it returns different sizes of the same image, 1600, 500, 75 images_in_marketplace.each do |image| variant = image['variant'] # If we haven't seen this variant yet, or if the current image is larger, update the hash largest_images[variant] = image if !largest_images[variant] || (image['width'] * image['height'] > largest_images[variant]['width'] * largest_images[variant]['height']) end # Get the resulting images as an array current_images = largest_images.transform_values { |value| value[:link] } end current_images end |
#amazon_delete_information(attribute_actions: nil) ⇒ Hash{Symbol => Symbol, String, Array<EdiCommunicationLog>}
Deletes Amazon listing information for this catalog item.
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 379 def amazon_delete_information(attribute_actions: nil) return { status: :skipped, message: 'Not an amazon item' } unless amazon_catalog_item? return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog.load_orchestrator).respond_to?(:delete_listing_from_catalog_item) res = orchestrator.delete_listing_from_catalog_item(catalog_item: self, http_method: 'DELETE', attribute_actions:) result = if res.any?(false) { status: :error, message: "Catalog Item ASIN #{amazon_asin}: could not delete listing from Amazon! ASIN not found?" } elsif has_amazon_fba? res = orchestrator.delete_listing_from_catalog_item(catalog_item: self, http_method: 'DELETE', attribute_actions:, use_fba_sku: true) if res.any?(false) status = :error if res.all?(false) status = :partial_error if res.any? { |r| r == false || r.exception? } && res.any?(&:processed?) { status: status, message: "Catalog Item ASIN #{amazon_asin}: listing deleted from Amazon! but FBA SKU #{amazon_fba_sku}: listing could not be deleted from Amazon! ASIN not found?", ecls: res.reject { |r| r == false } } else amazon_delete_result(res, "Catalog Item ASIN #{amazon_asin} and FBA SKU #{amazon_fba_sku}: listings deleted from Amazon!") end else amazon_delete_result(res, "Catalog Item SKU #{sku}: listing deleted from Amazon!") end # After a successful DELETE, transition pending_discontinue -> discontinued discontinue! if result[:status] == :success && pending_discontinue? && can_discontinue? result end |
#amazon_description ⇒ Object
Alias for
to: :item#amazon_description
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_effective_business_price_discount ⇒ Integer
What is the effective business discount for amazon
753 754 755 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 753 def amazon_effective_business_price_discount business_discount_override.presence || AMAZON_DEFAULT_BUSINESS_DISCOUNT end |
#amazon_effective_desired_product_type ⇒ String?
The catalog classification we want the ASIN to carry (e.g. TOWEL_WARMER).
Read-only on Amazon's side; used as the yardstick for misclassification
(see #amazon_product_type_incoherent?) — NOT what the listing feed submits.
515 516 517 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 515 def amazon_effective_desired_product_type amazon_desired_product_type.presence || variant_group&.amazon_desired_product_type end |
#amazon_effective_product_schema ⇒ String?
Submittable Listings product type (e.g. TOWEL_HOLDER), falling back to the
variation's when the catalog item itself has none — mirrors
#amazon_effective_desired_product_type.
507 508 509 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 507 def amazon_effective_product_schema amazon_product_schema.presence || variant_group&.amazon_product_schema end |
#amazon_feature_1 ⇒ Object
Alias for
to: :item#amazon_feature_1
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_2 ⇒ Object
Alias for
to: :item#amazon_feature_2
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_3 ⇒ Object
Alias for
to: :item#amazon_feature_3
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_4 ⇒ Object
Alias for
to: :item#amazon_feature_4
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_5 ⇒ Object
Alias for
to: :item#amazon_feature_5
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_6 ⇒ Object
Alias for
to: :item#amazon_feature_6
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_7 ⇒ Object
Alias for
to: :item#amazon_feature_7
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_8 ⇒ Object
Alias for
to: :item#amazon_feature_8
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_feature_9 ⇒ Object
Alias for
to: :item#amazon_feature_9
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_gated_locales ⇒ Array<Symbol>
The complement of #amazon_locales: languages this catalog carries that its
marketplace REFUSES on listing attributes.
These are not hypothetical. Values in them are sitting on live listings —
written before the gate landed, or machine-translated onto the listing by
Amazon itself — and Amazon keeps flagging them (100720 on Amazon.ca for
every attribute still holding an fr_CA entry). Because a PATCH is
selector-scoped, re-pushing en_CA overwrites only the English entry and
leaves the French one untouched, so no amount of resubmission clears it.
Removal takes an explicit op: delete carrying the language_tag selector,
which is what Edi::Amazon::JsonListingGenerator::BaseGenerator#get_patches
emits from this list.
458 459 460 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 458 def amazon_gated_locales (catalog.locales & AMAZON_LISTING_LANGUAGES) - amazon_locales end |
#amazon_generic_keyword ⇒ Object
Alias for
to: :item#amazon_generic_keyword
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_item_cost ⇒ BigDecimal, Float
This can be refined later to add referral fee and shipping cogs
966 967 968 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 966 def amazon_item_cost store_item.unit_cogs.round(4) end |
#amazon_json_generator(marketplace_id: nil, attribute_actions: nil, fba: false, language_tag: nil, business_price_available: true) ⇒ Edi::Amazon::JsonListingGenerator::BaseGenerator
:reek:UnusedParameters
Builds the JSON listing generator for this catalog item.
marketplace_id is accepted and ignored on purpose: callers reach this
through catalog_item_or_variation.amazon_json_generator(marketplace_id:)
(Edi::Amazon::ListingMessageProcessor), and VariantGroup#amazon_json_generator
REQUIRES it. A catalog item already knows its marketplace via its catalog, so
dropping the keyword here would break the shared call site.
534 535 536 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 534 def amazon_json_generator(marketplace_id: nil, attribute_actions: nil, fba: false, language_tag: nil, business_price_available: true) # rubocop:disable Lint/UnusedMethodArgument Edi::Amazon::JsonListingGenerator::Factory.generator_for_catalog_item(self, attribute_actions:, fba:, language_tag:, business_price_available:) end |
#amazon_label_requirements ⇒ Array<String>
Lists missing fields that prevent printing Amazon FBA labels.
738 739 740 741 742 743 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 738 def amazon_label_requirements errors = [] errors << 'ASIN number missing' if third_party_part_number.blank? errors << 'UPC missing' if item.upc.blank? errors end |
#amazon_list_price ⇒ BigDecimal, ...
The list price (MSRP) for Amazon, preferring the tax-inclusive value.
909 910 911 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 909 def amazon_list_price msrp_with_vat || msrp end |
#amazon_locales ⇒ Array<Symbol>
Locales we may submit listing content in for this catalog's marketplace.
The catalog's locales are the languages the COUNTRY speaks; a marketplace
can accept fewer. Submitting one it does not accept fails the whole PATCH
with 90244, so the marketplace has the final say where we know it differs.
Canada is the reason this is about marketplace CAPABILITY and not about our
content. Amazon.ca answers 100720 ("Invalid language data") for every
attribute carrying an fr_CA entry — measured on a live listing: 20
attributes held fr_CA, all 20 were flagged, item_name included. A full
French translation does not help, so this is not a coverage gap we can fill
from our side. French copy stays in Mobility for the website and for the
day Amazon.ca accepts it; we just do not put it on the wire.
438 439 440 441 442 443 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 438 def amazon_locales # Include supported marketplace languages; keep two-letter language tags to match catalog.locales locales = catalog.locales & AMAZON_LISTING_LANGUAGES supported = AMAZON_MARKETPLACE_LISTING_LOCALES[catalog.amazon_marketplace&.marketplace_identifier] supported ? locales & supported : locales end |
#amazon_lowest_quantity_discount_price ⇒ BigDecimal, ...
The lowest active quantity-discount price for Amazon.
793 794 795 796 797 798 799 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 793 def amazon_lowest_quantity_discount_price (1..5).to_a.reverse_each do |threshold_index| discounted_price = amazon_quantity_discount_price(threshold_index) return discounted_price if discounted_price.present? end nil end |
#amazon_lowest_quantity_discounted_price ⇒ nil
Placeholder for the lowest quantity-discounted price calculation.
816 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 816 def amazon_lowest_quantity_discounted_price; end |
#amazon_maximum_seller_allowed_price_with_tax ⇒ BigDecimal, Float
The maximum price Amazon is allowed to offer for this item, with tax.
826 827 828 829 830 831 832 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 826 def amazon_maximum_seller_allowed_price_with_tax price = [msrp, amount].max return price unless tax_rate ((tax_rate + 1) * (price || 0.0)).round(2) end |
#amazon_maximum_seller_allowed_price_with_tax_fba ⇒ BigDecimal, ...
The maximum allowed price with tax and the FBA discount applied.
836 837 838 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 836 def amazon_maximum_seller_allowed_price_with_tax_fba amazon_maximum_seller_allowed_price_with_tax&.-fba_discount end |
#amazon_minimum_profit_margin ⇒ Integer
The configured minimum profit margin percentage for Amazon.
820 821 822 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 820 def amazon_minimum_profit_margin AMAZON_MIN_PROFIT_MARGIN end |
#amazon_minimum_seller_allowed_price_with_tax ⇒ BigDecimal, Float
Creates the lower threshold for minimum price (in case of automated rule)
Take the smallest of
- the price yielding AMAZON_MIN_PROFIT_MARGIN on unit_cogs
- the lowest quantity-discount price
- catalog item amount (our price)
- Amazon business price
- Effective price (in case a sale is in effect)
- amz_min_seller_price_override (manual, lowers only — it is a
.min
candidate, not an authoritative floor)
This value ships to Amazon as minimum_seller_allowed_price, so it must stay
below EVERY price we actually offer — including the B2B price and the
quantity-discount price, which is why both are in the list. Declaring a
minimum above one of our own offers is Amazon error 18155 ("your selling
price is below your minimum price threshold") and suppresses the listing.
MAP deliberately does NOT appear here; see #amazon_repricing_floor_with_tax.
857 858 859 860 861 862 863 864 865 866 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 857 def amazon_minimum_seller_allowed_price_with_tax min_margin_price = calculate_minimum_price_for_margin(amazon_minimum_profit_margin) msap = [min_margin_price, amazon_lowest_quantity_discount_price, amount, amazon_business_price, effective_price, amz_min_seller_price_override].compact.min # Whatever our msap is because of another random rule on amazon, shave it by one percent, amazon price upload requires it price = (msap * 0.99).round(2) return price unless tax_rate ((tax_rate + 1) * (price || 0.0)).round(2) end |
#amazon_minimum_seller_allowed_price_with_tax_fba ⇒ BigDecimal, ...
The minimum allowed price with tax and the FBA discount applied.
903 904 905 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 903 def amazon_minimum_seller_allowed_price_with_tax_fba amazon_minimum_seller_allowed_price_with_tax&.-fba_discount end |
#amazon_patch_information(attribute_actions: nil, use_fba_sku: false) ⇒ Hash{Symbol => Symbol, String, Array<EdiCommunicationLog>}
Patches Amazon listing attributes for this catalog item.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 320 def amazon_patch_information(attribute_actions: nil, use_fba_sku: false) return { status: :skipped, message: 'Not an amazon item' } unless amazon_catalog_item? return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog.load_orchestrator).respond_to?(:push_listing_from_catalog_item) return { status: :skipped, message: 'Not an FBA amazon catalog item' } if use_fba_sku && !has_amazon_fba? fbm_vs_fba_sku_snippet = (use_fba_sku ? "FBA SKU #{amazon_fba_sku}" : "SKU #{sku}") res = orchestrator.push_listing_from_catalog_item(self, http_method: 'PATCH', attribute_actions:, use_fba_sku:) # Separate error hashes from ECL objects error_results, ecl_results = res.partition { |r| r.is_a?(Hash) && r[:error] } if error_results.any? = error_results.pluck(:message).join('; ') status = ecl_results.any?(&:processed?) ? :partial_error : :error { status: status, message: "#{fbm_vs_fba_sku_snippet}: #{}", ecls: ecl_results } elsif ecl_results.all?(&:processed?) { status: :success, message: "Catalog Item #{fbm_vs_fba_sku_snippet}: information patched to Amazon!", ecls: ecl_results } elsif ecl_results.any?(&:exception?) status = ecl_results.any?(&:processed?) ? :partial_error : :error { status: status, message: "Catalog Item #{fbm_vs_fba_sku_snippet}: some operations failed", ecls: ecl_results } else { status: :error, message: "Catalog Item #{fbm_vs_fba_sku_snippet}: unknown state", ecls: ecl_results } end end |
#amazon_price_with_tax ⇒ BigDecimal, Float
The regular Amazon price, preferring the tax-inclusive value.
924 925 926 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 924 def amazon_price_with_tax price_with_vat || amount end |
#amazon_price_with_tax_fba ⇒ BigDecimal, ...
The regular Amazon price with tax and the FBA discount applied.
930 931 932 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 930 def amazon_price_with_tax_fba amazon_price_with_tax&.-fba_discount end |
#amazon_product_data(fba: false) ⇒ Hash
Generates the Amazon product data payload for this catalog item.
420 421 422 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 420 def amazon_product_data(fba: false) amazon_json_generator(fba: fba).generate end |
#amazon_product_type_in_effect ⇒ String?
The Amazon Listings product type actually submitted (patch/put productType)
and used to load the attribute schema.
Precedence submits what Amazon ALREADY HAS before what we want, because a
type Amazon has not launched for listings (e.g. TOWEL_WARMER) is rejected
with issue 4000003 — see SP-API Developer Support case #21004871501:
- explicit override (amazon_product_schema) — the escape hatch, needed to
ONBOARD an item whose desired type is unlaunched (nothing reported yet), - the type our live listing is registered under (listing pull),
- Amazon's catalog classification for the ASIN,
- our desired type — reached only during onboarding, when Amazon has
reported nothing back yet.
amazon_desired_product_type is therefore the TARGET we appeal toward and the
yardstick for misclassification reporting (#amazon_product_type_incoherent?),
NOT the value we submit while Amazon still reports something else. When Amazon
flips the listing, the reported type changes and submissions follow on their own.
481 482 483 484 485 486 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 481 def amazon_product_type_in_effect amazon_effective_product_schema.presence || amazon_listing_reported_product_type.presence || amazon_reported_product_type.presence || amazon_effective_desired_product_type.presence end |
#amazon_product_type_incoherent? ⇒ Boolean
Whether Amazon's reported product type differs from our desired type.
659 660 661 662 663 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 659 def amazon_product_type_incoherent? return false unless amazon_reported_product_type && amazon_effective_desired_product_type amazon_reported_product_type != amazon_effective_desired_product_type end |
#amazon_product_url ⇒ String?
Builds the product detail URL on Amazon for this item's ASIN.
651 652 653 654 655 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 651 def amazon_product_url return unless amazon_marketplace && amazon_asin amazon_marketplace.url_for_asin(amazon_asin) end |
#amazon_pull_buy_box_status ⇒ Hash{Symbol => Symbol, String}
Pulls the current Buy Box status from Amazon for this item.
209 210 211 212 213 214 215 216 217 218 219 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 209 def amazon_pull_buy_box_status return { status: :skipped, message: 'Not an amazon item or no asin present' } unless amazon_catalog_item? && amazon_asin.present? return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog.load_orchestrator).respond_to?(:pull_buy_box_status) res = orchestrator.pull_buy_box_status(self) if res.any? { |h| h.values.any?(false) } { status: :error, message: "Catalog Item ASIN #{amazon_asin}: could not pull buy box status from Amazon! ASIN not found?" } else { status: :success, message: "Catalog Item ASIN #{amazon_asin}: buy box status pulled from Amazon!" } end end |
#amazon_pull_catalog_information ⇒ Hash{Symbol => Symbol, String}
Pulls catalog information from Amazon for this item's ASIN.
193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 193 def amazon_pull_catalog_information return { status: :skipped, message: 'Not an amazon item or no asin present' } unless amazon_catalog_item? && amazon_asin.present? # A Vendor Central catalog can resolve to the order-only Edi::MftGateway::Orchestrator, # which lacks these listing methods — respond_to? guards both nil and wrong-orchestrator (AppSignal #6328). return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog.load_orchestrator).respond_to?(:pull_catalog_information) res = orchestrator.pull_catalog_information(self) if res.any? { |h| h.values.any?(false) } { status: :error, message: "Catalog Item ASIN #{amazon_asin}: could not pull information from Amazon! ASIN not found?" } else { status: :success, message: "Catalog Item ASIN #{amazon_asin}: information pulled from Amazon!" } end end |
#amazon_pull_listing_information ⇒ Hash{Symbol => Symbol, String}
Pulls the current listing information from Amazon for this item.
223 224 225 226 227 228 229 230 231 232 233 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 223 def amazon_pull_listing_information return { status: :skipped, message: 'Not an amazon item' } unless amazon_catalog_item? return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog.load_orchestrator).respond_to?(:pull_listing_information) res = orchestrator.pull_listing_information(self) if res.any? { |h| h.values.any?(false) } { status: :error, message: "Catalog Item ID #{id}: could not pull information from Amazon!" } else { status: :success, message: "Catalog Item ID #{id}: information pulled from Amazon!" } end end |
#amazon_pull_listing_schema(product_type = nil) ⇒ Hash{Symbol => Symbol, String}
Pulls the JSON attribute schema for an Amazon listing product type.
238 239 240 241 242 243 244 245 246 247 248 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 238 def amazon_pull_listing_schema(product_type = nil) return { status: :skipped, message: 'Not an amazon item' } unless amazon_catalog_item? return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog.load_orchestrator).respond_to?(:pull_listing_schema) res = orchestrator.pull_listing_schema(self, product_type) if res.any? { |h| h.values.any?(false) } { status: :error, message: "Catalog Item ID #{id}: could not pull schema from Amazon!" } else { status: :success, message: "Catalog Item ID #{id}: schema pulled from Amazon!" } end end |
#amazon_put_information(attribute_actions: nil, use_fba_sku: false) ⇒ Hash{Symbol => Symbol, String, Array<EdiCommunicationLog>}
Puts (fully replaces) Amazon listing attributes for this catalog item.
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 350 def amazon_put_information(attribute_actions: nil, use_fba_sku: false) return { status: :skipped, message: 'Not an amazon item' } unless amazon_catalog_item? return { status: :skipped, message: 'Orchestrator could not be loaded' } unless (orchestrator = catalog.load_orchestrator).respond_to?(:push_listing_from_catalog_item) return { status: :skipped, message: 'Not an FBA amazon catalog item' } if use_fba_sku && !has_amazon_fba? fbm_vs_fba_sku_snippet = (use_fba_sku ? "FBA SKU #{amazon_fba_sku}" : "SKU #{sku}") res = orchestrator.push_listing_from_catalog_item(self, http_method: 'PUT', attribute_actions:, use_fba_sku:) # Separate error hashes from ECL objects error_results, ecl_results = res.partition { |r| r.is_a?(Hash) && r[:error] } if error_results.any? = error_results.pluck(:message).join('; ') status = ecl_results.any?(&:processed?) ? :partial_error : :error { status: status, message: "#{fbm_vs_fba_sku_snippet}: #{}", ecls: ecl_results } elsif ecl_results.all?(&:processed?) { status: :success, message: "Catalog Item #{fbm_vs_fba_sku_snippet}: information put to Amazon!", ecls: ecl_results } elsif ecl_results.any?(&:exception?) status = ecl_results.any?(&:processed?) ? :partial_error : :error { status: status, message: "Catalog Item #{fbm_vs_fba_sku_snippet}: some operations failed", ecls: ecl_results } else { status: :error, message: "Catalog Item #{fbm_vs_fba_sku_snippet}: unknown state", ecls: ecl_results } end end |
#amazon_quantity_discount_price(threshold_index) ⇒ BigDecimal, ...
Calculates the quantity-discount price for a given threshold index.
804 805 806 807 808 809 810 811 812 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 804 def amazon_quantity_discount_price(threshold_index) return unless (discount = send(:"quantity_#{threshold_index}_price_discount")) if quantity_discount_price_type_percent? (amount * ((100.0 - discount.to_f) / 100.0)).round(2) elsif quantity_discount_price_type_fixed? (amount - discount) end end |
#amazon_repricing_floor_with_tax ⇒ BigDecimal, ...
The floor the REPRICER may not cross — the declared Amazon minimum, raised
to MAP where MAP is knowable.
Deliberately separate from #amazon_minimum_seller_allowed_price_with_tax.
That value is fed to Amazon and is bounded by our own offers; this one is
an internal guard and is bounded by nothing, so MAP can genuinely stop the
repricer from undercutting the price we enforce on resellers.
Merging the two (2026-08-01) declared minimums above our own B2B and
quantity-discount prices on 220 listings and had Amazon suppress them.
880 881 882 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 880 def amazon_repricing_floor_with_tax [amazon_minimum_seller_allowed_price_with_tax, map_price_with_tax].compact.max end |
#amazon_sale_price_with_tax ⇒ BigDecimal, ...
The Amazon sale price, preferring the tax-inclusive value.
936 937 938 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 936 def amazon_sale_price_with_tax sale_price_with_vat || sale_price end |
#amazon_sale_price_with_tax_fba ⇒ BigDecimal, ...
The Amazon sale price with tax and the FBA discount applied.
942 943 944 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 942 def amazon_sale_price_with_tax_fba amazon_sale_price_with_tax&.-fba_discount end |
#amazon_schema ⇒ AmazonSchema?
Loads the cached AmazonSchema row for the effective product type and marketplace.
490 491 492 493 494 495 496 497 498 499 500 501 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 490 def amazon_schema product_type = amazon_product_type_in_effect return if product_type.blank? AmazonSchema .amazon_channel_seller .where( amazon_marketplace: catalog&.amazon_marketplace&.id, product_type: product_type ) .first end |
#amazon_seller_item? ⇒ Boolean
Whether this item is sold through an Amazon Seller (SC) catalog.
181 182 183 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 181 def amazon_seller_item? catalog_id.in?(CatalogConstants::AMAZON_SC_CATALOG_IDS + CatalogConstants::AMAZON_EU_CATALOG_IDS) end |
#amazon_send_patch_listing_information(attribute_actions: nil, fbm_vs_fba: 'Both') ⇒ Hash{Symbol => Symbol, ActiveSupport::SafeBuffer, Array<EdiCommunicationLog>}
Sends a PATCH listing update to Amazon for FBM and/or FBA SKUs.
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 254 def amazon_send_patch_listing_information(attribute_actions: nil, fbm_vs_fba: 'Both') if api_ready_state? res_arr = [] sku_labels = ['', '-FBA'] if %w[Both].include?(fbm_vs_fba) sku_labels = [''] if %w[FBM].include?(fbm_vs_fba) sku_labels = ['-FBA'] if %w[FBA].include?(fbm_vs_fba) res_arr << amazon_patch_information(attribute_actions:, use_fba_sku: false) if %w[Both FBM].include?(fbm_vs_fba) res_arr << amazon_patch_information(attribute_actions:, use_fba_sku: true) if %w[Both FBA].include?(fbm_vs_fba) status = :success if res_arr.all? { |r| r[:status] == :success } status = :partial_skipped if res_arr.any? { |r| r[:status] == :skipped } status = :skipped if res_arr.all? { |r| r[:status] == :skipped } status = :partial_error if res_arr.any? { |r| r[:status] == :error } status = :error if res_arr.all? { |r| r[:status] == :error } ecl_links = res_arr.map.with_index do |r, i| Array(r[:ecls]).map do |ecl| link_to("#{sku}#{sku_labels[i]} (#{ecl.state})", edi_communication_log_path(ecl), target: '_blank', rel: 'noopener noreferrer') end end.flatten.compact = res_arr.pluck(:message).join(' ') = ecl_links.any? ? " See: #{ecl_links.join(', ')}" : '' { status: status, message: "#{}#{}".html_safe } else { status: :skipped, message: 'Not an active or pending onboarding item' } end end |
#amazon_send_put_listing_information(attribute_actions: nil, fbm_vs_fba: 'Both') ⇒ Hash{Symbol => Symbol, ActiveSupport::SafeBuffer, Array<EdiCommunicationLog>}
Sends a PUT listing update to Amazon for FBM and/or FBA SKUs.
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 287 def amazon_send_put_listing_information(attribute_actions: nil, fbm_vs_fba: 'Both') if api_ready_state? res_arr = [] sku_labels = ['', '-FBA'] if %w[Both].include?(fbm_vs_fba) sku_labels = [''] if %w[FBM].include?(fbm_vs_fba) sku_labels = ['-FBA'] if %w[FBA].include?(fbm_vs_fba) res_arr << amazon_put_information(attribute_actions:, use_fba_sku: false) if %w[Both FBM].include?(fbm_vs_fba) res_arr << amazon_put_information(attribute_actions:, use_fba_sku: true) if %w[Both FBA].include?(fbm_vs_fba) status = :success if res_arr.all? { |r| r[:status] == :success } status = :partial_skipped if res_arr.any? { |r| r[:status] == :skipped } status = :skipped if res_arr.all? { |r| r[:status] == :skipped } status = :partial_error if res_arr.any? { |r| r[:status] == :error } status = :error if res_arr.all? { |r| r[:status] == :error } ecl_links = res_arr.map.with_index do |r, i| Array(r[:ecls]).map do |ecl| link_to("#{sku}#{sku_labels[i]} (#{ecl.state})", edi_communication_log_path(ecl), target: '_blank', rel: 'noopener noreferrer') end end.flatten.compact = res_arr.pluck(:message).join(' ') = ecl_links.any? ? " See: #{ecl_links.join(', ')}" : '' { status: status, message: "#{}#{}".html_safe } else { status: :skipped, message: 'Not an active or pending onboarding item' } end end |
#amazon_target_keywords ⇒ Object
Alias for
to: :item#amazon_target_keywords
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_title ⇒ Object
Alias for
to: :item#amazon_title
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |
#amazon_vendor_code ⇒ String?
The vendor code for this item's catalog, if any.
413 414 415 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 413 def amazon_vendor_code catalog.vendor_code.presence end |
#amazon_vendor_item? ⇒ Boolean
Whether this item is sold through an Amazon Vendor Central catalog.
187 188 189 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 187 def amazon_vendor_item? catalog_id.in?(CatalogConstants::AMAZON_VC_CATALOG_IDS) end |
#amz_available_attributes ⇒ Array<String>
List all available Amazon attribute names that can be used with get_amz_attribute
626 627 628 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 626 def amz_available_attributes Edi::Amazon::JsonListingGenerator::Attributes::AttributeFactory.attributes_available end |
#api_ready_state? ⇒ Boolean
Whether the item is in a state that can receive Amazon API pushes/pulls.
175 176 177 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 175 def api_ready_state? active? || active_hidden? || pending_onboarding? || require_vendor_update? || pending_vendor_update? end |
#broadcast_amazon_dashboard_update(skip_amazon_check: false) ⇒ void
This method returns an undefined value.
Broadcast updates to the Amazon Catalog Items dashboard via Turbo Streams
Called after Amazon operations complete (pull buy box, pull catalog info, etc.)
1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 1014 def broadcast_amazon_dashboard_update(skip_amazon_check: false) return unless skip_amazon_check || amazon_catalog_item? Rails.logger.info "[CatalogItem] Broadcasting Amazon dashboard update for #{id}" # Broadcast updates to each cell that may have changed cells = { "amazon_item_status_#{id}" => 'crm/amazon_products/status_cell', "amazon_item_price_#{id}" => 'crm/amazon_products/price_cell', "amazon_item_minmax_#{id}" => 'crm/amazon_products/minmax_cell', "amazon_item_comp_price_#{id}" => 'crm/amazon_products/competitive_price_cell', "amazon_item_buybox_#{id}" => 'crm/amazon_products/buybox_cell', "amazon_item_fm_#{id}" => 'crm/amazon_products/fm_cell', "amazon_item_issues_#{id}" => 'crm/amazon_products/issues_cell', "amazon_item_price_updated_#{id}" => 'crm/amazon_products/price_updated_cell', "amazon_item_data_#{id}" => 'crm/amazon_products/data_freshness_cell' } stream = "amazon_catalog_items:catalog_#{catalog_id}" cells.each do |target, partial| Turbo::StreamsChannel.broadcast_update_to( stream, target: target, partial: partial, locals: { catalog_item: self } ) end rescue StandardError => e Rails.logger.warn "[CatalogItem] Amazon dashboard broadcast failed: #{e.}" end |
#extract_amazon_procurement_cost_price ⇒ Float?
Extracts the procurement cost price from retailer_information.
722 723 724 725 726 727 728 729 730 731 732 733 734 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 722 def extract_amazon_procurement_cost_price return unless amazon_catalog_item? amz_locale = retailer_information&.keys&.first return unless amz_locale begin hsh = retailer_information&.fetch(amz_locale)&.fetch('procurement')&.first&.fetch('costPrice') hsh&.fetch('amount').to_f rescue KeyError hsh&.to_s end end |
#extract_retailer_information(amz_attribute_sym) ⇒ Object?
Extracts a single attribute value from retailer_information.
706 707 708 709 710 711 712 713 714 715 716 717 718 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 706 def extract_retailer_information(amz_attribute_sym) return unless amazon_catalog_item? amz_locale = retailer_information&.keys&.first return unless amz_locale begin hsh = retailer_information&.fetch(amz_locale)&.fetch('attributes')&.fetch(amz_attribute_sym.to_s)&.first hsh&.fetch('value') rescue KeyError hsh&.to_s end end |
#fba_discount ⇒ Float
This allows you to control the discount applied to the FBA price
However I noticed that if you have no FBA stock, the FBM offer doesn't automatically
take over and detects the FBA as a competitive price, so it is better to match
them and let amazon decides which to present
918 919 920 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 918 def fba_discount 0.0 end |
#get_amz_all_patches(fba: false, language_tag: nil) ⇒ Array<Hash>
Returns all Amazon attribute patches for this catalog item
Useful for debugging to see what would be sent to Amazon
612 613 614 615 616 617 618 619 620 621 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 612 def get_amz_all_patches(fba: false, language_tag: nil) return [] unless amazon_catalog_item? begin amazon_json_generator(fba:, language_tag:).get_patches rescue StandardError => e Rails.logger.error "[CatalogItem] Failed to get Amazon patches: #{e.class} - #{e.}" [] end end |
#get_amz_attribute(attribute_name, fba: false, language_tag: nil, format: :patch, operation: :replace) ⇒ Hash, ...
Retrieves a single Amazon attribute's PATCH JSON through the full listing generator stack.
558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 558 def get_amz_attribute(attribute_name, fba: false, language_tag: nil, format: :patch, operation: :replace) return nil unless amazon_catalog_item? attribute_name = attribute_name.to_s.underscore.to_sym attribute_actions = { attribute_name => operation } begin generator = amazon_json_generator(attribute_actions:, fba:, language_tag:) attribute_builder = Edi::Amazon::JsonListingGenerator::Attributes::AttributeFactory.build( attribute_name, catalog_item: self, variation: nil, language_tag: generator.language_tag, enum_mapper: generator.enum_mapper, marketplace_id: generator.marketplace_id, fba: fba ) built_value = attribute_builder.build case format when :raw attribute_builder.value when :value built_value when :patch return nil if built_value.blank? && operation != :delete { op: operation, path: "/attributes/#{attribute_name}", value: operation == :delete ? [{ marketplace_id: generator.marketplace_id }] : built_value } else built_value end rescue NameError => e Rails.logger.warn "[CatalogItem] Amazon attribute '#{attribute_name}' not found: #{e.}" nil rescue ArgumentError => e Rails.logger.warn "[CatalogItem] Amazon attribute '#{attribute_name}' failed to initialize: #{e.}" nil rescue StandardError => e Rails.logger.error "[CatalogItem] Amazon attribute '#{attribute_name}' error: #{e.class} - #{e.}" nil end end |
#get_amz_attributes(attribute_names, fba: false, language_tag: nil, format: :patch) ⇒ Hash
Retrieves multiple Amazon attributes at once
641 642 643 644 645 646 647 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 641 def get_amz_attributes(attribute_names, fba: false, language_tag: nil, format: :patch) return {} unless amazon_catalog_item? attribute_names.to_h do |attr_name| [attr_name.to_sym, get_amz_attribute(attr_name, fba:, language_tag:, format:)] end end |
#has_amazon_fba? ⇒ Boolean
Whether this catalog item has Amazon FBA configured with a labeling choice.
97 98 99 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 97 def has_amazon_fba? amazon_fba_sku.present? && (amazon_fba_labeling_upc? || amazon_fba_labeling_fnsku?) end |
#map_price_with_tax ⇒ BigDecimal, ...
MAP in the same tax-inclusive units as the rest of this surface.
CatalogItem#map_price is derived from msrp, which is the root catalog
item's amount and therefore pre-tax. Comparing it raw against a
tax-inclusive minimum understates MAP wherever tax_rate is positive (the
EU catalogs), and the lowering service divides the chosen floor by
1 + tax_rate before storing amount — so a raw comparison could store a
price below MAP, which is the one thing the floor exists to prevent.
894 895 896 897 898 899 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 894 def map_price_with_tax return if map_price.blank? return map_price unless tax_rate ((tax_rate + 1) * map_price).round(2) end |
#max_discount_allowed ⇒ Hash{Symbol => Float}
Computes the maximum discount that still meets the minimum profit margin.
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 972 def max_discount_allowed # Calculate the minimum sale price required to meet the minimum profit margin minimum_sale_price = amazon_item_cost / (1 - (amazon_minimum_profit_margin / 100.0)) # Calculate the maximum discount in dollars, 0 being the maximum max_discount = [msrp - minimum_sale_price, 0.0].max # Calculate the maximum discount as a percentage of MSRP max_discount_percentage = ((max_discount / msrp) * 100).round(2) # Return both dollar and percentage values { max_discount_dollars: max_discount.round(2), max_discount_percentage: max_discount_percentage } end |
#max_discount_allowed_dollars ⇒ Float
The dollar component of #max_discount_allowed.
991 992 993 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 991 def max_discount_allowed_dollars max_discount_allowed[:max_discount_dollars] end |
#max_discount_allowed_percentage ⇒ Float
The percentage component of #max_discount_allowed.
997 998 999 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 997 def max_discount_allowed_percentage max_discount_allowed[:max_discount_percentage] end |
#profit_margin_amazon_minimum_below_target ⇒ Boolean
Whether the minimum allowed price yields a margin below the target.
954 955 956 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 954 def profit_margin_amazon_minimum_below_target profit_marging_amazon_minimum_seller_allowed_price < amazon_minimum_profit_margin end |
#profit_marging_amazon_maximum_seller_allowed_price ⇒ BigDecimal, ...
The profit margin implied by the maximum allowed Amazon price.
960 961 962 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 960 def profit_marging_amazon_maximum_seller_allowed_price calculate_profit_margin amazon_maximum_seller_allowed_price_with_tax end |
#profit_marging_amazon_minimum_seller_allowed_price ⇒ BigDecimal, ...
The profit margin implied by the minimum allowed Amazon price.
948 949 950 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 948 def profit_marging_amazon_minimum_seller_allowed_price calculate_profit_margin amazon_minimum_seller_allowed_price_with_tax end |
#ready_to_print_amazon_fba_labels? ⇒ Boolean
Whether this item is ready to print Amazon FBA labels.
747 748 749 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 747 def ready_to_print_amazon_fba_labels? catalog.amazon_catalog? && amazon_label_requirements.blank? end |
#reprice_allows_lowering? ⇒ Boolean
Returns whether the rule routes through the lowering service.
122 123 124 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 122 def reprice_allows_lowering? reprice_rule.in?(CatalogItem::REPRICE_RULES_LOWERING) end |
#reprice_allows_raising? ⇒ Boolean
Returns whether the rule routes through the raising service.
117 118 119 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 117 def reprice_allows_raising? reprice_rule.in?(CatalogItem::REPRICE_RULES_RAISING) end |
#reprice_revert_due? ⇒ Boolean
A raise from the automation is still "on probation": we lost the Buy Box
within the revert window, so the raise is deemed the cause and gets rolled
back rather than chased back down by the lowering service.
135 136 137 138 139 140 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 135 def reprice_revert_due? reprice_reverts_on_buy_box_loss? && !is_amz_buy_box_winner && last_raise_prior_amount.present? && last_raised_at.present? && last_raised_at > CatalogItem::REPRICE_REVERT_WINDOW.ago end |
#reprice_revert_last_raise!(whodunnit:) ⇒ BigDecimal
Roll back the last automated raise and clear the bookkeeping. Regaining
the Buy Box resets amz_last_buy_box_winner_change, so the raising
service's stability gate is the natural cooldown before the next attempt.
147 148 149 150 151 152 153 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 147 def reprice_revert_last_raise!(whodunnit:) prior = last_raise_prior_amount PaperTrail.request(whodunnit: whodunnit) do update!(amount: prior, last_raise_prior_amount: nil, last_raised_at: nil) end prior end |
#reprice_reverts_on_buy_box_loss? ⇒ Boolean
Returns whether losing the Buy Box after a raise rolls the price back.
127 128 129 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 127 def reprice_reverts_on_buy_box_loss? reprice_rule_up_only_preserve_buy_box? || reprice_rule_auto_buy_box? end |
#reprice_settings_amazon_only ⇒ void
This method returns an undefined value.
Validation: reprice settings may only move off their defaults on Amazon
catalog items. A record with no catalog yet skips this check — the
belongs_to presence validation reports that on its own.
(Not private — methods below must stay public.)
160 161 162 163 164 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 160 def reprice_settings_amazon_only return if catalog.blank? || amazon_catalog_item? errors.add(:reprice_rule, 'repricing settings only apply to Amazon catalog items') end |
#reprice_settings_customized? ⇒ Boolean
Any reprice setting moved off its column default?
103 104 105 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 103 def reprice_settings_customized? reprice_rule != 'auto_buy_box' || reprice_target_price.present? end |
#reset_reprice_raise_bookkeeping ⇒ void
This method returns an undefined value.
Clear the raise-streak bookkeeping (see the before_save above).
109 110 111 112 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 109 def reset_reprice_raise_bookkeeping self.last_raise_prior_amount = nil self.last_raised_at = nil end |
#retailer_information_first_locale ⇒ Hash
The first locale hash from retailer_information, or an empty hash.
407 408 409 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 407 def retailer_information_first_locale retailer_information&.with_indifferent_access&.values&.first || {} end |
#title_for_amazon ⇒ Object
Alias for
to: :item#title_for_amazon
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/concerns/models/catalog_item_amazon_helper.rb', line 77 delegate :title_for_amazon, :amazon_title, :amazon_description, :amazon_generic_keyword, :amazon_target_keywords, :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, allow_nil: true, to: :item |