Class: Edi::Amazon::JsonListingGenerator::BaseGenerator

Inherits:
BaseService
  • Object
show all
Includes:
Memery
Defined in:
app/services/edi/amazon/json_listing_generator/base_generator.rb

Overview

Service object: base generator.

Defined Under Namespace

Classes: GenerateResult

Constant Summary collapse

OFFER_ATTRIBUTES =

Offer/fulfillment attributes — excluded from the DEFAULT product-listing payload.
They are submitted through their own channels: price via price_advice (purchasable_offer,
list_price), inventory via inventory_advice (fulfillment_availability). condition_type and
skip_offer are offer-creation controls that don't belong in a LISTING_PRODUCT_ONLY submission
(Amazon rejects them with INVALID_ATTRIBUTE / 90000900). Callers that genuinely need these
(the price/inventory feeds) pass an explicit attribute_actions hash, which bypasses the default.
NOTE: merchant_suggested_asin is intentionally NOT here — it identifies the product/ASIN and
is accepted on a product-only PUT.

%i[purchasable_offer list_price fulfillment_availability condition_type skip_offer].freeze

Instance Attribute Summary collapse

Attributes inherited from BaseService

#options

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseService

#log_debug, #log_error, #log_info, #log_warning, #logger, #process, #tagged_logger

Constructor Details

#initialize(catalog_item, locale: nil, attribute_actions: nil, product_type: nil, logger: nil, variation: nil, marketplace_id: nil, fba: false, language_tag: nil, business_price_available: true) ⇒ BaseGenerator

Returns a new instance of BaseGenerator.

Raises:

  • (ArgumentError)


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 37

def initialize(catalog_item, locale: nil, attribute_actions: nil, product_type: nil, logger: nil, variation: nil, marketplace_id: nil, fba: false, language_tag: nil, business_price_available: true)
  raise ArgumentError, "catalog_item is required for #{self.class.name}" if catalog_item.nil?

  @catalog_item = catalog_item
  @variation = variation
  @item = catalog_item.item
  @product_type = product_type || @variation&.amazon_desired_product_type || catalog_item.amazon_product_type_in_effect
  @locale = locale || @catalog_item.amazon_locales.first
  # Normalize locale
  @language_tag = language_tag || @locale.to_s.tr('-', '_').presence # Ensure we use a normalized tag
  @business_price_available = business_price_available
  @marketplace_id = marketplace_id || @catalog_item.catalog.amazon_marketplace.marketplace_identifier # MUST be present
  @marketplace = AmazonMarketplace.find_by(marketplace_identifier: @marketplace_id)
  @marketplace_country_iso = @marketplace.country.iso
  if @locale.blank?
    raise ArgumentError, "Cannot initialize #{self.class.name} without locale defined, product_type: #{@product_type}, @marketplace&.id: #{@marketplace&.id}, catalog_item&.sku: #{@catalog_item&.sku}, variation&.id: #{@variation&.id}"
  end
  raise ArgumentError, "Cannot initialize #{self.class.name} for CatalogItem[#{@catalog_item&.id}] (sku: #{@catalog_item&.sku}) — no product_type set. Assign an amazon_product_type on the catalog item or variation first." if @product_type.blank?

  @amazon_schema = AmazonSchema.amazon_channel_seller.where(product_type: @product_type).where(amazon_marketplace_id: @marketplace.id).first

  # If we don't have the schema, try to pull it
  if @amazon_schema.blank?
    @catalog_item.amazon_pull_listing_schema(@product_type)
    @amazon_schema = AmazonSchema.amazon_channel_seller.where(product_type: @product_type).where(amazon_marketplace_id: @marketplace.id).first
  end

  # If we still don't have the schema raise the error
  raise ArgumentError, "Cannot initialize #{self.class.name} for CatalogItem[#{@catalog_item&.id}] (sku: #{@catalog_item&.sku}) without #{@product_type} schema present for marketplace: #{@marketplace.name}" if @amazon_schema.blank?

  # Pass attributes and actions as a hash of symbols,.
  # For e.g. for inventory we pass:
  # see: https://developer-docs.amazon.com/sp-api/docs/listing-workflow-migration-tutorial#submitting-inventory-data
  # attribute_actions: {fulfillment_availability: :replace}
  # For e.g. for creating new listing offers we might pass:
  # see: https://developer-docs.amazon.com/sp-api/docs/listing-workflow-migration-tutorial
  # attribute_actions: {condition_type: :replace, merchant_suggested_asin: :replace, purchasable_offer: :replace}

  # If attribute actions are not specified, we use the product-listing defaults: every schema
  # attribute set to :replace, minus offer/fulfillment (OFFER_ATTRIBUTES) and other_*_image_locator
  # attributes. Offers/inventory are submitted separately via price_advice / inventory_advice.
  @attribute_actions = attribute_actions&.transform_keys(&:to_sym)&.transform_values(&:to_sym) ||
                       self.class.default_product_attribute_actions(@amazon_schema.keys)
  # Until we refactor FBA into catalog_items, we need to pass this to generate the correct FBA logic for JSON feeds
  @fba = fba

  super(logger:)
end

Instance Attribute Details

#amazon_schemaObject (readonly)

Returns the value of attribute amazon_schema.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def amazon_schema
  @amazon_schema
end

#attribute_actionsObject (readonly)

Returns the value of attribute attribute_actions.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def attribute_actions
  @attribute_actions
end

#business_price_availableObject (readonly)

Returns the value of attribute business_price_available.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def business_price_available
  @business_price_available
end

#catalog_itemObject (readonly)

Returns the value of attribute catalog_item.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def catalog_item
  @catalog_item
end

#fbaObject (readonly)

Returns the value of attribute fba.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def fba
  @fba
end

#itemObject (readonly)

Returns the value of attribute item.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def item
  @item
end

#language_tagObject (readonly)

Returns the value of attribute language_tag.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def language_tag
  @language_tag
end

#localeObject (readonly)

Returns the value of attribute locale.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def locale
  @locale
end

#marketplace_country_isoObject (readonly)

Returns the value of attribute marketplace_country_iso.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def marketplace_country_iso
  @marketplace_country_iso
end

#marketplace_idObject (readonly)

Returns the value of attribute marketplace_id.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def marketplace_id
  @marketplace_id
end

#missing_attributesObject (readonly)

Returns the value of attribute missing_attributes.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def missing_attributes
  @missing_attributes
end

#product_typeObject (readonly)

Returns the value of attribute product_type.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def product_type
  @product_type
end

#variationObject (readonly)

Returns the value of attribute variation.



7
8
9
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 7

def variation
  @variation
end

Class Method Details

.default_product_attribute_actions(schema_keys) ⇒ Object

Default action set for a full product listing PUT: every schema attribute set to :replace,
minus OFFER_ATTRIBUTES and the other_*_image_locator attributes (only reliably settable via
Seller Central bulk upload, per Christian Billen's (cbillen@warmlyyours.com) email of 11/21/25
with subject 'Amazon patch/put').



26
27
28
29
30
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 26

def self.default_product_attribute_actions(schema_keys)
  schema_keys.reject do |k|
    OFFER_ATTRIBUTES.include?(k.to_sym) || (k.to_s.include?('other_') && k.to_s.include?('image_locator'))
  end.index_with { |_attribute| :replace }
end

Instance Method Details

#append_attribute(attributes_hash, attribute, include_nil: false) ⇒ Object

Appends an attribute to the given attributes hash based on the attribute name.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 136

def append_attribute(attributes_hash, attribute, include_nil: false)
  attribute_builder = begin
    Edi::Amazon::JsonListingGenerator::Attributes::AttributeFactory.build(attribute, catalog_item:, variation:, language_tag:, enum_mapper:, marketplace_id:, fba:)
  rescue StandardError
    nil
  end
  return attributes_hash unless attribute_builder

  r = attribute_builder.build
  if r.nil? && !include_nil
    @missing_attributes << attribute
    logger.error "#{attribute_builder.class.name} returned nil"
  else
    attributes_hash[attribute] = r
  end
  attributes_hash
end

#build_attributes(include_nil: false) ⇒ Object

Builds a hash of attributes for a product listing.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 92

def build_attributes(include_nil: false)
  attributes_hash = {}
  @missing_attributes = []
  # To make everything with locale, we will wrap into a mobility call
  Mobility.with_locale(locale) do
    @attribute_actions.each do |attribute, attribute_action|
      next if attribute_action == :skip

      include_nil = true if attribute_action.to_sym == :delete
      append_attribute(attributes_hash, attribute, include_nil:)
    end
  end
  attributes_hash
end

#enum_mapperObject



86
87
88
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 86

def enum_mapper
  Edi::Amazon::EnumMapper.new(amazon_schema.schema)
end

#generateObject



107
108
109
110
111
112
113
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 107

def generate
  GenerateResult.new(
    attributes_hash: build_attributes,
    product_data_hash: product_data_hash,
    missing_attributes: @missing_attributes
  )
end

#get_patchesObject



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 115

def get_patches
  patches = generate.product_data_hash.values.flat_map do |locale_data|
    locale_data[:attributes].map do |attribute, value_arr|
      operation = attribute_actions[attribute]
      next if value_arr.blank? && operation.in?(%i[skip replace])

      # Delete operation value
      value_arr = [{ marketplace_id: }] if operation == :delete

      {
        op: operation,
        path: "/attributes/#{attribute}",
        value: value_arr
      }.compact
    end
  end

  patches.compact.uniq # we might have the same attribute generated for two different locales that are not locale dependent
end

#product_data_hashObject



154
155
156
157
158
159
160
161
# File 'app/services/edi/amazon/json_listing_generator/base_generator.rb', line 154

def product_data_hash
  {
    language_tag => {
      asin: @catalog_item.amazon_asin,
      attributes: build_attributes
    }
  }
end