spree_core 5.4.0.beta10 → 5.4.0.rc2

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: 73ed6afd1feda3d20e24a8910273785420d08dfadebf8af34aa90c20b017a0b8
4
- data.tar.gz: 323a92ea512ad2d37bac8be8b8f878f60dbbc4058ed020875c5740ef02a7481b
3
+ metadata.gz: 5de0595dccb2046acb60805390c641e66c9e87c070c25df2587d5a8eaf9bc358
4
+ data.tar.gz: 6cd99e1b81481dd826565d1f579ea64965dfa04a808905f830505110f330473d
5
5
  SHA512:
6
- metadata.gz: 711d0f622de8f398bd3f0e8696c5e12e476d3671a6330781fd5a34a0ecb15cc32f17fcf9d8e1e3028584108a354465ba7721ca990e723077c89dddb9c428fcbb
7
- data.tar.gz: 3634efec05dc63e7c44a27bcfe0265331224592ee8d88bc1b417446967af051ccd450723344bc287977de3793b95d17ca0fef1235e905aa25e7a787f65325a81
6
+ metadata.gz: 1d27ab08a797c2f5419f36bd3ad368f1cecaf6610d1ca9289e9b5dea81f62a4856b8b767b23129b1dd20e0e0fd0c02f619581e58b7b347642cc801a2808de453
7
+ data.tar.gz: d551a808c3d989cf88e02c18b00bd9582b7672459b59e9c0ea281b6797cf5b0b95b698e32f98adc7a180723ab98e84aae99bc163ac5ee7de7a3d6745abc97e80
@@ -1,3 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
2
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z" />
3
- </svg>
1
+ <svg width="38" height="24" viewBox="0 0 38 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="pi-banktransfer"><title id="pi-banktransfer">Bank Transfer</title><g clip-path="url(#pi-banktransfer-clip)"><path opacity=".07" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3z" fill="#000"/><path d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2h32z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M10 7l-4 2v1h8V9L10 7zM7 10.75v3.5h1v-3.5H7zm2 0v3.5h1v-3.5H9zm2 0v3.5h1.5v-3.5H11zM6 15v1h8v-1H6zM28 7l-4 2v1h8V9l-4-2zm-3 3.75v3.5h1v-3.5h-1zm2 0v3.5h1v-3.5h-1zm2 0v3.5h1.5v-3.5H29zM24 15v1h8v-1h-8z" fill="#4A4A4A"/><path d="M15.5 10h7M15.5 10l1.5-1.25M15.5 10l1.5 1.25" stroke="#4A4A4A" stroke-width=".9" stroke-linecap="round" stroke-linejoin="round"/><path d="M22.5 14h-7M22.5 14l-1.5-1.25M22.5 14l-1.5 1.25" stroke="#4A4A4A" stroke-width=".9" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="pi-banktransfer-clip"><path fill="#fff" d="M0 0h38v24H0z"/></clipPath></defs></svg>
@@ -29,7 +29,7 @@ module Spree
29
29
  # => { id: 1, name: "Shirt", price_USD: 19.99, ... }
30
30
  def search_presentation(store = nil)
31
31
  store ||= Spree::Current.store
32
- Spree::SearchProvider::ProductPresenter.new(self, store).call
32
+ Spree::Dependencies.search_product_presenter_class.new(self, store).call
33
33
  end
34
34
 
35
35
  # Remove this record from search index synchronously (inline, no job).
@@ -106,12 +106,26 @@ module Spree
106
106
  end
107
107
 
108
108
  def user_default_billing?
109
- user.present? && id == user.bill_address_id
109
+ Spree::Deprecation.warn('Spree::Address#user_default_billing? is deprecated and will be removed in Spree 6.0. Use #is_default_billing? instead.')
110
+ is_default_billing?
110
111
  end
111
112
 
112
113
  def user_default_shipping?
114
+ Spree::Deprecation.warn('Spree::Address#user_default_shipping? is deprecated and will be removed in Spree 6.0. Use #is_default_shipping? instead.')
115
+ is_default_shipping?
116
+ end
117
+
118
+ # In 6.0 these become real columns on Address, replacing User#bill_address_id / ship_address_id.
119
+ # For now they delegate to the User FK so the API shape is stable.
120
+ def is_default_billing?
121
+ user.present? && id == user.bill_address_id
122
+ end
123
+ alias_method :is_default_billing, :is_default_billing?
124
+
125
+ def is_default_shipping?
113
126
  user.present? && id == user.ship_address_id
114
127
  end
128
+ alias_method :is_default_shipping, :is_default_shipping?
115
129
 
116
130
  # first_name / last_name aliases are defined via alias_attribute above
117
131
 
@@ -46,7 +46,6 @@ module Spree
46
46
  private
47
47
 
48
48
  def build_pagy(count, page, limit)
49
- require 'pagy'
50
49
  Pagy::Offset.new(count: count, page: page, limit: limit)
51
50
  end
52
51
 
@@ -1,3 +1,5 @@
1
+ require 'pagy/toolbox/paginators/meilisearch'
2
+
1
3
  module Spree
2
4
  module SearchProvider
3
5
  class Meilisearch < Base
@@ -29,7 +31,13 @@ module Spree
29
31
 
30
32
  Rails.logger.debug { "[Meilisearch] index=#{index_name} query=#{query.inspect} #{search_params.compact.inspect}" }
31
33
 
32
- ms_result = client.index(index_name).search(query.to_s, search_params)
34
+ begin
35
+ ms_result = client.index(index_name).search(query.to_s, search_params)
36
+ rescue ::Meilisearch::ApiError => e
37
+ Rails.logger.warn { "[Meilisearch] Search failed: #{e.message}. Run `rake spree:search:reindex` to initialize the index." }
38
+ Rails.error.report(e, handled: true, context: { index: index_name, query: query })
39
+ return empty_result(scope, page, limit)
40
+ end
33
41
 
34
42
  Rails.logger.debug { "[Meilisearch] #{ms_result['estimatedTotalHits']} hits in #{ms_result['processingTimeMs']}ms" }
35
43
 
@@ -60,7 +68,7 @@ module Spree
60
68
  end
61
69
 
62
70
  def index(product)
63
- documents = ProductPresenter.new(product, store).call
71
+ documents = presenter_class.new(product, store).call
64
72
  client.index(index_name).add_documents(documents, 'prefixed_id')
65
73
  end
66
74
 
@@ -85,9 +93,9 @@ module Spree
85
93
  ensure_index_settings!
86
94
 
87
95
  scope.reorder(id: :asc)
88
- .preload(*ProductPresenter::REQUIRED_PRELOADS)
96
+ .preload_associations_lazily
89
97
  .find_in_batches(batch_size: 500) do |batch|
90
- documents = batch.flat_map { |product| ProductPresenter.new(product, store).call }
98
+ documents = batch.flat_map { |product| presenter_class.new(product, store).call }
91
99
  index_batch(documents)
92
100
  end
93
101
  end
@@ -103,6 +111,10 @@ module Spree
103
111
 
104
112
  private
105
113
 
114
+ def presenter_class
115
+ Spree::Dependencies.search_product_presenter_class
116
+ end
117
+
106
118
  def client
107
119
  @client ||= ::Meilisearch::Client.new(
108
120
  ENV.fetch('MEILISEARCH_URL', 'http://localhost:7700'),
@@ -134,153 +146,165 @@ module Spree
134
146
  %w[price -price name -name -available_on available_on best_selling]
135
147
  end
136
148
 
137
- # Convert Ransack-style filters to Meilisearch filter syntax.
138
- # Always applies store scoping, active status, and currency availability
139
- # so that Meilisearch results match what the AR scope would return.
140
- # All values are sanitized to prevent filter injection.
149
+ # Build Meilisearch filter conditions from API params.
150
+ # Combines system scoping (always applied) with user-facing filters.
141
151
  def build_filters(filters)
142
- conditions = []
152
+ conditions = system_filter_conditions
153
+ conditions.concat(user_filter_conditions(filters))
154
+ conditions
155
+ end
143
156
 
144
- # Always scope to current store, locale, currency, active, not discontinued.
145
- # This mirrors the AR scope: store.products.active(currency) with locale
157
+ # System scoping always applied. Rarely overridden.
158
+ # Mirrors the AR scope: store.products.active(currency) with locale.
159
+ def system_filter_conditions
160
+ conditions = []
146
161
  conditions << "store_ids = '#{store.id}'"
147
162
  conditions << "status = 'active'"
148
163
  conditions << "locale = '#{locale.to_s.gsub(/[^a-zA-Z_-]/, '')}'"
149
164
  conditions << "currency = '#{currency.to_s.gsub(/[^A-Z]/, '')}'"
150
165
  conditions << "(discontinue_on = 0 OR discontinue_on > #{Time.current.to_i})"
166
+ conditions
167
+ end
151
168
 
169
+ # User-facing filters — override to add custom filter pre/post processing.
170
+ def user_filter_conditions(filters)
171
+ conditions = []
152
172
  filters = filters.to_unsafe_h if filters.respond_to?(:to_unsafe_h)
153
173
  return conditions if filters.blank?
154
174
 
155
175
  filters.each do |key, value|
156
176
  next if value.blank?
157
177
 
158
- key = key.to_s
159
- case key
160
- when 'price_gte'
161
- conditions << "price >= #{value.to_f}"
162
- when 'price_lte'
163
- conditions << "price <= #{value.to_f}"
164
- when 'in_stock'
165
- conditions << 'in_stock = true' if value.to_s != '0'
166
- when 'out_of_stock'
167
- conditions << 'in_stock = false' if value.to_s != '0'
168
- when 'categories_id_eq'
169
- conditions << "category_ids = '#{sanitize_prefixed_id(value)}'" if valid_prefixed_id?(value)
170
- when 'with_option_value_ids'
171
- Array(value).each do |ov|
172
- conditions << "option_value_ids = '#{sanitize_prefixed_id(ov)}'" if valid_prefixed_id?(ov)
173
- end
178
+ condition = build_filter_condition(key.to_s, value)
179
+ if condition.is_a?(Array)
180
+ conditions.concat(condition)
181
+ elsif condition
182
+ conditions << condition
174
183
  end
175
184
  end
176
185
 
177
186
  conditions
178
187
  end
179
188
 
189
+ # Translate a single Ransack-style filter param into Meilisearch filter syntax.
190
+ # Override in subclasses to handle custom filter keys — call super for built-in filters.
191
+ def build_filter_condition(key, value)
192
+ case key
193
+ when 'price_gte'
194
+ "price >= #{value.to_f}"
195
+ when 'price_lte'
196
+ "price <= #{value.to_f}"
197
+ when 'in_stock'
198
+ 'in_stock = true' if value.to_s != '0'
199
+ when 'out_of_stock'
200
+ 'in_stock = false' if value.to_s != '0'
201
+ when 'categories_id_eq'
202
+ "category_ids = '#{sanitize_prefixed_id(value)}'" if valid_prefixed_id?(value)
203
+ when 'with_option_value_ids'
204
+ Array(value).filter_map { |ov| "option_value_ids = '#{sanitize_prefixed_id(ov)}'" if valid_prefixed_id?(ov) }
205
+ end
206
+ end
207
+
208
+ # Sort param to Meilisearch sort syntax.
209
+ # Override in subclasses to handle custom sort keys — call super for built-in sorts.
180
210
  def build_sort(sort)
181
211
  return nil if sort.blank?
182
212
 
213
+ sort_mapping(sort)
214
+ end
215
+
216
+ # Map a sort param to Meilisearch sort syntax.
217
+ # Override in subclasses to add custom sorts — call super for built-in sorts.
218
+ def sort_mapping(sort)
183
219
  case sort
184
- when 'price'
185
- ['price:asc']
186
- when '-price'
187
- ['price:desc']
188
- when 'name'
189
- ['name:asc']
190
- when '-name'
191
- ['name:desc']
192
- when '-available_on'
193
- ['available_on:desc']
194
- when 'available_on'
195
- ['available_on:asc']
196
- when 'best_selling'
197
- ['units_sold_count:desc']
220
+ when 'price' then ['price:asc']
221
+ when '-price' then ['price:desc']
222
+ when 'name' then ['name:asc']
223
+ when '-name' then ['name:desc']
224
+ when '-available_on' then ['available_on:desc']
225
+ when 'available_on' then ['available_on:asc']
226
+ when 'best_selling' then ['units_sold_count:desc']
198
227
  end
199
228
  end
200
229
 
201
- # Transform Meilisearch facetDistribution into our standard filter response format
230
+ # Transform Meilisearch facetDistribution into standard filter response format.
231
+ # Override in subclasses to add custom facets — call super and append.
202
232
  def build_facet_response(facet_distribution)
203
- filters = []
204
-
205
- # Price range
206
- if facet_distribution['price'].present?
207
- amounts = facet_distribution['price'].keys.map(&:to_f)
208
- filters << {
209
- id: 'price',
210
- type: 'price_range',
211
- min: amounts.min,
212
- max: amounts.max,
213
- currency: currency
214
- }
215
- end
233
+ facets = []
234
+ facets << build_price_facet(facet_distribution['price']) if facet_distribution['price'].present?
235
+ facets << build_availability_facet(facet_distribution['in_stock']) if facet_distribution['in_stock'].present?
236
+ facets.concat(build_option_facets(facet_distribution['option_value_ids'])) if facet_distribution['option_value_ids'].present?
237
+ facets << build_category_facet(facet_distribution['category_ids']) if facet_distribution['category_ids'].present?
238
+ facets.compact
239
+ end
216
240
 
217
- # Availability
218
- if facet_distribution['in_stock'].present?
219
- in_stock = facet_distribution['in_stock']['true'] || 0
220
- out_of_stock = facet_distribution['in_stock']['false'] || 0
221
- filters << {
222
- id: 'availability',
223
- type: 'availability',
224
- options: [
225
- { id: 'in_stock', count: in_stock },
226
- { id: 'out_of_stock', count: out_of_stock }
227
- ]
228
- }
229
- end
241
+ def build_price_facet(distribution)
242
+ amounts = distribution.keys.map(&:to_f)
243
+ {
244
+ id: 'price',
245
+ type: 'price_range',
246
+ min: amounts.min,
247
+ max: amounts.max,
248
+ currency: currency
249
+ }
250
+ end
230
251
 
231
- # Option values — group by option type for faceted display
232
- if facet_distribution['option_value_ids'].present?
233
- prefixed_ids = facet_distribution['option_value_ids'].keys
234
- raw_ids = prefixed_ids.filter_map { |pid| Spree::OptionValue.decode_prefixed_id(pid) }
235
- option_values = Spree::OptionValue.where(id: raw_ids).includes(:option_type).preload_associations_lazily.index_by(&:prefixed_id)
236
-
237
- # Group by option type
238
- by_option_type = {}
239
- facet_distribution['option_value_ids'].each do |ov_prefixed_id, count|
240
- ov = option_values[ov_prefixed_id]
241
- next unless ov
242
-
243
- ot = ov.option_type
244
- by_option_type[ot] ||= []
245
- by_option_type[ot] << { id: ov.prefixed_id, name: ov.name, label: ov.label, position: ov.position, count: count }
246
- end
252
+ def build_availability_facet(distribution)
253
+ {
254
+ id: 'availability',
255
+ type: 'availability',
256
+ options: [
257
+ { id: 'in_stock', count: distribution['true'] || 0 },
258
+ { id: 'out_of_stock', count: distribution['false'] || 0 }
259
+ ]
260
+ }
261
+ end
247
262
 
248
- by_option_type.each do |option_type, values|
249
- filters << {
250
- id: option_type.prefixed_id,
251
- type: 'option',
252
- name: option_type.name,
253
- label: option_type.label,
254
- options: values.sort_by { |o| o[:position] }
255
- }
256
- end
263
+ def build_option_facets(distribution)
264
+ prefixed_ids = distribution.keys
265
+ raw_ids = prefixed_ids.filter_map { |pid| Spree::OptionValue.decode_prefixed_id(pid) }
266
+ option_values = Spree::OptionValue.where(id: raw_ids).includes(:option_type).preload_associations_lazily.index_by(&:prefixed_id)
267
+
268
+ # Group by option type
269
+ by_option_type = {}
270
+ distribution.each do |ov_prefixed_id, count|
271
+ ov = option_values[ov_prefixed_id]
272
+ next unless ov
273
+
274
+ ot = ov.option_type
275
+ by_option_type[ot] ||= []
276
+ by_option_type[ot] << { id: ov.prefixed_id, name: ov.name, label: ov.label, position: ov.position, count: count }
257
277
  end
258
278
 
259
- # Categories
260
- if facet_distribution['category_ids'].present?
261
- prefixed_ids = facet_distribution['category_ids'].keys
262
- raw_ids = prefixed_ids.filter_map { |pid| Spree::Taxon.decode_prefixed_id(pid) }
263
- categories = Spree::Taxon.where(id: raw_ids).index_by(&:prefixed_id)
264
-
265
- filters << {
266
- id: 'categories',
267
- type: 'category',
268
- options: facet_distribution['category_ids'].filter_map do |prefixed_id, count|
269
- cat = categories[prefixed_id]
270
- next unless cat
271
-
272
- { id: cat.prefixed_id, name: cat.name, permalink: cat.permalink, count: count }
273
- end
279
+ by_option_type.map do |option_type, values|
280
+ {
281
+ id: option_type.prefixed_id,
282
+ type: 'option',
283
+ name: option_type.name,
284
+ label: option_type.label,
285
+ options: values.sort_by { |o| o[:position] }
274
286
  }
275
287
  end
288
+ end
289
+
290
+ def build_category_facet(distribution)
291
+ prefixed_ids = distribution.keys
292
+ raw_ids = prefixed_ids.filter_map { |pid| Spree::Taxon.decode_prefixed_id(pid) }
293
+ categories = Spree::Taxon.where(id: raw_ids).index_by(&:prefixed_id)
276
294
 
277
- filters
295
+ {
296
+ id: 'categories',
297
+ type: 'category',
298
+ options: distribution.filter_map do |prefixed_id, count|
299
+ cat = categories[prefixed_id]
300
+ next unless cat
301
+
302
+ { id: cat.prefixed_id, name: cat.name, permalink: cat.permalink, count: count }
303
+ end
304
+ }
278
305
  end
279
306
 
280
307
  def build_pagy(ms_result, page, limit)
281
- require 'pagy'
282
- require 'pagy/toolbox/paginators/meilisearch'
283
-
284
308
  fake_result = Struct.new(:raw_answer).new({
285
309
  'totalHits' => ms_result['estimatedTotalHits'] || 0,
286
310
  'hitsPerPage' => limit,
@@ -290,6 +314,17 @@ module Spree
290
314
  Pagy::MeilisearchPaginator.paginate(fake_result, {})
291
315
  end
292
316
 
317
+ def empty_result(scope, page, limit)
318
+ SearchResult.new(
319
+ products: scope.none,
320
+ filters: [],
321
+ sort_options: available_sort_options.map { |id| { id: id } },
322
+ default_sort: 'manual',
323
+ total_count: 0,
324
+ pagy: Pagy::Offset.new(count: 0, page: page, limit: limit)
325
+ )
326
+ end
327
+
293
328
  def valid_prefixed_id?(value)
294
329
  value.to_s.match?(PREFIXED_ID_PATTERN)
295
330
  end
@@ -1,12 +1,6 @@
1
1
  module Spree
2
2
  module SearchProvider
3
3
  class ProductPresenter
4
- # Associations needed by this presenter — used by reindex and rake task for preloading
5
- REQUIRED_PRELOADS = [
6
- :taxons, :option_types, :primary_media, :store_products,
7
- { variants_including_master: [:prices, :option_values] }
8
- ].freeze
9
-
10
4
  attr_reader :product, :store
11
5
 
12
6
  def initialize(product, store)
@@ -8,8 +8,8 @@ module Spree
8
8
 
9
9
  def call(address_params: {}, user: nil, **opts)
10
10
  order = opts[:order]
11
- default_billing = opts.fetch(:default_billing, false)
12
- default_shipping = opts.fetch(:default_shipping, false)
11
+ default_billing = address_params.key?(:is_default_billing) ? address_params.delete(:is_default_billing) : opts.fetch(:default_billing, false)
12
+ default_shipping = address_params.key?(:is_default_shipping) ? address_params.delete(:is_default_shipping) : opts.fetch(:default_shipping, false)
13
13
 
14
14
  address_params = fill_country_and_state_ids(address_params)
15
15
 
@@ -8,8 +8,8 @@ module Spree
8
8
 
9
9
  def call(address:, address_params:, **opts)
10
10
  order = opts[:order]
11
- default_billing = opts.fetch(:default_billing, false)
12
- default_shipping = opts.fetch(:default_shipping, false)
11
+ default_billing = address_params.key?(:is_default_billing) ? address_params.delete(:is_default_billing) : opts.fetch(:default_billing, false)
12
+ default_shipping = address_params.key?(:is_default_shipping) ? address_params.delete(:is_default_shipping) : opts.fetch(:default_shipping, false)
13
13
  address_changes_except = opts.fetch(:address_changes_except, [])
14
14
  create_new_address_on_update = opts.fetch(:create_new_address_on_update, false)
15
15
  Spree::Deprecation.warn('Spree::Addresses::Update create_new_address_on_update parameter is deprecated and will be removed in Spree 5.5.') if create_new_address_on_update
@@ -50,6 +50,8 @@ en:
50
50
  spree/customer_group:
51
51
  description: Description
52
52
  name: Name
53
+ spree/customer_return:
54
+ stock_location_id: Stock Location
53
55
  spree/gift_card:
54
56
  amount_used: Amount used
55
57
  expires_at: Expires at
@@ -103,7 +103,10 @@ module Spree
103
103
  products_finder: 'Spree::Products::Find',
104
104
  taxon_finder: 'Spree::Taxons::Find',
105
105
  line_item_by_variant_finder: 'Spree::LineItems::FindByVariant',
106
- variant_finder: 'Spree::Variants::Find'
106
+ variant_finder: 'Spree::Variants::Find',
107
+
108
+ # search
109
+ search_product_presenter: 'Spree::SearchProvider::ProductPresenter'
107
110
  }.freeze
108
111
 
109
112
  include Spree::DependenciesHelper
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.4.0.beta10'.freeze
2
+ VERSION = '5.4.0.rc2'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
data/lib/spree/core.rb CHANGED
@@ -7,6 +7,7 @@ require 'active_record/railtie'
7
7
  require 'active_storage/engine'
8
8
  require 'action_text/engine'
9
9
  require 'action_cable/engine'
10
+ require 'pagy'
10
11
 
11
12
  require 'mail'
12
13
  require 'action_mailer/railtie'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.0.beta10
4
+ version: 5.4.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Schofield
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-03-19 00:00:00.000000000 Z
13
+ date: 2026-03-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n-tasks
@@ -1695,9 +1695,9 @@ licenses:
1695
1695
  - BSD-3-Clause
1696
1696
  metadata:
1697
1697
  bug_tracker_uri: https://github.com/spree/spree/issues
1698
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta10
1698
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.rc2
1699
1699
  documentation_uri: https://docs.spreecommerce.org/
1700
- source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta10
1700
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.0.rc2
1701
1701
  post_install_message:
1702
1702
  rdoc_options: []
1703
1703
  require_paths: