spree_api 5.4.1 → 5.4.2

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: 83149ce5fb5f298d828f2266bafc070b187ae4991b58e8d0f818ffc97c86d3f1
4
- data.tar.gz: 174d77410c9c542fa39c0aaa8f46043013f49a7ea65ac441e28d08ee388a052e
3
+ metadata.gz: 4b295d2c49585c2fd0368b2b31bb782e19b77af7c4580e243896257151f5fd47
4
+ data.tar.gz: fea6f271b52ec0218ab2991e4bdaaa496d465a1fc2af7ada90bfee238de2216a
5
5
  SHA512:
6
- metadata.gz: e5b4ae5e127f166c53b143148277ff4d096db433553834f0e6f6f752ae2c7956b9066fd5f96061fab1eff2d74fffaeefa61309aee56284fae3d0b6c8c41c5c89
7
- data.tar.gz: dd94385142e6fd7fdb2bbdc579089d0ad69e22c35a8cb3ccb80b46d6775ade8cab7eb6d6ebd92225ed795d7bda0916911d671bba6fdb0b8b70c03d80686d6249
6
+ metadata.gz: d256c6250da995fd8762ed02f924e0d8f4c97fc3f39c3d94c9fc21b43b2d1878da871c0baff222e33d8d5c42b043878f6ec6aeea7baee6e3963fadb7bc6449b4
7
+ data.tar.gz: 4c30eff24e0d890cc2cb967e7f6cb0c476783324ea9724d46dc4fb3532bcd37400e81d07640e1e3b764e283ca3bf23c7f3148d7183060eb47b94ce238c129775
@@ -46,11 +46,11 @@ module Spree
46
46
 
47
47
  # Temporary — Spree 6 removes the checkout state machine.
48
48
  def try_advance
49
- return if @cart.complete? || @cart.canceled?
49
+ return if @cart.confirm? || @cart.complete? || @cart.canceled?
50
50
 
51
51
  loop do
52
+ break if @cart.payment?
52
53
  break unless @cart.next
53
- break if @cart.confirm? || @cart.complete?
54
54
  end
55
55
  rescue StandardError => e
56
56
  Rails.error.report(e, context: { order_id: @cart.id, state: @cart.state }, source: 'spree.checkout')
@@ -5,24 +5,27 @@ module Spree
5
5
  module Carts
6
6
  class PaymentSessionsController < Store::BaseController
7
7
  include Spree::Api::V3::CartResolvable
8
+ include Spree::Api::V3::OrderLock
8
9
 
9
10
  before_action :find_cart!
10
11
  before_action :set_payment_session, only: [:show, :update, :complete]
11
12
 
12
13
  # POST /api/v3/store/carts/:cart_id/payment_sessions
13
14
  def create
14
- payment_method = current_store.payment_methods.find_by_prefix_id!(permitted_params[:payment_method_id])
15
-
16
- @payment_session = payment_method.create_payment_session(
17
- order: @cart,
18
- amount: permitted_params[:amount],
19
- external_data: permitted_params[:external_data] || {}
20
- )
21
-
22
- if @payment_session.persisted?
23
- render json: serialize_resource(@payment_session), status: :created
24
- else
25
- render_errors(@payment_session.errors)
15
+ with_order_lock do
16
+ payment_method = current_store.payment_methods.find_by_prefix_id!(permitted_params[:payment_method_id])
17
+
18
+ @payment_session = payment_method.create_payment_session(
19
+ order: @cart,
20
+ amount: permitted_params[:amount],
21
+ external_data: permitted_params[:external_data] || {}
22
+ )
23
+
24
+ if @payment_session.persisted?
25
+ render json: serialize_resource(@payment_session), status: :created
26
+ else
27
+ render_errors(@payment_session.errors)
28
+ end
26
29
  end
27
30
  end
28
31
 
@@ -33,30 +36,43 @@ module Spree
33
36
 
34
37
  # PATCH /api/v3/store/carts/:cart_id/payment_sessions/:id
35
38
  def update
36
- @payment_session.payment_method.update_payment_session(
37
- payment_session: @payment_session,
38
- amount: permitted_params[:amount],
39
- external_data: permitted_params[:external_data] || {}
40
- )
41
-
42
- if @payment_session.errors.empty?
43
- render json: serialize_resource(@payment_session.reload)
44
- else
45
- render_errors(@payment_session.errors)
39
+ with_order_lock do
40
+ @payment_session.reload
41
+
42
+ @payment_session.payment_method.update_payment_session(
43
+ payment_session: @payment_session,
44
+ amount: permitted_params[:amount],
45
+ external_data: permitted_params[:external_data] || {}
46
+ )
47
+
48
+ if @payment_session.errors.empty?
49
+ render json: serialize_resource(@payment_session.reload)
50
+ else
51
+ render_errors(@payment_session.errors)
52
+ end
46
53
  end
47
54
  end
48
55
 
49
56
  # PATCH /api/v3/store/carts/:cart_id/payment_sessions/:id/complete
50
57
  def complete
51
- @payment_session.payment_method.complete_payment_session(
52
- payment_session: @payment_session,
53
- params: complete_params
54
- )
55
-
56
- if @payment_session.errors.empty?
57
- render json: serialize_resource(@payment_session.reload)
58
- else
59
- render_errors(@payment_session.errors)
58
+ with_order_lock do
59
+ @payment_session.reload
60
+
61
+ if @payment_session.completed?
62
+ render json: serialize_resource(@payment_session)
63
+ return
64
+ end
65
+
66
+ @payment_session.payment_method.complete_payment_session(
67
+ payment_session: @payment_session,
68
+ params: complete_params
69
+ )
70
+
71
+ if @payment_session.errors.empty?
72
+ render json: serialize_resource(@payment_session.reload)
73
+ else
74
+ render_errors(@payment_session.errors)
75
+ end
60
76
  end
61
77
  end
62
78
 
@@ -77,7 +93,8 @@ module Spree
77
93
  private
78
94
 
79
95
  def set_payment_session
80
- @payment_session = @cart.payment_sessions.find_by_prefix_id!(params[:id])
96
+ @payment_session = @cart.payment_sessions.find_by_prefix_id(params[:id]) ||
97
+ @cart.payment_sessions.find_by!(external_id: params[:id])
81
98
  end
82
99
 
83
100
  def serialize_resource(resource)
@@ -7,24 +7,48 @@ module Spree
7
7
  include Spree::Api::V3::Store::SearchProviderSupport
8
8
 
9
9
  def index
10
- result = search_provider.search_and_filter(
11
- scope: filters_scope,
12
- query: search_query,
13
- filters: (search_filters || {}).merge('_category' => category),
14
- page: 1,
15
- limit: 0
16
- )
17
-
18
- render json: {
19
- filters: result.filters,
20
- sort_options: result.sort_options,
21
- default_sort: result.default_sort,
22
- total_count: result.total_count
23
- }
10
+ json = Rails.cache.fetch(filters_cache_key, expires_in: 15.minutes) do
11
+ result = search_provider.filters(
12
+ scope: filters_scope,
13
+ query: search_query,
14
+ filters: (search_filters || {}).merge('_category' => category)
15
+ )
16
+
17
+ {
18
+ filters: result.filters,
19
+ sort_options: result.sort_options,
20
+ default_sort: result.default_sort,
21
+ total_count: result.total_count
22
+ }
23
+ end
24
+
25
+ render json: json
24
26
  end
25
27
 
26
28
  private
27
29
 
30
+ def filters_cache_key
31
+ products_table = Spree::Product.table_name
32
+ stats = current_store.products.active(current_currency)
33
+ .pick(Arel.sql("MAX(#{products_table}.updated_at)"), Arel.sql("COUNT(DISTINCT #{products_table}.id)"))
34
+ max_updated = stats&.first&.to_i
35
+ product_count = stats&.last || 0
36
+
37
+ parts = [
38
+ 'spree/api/v3/store/filters',
39
+ current_store.id,
40
+ current_currency,
41
+ current_locale,
42
+ category&.cache_key_with_version,
43
+ search_query,
44
+ search_filters&.sort_by(&:first)&.to_json,
45
+ max_updated,
46
+ product_count
47
+ ]
48
+
49
+ parts.compact.join('/')
50
+ end
51
+
28
52
  def filters_scope
29
53
  scope = current_store.products.active(current_currency)
30
54
  scope = scope.in_category(category) if category.present?
@@ -55,7 +55,6 @@ module Spree
55
55
  limit: limit
56
56
  )
57
57
 
58
- @search_result = result
59
58
  @pagy = result.pagy
60
59
  @collection = result.products
61
60
  end
@@ -31,7 +31,7 @@ module Spree
31
31
 
32
32
  # Process asynchronously — gateways have timeout limits and will
33
33
  # retry on timeouts, so we must return 200 quickly.
34
- Spree::Payments::HandleWebhookJob.perform_later(
34
+ Spree::Payments::HandleWebhookJob.set(wait: 30.seconds).perform_later(
35
35
  payment_method_id: payment_method.id,
36
36
  action: result[:action].to_s,
37
37
  payment_session_id: result[:payment_session].id
@@ -36,7 +36,9 @@ module Spree
36
36
  # Which items (and how many) are in this fulfillment.
37
37
  # A line item can be split across fulfillments with different quantities.
38
38
  attribute :items do |shipment|
39
- shipment.manifest.map do |item|
39
+ shipment.manifest.filter_map do |item|
40
+ next unless item.line_item
41
+
40
42
  {
41
43
  item_id: item.line_item.prefixed_id,
42
44
  variant_id: item.variant.prefixed_id,
@@ -81,13 +81,106 @@ module Spree
81
81
  end
82
82
 
83
83
  def option_type_filters
84
- Spree::OptionType.filterable.includes(:option_values).order(:position).filter_map do |option_type|
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)
88
- next if values.empty?
84
+ option_types = Spree::OptionType.filterable.order(:position).to_a
85
+ return [] if option_types.empty?
89
86
 
90
- count_ids = count_scope.reorder('').distinct.pluck(:id)
87
+ type_ids = option_types.map(&:id)
88
+
89
+ # Pluck only the columns we need — avoids instantiating thousands of AR models.
90
+ # Force default locale so Mobility returns column values (not translations);
91
+ # translated labels are overlaid separately via load_option_value_translations.
92
+ ov_rows = Mobility.with_locale(I18n.default_locale) do
93
+ Spree::OptionValue
94
+ .where(option_type_id: type_ids)
95
+ .order(:position)
96
+ .pluck(:id, :option_type_id, :name, :presentation, :position, :color_code)
97
+ end
98
+ return [] if ov_rows.empty?
99
+
100
+ all_ov_ids = ov_rows.map(&:first)
101
+
102
+ # Batch-load image attachment existence (single query)
103
+ ov_ids_with_images = ActiveStorage::Attachment
104
+ .where(record_type: 'Spree::OptionValue', name: 'image', record_id: all_ov_ids)
105
+ .pluck(:record_id)
106
+ .to_set
107
+
108
+ # Batch-load translations for current locale (single query, skip for default locale with column_fallback)
109
+ ov_translations = load_option_value_translations(all_ov_ids)
110
+
111
+ # Group rows by option type
112
+ ov_rows_by_type = ov_rows.group_by { |row| row[1] }
113
+
114
+ # Batch counts
115
+ if grouped_selected_options.empty?
116
+ counts = batch_option_value_counts(@scope_before_options, all_ov_ids)
117
+ else
118
+ scope_groups = option_types.group_by { |ot| disjunctive_scope_for(ot) }
119
+ counts = {}
120
+ scope_groups.each do |scope, types|
121
+ ov_ids = types.flat_map { |t| ov_rows_by_type[t.id]&.map(&:first) || [] }
122
+ counts.merge!(batch_option_value_counts(scope, ov_ids))
123
+ end
124
+ end
125
+
126
+ build_option_type_results(option_types, ov_rows_by_type, counts, ov_ids_with_images, ov_translations)
127
+ end
128
+
129
+ # Single grouped COUNT query for all option value IDs against a product scope.
130
+ # Returns { option_value_id => product_count }
131
+ def batch_option_value_counts(product_scope, option_value_ids)
132
+ return {} if option_value_ids.empty?
133
+
134
+ ovv_table = Spree::OptionValueVariant.table_name
135
+ var_table = Spree::Variant.table_name
136
+
137
+ Spree::OptionValueVariant
138
+ .joins("INNER JOIN #{var_table} ON #{var_table}.id = #{ovv_table}.variant_id AND #{var_table}.deleted_at IS NULL")
139
+ .where(var_table => { product_id: product_scope.reorder('').select(:id) })
140
+ .where(ovv_table => { option_value_id: option_value_ids })
141
+ .group("#{ovv_table}.option_value_id")
142
+ .distinct
143
+ .count("#{var_table}.product_id")
144
+ end
145
+
146
+ # Load translated presentations for option values.
147
+ # Returns { option_value_id => translated_presentation } or empty hash when using the default locale.
148
+ def load_option_value_translations(ov_ids)
149
+ locale = Spree::Current.locale || I18n.locale.to_s
150
+ return {} if locale.to_s == I18n.default_locale.to_s
151
+
152
+ Spree::OptionValue::Translation
153
+ .where(spree_option_value_id: ov_ids, locale: locale)
154
+ .pluck(:spree_option_value_id, :presentation)
155
+ .to_h
156
+ end
157
+
158
+ def build_option_type_results(option_types, ov_rows_by_type, counts, ov_ids_with_images, ov_translations)
159
+ # Pre-load image URLs for option values that have images (single batch)
160
+ image_urls = load_image_urls(ov_ids_with_images)
161
+
162
+ option_types.filter_map do |option_type|
163
+ rows = ov_rows_by_type[option_type.id]
164
+ next if rows.blank?
165
+
166
+ # rows: [id, option_type_id, name, presentation, position, color_code]
167
+ options = rows.filter_map do |id, _, name, presentation, position, color_code|
168
+ count = counts[id] || 0
169
+ next if count.zero?
170
+
171
+ label = ov_translations[id] || presentation
172
+
173
+ {
174
+ id: encode_prefixed_id(:optval, id),
175
+ name: name,
176
+ label: label,
177
+ position: position,
178
+ color_code: color_code,
179
+ image_url: image_urls[id],
180
+ count: count
181
+ }
182
+ end
183
+ next if options.empty?
91
184
 
92
185
  {
93
186
  id: option_type.prefixed_id,
@@ -95,28 +188,25 @@ module Spree
95
188
  name: option_type.name,
96
189
  label: option_type.label,
97
190
  kind: option_type.kind,
98
- options: values.map { |ov| option_value_data(count_ids, ov) }
191
+ options: options
99
192
  }
100
193
  end
101
194
  end
102
195
 
103
- def option_value_data(product_ids, option_value)
104
- count = Spree::Product
105
- .where(id: product_ids)
106
- .joins(:option_value_variants)
107
- .where(Spree::OptionValueVariant.table_name => { option_value_id: option_value.id })
108
- .distinct
109
- .count
196
+ # Load image URLs for option values that have images.
197
+ # Only instantiates AR models for the small subset with images (typically color swatches).
198
+ def load_image_urls(ov_ids_with_images)
199
+ return {} if ov_ids_with_images.empty?
110
200
 
111
- {
112
- id: option_value.prefixed_id,
113
- name: option_value.name,
114
- label: option_value.label,
115
- position: option_value.position,
116
- color_code: option_value.color_code,
117
- image_url: option_value.image.attached? ? Rails.application.routes.url_helpers.cdn_image_url(option_value.image) : nil,
118
- count: count
119
- }
201
+ Spree::OptionValue.where(id: ov_ids_with_images.to_a)
202
+ .includes(image_attachment: :blob)
203
+ .each_with_object({}) do |ov, urls|
204
+ urls[ov.id] = Rails.application.routes.url_helpers.cdn_image_url(ov.image)
205
+ end
206
+ end
207
+
208
+ def encode_prefixed_id(prefix, id)
209
+ "#{prefix}_#{Spree::PrefixedId::SQIDS.encode([id])}"
120
210
  end
121
211
 
122
212
  # Returns the scope with all option type filters EXCEPT the given one applied.
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.1
4
+ version: 5.4.2
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-04-14 00:00:00.000000000 Z
11
+ date: 2026-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.16'
55
- - !ruby/object:Gem::Dependency
56
- name: pagy
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '43.0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '43.0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: typelizer
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +72,14 @@ dependencies:
86
72
  requirements:
87
73
  - - '='
88
74
  - !ruby/object:Gem::Version
89
- version: 5.4.1
75
+ version: 5.4.2
90
76
  type: :runtime
91
77
  prerelease: false
92
78
  version_requirements: !ruby/object:Gem::Requirement
93
79
  requirements:
94
80
  - - '='
95
81
  - !ruby/object:Gem::Version
96
- version: 5.4.1
82
+ version: 5.4.2
97
83
  description: Spree's API
98
84
  email:
99
85
  - hello@spreecommerce.org
@@ -277,9 +263,9 @@ licenses:
277
263
  - BSD-3-Clause
278
264
  metadata:
279
265
  bug_tracker_uri: https://github.com/spree/spree/issues
280
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.1
266
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.2
281
267
  documentation_uri: https://docs.spreecommerce.org/
282
- source_code_uri: https://github.com/spree/spree/tree/v5.4.1
268
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.2
283
269
  post_install_message:
284
270
  rdoc_options: []
285
271
  require_paths: