spree_core 5.5.0 → 5.5.1

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: 5522880aae16fc9206a41a28edd797d20fc655337b0f5d3912de30128cec5177
4
- data.tar.gz: 85a010b3e34acc15dca863636adc8bfe0e0da0373ffd711401b64e2e853d9858
3
+ metadata.gz: 6b15a0aab09b99fce9b5a361577d7cf8c45576e4e4fff0bde398374eb876dfa9
4
+ data.tar.gz: 8dea678d1da4bfe11d29aaa9b7806ebe702ae3e339318e2b72063ef330dd4b79
5
5
  SHA512:
6
- metadata.gz: 966414759bbb3d5eab246e5321cf5ac659f2edff4e0d0579949eef912a5277d9fac7b9bc4046dcf11157786281a6e2ecf2b0d6db709aea17afa4d7cf6094cf1f
7
- data.tar.gz: 96f8da6855281ae93ea61991218ed58db3362b9d8eff4e4c31c60894d233969528fa579d065f950082a4160dcb7a7477735a99bbef6c26bb037e459c44daeea2
6
+ metadata.gz: 5b6a38e339e292481b3ffea592632848b81dedec28bc1324896958366130ad55f3d06e85e0558d08d7111c2ba0ecdf4a4916efb91fb221a6d9bc5dac6dd49edd
7
+ data.tar.gz: 2cb8797d32de652508f96640a40a2157bbc69c5f73601f7369f4231daf98eecfb61e8bafec10b135bb4cd96189a1804d71e7f2a0f1bab1b9dad5b892a090b281
@@ -72,7 +72,8 @@ module Spree
72
72
  currency: currency,
73
73
  store: store,
74
74
  zone: zone,
75
- market: market
75
+ market: market,
76
+ channel: channel
76
77
  )
77
78
  end
78
79
  end
@@ -62,17 +62,20 @@ module Spree
62
62
  [outstanding_balance - total_applied_store_credit, 0].max
63
63
  end
64
64
 
65
- # Transient warnings populated by remove_out_of_stock_items!
65
+ # Transient warnings populated by remove_out_of_stock_items! and ensure_available_shipping_rates
66
66
  attribute :warnings, default: -> { [] }
67
67
 
68
68
  # Removes out-of-stock/discontinued items and populates warnings.
69
69
  # Returns self (reloaded if items were removed) with warnings set.
70
+ # Captured before the call because removing items reloads the order, which
71
+ # would drop warnings already recorded upstream.
70
72
  def remove_out_of_stock_items!
73
+ existing_warnings = warnings
71
74
  result = Spree::Cart::RemoveOutOfStockItems.call(order: self)
72
75
  return self unless result.success?
73
76
 
74
- order, _messages, warnings = result.value
75
- order.warnings = warnings || []
77
+ order, _messages, new_warnings = result.value
78
+ order.warnings = existing_warnings | (new_warnings || [])
76
79
  order
77
80
  end
78
81
 
@@ -1081,8 +1084,17 @@ module Spree
1081
1084
 
1082
1085
  if line_items_without_shipping_rates.present?
1083
1086
  errors.add(:base, Spree.t(:products_cannot_be_shipped, product_names: line_items_without_shipping_rates.map(&:name).to_sentence))
1087
+ self.warnings |= line_items_without_shipping_rates.map do |line_item|
1088
+ {
1089
+ code: 'delivery_unavailable',
1090
+ message: Spree.t('cart_line_item.delivery_unavailable', li_name: line_item.name),
1091
+ line_item_id: line_item.prefixed_id,
1092
+ variant_id: line_item.variant&.prefixed_id
1093
+ }
1094
+ end
1084
1095
  else
1085
1096
  errors.add(:base, Spree.t(:items_cannot_be_shipped))
1097
+ self.warnings |= [{ code: 'delivery_unavailable', message: Spree.t(:items_cannot_be_shipped) }]
1086
1098
  end
1087
1099
 
1088
1100
  false
@@ -21,6 +21,7 @@ module Spree
21
21
  can :manage, Spree::PriceList
22
22
  can :manage, Spree::PriceRule
23
23
  can :manage, Spree::Asset
24
+ can :manage, Spree::ProductPublication
24
25
  end
25
26
  end
26
27
  end
