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 +4 -4
- data/app/controllers/spree/api/v3/store/carts/fulfillments_controller.rb +2 -2
- data/app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb +49 -32
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +38 -14
- data/app/controllers/spree/api/v3/store/products_controller.rb +0 -1
- data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +1 -1
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +3 -1
- data/app/services/spree/api/v3/filters_aggregator.rb +113 -23
- metadata +6 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b295d2c49585c2fd0368b2b31bb782e19b77af7c4580e243896257151f5fd47
|
|
4
|
+
data.tar.gz: fea6f271b52ec0218ab2991e4bdaaa496d465a1fc2af7ada90bfee238de2216a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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?
|
|
@@ -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.
|
|
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.
|
|
85
|
-
|
|
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
|
-
|
|
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:
|
|
191
|
+
options: options
|
|
99
192
|
}
|
|
100
193
|
end
|
|
101
194
|
end
|
|
102
195
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|