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 +4 -4
- data/app/models/spree/current.rb +2 -1
- data/app/models/spree/order.rb +15 -3
- data/app/models/spree/permission_sets/product_management.rb +1 -0
- data/app/models/spree/price_rules/channel_rule.rb +36 -0
- data/app/models/spree/product.rb +0 -1
- data/app/models/spree/store.rb +6 -1
- data/app/services/spree/carts/update.rb +3 -0
- data/app/services/spree/products/prepare_nested_attributes.rb +2 -0
- data/config/locales/en.yml +3 -0
- data/lib/spree/core/engine.rb +2 -1
- data/lib/spree/core/pricing/context.rb +6 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/testing_support/common_rake.rb +75 -1
- data/lib/spree/testing_support/factories/price_rule_factory.rb +10 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b15a0aab09b99fce9b5a361577d7cf8c45576e4e4fff0bde398374eb876dfa9
|
|
4
|
+
data.tar.gz: 8dea678d1da4bfe11d29aaa9b7806ebe702ae3e339318e2b72063ef330dd4b79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b6a38e339e292481b3ffea592632848b81dedec28bc1324896958366130ad55f3d06e85e0558d08d7111c2ba0ecdf4a4916efb91fb221a6d9bc5dac6dd49edd
|
|
7
|
+
data.tar.gz: 2cb8797d32de652508f96640a40a2157bbc69c5f73601f7369f4231daf98eecfb61e8bafec10b135bb4cd96189a1804d71e7f2a0f1bab1b9dad5b892a090b281
|
data/app/models/spree/current.rb
CHANGED
data/app/models/spree/order.rb
CHANGED
|
@@ -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,
|
|
75
|
-
order.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
|
|
@@ -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
|
data/app/models/spree/product.rb
CHANGED
data/app/models/spree/store.rb
CHANGED
|
@@ -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.
|
|
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
|
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
data/lib/spree/core/engine.rb
CHANGED
|
@@ -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
|
data/lib/spree/core/version.rb
CHANGED
|
@@ -163,7 +163,9 @@ task :parallel_setup, [:count] do |_t, args|
|
|
|
163
163
|
require 'erb'
|
|
164
164
|
require 'yaml'
|
|
165
165
|
|
|
166
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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:
|