@@ -0,0 +1,36 @@
1
+ module Spree
2
+ module PriceRules
3
+ class ChannelRule < Spree::PriceRule
4
+ # Stored as raw IDs. Accepts prefixed IDs (`ch_…`) from API
5
+ # callers and decodes them on write so eligibility checks compare
6
+ # against raw `channel_id` rows directly. Scope confines the
7
+ # existence check to the price-list's store so cross-store channel
8
+ # IDs can't sneak in.
9
+ preference :channel_ids, :array, default: [],
10
+ parse_on_set: normalize_id_preference(
11
+ klass: Spree::Channel,
12
+ scope: ->(rule) { rule.store.channels }
13
+ )
14
+
15
+ def channels
16
+ return [] if preferred_channel_ids.blank?
17
+
18
+ store.channels.where(id: preferred_channel_ids)
19
+ end
20
+
21
+ def applicable?(context)
22
+ # An empty preference means the rule is unrestricted, so it applies
23
+ # regardless of (and even without) a channel in the context.
24
+ return true if preferred_channel_ids.empty?
25
+ return false unless context.channel
26
+
27
+ # Compare as strings to support both integer and UUID primary keys
28
+ preferred_channel_ids.map(&:to_s).include?(context.channel.id.to_s)
29
+ end
30
+
31
+ def self.description
32
+ Spree.t('price_rules.channel_rule.description')
33
+ end
34
+ end
35
+ end
36
+ end
@@ -966,7 +966,6 @@ module Spree
966
966
  end
967
967
 
968
968
  def requires_price?
969
- Spree::Deprecation.warn('Spree::Product#requires_price? is deprecated and will be removed in Spree 6.0.')
970
969
  Spree::Config[:require_master_price]
971
970
  end
972
971
 
@@ -186,8 +186,13 @@ module Spree
186
186
  Spree::Store.default&.supported_locales_list || []
187
187
  end
188
188
 
189
+ # Resolves the store's default channel via the +default+ boolean column
190
+ # so promoting another channel in the admin takes effect immediately.
191
+ # Falls back to the first active channel only for malformed data with no
192
+ # flagged default.
193
+ # @return [Spree::Channel, nil]
189
194
  def default_channel
190
- channels.find_by(code: Spree::Channel::DEFAULT_CODE) || channels.active.first
195
+ channels.default.first || channels.active.first
191
196
  end
192
197
 
193
198
  # @deprecated Use Markets instead. Will be removed in Spree 5.5.
@@ -144,11 +144,14 @@ module Spree
144
144
  rescue StandardError => e
145
145
  Rails.error.report(e, context: { order_id: cart.id, state: cart.state }, source: 'spree.checkout')
146
146
  ensure
147
+ # A halted transition records warnings on the cart, which reload would drop, so carry them across the reload.
148
+ warnings = cart.warnings
147
149
  begin
148
150
  cart.reload
149
151
  rescue StandardError # rubocop:disable Lint/SuppressedException
150
152
  # reload failure must not mask the original result
151
153
  end
154
+ cart.warnings |= warnings if warnings.present?
152
155
  end
153
156
  end
154
157
  end
@@ -67,6 +67,8 @@ module Spree
67
67
  end
68
68
  end
69
69
 
70
+ params.delete(:legacy_product_publications_attributes) unless can?(:manage, Spree::ProductPublication)
71
+
70
72
  # ensure the product is owned by a store
71
73
  params[:store_id] = store.id if params[:store_id].blank? && product.store_id.blank?
72
74
 
@@ -866,6 +866,7 @@ en:
866
866
  cart: Cart
867
867
  cart_already_updated: The cart has already been updated.
868
868
  cart_line_item:
869
+ delivery_unavailable: "%{li_name} cannot be delivered to the selected address"
869
870
  discontinued: "%{li_name} was removed because it was discontinued"
870
871
  out_of_stock: "%{li_name} was removed because it was sold out"
871
872
  cart_page:
@@ -1840,6 +1841,8 @@ en:
1840
1841
  description: Apply pricing based on geographic zone
1841
1842
  name: Zone
1842
1843
  price_rules:
1844
+ channel_rule:
1845
+ description: Apply pricing based on the sales channel
1843
1846
  customer_group_rule:
1844
1847
  description: Apply pricing to specific customer groups
1845
1848
  price_sack: Price Sack
@@ -201,7 +201,8 @@ module Spree
201
201
  Spree::PriceRules::UserRule,
202
202
  Spree::PriceRules::CustomerGroupRule,
203
203
  Spree::PriceRules::VolumeRule,
204
- Spree::PriceRules::MarketRule
204
+ Spree::PriceRules::MarketRule,
205
+ Spree::PriceRules::ChannelRule
205
206
  ]
206
207
 
207
208
  Rails.application.config.spree.promotions.actions = [
@@ -1,7 +1,7 @@
1
1
  module Spree
2
2
  module Pricing
3
3
  class Context
4
- attr_reader :variant, :currency, :store, :zone, :market, :user, :quantity, :date, :order
4
+ attr_reader :variant, :currency, :store, :zone, :market, :channel, :user, :quantity, :date, :order
5
5
 
6
6
  # Initializes the context
7
7
  # @param variant [Spree::Variant]
@@ -9,16 +9,18 @@ module Spree
9
9
  # @param store [Spree::Store]
10
10
  # @param zone [Spree::Zone]
11
11
  # @param market [Spree::Market]
12
+ # @param channel [Spree::Channel]
12
13
  # @param user [Spree::User]
13
14
  # @param quantity [Integer]
14
15
  # @param date [Time]
15
16
  # @param order [Spree::Order]
16
- def initialize(variant: nil, currency:, store: nil, zone: nil, market: nil, user: nil, quantity: nil, date: nil, order: nil)
17
+ def initialize(variant: nil, currency:, store: nil, zone: nil, market: nil, channel: nil, user: nil, quantity: nil, date: nil, order: nil)
17
18
  @variant = variant
18
19
  @currency = currency
19
20
  @store = store || Spree::Current.store
20
21
  @zone = zone || Spree::Current.zone
21
22
  @market = market || Spree::Current.market
23
+ @channel = channel || Spree::Current.channel
22
24
  @user = user
23
25
  @quantity = quantity
24
26
  @date = date || Time.current
@@ -39,6 +41,7 @@ module Spree
39
41
  currency: order.currency,
40
42
  store: order.store,
41
43
  zone: order.tax_zone || Spree::Zone.default_tax,
44
+ channel: order.channel,
42
45
  user: order.user,
43
46
  quantity: quantity || order.line_items.find_by(variant: variant)&.quantity,
44
47
  order: order
@@ -56,6 +59,7 @@ module Spree
56
59
  store&.id,
57
60
  zone&.id,
58
61
  market&.id,
62
+ channel&.id,
59
63
  user&.id,
60
64
  quantity,
61
65
  date&.to_i
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- VERSION = '5.5.0'.freeze
2
+ VERSION = '5.5.1'.freeze
3
3
 
4
4
  def self.version
5
5
  VERSION
@@ -163,7 +163,9 @@ task :parallel_setup, [:count] do |_t, args|
163
163
  require 'erb'
164
164
  require 'yaml'
165
165
 
166
- db_config = YAML.safe_load(ERB.new(File.read(db_config_path)).result, permitted_classes: [Symbol])
166
+ # database.yml uses YAML anchors/aliases (&postgres / <<: *postgres); Psych 5.4
167
+ # disables alias parsing in safe_load by default, so enable it explicitly.
168
+ db_config = YAML.safe_load(ERB.new(File.read(db_config_path)).result, permitted_classes: [Symbol], aliases: true)
167
169
  adapter = db_config.dig('test', 'adapter')
168
170
 
169
171
  if adapter == 'sqlite3'
@@ -198,3 +200,75 @@ task :parallel_spec, [:count] do |_t, args|
198
200
  success = system("bundle exec parallel_rspec #{count_arg} spec")
199
201
  exit(success ? 0 : 1)
200
202
  end
203
+
204
+ # Run one CI shard's slice of the suite, balanced by file size.
205
+ #
206
+ # The suite is split into TOTAL_GROUPS size-weighted groups (TOTAL_GROUPS =
207
+ # number of CI runners for this project × processes per runner). Each runner
208
+ # claims a contiguous block of group indices via SHARD/TOTAL_SHARDS and runs
209
+ # them with parallel_rspec, so balancing happens once across both the
210
+ # cross-runner and the in-runner split. Replaces the previous filename
211
+ # round-robin, which left core ~1.9x imbalanced.
212
+ #
213
+ # Grouping is by file size, NOT recorded runtime: under cross-runner sharding,
214
+ # --only-group only tiles the suite correctly if every runner computes the
215
+ # identical global partition. File size is a deterministic, log-free weight
216
+ # (same checkout ⇒ same files + sizes on every runner), so the partition is
217
+ # byte-identical everywhere and no spec is skipped or double-run. Recorded
218
+ # runtime cannot guarantee this: each shard only records the files it ran, so
219
+ # the per-runner partial logs — and thus the partitions — would diverge.
220
+ #
221
+ # Env:
222
+ # SHARD 1-based index of this runner (default 1)
223
+ # TOTAL_SHARDS number of runners for this project (default 1)
224
+ # PROCS processes per runner (default: nproc)
225
+ # RSPEC_OPTS extra options forwarded to rspec (formatters, junit output, …)
226
+ desc 'Run a size-balanced shard of the suite in parallel (CI)'
227
+ task :parallel_shard do
228
+ require 'etc'
229
+
230
+ shard = Integer(ENV.fetch('SHARD', '1'))
231
+ total_shards = Integer(ENV.fetch('TOTAL_SHARDS', '1'))
232
+ procs = Integer(ENV.fetch('PROCS', Etc.nprocessors.to_s))
233
+ rspec_opts = ENV['RSPEC_OPTS']
234
+
235
+ # Guard against a misconfigured matrix silently running zero specs (a
236
+ # false-green shard) or producing an invalid --only-group.
237
+ raise "PROCS must be >= 1 (got #{procs})" if procs < 1
238
+ raise "TOTAL_SHARDS must be >= 1 (got #{total_shards})" if total_shards < 1
239
+ raise "SHARD must be between 1 and TOTAL_SHARDS (got #{shard} of #{total_shards})" unless (1..total_shards).cover?(shard)
240
+
241
+ total_groups = total_shards * procs
242
+
243
+ # Contiguous block of 1-based group indices owned by this runner.
244
+ first = ((shard - 1) * procs) + 1
245
+ groups = (first...(first + procs)).to_a.join(',')
246
+
247
+ # Output readability under parallelism:
248
+ # --serialize-stdout each process's output is buffered and reprinted
249
+ # contiguously instead of interleaving, so a failing
250
+ # process's summary + "Failures:" block isn't buried
251
+ # between other processes' "0 failures" lines.
252
+ # --combine-stderr fold stderr into that serialized stream.
253
+ # --verbose-rerun-command print a final "Tests have failed…" footer naming
254
+ # the failed group + an exact rerun command (w/ seed).
255
+ # Build as an argv array and exec without a shell (system(*argv)), so values
256
+ # like RSPEC_OPTS can't be interpreted as shell metacharacters. parallel_tests
257
+ # still performs its own $TEST_ENV_NUMBER interpolation on the -o string.
258
+ cmd = [
259
+ 'bundle', 'exec', 'parallel_rspec',
260
+ '-n', total_groups.to_s,
261
+ '--only-group', groups,
262
+ '--group-by', 'filesize',
263
+ '--highest-exit-status',
264
+ '--verbose-rerun-command'
265
+ ]
266
+ cmd += ['--serialize-stdout', '--combine-stderr'] if total_groups > 1
267
+ cmd += ['-o', rspec_opts] if rspec_opts && !rspec_opts.empty?
268
+ cmd << 'spec'
269
+
270
+ puts "Shard #{shard}/#{total_shards}: running groups #{groups} of #{total_groups} with #{procs} processes"
271
+ puts cmd.join(' ')
272
+ success = system(*cmd)
273
+ exit(success ? 0 : 1)
274
+ end
@@ -55,5 +55,15 @@ FactoryBot.define do
55
55
  market_ids { [] }
56
56
  end
57
57
  end
58
+
59
+ factory :channel_price_rule, class: Spree::PriceRules::ChannelRule do
60
+ after(:build) do |rule, evaluator|
61
+ rule.preferred_channel_ids = evaluator.channel_ids if evaluator.respond_to?(:channel_ids)
62
+ end
63
+
64
+ transient do
65
+ channel_ids { [] }
66
+ end
67
+ end
58
68
  end
59
69
  end
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.5.0
4
+ version: 5.5.1
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-06-18 00:00:00.000000000 Z
13
+ date: 2026-06-29 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n-tasks
@@ -1097,6 +1097,7 @@ files:
1097
1097
  - app/models/spree/price_history.rb
1098
1098
  - app/models/spree/price_list.rb
1099
1099
  - app/models/spree/price_rule.rb
1100
+ - app/models/spree/price_rules/channel_rule.rb
1100
1101
  - app/models/spree/price_rules/customer_group_rule.rb
1101
1102
  - app/models/spree/price_rules/market_rule.rb
1102
1103
  - app/models/spree/price_rules/user_rule.rb
@@ -1816,9 +1817,9 @@ licenses:
1816
1817
  - BSD-3-Clause
1817
1818
  metadata:
1818
1819
  bug_tracker_uri: https://github.com/spree/spree/issues
1819
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.0
1820
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.1
1820
1821
  documentation_uri: https://docs.spreecommerce.org/
1821
- source_code_uri: https://github.com/spree/spree/tree/v5.5.0
1822
+ source_code_uri: https://github.com/spree/spree/tree/v5.5.1
1822
1823
  post_install_message:
1823
1824
  rdoc_options: []
1824
1825
  require_paths: