Skip to content

Instantly share code, notes, and snippets.

@jakecraige
Created October 21, 2013 19:55
Show Gist options
  • Save jakecraige/7089865 to your computer and use it in GitHub Desktop.
Save jakecraige/7089865 to your computer and use it in GitHub Desktop.
class Product < ActiveRecord::Base
belongs_to :category
has_many :trackings
has_many :site_product_prices, -> { order(:id => :asc) }
has_many :user_price_alerts
after_create :extract_color_from_name, :update_brand_and_model_number_if_blank, :remove_false_values, :run_delayed_jobs
validates_presence_of :name
include Tire::Model::Search
index_name("worthit#{Rails.env}products")
mapping do
indexes :name, analyzer: 'snowball'
indexes :manufacturer, analyzer: 'snowball'
indexes :brand, analyzer: 'snowball'
end
after_commit { tire.delay.update_index }
after_destroy { tire.delay.update_index }
INVALID_TITLE_WORDS = %w(
size sz men mens men's man mans man's women womens women's woman womans
woman's unisex unisex's red black orange gray white yellow green blue
)
def to_s
name
end
def self.match_to_existing_product_or_initalize_new(source, filtered_product_attributes, api_product_attributes)
# Create the product attributes that combines what was returned from
# the api and the tracking
product_attributes = {}
# Loop the product columns and to populate the product_attributes hash
Product.column_names.each do |column_name|
if filtered_product_attributes[column_name].present?
# Don't call .to_sym on the column name because the
# filtered_product_attributes needs this format: filtered_product_attributes['attribute']
product_attributes[column_name.to_sym] = filtered_product_attributes[column_name]
end
# Overwrite the attribute if the api has info
if api_product_attributes.present?
if api_product_attributes[column_name.to_sym].present?
product_attributes[column_name.to_sym] = api_product_attributes[column_name.to_sym]
end
end # if api_product_attributes.present?
end
product = Product.where(product_attributes).first_or_initialize
return product# MATCHING TURNED OFF
# Search for existing product
# First, search only by UPC
not_found_product = true
if product_attributes[:upc].present? && product = Product.find_by(upc: product_attributes[:upc])
not_found_product = false
::Rails.logger.info '-------------- Product Match 1 --------'
end
# Search by Model number and Brand / Manufacturer & color and size
# Have to add color and size because some items have generic model numbers
# Make sure that these fields have info
search_hash = {}
search_hash[:color] = product_attributes[:color] if product_attributes[:color].present?
search_hash[:size] = product_attributes[:size] if product_attributes[:size].present?
search_hash[:model_number] = product_attributes[:model_number] if product_attributes[:model_number].present?
search_hash[:brand] = product_attributes[:manufacturer] if product_attributes[:manufacturer].present?
search_hash[:brand] = product_attributes[:brand] if product_attributes[:brand].present? # Overwrite manufacturer
# if not_found_product && product = Product.where(model_number: product_attributes[:model_number], color: product_attributes[:color], size: product_attributes[:size]).where("brand IN (:brand, :manufacturer) OR manufacturer IN (:brand, :manufacturer)", brand: product_attributes[:brand], manufacturer: product_attributes[:manufacturer]).first
# not_found_product = false
# ::Rails.logger.info '-------------- Product Match 2 --------'
# end
#
if not_found_product && search_hash.any?
if product = Product.where(search_hash).first
not_found_product = false
::Rails.logger.info '-------------- Product Match 2 --------'
end
end
# Search by name and model number
if not_found_product && product_attributes[:model_number].present? && product = Product.where(model_number: product_attributes[:model_number], name: product_attributes[:name]).first
not_found_product = false
::Rails.logger.info '-------------- Product Match 3 --------'
end
# Search by name, size and color and brand
search_hash = {}
search_hash[:color] = product_attributes[:color] if product_attributes[:color].present?
search_hash[:size] = product_attributes[:size] if product_attributes[:size].present?
search_hash[:brand] = product_attributes[:manufacturer] if product_attributes[:manufacturer].present?
search_hash[:brand] = product_attributes[:brand] if product_attributes[:brand].present? # Overwrite manufacturer
# if not_found_product && product = Product.find_by(name: product_attributes[:name], size: product_attributes[:size], color: product_attributes[:color], brand: product_attributes[:brand])
# not_found_product = false
# ::Rails.logger.info '-------------- Product Match 4 --------'
# end
#
if not_found_product && search_hash.any?
if product = Product.where(name: product_attributes[:name]).where(search_hash).first
not_found_product = false
::Rails.logger.info '-------------- Product Match 4 --------'
end
end
# If no product is found yet,
# intialize a product based on the tracking info
if not_found_product
product = Product.where(product_attributes).first_or_initialize
::Rails.logger.info '-------------- Product Match 5 --------'
else
# Update the product with the product_params
product.update_attributes_with_found_attributes(product_attributes)
::Rails.logger.info '-------------- Product Match 6 --------'
::Rails.logger.info ' product_attributes '
::Rails.logger.info product_attributes
end
# unless product = Product.find_by(upc: product_attributes[:upc])
#
# # Search by Model number and Brand / Manufacturer
# unless product = Product.where(model_number: product_attributes[:model_number]).where("brand IN (:brand, :manufacturer) OR manufacturer IN (:brand, :manufacturer)", brand: product_attributes[:brand], manufacturer: product_attributes[:manufacturer]).first
#
# # If no product is found yet,
# # intialize a product based on the tracking info
# product = Product.where(product_attributes).first_or_initialize
#
# end # model number
#
# end # unless product = Product.find_by(upc: upc)
product
end
# ------------------------------------------------------------------------------------------------------------
def update_attributes_with_found_attributes(product_attributes)
# Loop the product columns and update teh columns that are blank
Product.column_names.each do |column_name|
self[column_name.to_sym] = product_attributes[column_name.to_sym] if self[column_name.to_sym].blank?
end
self.save
end
# ------------------------------------------------------------------------------------------------------------
def check_current_price
# Lookup product at all places we know
# Always look for the asin
self.look_for_asin
# Amazon ----
if asin
::Rails.logger.info "------------ Starting to look up products on Amazon via ASIN lookup -----------"
asin_product_lookup = Amazon.asin_product_lookup(asin)
if asin_product_lookup
api_product_attributes = Amazon.product_attributes(asin_product_lookup)
SiteProductPrice.create_new(self, Site.amazon, api_product_attributes, 'amazon') if api_product_attributes
end
end
if upc.present? && upc != 'undefined' && upc != 'false' && upc != ''
# Shopzilla -----
product_lookup = Shopzilla.product_lookup(upc)
if product_lookup
::Rails.logger.info "------------ Starting to look up products on Shopzilla -----------"
Shopzilla.find_product_prices(self, product_lookup)
end
# Commission Junction -----
product_lookup = Commissionjunction.product_lookup(upc)
if product_lookup
::Rails.logger.info "------------ Starting to look up products on Commission Junction -----------"
Commissionjunction.find_product_prices(self, product_lookup)
end
# Become
product_lookup = Become.product_lookup(upc)
if product_lookup
::Rails.logger.info "------------ Starting to look up products on Become -----------"
Become.find_product_prices(self, product_lookup)
end
# Search UPC
product_lookup = Searchupc.product_lookup(upc)
if product_lookup
::Rails.logger.info "------------ Starting to look up products on Search UPC -----------"
Searchupc.find_product_prices(self, product_lookup)
end
# # PriceSpin
# product_lookup = Pricespin.product_lookup(upc)
# if product_lookup
# ::Rails.logger.info "------------ Starting to look up products on Pricespin -----------"
# Pricespin.delay.find_product_prices(self, product_lookup)
# end
end # if upc && upc != 'undefined'
# Start checking to see if this price matches a site product price
# TODO: make sure this runs after the above jobs are done
trackings.each do |tracking|
tracking.delay.match_price_to_tracking
end
end
# ------------------------------------------------------------------------------------------------------------
def self.create_product_without_api(product_params, site, user = nil)
product_attributes = {:name => product_params[:title]}
%w(manufacturer brand size color material gender width model_number upc ean asin manufacturer_part_number).each do |field|
product_attributes[field.to_sym] = product_params[field.to_sym] if product_params[field.to_sym].present?
end
product = Product.find_or_create_by(product_attributes)
# product.search_for_product_upc
product
end
# ------------------------------------------------------------------------------------------------------------
def search_for_prices
# Start by searching on amazon
# identifiers.each do |identifier|
# Amazon.offers_lookup(identifier.unique_id)
# end
# identifiers.each do |identifier|
# Amazon.search(identifier.unique_id)
# end
end
# ------------------------------------------------------------------------------------------------------------
def search_for_product_upc
# Search Become, Amazon, Semantics, etc for product UPC
# ----
# results = Amazon.keyword_search(self.search_name, 1)
#
# matched = self.find_product_match(results)
#
# unless matched
# page = 2
# total_pages = 2
# while page <= total_pages
# results = Amazon.keyword_search(self.search_name, page)
#
# matched = self.find_product_match(results)
#
# page = matched ? 1000 : page + 1
# end
# end
#
# return true if matched
# ----
# If not matched, try semantics 3
results = Semantics.keyword_search(self.search_name, 1)
matched = self.find_product_match(results)
# return true if matched
end
# ------------------------------------------------------------------------------------------------------------
def find_product_match(results)
this_products_brand = self.brand
this_products_manufacturer = self.manufacturer
this_products_size = self.size
this_products_color = self.color
this_products_compare_title = Product.compare_title(self.name, this_products_brand, this_products_manufacturer, this_products_size, this_products_color)
# Compare Brand, Manufacturer, Size, Color, and Title
results.each do |result|
match = false
result_title = result[:title]
compare_title = Product.compare_title(result_title, this_products_brand, this_products_manufacturer, this_products_size, this_products_color)
::Rails.logger.info "------------ Compare title -----------"
::Rails.logger.info this_products_compare_title
::Rails.logger.info compare_title
::Rails.logger.info " "
if this_products_compare_title == compare_title
result_size = result[:size]
result_color = result[:color]
# compare the rest of the attributes
if result_brand == self.brand && result_manufacturer == self.manufacturer
match = true
if self.size.present? && result_size != self.size
# size: 11.5 D(M) US
match = false
end
if match && self.color.present? && result_color != self.color
match = false
end
end
# matches << result if match
if match
self.update_column(:upc, result[:upc])
self.update_column(:ean, result[:ean])
return true
end
end # if this_products_compare_title == compare_title
end # results
false
end
# ------------------------------------------------------------------------------------------------------------
# Returns a title with invalid attributes removed for comparison
def self.compare_title(title, brand, manufacturer, size, color)
regex = /[^0-9A-Za-z]/
remove_invalid_title_words!(title)
title = title.downcase.gsub(regex, '')
brand = brand.to_s.downcase.gsub(regex, '')
manufacturer = manufacturer.to_s.downcase.gsub(regex, '')
size = size.to_s.downcase.gsub(regex, '')
color = color.to_s.downcase.gsub(regex, '')
title.gsub(brand, '').gsub(manufacturer,'').gsub(size,'').gsub(color,'')
end
# ------------------------------------------------------------------------------------------------------------
def set_search_name
this_search_name = name.gsub(/[^0-9A-Za-z 9 \/\()-]/, '')
self.update_column(:search_name, this_search_name)
end
def self.clean_price(price)
return price if price.blank?
price.to_s.strip.gsub('$','').gsub(',','').to_f
end
def self.clear_all_products
# [email protected]
# Product.destroy_all
# Tracking.destroy_all
# SiteProductPrice.destroy_all
# UserPriceAlert.destroy_all
# Delayed::Job.destroy_all
# Tag.destroy_all
end
def best_site_product_price
best_price_cache.best_site_product_price
end
def best_price
best_price_cache.best_price
end
# TODO: refactor to make better
def image_path
if image_url.present?
# Ensure http:// is on the image
full_image_url = self.image_url
full_image_url = 'http://' + full_image_url if full_image_url.exclude?('http://')
return full_image_url
end
self.site_product_prices.each do |site_product|
return site_product.image_url if site_product.has_image?
end
nil # return nil and let calling function handle default image
end
def self.search(search_string, user)
# map out this users products so we are able to exclude products that don't
# match from the results
tracked_product_ids = user.trackings.select(:product_id).map(&:product_id)
results = tire.search(load: true) do
query { string search_string.match(/\w+/).to_s }
end
results.select { |product| tracked_product_ids.include? product.id }
end
def extract_color_from_name
return # MATCHING TURNED OFF
return if color?
# TODO: refactor - this is ugly
downcase_name = name.downcase
colors = ["grey", "espresso", "alice blue", "antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanched almond","blue","blue violet","brown","burlywood","cadet blue","chartreuse","chocolate","coral","cornflower blue","cornsilk","crimson","cyan","dark blue","dark cyan","dark goldenrod","dark gray","dark green","dark khaki","dark magenta","dark olive green","dark orange","dark orchid","dark red","dark salmon","dark sea green","dark slate blue","dark slate gray","dark turquoise","dark violet","deep pink","deep sky blue","dim gray","dodger blue","firebrick","floral white","forest green","fuchsia","gainsboro","ghost white","gold","goldenrod","gray","green","green yellow","honeydew","hot pink","indian red","indigo","ivory","khaki","lavender","lavender blush","lawn green","lemon chiffon","light blue","light coral","light cyan","light goldenrod yellow","light gray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","medium aquamarine","medium blue","medium orchid","medium purple","medium seag reen","medium slate blue","medium sprin ggreen","medium turquoise","medium violetred","midnight blue","mint cream","misty rose","moccasin","navajo white","navy","oldlace","olive","olive drab","orange","orange red","orchid","pale goldenrod","pale green","pale turquoise","pale violet red","papaya whip","peach puff","peru","pink","plum","powderblue","purple","red","rosy brown","royal blue","saddle brown","salmon","sandy brown","sea green","seashell","sienna","silver","sky blue","slate blue","slate gray","snow","spring green","steel blue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","white smoke","yellow","yellow green"]
colors.each do |this_color|
if downcase_name.include?(' ' + this_color + ' ')
self.update_column(:color, this_color.titleize)
return
end
end
end
def update_brand_and_model_number_if_blank
# Manufacturer are usually the same thing
# Populate brand if manufacturer is set
self.update_column(:brand, manufacturer) if brand.blank? && manufacturer?
self.update_column(:model_number, manufacturer_part_number) if model_number.blank? && manufacturer_part_number?
end
def remove_false_values
# TODO: find why "false" is being entered and remove it
self.update_column(:model_number, nil) if model_number == 'false'
end
def run_delayed_jobs
# Run these as delayed jobs so we cut down on live api calls
self.delay.get_upc_from_amazon_by_model_number
self.delay.look_for_asin
end
def get_upc_from_amazon_by_model_number
return # MATCHING TURNED OFF
return if upc?
if Amazon.model_number_lookup(self)
self.look_for_asin
end
end
def look_for_asin
return if asin? || !upc?
if asin = Amazon.asin_lookup_by_upc(upc)
self.update_column(:asin, asin)
# Search for the product on Amazon
asin_product_lookup = Amazon.asin_product_lookup(asin)
if asin_product_lookup
api_product_attributes = Amazon.product_attributes(asin_product_lookup)
SiteProductPrice.create_new(self, Site.amazon, api_product_attributes, 'amazon') if api_product_attributes
end
end
end
def top_retailers
retailers = self.site_product_prices.last_24_hours.trusted.by_price
retailers = retailers.group_by { |retailer| retailer.site }
.map(&:last)
.each{ |sites_array| sites_array.sort_by! { |site| site.weight }.reverse! }
.map(&:first).reverse
.sort_by { |x| x.price }
if retailers.count > 1
# remove best price from results
retailers.shift
elsif retailers.count == 1
# If the first retailor is the same as the best price,
# don't use it
if retailers.first.site == self.best_site_product_price.site
retailers.shift
end
end
# If the top retailers is blank
# and the origiinal site product price is not the same as the best price
# add the originail site product price
if retailers.blank?
first_site_product_price = self.site_product_prices.first
if first_site_product_price.site != self.best_site_product_price.site
retailers = [first_site_product_price]
end
end
retailers[0..5] # return max of 6 results
end
def self.remove_invalid_title_words!(title)
INVALID_TITLE_WORDS.each do |word|
title.gsub!(/\b(#{word}|'s)\b/i, '')
end
end
private
def best_price_cache
@best_price_cache ||= ProductBestPriceCache.new(self)
end
def group_by_site! retailers
retailers = retailers.group_by { |retailer| retailer.site }
end
def sort_by_weight! retailers
retailers = retailers.map(&:last).map(&:first)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment