Class: Item::ArticleRetriever

Inherits:
BaseService show all
Defined in:
app/services/item/article_retriever.rb

Overview

Retrieves all the articles associated with a particular item.

Defined Under Namespace

Classes: Result

Constant Summary collapse

ARTICLE_TYPE_TO_STI =

Article type to sti.

{
  'faq' => 'ArticleFaq',
  'technical' => 'ArticleTechnical',
  'training' => 'ArticleTraining',
  'procedure' => 'ArticleProcedure',
  'blog_post' => 'Post'
}.freeze

Instance Attribute Summary collapse

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

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

Constructor Details

#initialize(article_types:, sales: nil, support: nil, add_vote_data: false) ⇒ ArticleRetriever

==== Initialization options

  • +:article_types+ - required, filter by STI type. Accepts old enum names (:faq, :technical, etc.)
  • +:sales+ - true/false or nil for any
  • +:support+ - true/false or nil for any
  • +:add_vote_data+ - true/false. If true, custom columns vote_count and positive_votes will be added


28
29
30
31
32
33
34
35
# File 'app/services/item/article_retriever.rb', line 28

def initialize(article_types:, sales: nil, support: nil, add_vote_data: false)
  raise "Invalid article types, must be a non-empty array." unless article_types.present? && article_types.is_a?(Array)

  @sti_types = article_types.filter_map { |at| ARTICLE_TYPE_TO_STI[at.to_s] || at.to_s }
  @sales = sales
  @support = support
  @add_vote_data = add_vote_data
end

Instance Attribute Details

#add_vote_dataObject (readonly)

Returns the value of attribute add_vote_data.



19
20
21
# File 'app/services/item/article_retriever.rb', line 19

def add_vote_data
  @add_vote_data
end

#salesObject (readonly)

Returns the value of attribute sales.



19
20
21
# File 'app/services/item/article_retriever.rb', line 19

def sales
  @sales
end

#sti_typesObject (readonly)

Returns the value of attribute sti_types.



19
20
21
# File 'app/services/item/article_retriever.rb', line 19

def sti_types
  @sti_types
end

#supportObject (readonly)

Returns the value of attribute support.



19
20
21
# File 'app/services/item/article_retriever.rb', line 19

def support
  @support
end

Instance Method Details

#process(item: nil, product_line: nil, product_category: nil, tags: nil) ⇒ Result

Retrieves all product specifications available to a given item

==== Parameters

  • +item+ - The item for which you want to retrieve articles

==== Returns

A Result object with

Retrieves Article records matching the provided context and configured filters.

Applies configured STI types, sales/support flags, and optional vote enrichment; can filter by tags and by item, product line, and/or product category context, preloads tag associations, and returns a sorted, deduplicated set of articles.

Parameters:

  • item (Object, nil) (defaults to: nil)
    • An item object whose sku will be used to scope articles; if present and it has a primary_product_line, product-line-level articles for that product line (and its category) are also included.
  • product_line (Object, nil) (defaults to: nil)
    • A product line object whose slug_ltree (via slug_ltree.to_s) and product_category_ids are used to scope and optionally restrict articles.
  • product_category (Object, nil) (defaults to: nil)
    • A product category object whose id is used to restrict articles to matching category(s).
  • tags (String, Array<String>, nil) (defaults to: nil)
    • One or more tag names (string or array) used to filter articles; tag names are normalized (trimmed, downcased) before matching.

Returns:

  • (Result)

    Result object with :articles set to an array of Article instances matching the requested filters and context.



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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/services/item/article_retriever.rb', line 56

def process(item: nil, product_line: nil, product_category: nil, tags: nil)
  articles = []
  article_scope = Article.all
  article_scope = article_scope.where(type: sti_types) if sti_types.present?
  article_scope = article_scope.where(sales: sales) unless sales.nil?
  article_scope = article_scope.where(support: support) unless support.nil?
  article_scope = article_scope.with_votes if add_vote_data
  # `tagged_with` called on the Article base class generates taggable_type = 'Article',
  # but taggings are stored using the actual STI class name (e.g. 'ArticleFaq', 'Post').
  # Build a correlated EXISTS subquery with the correct taggable_type(s) explicitly.
  if tags.present?
    tag_names = Array(tags).flatten.filter_map(&:presence).map { |t| t.to_s.strip.downcase }.uniq
    taggable_types = sti_types.presence || [Article.base_class.name]
    tag_subquery = Tagging
                   .where(Tagging[:taggable_id].eq(Article.arel_table[:id]))
                   .where(taggable_type: taggable_types)
                   .joins(:tag)
                   .where(Tag[:name].lower.in(tag_names))
                   .select('1')
    article_scope = article_scope.where("EXISTS (#{tag_subquery.to_sql})")
  end
  # Preload tags for efficient rendering with filter badges
  # Use preload (not includes) to avoid GROUP BY conflicts with with_votes scope
  article_scope = article_scope.preload(taggings: :tag)
  # Retrieve articles tagged to this specific item
  if item
    article_scope = article_scope.for_item_sku(item.sku)
    articles = article_scope.to_a
    if item.primary_product_line
      pl_articles = process(product_line: item.primary_product_line, product_category: item.product_category).articles
      articles += pl_articles
      # since we added product line articles, we have to resort now using both item and product line in our array
    end
  elsif product_line
    article_scope = article_scope.for_product_line_url_without_order(product_line.slug_ltree.to_s)
    # A product category can only be applied if a product line is specified
    # Match articles tagged for the relevant category OR articles with no
    # category restriction (empty array sentinel).
    if product_category
      article_scope = article_scope.where(
        "articles.applies_to_product_category_ids && ARRAY[?]::integer[] OR articles.applies_to_product_category_ids = '{}'",
        [product_category.id]
      )
    elsif (pc_ids = product_line.product_category_ids.compact).present?
      article_scope = article_scope.where(
        "articles.applies_to_product_category_ids && ARRAY[?]::integer[] OR articles.applies_to_product_category_ids = '{}'",
        pc_ids
      )
    end
    articles = article_scope.to_a
  else
    articles = article_scope.to_a
  end

  articles = if add_vote_data
               articles.sort_by { |a| [-(a.positive_votes.to_i + a.sort_booster.to_i), -(a.vote_count.to_i + a.sort_booster.to_i), a.subject] }.uniq
             else
               articles.sort_by(&:subject)
             end

  Result.new(articles: articles)
end