module Workarea module Catalog class Product include ApplicationDocument include Mongoid::Document::Taggable include Releasable include Navigable include Commentable include Details field :_id, type: String, default: -> { SecureRandom.hex(5).upcase } field :name, type: String, localize: true field :filters, type: Hash, default: {}, localize: true field :template, type: String, default: 'generic' field :digital, type: Boolean, default: false field :customizations, type: String field :browser_title, type: String, localize: true field :meta_description, type: String, localize: true field :description, type: String, localize: true field :last_indexed_at, type: Time field :purchasable, type: Boolean, default: true field :default_category_id, type: String index({ 'variants.sku': 1 }) index({ last_indexed_at: 1 }) index({ 'images.option': 1 }) index(purchasable: 1) index(created_at: 1) embeds_many :variants, class_name: 'Workarea::Catalog::Variant' embeds_many :images, class_name: 'Workarea::Catalog::ProductImage' belongs_to :copied_from, class_name: 'Workarea::Catalog::Product', optional: true validate :no_type_filter validates :name, presence: true scope :recent, ->(l = 5) { order_by([:created_at, :desc]).limit(l) } scope :purchasable, -> { where(purchasable: true) } before_validation :ensure_template def self.sorts [Sort.newest, Sort.modified, Sort.sales, Sort.name_asc, Sort.name_desc] end # Finds the first product that has the given SKU. Tries to match exactly, # but will look for a case-insensitive match if exact match isn't found. # # @param sku [String] # @return [Catalog::Product, nil] # def self.find_by_sku(sku) return unless sku.present? begin find_by('variants.sku' => sku) rescue Mongoid::Errors::DocumentNotFound regex = /^#{::Regexp.quote(sku)}$/i find_by('variants.sku' => regex) rescue nil end end # Find any and all products which have a variant set with the SKU, for # the purposes of updating search indexes, caches, etc. # # @param sku [String] # @return [Array] # def self.find_for_update_by_sku(sku) where('variants.sku' => sku) end # Finds all products that were updated: # * after options[:start], if supplied # * before options[:end], if supplied # # If neither are supplied, then all products are returned. # # @param options [Hash] # @return [Mongoid::Criteria] # def self.find_by_update(options) context = all context = context.where(:updated_at.gte => options[:start]) if options[:start].present? context = context.where(:updated_at.lte => options[:end]) if options[:end].present? context end # Finds the corresponding products for the given ids that are active # and returns them in the same order. # # @return [Array] representing Catalog::Product#id # @return [Array] of Catalog::Product # def self.find_ordered_for_display(*ids) find_ordered(ids).select(&:active?) end def self.autocomplete_image_options(string) regex = /#{::Regexp.quote(string)}/i where('images.option' => regex).map do |product| product.images.where(option: regex).map(&:option) end.flatten.map(&:titleize).uniq end def categories Category.any_in(product_ids: id) end def category_ids categories.map(&:id) end def skus variants.map(&:sku).uniq end def active? super && variants.active.any? end # Whether to allow purchasing on this product. This is always false if # there are no variants associated with the product because of all the # detail page and cart logic depending on the existence of SKUs and # variants. # # @return [Boolean] # def purchasable? variants.active.blank? ? false : super end private def no_type_filter self.filters.keys.each do |key| if key =~ /^type$/i errors.add( :filters, I18n.t('workarea.errors.messages.contains_type') ) end end end def ensure_template self.template = 'generic' if template.blank? end end end end