Class: Edi::Wayfair::ListingGenerator::Attributes::BaseAttribute

Inherits:
Object
  • Object
show all
Defined in:
app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb

Overview

Base class for Wayfair attribute mappers
Maps internal product data to Wayfair's taxonomy attribute format

Wayfair attribute format:
{
taxonomyAttributeId: "218029",
value: "Electric"
}

For multi-value attributes:
{
taxonomyAttributeId: "218029",
values: ["Electric", "Infrared"]
}

Constant Summary collapse

UNIT_MAPPINGS =

Unit conversions for Wayfair

{
  'in' => 'inches',
  'ft' => 'feet',
  'cm' => 'centimeters',
  'm' => 'meters',
  'mm' => 'millimeters',
  'lb' => 'pounds',
  'lbs' => 'pounds',
  'kg' => 'kilograms',
  'oz' => 'ounces',
  'g' => 'grams',
  'W' => 'watts',
  'V' => 'volts',
  'A' => 'amps',
  'degF' => 'fahrenheit',
  'degC' => 'celsius'
}.with_indifferent_access.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(catalog_item:, wayfair_schema: nil, taxonomy_attribute_id: nil) ⇒ BaseAttribute

Initialize the mapper, resolving the live schema attribute it targets.

Parameters:

  • catalog_item (CatalogItem)

    the catalog item to map

  • wayfair_schema (WayfairSchema, nil) (defaults to: nil)

    schema for the item's Wayfair class
    (fetched from the catalog item when omitted)

  • taxonomy_attribute_id (String, Integer, nil) (defaults to: nil)

    explicit taxonomy attribute ID
    override; title resolution against the live schema is preferred



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 46

def initialize(catalog_item:, wayfair_schema: nil, taxonomy_attribute_id: nil)
  @catalog_item = catalog_item
  @item = catalog_item.item
  @wayfair_schema = wayfair_schema || catalog_item.wayfair_pull_taxonomy_schema
  # Resolve the schema attribute this mapper targets from the LIVE schema (by
  # title), not a hardcoded taxonomy id — the same concept carries a different
  # id per Wayfair class (Wattage is 895 on space heaters but 383497 on
  # Underfloor Heating / class 7441). The hardcoded id is only a fallback.
  # When the resolved attribute isn't part of this class's schema the mapper
  # won't build (see #can_build?), so we never send a heater attribute on a
  # floor-heating listing — Wayfair rejects those as "invalid attribute".
  @attribute_definition = resolve_attribute_definition(taxonomy_attribute_id)
  @taxonomy_attribute_id = attribute_definition&.dig('taxonomyAttributeId')&.to_s.presence ||
                           taxonomy_attribute_id&.to_s ||
                           self.class.taxonomy_attribute_id
end

Class Attribute Details

.taxonomy_attribute_idObject (readonly)

Legacy/fallback taxonomy attribute id. Used only when the live schema has
no attribute matching schema_titles — title resolution is preferred so
the per-class id is always correct.



135
136
137
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 135

def taxonomy_attribute_id
  @taxonomy_attribute_id
end

Instance Attribute Details

#attribute_definitionObject (readonly)

Returns the value of attribute attribute_definition.



38
39
40
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 38

def attribute_definition
  @attribute_definition
end

#catalog_itemObject (readonly)

Returns the value of attribute catalog_item.



38
39
40
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 38

def catalog_item
  @catalog_item
end

#itemObject (readonly)

Returns the value of attribute item.



38
39
40
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 38

def item
  @item
end

#taxonomy_attribute_idObject (readonly)

Returns the value of attribute taxonomy_attribute_id.



38
39
40
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 38

def taxonomy_attribute_id
  @taxonomy_attribute_id
end

#wayfair_schemaObject (readonly)

Returns the value of attribute wayfair_schema.



38
39
40
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 38

def wayfair_schema
  @wayfair_schema
end

Class Method Details

.attribute_nameString

Class method to get the attribute name from class name

Returns:

  • (String)

    underscored attribute name (e.g., "btu_output")



165
166
167
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 165

def self.attribute_name
  name.demodulize.underscore
end

.schema_titlesArray<String>

The schema attribute title(s) this mapper satisfies. Defaults to the
humanized class name (MaximumTemperature -> "Maximum Temperature") which,
combined with the punctuation-insensitive match, already covers titles like
"BTU Output" and "Plug-In".

Returns:

  • (Array<String>)


159
160
161
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 159

def self.schema_titles
  @schema_titles || [name.demodulize.titleize]
end

.set_schema_titles(*titles) ⇒ Array<String>

Declare the schema attribute title(s) this mapper satisfies. The live schema
is searched for an attribute whose title matches one of these
(case/punctuation-insensitive) to resolve the per-class taxonomy id.

Parameters:

  • titles (Array<String>)

    one or more candidate titles

Returns:

  • (Array<String>)


150
151
152
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 150

def self.set_schema_titles(*titles)
  @schema_titles = titles.flatten.map(&:to_s)
end

.set_taxonomy_attribute_id(id) ⇒ String

Set the legacy/fallback taxonomy attribute ID for this mapper class.

Parameters:

  • id (String, Integer)

    the Wayfair taxonomy attribute ID

Returns:

  • (String)


141
142
143
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 141

def self.set_taxonomy_attribute_id(id)
  @taxonomy_attribute_id = id.to_s
end

Instance Method Details

#attribute_titleString?

Get the attribute title from schema

Returns:

  • (String, nil)


92
93
94
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 92

def attribute_title
  attribute_definition&.dig('title')
end

#buildHash?

Build the attribute payload for Wayfair API

Returns:

  • (Hash, nil)

    The attribute hash or nil if no value



65
66
67
68
69
70
71
72
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 65

def build
  return nil unless can_build?

  v = value
  return nil if v.nil? || (v.respond_to?(:blank?) && v.blank?)

  build_attribute_hash(v)
end

#build_attribute_hash(val) ⇒ Hash (protected)

Build the Wayfair API payload hash for a resolved value, keyed on datatype.

Parameters:

  • val (Object)

    the resolved value from #value

Returns:

  • (Hash)

    attribute hash with taxonomyAttributeId and value/values



174
175
176
177
178
179
180
181
182
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 174

def build_attribute_hash(val)
  case datatype
  when 'MULTI_CHOICE'
    values = val.is_a?(Array) ? val : [val]
    { taxonomyAttributeId: taxonomy_attribute_id, values: values.map(&:to_s) }
  else
    { taxonomyAttributeId: taxonomy_attribute_id, value: val.to_s }
  end
end

#can_build?Boolean

Check if this attribute can be built. Requires the resolved attribute to be
part of THIS class's schema (attribute_definition.present?) — a mapper
whose attribute isn't in the class schema never builds, which is what keeps
heater attributes off floor-heating listings.

Returns:

  • (Boolean)


86
87
88
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 86

def can_build?
  wayfair_schema.present? && attribute_definition.present?
end

#custom_values_allowed?Boolean

Check if custom values are allowed

Returns:

  • (Boolean)


110
111
112
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 110

def custom_values_allowed?
  attribute_definition&.dig('valueFormat', 'canValueBeCustomized') == true
end

#datatypeString?

Get the datatype (SINGLE_CHOICE, MULTI_CHOICE, DECIMAL, BOOLEAN, STRING, etc.)

Returns:

  • (String, nil)


98
99
100
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 98

def datatype
  attribute_definition&.dig('valueFormat', 'datatype')
end

#fetch_value(attribute, output_unit: nil) ⇒ Object? (protected)

Fetch a value from item specs or catalog_item/item attributes

Parameters:

  • attribute (Symbol)

    attribute/spec name to look up

  • output_unit (String, Symbol, nil) (defaults to: nil)

    unit to convert spec values to

Returns:

  • (Object, nil)

    the first non-nil value found



188
189
190
191
192
193
194
195
196
197
198
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 188

def fetch_value(attribute, output_unit: nil)
  # Try spec first (with wayfair_ prefix)
  v = item.spec_value(:"wayfair_#{attribute}", output_unit:) if item.respond_to?(:spec_value)
  v ||= item.spec_value(attribute, output_unit:) if item.respond_to?(:spec_value)

  # Fall back to direct attributes
  v ||= catalog_item.send(attribute) if catalog_item.respond_to?(attribute)
  v ||= item.send(attribute) if item.respond_to?(attribute)

  v
end

#format_boolean(val) ⇒ String? (protected)

Format a boolean value

Parameters:

  • val (Boolean, String, Integer)

    loosely-typed boolean value

Returns:

  • (String, nil)

    "Yes"/"No", or nil when unrecognized



219
220
221
222
223
224
225
226
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 219

def format_boolean(val)
  case val
  when true, 'true', 1, '1', 'yes', 'Yes'
    'Yes'
  when false, 'false', 0, '0', 'no', 'No'
    'No'
  end
end

#format_decimal(val) ⇒ String? (protected)

Format a decimal value (Wayfair expects strings for decimals)

Parameters:

  • val (Numeric, String, nil)

    the numeric value

Returns:

  • (String, nil)

    value rounded to 2 decimals as a string, or nil



210
211
212
213
214
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 210

def format_decimal(val)
  return nil if val.nil?

  val.to_f.round(2).to_s
end

#map_unit(unit) ⇒ String (protected)

Map a Ruby unit to Wayfair unit

Parameters:

  • unit (String)

    internal unit abbreviation (e.g., "in", "W")

Returns:

  • (String)

    the Wayfair unit name, or the input when unmapped



203
204
205
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 203

def map_unit(unit)
  UNIT_MAPPINGS[unit] || unit
end

#possible_valuesArray<String>

Get possible values for this attribute

Returns:

  • (Array<String>)


116
117
118
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 116

def possible_values
  wayfair_schema&.possible_values_for(taxonomy_attribute_id) || []
end

#requirementString?

Get the requirement level (REQUIRED, RECOMMENDED, OPTIONAL)

Returns:

  • (String, nil)


104
105
106
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 104

def requirement
  attribute_definition&.dig('requirement')
end

#valid_value?(val) ⇒ Boolean

Validate a value against possible values

Parameters:

  • val (Object)

    the candidate value (compared as a string)

Returns:

  • (Boolean)


123
124
125
126
127
128
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 123

def valid_value?(val)
  return true if custom_values_allowed?
  return true if possible_values.empty?

  possible_values.include?(val.to_s)
end

#valueObject?

Override in subclasses to provide the value

Returns:

  • (Object, nil)

    the raw attribute value (String, Array, Numeric, …)

Raises:

  • (NotImplementedError)

    when a subclass does not implement it



77
78
79
# File 'app/services/edi/wayfair/listing_generator/attributes/base_attribute.rb', line 77

def value
  raise NotImplementedError, "#{self.class.name} must implement #value"
end