spree_api 5.4.0.rc3 → 5.4.0.rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 314545a7037d2f446a6d764b8d130a6ba14c89cfdb82c380132595f74aa62435
4
- data.tar.gz: 5976dd28153ffc543f658ca56fab3a2d97cefb162ec25481e192deb599743939
3
+ metadata.gz: 6509e525568a3b8ea64e7f0f798f4aa97269c1f050d8df3f8a0674fc53001935
4
+ data.tar.gz: 38bb74377c55de6b865eb28a1014fce6e5744714f44e8bf9484403e92d935044
5
5
  SHA512:
6
- metadata.gz: 6856dc5fbe6411874444f3bc9ea16b4047ab11470d85b625daca9e054bc6289a37ac023a1fa899a6e9199aebfe1e6a2b1b4f3716611d34bc80ad829e0c57cf04
7
- data.tar.gz: ab453e9b05648c4ce61feca5febcaf815d898b17f098696b27dff28d006d015130987d9d8233532b5b74d4fb5c31f6e3f7d12ca4fac10d6ac070bfbb5a4f384b
6
+ metadata.gz: d748f15cd7c17a7c1bf1f9bc0bec2b22e4326c8a5088ab6989e5904ea2c315f0dad52035f6407a2995728e35ce2cf2b1adfb35f48a025d13007e658f4ee16ad1
7
+ data.tar.gz: c0c909f704cbdd2088b15fde911e7ceb4b919ea233c2145406bdac76e2e615046cd2b82a54c0992a4bdb518930123cd3e9c5b2a2fb4eb31d05bf98232a177cb7
@@ -2,13 +2,17 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  class FiltersAggregator
5
- # @param scope [ActiveRecord::Relation] Base product scope (already filtered by store, availability, category, etc.)
5
+ # @param scope [ActiveRecord::Relation] Base product scope (fully filtered, including option values)
6
6
  # @param currency [String] Currency for price range
7
7
  # @param category [Spree::Category, nil] Optional category for default_sort and category filtering context
8
- def initialize(scope:, currency:, category: nil)
8
+ # @param option_value_ids [Array<String>] Currently selected option value prefixed IDs (for disjunctive facet counts)
9
+ # @param scope_before_options [ActiveRecord::Relation] Scope before option value filters (for disjunctive counts)
10
+ def initialize(scope:, currency:, category: nil, option_value_ids: [], scope_before_options: nil)
9
11
  @scope = scope
10
12
  @currency = currency
11
13
  @category = category
14
+ @option_value_ids = option_value_ids
15
+ @scope_before_options = scope_before_options || scope
12
16
  end
13
17
 
14
18
  def call
@@ -78,25 +82,26 @@ module Spree
78
82
 
79
83
  def option_type_filters
80
84
  Spree::OptionType.filterable.includes(:option_values).order(:position).filter_map do |option_type|
81
- values = option_type.option_values.for_products(@scope).distinct.order(:position)
85
+ # Disjunctive: count against scope WITHOUT this option type's filter
86
+ count_scope = disjunctive_scope_for(option_type)
87
+ values = option_type.option_values.for_products(count_scope).distinct.order(:position)
82
88
  next if values.empty?
83
89
 
90
+ count_ids = count_scope.reorder('').distinct.pluck(:id)
91
+
84
92
  {
85
93
  id: option_type.prefixed_id,
86
94
  type: 'option',
87
95
  name: option_type.name,
88
96
  label: option_type.label,
89
- options: values.map { |ov| option_value_data(option_type, ov) }
97
+ options: values.map { |ov| option_value_data(count_ids, ov) }
90
98
  }
91
99
  end
92
100
  end
93
101
 
94
- def option_value_data(option_type, option_value)
95
- # Count products in scope that have this option value
96
- # We use a subquery approach to avoid GROUP BY conflicts when scope includes joins (like in_category)
97
- # Join directly to option_value_variants for efficiency (skips joining through option_values table)
102
+ def option_value_data(product_ids, option_value)
98
103
  count = Spree::Product
99
- .where(id: base_scope_product_ids)
104
+ .where(id: product_ids)
100
105
  .joins(:option_value_variants)
101
106
  .where(Spree::OptionValueVariant.table_name => { option_value_id: option_value.id })
102
107
  .distinct
@@ -111,8 +116,38 @@ module Spree
111
116
  }
112
117
  end
113
118
 
114
- def base_scope_product_ids
115
- @base_scope_product_ids ||= @scope.reorder('').distinct.pluck(:id)
119
+ # Returns the scope with all option type filters EXCEPT the given one applied.
120
+ # This gives disjunctive counts: selecting Blue still shows Red's true count.
121
+ def disjunctive_scope_for(option_type)
122
+ return @scope_before_options if grouped_selected_options.empty?
123
+
124
+ other_groups = grouped_selected_options.except(option_type.id)
125
+
126
+ # If this type has selections but no other types do, use scope before any option filters
127
+ return @scope_before_options if other_groups.empty?
128
+
129
+ # Rebuild: start from scope before options, apply only other option types
130
+ scope = @scope_before_options
131
+ other_groups.each_value do |ov_ids|
132
+ matching = Spree::Variant.where(deleted_at: nil)
133
+ .joins(:option_value_variants)
134
+ .where(Spree::OptionValueVariant.table_name => { option_value_id: ov_ids })
135
+ .select(:product_id)
136
+ scope = scope.where(id: matching)
137
+ end
138
+ scope
139
+ end
140
+
141
+ # Group selected option value IDs by option type (cached, single query)
142
+ def grouped_selected_options
143
+ @grouped_selected_options ||= begin
144
+ return {} if @option_value_ids.blank?
145
+
146
+ decoded = @option_value_ids.filter_map { |id| Spree::OptionValue.decode_prefixed_id(id) }
147
+ return {} if decoded.empty?
148
+
149
+ Spree::OptionValue.where(id: decoded).group_by(&:option_type_id).transform_values { |ovs| ovs.map(&:id) }
150
+ end
116
151
  end
117
152
 
118
153
  def category_filter
@@ -49,22 +49,31 @@ module Spree
49
49
  private
50
50
 
51
51
  def make_request
52
- SsrfFilter.post(
53
- @delivery.url,
54
- headers: {
55
- 'Content-Type' => 'application/json',
56
- 'User-Agent' => 'Spree-Webhooks/1.0',
57
- 'X-Spree-Webhook-Signature' => generate_signature,
58
- 'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
59
- 'X-Spree-Webhook-Event' => @delivery.event_name
60
- },
61
- body: @delivery.payload.to_json,
62
- http_options: {
63
- open_timeout: TIMEOUT,
64
- read_timeout: TIMEOUT,
65
- verify_mode: ssl_verify_mode
66
- }
67
- )
52
+ headers = {
53
+ 'Content-Type' => 'application/json',
54
+ 'User-Agent' => 'Spree-Webhooks/1.0',
55
+ 'X-Spree-Webhook-Signature' => generate_signature,
56
+ 'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
57
+ 'X-Spree-Webhook-Event' => @delivery.event_name
58
+ }
59
+ body = @delivery.payload.to_json
60
+ http_options = { open_timeout: TIMEOUT, read_timeout: TIMEOUT, verify_mode: ssl_verify_mode }
61
+
62
+ # SSRF protection is disabled in development so webhooks can reach
63
+ # localhost / host.docker.internal (the storefront running on the host).
64
+ if Rails.env.development?
65
+ uri = URI.parse(@delivery.url)
66
+ http = Net::HTTP.new(uri.host, uri.port)
67
+ http.use_ssl = uri.scheme == 'https'
68
+ http_options.each { |k, v| http.send(:"#{k}=", v) }
69
+
70
+ request = Net::HTTP::Post.new(uri.request_uri)
71
+ headers.each { |k, v| request[k] = v }
72
+ request.body = body
73
+ http.request(request)
74
+ else
75
+ SsrfFilter.post(@delivery.url, headers: headers, body: body, http_options: http_options)
76
+ end
68
77
  end
69
78
 
70
79
  def generate_signature
@@ -21,13 +21,15 @@ module Spree
21
21
  return unless Spree::Api::Config.webhooks_enabled
22
22
  return if event.store_id.blank?
23
23
 
24
- # Find all active endpoints for this store subscribed to this event
25
- endpoints = Spree::WebhookEndpoint.active.where(store_id: event.store_id).select { |endpoint| endpoint.subscribed_to?(event.name) }
24
+ # Only load the columns we need for matching and delivery
25
+ endpoints = Spree::WebhookEndpoint
26
+ .enabled
27
+ .where(store_id: event.store_id)
28
+ .select(:id, :subscriptions)
26
29
 
27
- return if endpoints.empty?
28
-
29
- # Queue delivery for each endpoint
30
30
  endpoints.each do |endpoint|
31
+ next unless endpoint.subscribed_to?(event.name)
32
+
31
33
  queue_delivery(endpoint, event)
32
34
  end
33
35
  rescue StandardError => e
@@ -38,17 +40,25 @@ module Spree
38
40
  private
39
41
 
40
42
  def queue_delivery(endpoint, event)
41
- # Build base payload (without delivery ID)
42
43
  payload = build_payload(event)
43
44
 
44
- # Create delivery record
45
+ # Deduplicate: skip if we already have a delivery for this event + endpoint
46
+ if event.id.present?
47
+ return if Spree::WebhookDelivery.exists?(
48
+ webhook_endpoint_id: endpoint.id,
49
+ event_id: event.id
50
+ )
51
+ end
52
+
45
53
  delivery = endpoint.webhook_deliveries.create!(
46
54
  event_name: event.name,
55
+ event_id: event.id,
47
56
  payload: payload
48
57
  )
49
58
 
50
- # Queue the delivery job
51
59
  Spree::WebhookDeliveryJob.perform_later(delivery.id)
60
+ rescue ActiveRecord::RecordNotUnique
61
+ # Race condition: another thread already created this delivery — safe to ignore
52
62
  rescue StandardError => e
53
63
  Rails.logger.error "[Spree Webhooks] Error queuing delivery for endpoint #{endpoint.id}: #{e.message}"
54
64
  Rails.error.report(e)
data/config/routes.rb CHANGED
@@ -27,9 +27,7 @@ Spree::Core::Engine.add_routes do
27
27
  get :filters, to: 'products/filters#index'
28
28
  end
29
29
  end
30
- resources :categories, only: [:index, :show], id: /.+/ do
31
- resources :products, only: [:index], controller: 'categories/products'
32
- end
30
+ resources :categories, only: [:index, :show], id: /.+/
33
31
 
34
32
  # Carts
35
33
  resources :carts, only: [:index, :show, :create, :update, :destroy] do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.0.rc3
4
+ version: 5.4.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-25 00:00:00.000000000 Z
11
+ date: 2026-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,28 +72,28 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 0.10.0
75
+ version: 0.11.0
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 0.10.0
82
+ version: 0.11.0
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: spree_core
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: 5.4.0.rc3
89
+ version: 5.4.0.rc4
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - '='
95
95
  - !ruby/object:Gem::Version
96
- version: 5.4.0.rc3
96
+ version: 5.4.0.rc4
97
97
  description: Spree's API
98
98
  email:
99
99
  - hello@spreecommerce.org
@@ -129,7 +129,6 @@ files:
129
129
  - app/controllers/spree/api/v3/store/carts/payments_controller.rb
130
130
  - app/controllers/spree/api/v3/store/carts/store_credits_controller.rb
131
131
  - app/controllers/spree/api/v3/store/carts_controller.rb
132
- - app/controllers/spree/api/v3/store/categories/products_controller.rb
133
132
  - app/controllers/spree/api/v3/store/categories_controller.rb
134
133
  - app/controllers/spree/api/v3/store/countries_controller.rb
135
134
  - app/controllers/spree/api/v3/store/currencies_controller.rb
@@ -274,9 +273,9 @@ licenses:
274
273
  - BSD-3-Clause
275
274
  metadata:
276
275
  bug_tracker_uri: https://github.com/spree/spree/issues
277
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.rc3
276
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.rc4
278
277
  documentation_uri: https://docs.spreecommerce.org/
279
- source_code_uri: https://github.com/spree/spree/tree/v5.4.0.rc3
278
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.0.rc4
280
279
  post_install_message:
281
280
  rdoc_options: []
282
281
  require_paths:
@@ -1,37 +0,0 @@
1
- module Spree
2
- module Api
3
- module V3
4
- module Store
5
- module Categories
6
- class ProductsController < Store::ProductsController
7
- before_action :set_category
8
-
9
- protected
10
-
11
- def set_category
12
- @category = find_category
13
- end
14
-
15
- def scope
16
- super.in_category(@category)
17
- end
18
-
19
- private
20
-
21
- def find_category
22
- id = params[:category_id]
23
- category_scope = Spree::Category.for_store(current_store).accessible_by(current_ability, :show)
24
- category_scope = category_scope.i18n if Spree::Category.include?(Spree::TranslatableResource)
25
-
26
- if id.to_s.start_with?('ctg_')
27
- category_scope.find_by_prefix_id!(id)
28
- else
29
- find_with_fallback_default_locale { category_scope.i18n.find_by!(permalink: id) }
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end