spree_core 6.0.0.rc7 → 6.0.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/jobs/spree/maintenance_tasks/run_job.rb +321 -0
- data/app/jobs/spree/seller_transfers/execute_pending_due_job.rb +7 -5
- data/app/jobs/spree/seller_transfers/execute_pending_job.rb +37 -17
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/backfill_order_markets.rb +48 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/capture_methods.rb +40 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/deliveries.rb +15 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/order_coupon_codes.rb +15 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/promotion_option_value_rules.rb +15 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/rake_step.rb +83 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/returns.rb +20 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/store_regional_settings.rb +63 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade/users_to_customers.rb +28 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade_step.rb +44 -0
- data/app/models/concerns/spree/translatable_resource.rb +19 -0
- data/app/models/spree/ability.rb +9 -0
- data/app/models/spree/invitation.rb +11 -2
- data/app/models/spree/maintenance_task.rb +221 -0
- data/app/models/spree/maintenance_task_run.rb +78 -0
- data/app/models/spree/order_routing/strategy/rules.rb +8 -48
- data/app/models/spree/seller_transfer.rb +51 -0
- data/app/models/spree/stock/coordinator.rb +37 -17
- data/app/models/spree/stock/packer.rb +19 -9
- data/app/models/spree/stock/prioritizer.rb +30 -15
- data/app/models/spree/store.rb +35 -2
- data/app/models/spree/upgrade_record.rb +62 -0
- data/app/services/spree/carts/update.rb +2 -1
- data/app/services/spree/maintenance_tasks/argument_parser.rb +23 -0
- data/app/services/spree/maintenance_tasks/cancel.rb +51 -0
- data/app/services/spree/maintenance_tasks/start.rb +127 -0
- data/app/services/spree/seeds/all.rb +22 -0
- data/app/services/spree/stock_reservations/reserve.rb +73 -25
- data/app/workflows/spree/seller_transfers/reverse.rb +5 -44
- data/config/locales/en.yml +1 -0
- data/db/migrate/20260813130001_add_capture_method_to_payment_methods.rb +1 -1
- data/db/migrate/20260815000001_create_spree_maintenance_task_runs.rb +40 -0
- data/db/migrate/20260816000001_create_spree_upgrade_records.rb +17 -0
- data/db/migrate/20261029100001_add_lockout_columns_to_spree_admin_users.rb +8 -0
- data/lib/generators/spree/api_resource/api_resource_generator.rb +2 -2
- data/lib/spree/checkout/default_requirements.rb +33 -5
- data/lib/spree/core/configuration.rb +6 -0
- data/lib/spree/core/engine.rb +10 -1
- data/lib/spree/core/permissions/default_catalog.rb +7 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +17 -1
- data/lib/spree/testing_support/factories/maintenance_task_run_factory.rb +7 -0
- data/lib/spree/time_zones.rb +34 -0
- data/lib/spree/upgrade.rb +189 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +30 -10
- data/lib/tasks/maintenance_tasks.rake +42 -0
- data/lib/tasks/upgrade.rake +81 -60
- metadata +28 -6
- data/lib/tasks/capture_methods_migration.rake +0 -30
- data/lib/tasks/order_market_backfill.rake +0 -27
|
@@ -14,13 +14,17 @@ module Spree
|
|
|
14
14
|
|
|
15
15
|
ApplicationRecord.transaction do
|
|
16
16
|
targets = build_targets(cart)
|
|
17
|
-
break if targets.empty?
|
|
18
17
|
|
|
19
18
|
# Pessimistic lock + fresh read of count_on_hand. The lock serializes
|
|
20
19
|
# concurrent checkouts and we use the locked rows below so we never
|
|
21
|
-
# check stock against a stale association cache.
|
|
20
|
+
# check stock against a stale association cache. The shelves held
|
|
21
|
+
# now are locked with the new ones, in id order, so two checkouts
|
|
22
|
+
# moving holds past each other cannot deadlock.
|
|
23
|
+
stock_level_ids = targets.map { |_, si, _| si.id } |
|
|
24
|
+
Spree::StockReservation.for_order(cart).pluck(:stock_level_id)
|
|
22
25
|
locked_stock_levels = Spree::StockLevel
|
|
23
|
-
.where(id:
|
|
26
|
+
.where(id: stock_level_ids)
|
|
27
|
+
.order(:id)
|
|
24
28
|
.lock
|
|
25
29
|
.index_by(&:id)
|
|
26
30
|
|
|
@@ -29,22 +33,13 @@ module Spree
|
|
|
29
33
|
|
|
30
34
|
this_order_used = Hash.new(0)
|
|
31
35
|
|
|
32
|
-
targets.
|
|
36
|
+
reservations = targets.map do |line_item, stock_level, quantity|
|
|
33
37
|
stock_level = locked_stock_levels.fetch(stock_level.id)
|
|
34
38
|
available = stock_level.available_count - held.fetch(stock_level.id, 0) - this_order_used[stock_level.id]
|
|
35
39
|
|
|
36
|
-
if available <
|
|
37
|
-
raise InsufficientStockError.new(
|
|
38
|
-
line_item,
|
|
39
|
-
I18n.t(
|
|
40
|
-
'spree.insufficient_stock_for_reservation',
|
|
41
|
-
item: line_item.variant.name,
|
|
42
|
-
available: [available, 0].max
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
end
|
|
40
|
+
raise_insufficient_stock(line_item, available) if available < quantity
|
|
46
41
|
|
|
47
|
-
this_order_used[stock_level.id] +=
|
|
42
|
+
this_order_used[stock_level.id] += quantity
|
|
48
43
|
|
|
49
44
|
reservation = existing[[stock_level.id, line_item.id]] ||
|
|
50
45
|
Spree::StockReservation.new(stock_level: stock_level, line_item: line_item)
|
|
@@ -55,10 +50,15 @@ module Spree
|
|
|
55
50
|
reservation.order = cart
|
|
56
51
|
reservation.cart = nil
|
|
57
52
|
end
|
|
58
|
-
reservation.quantity =
|
|
53
|
+
reservation.quantity = quantity
|
|
59
54
|
reservation.expires_at = expires_at
|
|
60
55
|
reservation.save!
|
|
56
|
+
reservation
|
|
61
57
|
end
|
|
58
|
+
|
|
59
|
+
# Left in place, a hold the allocation moved away from blocks other buyers until it expires.
|
|
60
|
+
moved = Spree::StockReservation.for_order(cart).where.not(id: reservations.map(&:id))
|
|
61
|
+
Spree::StockReservation.withdraw(moved)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
success(cart)
|
|
@@ -68,23 +68,71 @@ module Spree
|
|
|
68
68
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
|
+
# Held where the allocation packs each unit, so the hold protects the stock the order will ship.
|
|
71
72
|
def build_targets(cart)
|
|
72
|
-
cart.line_items.
|
|
73
|
+
cart.line_items.reset
|
|
74
|
+
coordinator = Spree::Stock::Coordinator.new(cart)
|
|
75
|
+
packages = coordinator.allocated_packages
|
|
76
|
+
check_allocated_in_full(cart, packages_items(packages))
|
|
77
|
+
|
|
78
|
+
# Once the address is known, a unit only an origin unable to reach it
|
|
79
|
+
# would pack cannot ship, so it is left to the delivery check rather
|
|
80
|
+
# than held where it blocks buyers that origin does serve.
|
|
81
|
+
packages = packages.select { |package| coordinator.deliverable?(package) } if cart.ship_address.present?
|
|
82
|
+
items = packages_items(packages)
|
|
83
|
+
stock_levels = Spree::Stock::Packer.stock_levels_by_location(packages.map(&:stock_location).uniq,
|
|
84
|
+
cart.line_items.map(&:variant_id).uniq)
|
|
85
|
+
|
|
86
|
+
# Summed because a splitter can pack one line into several packages from one location.
|
|
87
|
+
quantities = Hash.new(0)
|
|
88
|
+
items.each do |stock_location, item|
|
|
89
|
+
next unless item.on_hand?
|
|
90
|
+
|
|
91
|
+
line_item = item.inventory_unit.line_item
|
|
73
92
|
variant = line_item.variant
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
stock_level = select_stock_level(variant)
|
|
93
|
+
stock_level = stock_levels.dig(stock_location.id, variant.id)
|
|
77
94
|
# Backorderable and pre-cart variants oversell past count_on_hand,
|
|
78
95
|
# so a reservation capped at on-hand stock would wrongly reject them;
|
|
79
96
|
# their cap is enforced by Stock::Quantifier#can_supply? instead.
|
|
97
|
+
next unless variant.should_track_inventory?
|
|
80
98
|
next if stock_level.nil? || stock_level.backorderable? || variant.preorder?
|
|
81
99
|
|
|
82
|
-
[line_item, stock_level]
|
|
100
|
+
quantities[[line_item, stock_level]] += item.quantity
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
quantities.map { |(line_item, stock_level), quantity| [line_item, stock_level, quantity] }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def packages_items(packages)
|
|
107
|
+
packages.flat_map { |package| package.contents.map { |item| [package.stock_location, item] } }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# A line placed nowhere is left to the delivery check, which can say why;
|
|
111
|
+
# one placed in part has shelves that fall short, and is refused.
|
|
112
|
+
def check_allocated_in_full(cart, items)
|
|
113
|
+
allocated = items.each_with_object(Hash.new(0)) do |(_, item), quantities|
|
|
114
|
+
quantities[item.inventory_unit.line_item_id] += item.quantity
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
cart.line_items.each do |line_item|
|
|
118
|
+
variant = line_item.variant
|
|
119
|
+
next unless variant&.should_track_inventory?
|
|
120
|
+
next if variant.preorder?
|
|
121
|
+
|
|
122
|
+
placed = allocated[line_item.id]
|
|
123
|
+
raise_insufficient_stock(line_item, placed) if placed.positive? && placed < line_item.quantity
|
|
83
124
|
end
|
|
84
125
|
end
|
|
85
126
|
|
|
86
|
-
def
|
|
87
|
-
|
|
127
|
+
def raise_insufficient_stock(line_item, available)
|
|
128
|
+
raise InsufficientStockError.new(
|
|
129
|
+
line_item,
|
|
130
|
+
I18n.t(
|
|
131
|
+
'spree.insufficient_stock_for_reservation',
|
|
132
|
+
item: line_item.variant.name,
|
|
133
|
+
available: [available, 0].max
|
|
134
|
+
)
|
|
135
|
+
)
|
|
88
136
|
end
|
|
89
137
|
|
|
90
138
|
def held_by_others(stock_level_ids, owner)
|
|
@@ -104,8 +152,8 @@ module Spree
|
|
|
104
152
|
def existing_reservations_for(targets)
|
|
105
153
|
return {} if targets.empty?
|
|
106
154
|
|
|
107
|
-
stock_level_ids = targets.map { |_, si| si.id }
|
|
108
|
-
line_item_ids = targets.map { |li, _| li.id }
|
|
155
|
+
stock_level_ids = targets.map { |_, si, _| si.id }
|
|
156
|
+
line_item_ids = targets.map { |li, _, _| li.id }
|
|
109
157
|
|
|
110
158
|
Spree::StockReservation
|
|
111
159
|
.where(stock_level_id: stock_level_ids, line_item_id: line_item_ids)
|
|
@@ -202,34 +202,15 @@ module Spree
|
|
|
202
202
|
kind: 'refund_reversal',
|
|
203
203
|
provider: @earning.provider,
|
|
204
204
|
status: 'pending'
|
|
205
|
-
}.merge(
|
|
205
|
+
}.merge(@earning.reversal_settlement(bounded))
|
|
206
206
|
)
|
|
207
207
|
end
|
|
208
208
|
end
|
|
209
209
|
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# never join the batch that pays the earning it cancels — the seller would
|
|
213
|
-
# be paid in full for goods that came back, and the row would sit in a
|
|
214
|
-
# currency their account cannot pay.
|
|
215
|
-
#
|
|
216
|
-
# Prorated from the earning's own settled figure rather than converted at
|
|
217
|
-
# today's rate, so the money comes back at the rate it went out at.
|
|
218
|
-
def settlement_of(bounded)
|
|
219
|
-
return {} if @earning.settled_amount.blank? || @earning.amount.zero?
|
|
220
|
-
|
|
221
|
-
share = @earning.settled_amount * (bounded / @earning.amount)
|
|
222
|
-
|
|
223
|
-
{
|
|
224
|
-
settled_amount: -Spree::Money::Rounding.quantize(
|
|
225
|
-
share, Spree::Money::Rounding.precision(@earning.settlement_currency)
|
|
226
|
-
),
|
|
227
|
-
settled_currency: @earning.settled_currency
|
|
228
|
-
}
|
|
229
|
-
end
|
|
230
|
-
|
|
210
|
+
# Asked of the provider that made the earning — see
|
|
211
|
+
# {Spree::SellerTransfer#payout_provider_instance}.
|
|
231
212
|
def execute_reversal
|
|
232
|
-
|
|
213
|
+
@earning.payout_provider_instance.reverse!(reversal)
|
|
233
214
|
rescue Spree::Core::AmbiguousGatewayError => e
|
|
234
215
|
# Whether the clawback happened is the provider's to say. Recorded as
|
|
235
216
|
# such rather than as a refusal, so an operator reconciling knows which
|
|
@@ -238,31 +219,11 @@ module Spree
|
|
|
238
219
|
Rails.error.report(e, handled: true, context: { seller_transfer_id: reversal.id }, source: 'spree.core')
|
|
239
220
|
failure(reversal, e.message)
|
|
240
221
|
rescue StandardError => e
|
|
222
|
+
# Left retryable: `ExecutePendingJob` asks the provider again.
|
|
241
223
|
reversal.update!(status: 'processing')
|
|
242
224
|
Rails.error.report(e, handled: true, context: { seller_transfer_id: reversal.id }, source: 'spree.core')
|
|
243
225
|
failure(reversal, e.message)
|
|
244
226
|
end
|
|
245
|
-
|
|
246
|
-
# The provider that made the earning, not whichever one the store uses
|
|
247
|
-
# now. A marketplace that changes provider still has money sitting with
|
|
248
|
-
# the old one, and asking the new one to reverse a transfer it never
|
|
249
|
-
# made leaves the original standing — the seller keeps a refunded sale.
|
|
250
|
-
def provider
|
|
251
|
-
@provider ||= begin
|
|
252
|
-
configured = Spree.payout_providers.find { |candidate| candidate.to_s == @earning.provider }
|
|
253
|
-
|
|
254
|
-
# No silent fallback to whatever the store uses now. A provider that
|
|
255
|
-
# is no longer installed still holds the transfer this reverses, and
|
|
256
|
-
# handing the job to a different one would mark the row reversed
|
|
257
|
-
# while the money stayed where it was. An operator has to know.
|
|
258
|
-
if configured.nil?
|
|
259
|
-
raise Spree::Core::GatewayError,
|
|
260
|
-
"Payout provider #{@earning.provider} is not registered, so its transfer cannot be reversed"
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
configured.new
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
227
|
end
|
|
267
228
|
end
|
|
268
229
|
end
|
data/config/locales/en.yml
CHANGED
|
@@ -1032,6 +1032,7 @@ en:
|
|
|
1032
1032
|
locale_not_supported_by_market: is not offered by the chosen market
|
|
1033
1033
|
locale_not_supported_by_store: is not supported by this store
|
|
1034
1034
|
locales_not_listed: 'includes locales Spree does not support: %{codes}'
|
|
1035
|
+
maintenance_tasks_turned_off: Maintenance tasks are turned off for this installation. Run upgrade steps from the command line with `spree upgrade` (or `bin/rake spree:upgrade`).
|
|
1035
1036
|
manual: Manual
|
|
1036
1037
|
market: Market
|
|
1037
1038
|
me_no_current_user: No admin user authenticated this request
|
|
@@ -10,7 +10,7 @@ class AddCaptureMethodToPaymentMethods < ActiveRecord::Migration[8.1]
|
|
|
10
10
|
# from the provider configuration form.
|
|
11
11
|
#
|
|
12
12
|
# Existing auto_capture values are carried over by
|
|
13
|
-
#
|
|
13
|
+
# the capture_methods maintenance task, never here.
|
|
14
14
|
def change
|
|
15
15
|
add_column :spree_payment_methods, :capture_method, :string
|
|
16
16
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class CreateSpreeMaintenanceTaskRuns < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_maintenance_task_runs do |t|
|
|
4
|
+
t.string :task_name, null: false
|
|
5
|
+
t.string :status, null: false
|
|
6
|
+
t.boolean :dry_run, null: false, default: false
|
|
7
|
+
|
|
8
|
+
t.string :cursor
|
|
9
|
+
t.integer :tick_count, null: false, default: 0
|
|
10
|
+
t.integer :tick_total
|
|
11
|
+
|
|
12
|
+
t.decimal :time_running, null: false, default: 0.0, precision: 12, scale: 3
|
|
13
|
+
t.datetime :started_at
|
|
14
|
+
t.datetime :ended_at
|
|
15
|
+
|
|
16
|
+
t.string :error_class
|
|
17
|
+
t.text :error_message
|
|
18
|
+
t.text :error_backtrace
|
|
19
|
+
|
|
20
|
+
t.string :job_id
|
|
21
|
+
t.string :initiated_via, null: false
|
|
22
|
+
|
|
23
|
+
t.references :admin_user, null: true
|
|
24
|
+
t.references :api_key, null: true
|
|
25
|
+
|
|
26
|
+
if t.respond_to?(:jsonb)
|
|
27
|
+
t.jsonb :arguments
|
|
28
|
+
t.jsonb :tallies
|
|
29
|
+
else
|
|
30
|
+
t.json :arguments
|
|
31
|
+
t.json :tallies
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
t.timestamps
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
add_index :spree_maintenance_task_runs, [:task_name, :status]
|
|
38
|
+
add_index :spree_maintenance_task_runs, :created_at
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateSpreeUpgradeRecords < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_upgrade_records do |t|
|
|
4
|
+
# The release boundary this installation has completed the data steps
|
|
5
|
+
# for — "5.6" means every manifest up to and including 5.5 → 5.6 is done.
|
|
6
|
+
t.string :version, null: false
|
|
7
|
+
# How we know: `walk` (a successful upgrade walk), `install` (a fresh
|
|
8
|
+
# install, which needs no historical steps) or `manual`.
|
|
9
|
+
t.string :source, null: false
|
|
10
|
+
t.datetime :completed_at, null: false
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_index :spree_upgrade_records, :version, unique: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Spree::AccountLockout reads these columns. Installs upgraded from a Devise
|
|
2
|
+
# admin model without :lockable reach 6.0 with neither of them.
|
|
3
|
+
class AddLockoutColumnsToSpreeAdminUsers < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :spree_admin_users, :failed_attempts, :integer unless column_exists?(:spree_admin_users, :failed_attempts)
|
|
6
|
+
add_column :spree_admin_users, :locked_at, :datetime unless column_exists?(:spree_admin_users, :locked_at)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -108,7 +108,7 @@ module Spree
|
|
|
108
108
|
|
|
109
109
|
# Override parent: skip if the model existed before this run. Migrations
|
|
110
110
|
# are append-only — schema changes get a separate migration:
|
|
111
|
-
# pnpm exec spree
|
|
111
|
+
# pnpm exec spree generate migration AddFooToBar foo:string
|
|
112
112
|
def create_migration_file
|
|
113
113
|
if @model_existed_before_run
|
|
114
114
|
say_status :skip, 'migration (model already exists; add a new migration for schema changes)', :yellow
|
|
@@ -295,7 +295,7 @@ module Spree
|
|
|
295
295
|
end
|
|
296
296
|
say " 4. Add `has_many :#{plural_name}` to Spree::Store" if store_scoped?
|
|
297
297
|
if options[:store] || options[:admin]
|
|
298
|
-
say " #{store_scoped? ? 5 : 4}. Run the specs: pnpm exec spree
|
|
298
|
+
say " #{store_scoped? ? 5 : 4}. Run the specs: pnpm exec spree test spec/controllers/spree/api/v3/"
|
|
299
299
|
end
|
|
300
300
|
say ''
|
|
301
301
|
end
|
|
@@ -10,6 +10,12 @@ module Spree
|
|
|
10
10
|
#
|
|
11
11
|
# @see Requirements
|
|
12
12
|
class DefaultRequirements
|
|
13
|
+
# Everything the discontinuation and stock checks read, so a set of
|
|
14
|
+
# lines is checked at once.
|
|
15
|
+
STOCK_CHECK_ASSOCIATIONS = {
|
|
16
|
+
variant: [{ product: %i[store product_publications] }, { stock_levels: %i[stock_location active_stock_reservations] }]
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
13
19
|
# @param cart [Spree::Cart]
|
|
14
20
|
def initialize(cart)
|
|
15
21
|
@cart = cart
|
|
@@ -33,6 +39,7 @@ module Spree
|
|
|
33
39
|
r << req('address', 'email', I18n.t('spree.checkout_requirements.email_required')) unless @cart.email.present?
|
|
34
40
|
r << req('address', 'ship_address', I18n.t('spree.checkout_requirements.ship_address_required')) if @cart.shipping_address_required? && @cart.ship_address.blank?
|
|
35
41
|
r << req('delivery', 'delivery_method', I18n.t('spree.checkout_requirements.delivery_method_required')) if delivery_step_required? && !delivery_method_selected?
|
|
42
|
+
r.concat(undeliverable_errors)
|
|
36
43
|
r << req('payment', 'payment', I18n.t('spree.checkout_requirements.payment_required')) if payment_required? && !payment_satisfied?
|
|
37
44
|
r << req('address', 'po_number', I18n.t('spree.checkout_requirements.po_number_required')) if po_number_missing?
|
|
38
45
|
r << order_minimum_requirement if below_order_minimum?
|
|
@@ -102,16 +109,37 @@ module Spree
|
|
|
102
109
|
end
|
|
103
110
|
end
|
|
104
111
|
|
|
112
|
+
# A rebuild drops a proposal no method can quote with only a transient
|
|
113
|
+
# warning, so part of a line can be in no fulfillment. Advisory, so the
|
|
114
|
+
# buyer reads it while they can still change the address or the cart.
|
|
115
|
+
def undeliverable_errors
|
|
116
|
+
return [] unless delivery_step_required? && @cart.fulfillments.any?
|
|
117
|
+
|
|
118
|
+
undelivered_line_items.map do |line_item|
|
|
119
|
+
req('delivery', 'line_items', I18n.t('spree.cart_line_item.delivery_unavailable', li_name: line_item.name),
|
|
120
|
+
code: 'delivery_unavailable')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A line short of stock is packed short too; the stock check already
|
|
125
|
+
# names it. Only lines short of a fulfillment pay for that check, so a
|
|
126
|
+
# cart read stays at one query.
|
|
127
|
+
def undelivered_line_items
|
|
128
|
+
allocated = Spree::FulfillmentItem.where(fulfillment_id: @cart.fulfillments.select(:id))
|
|
129
|
+
.group(:line_item_id).sum(:quantity)
|
|
130
|
+
short = @cart.line_items.select { |line_item| allocated.fetch(line_item.id, 0) < line_item.quantity }
|
|
131
|
+
return [] if short.empty?
|
|
132
|
+
|
|
133
|
+
ActiveRecord::Associations::Preloader.new(records: short, associations: STOCK_CHECK_ASSOCIATIONS).call
|
|
134
|
+
short.reject { |line_item| discontinued?(line_item) || !line_item.sufficient_stock? }
|
|
135
|
+
end
|
|
136
|
+
|
|
105
137
|
def discontinued?(line_item)
|
|
106
138
|
line_item.variant.nil? || line_item.variant.discontinued? || line_item.variant.product.discontinued?
|
|
107
139
|
end
|
|
108
140
|
|
|
109
141
|
def completion_line_items
|
|
110
|
-
|
|
111
|
-
# at once.
|
|
112
|
-
@completion_line_items ||= @cart.line_items.includes(
|
|
113
|
-
variant: [{ product: %i[store product_publications] }, { stock_levels: %i[stock_location active_stock_reservations] }]
|
|
114
|
-
).to_a
|
|
142
|
+
@completion_line_items ||= @cart.line_items.includes(STOCK_CHECK_ASSOCIATIONS).to_a
|
|
115
143
|
end
|
|
116
144
|
|
|
117
145
|
def delivery_step_required?
|
|
@@ -195,6 +195,12 @@ module Spree
|
|
|
195
195
|
# imports
|
|
196
196
|
preference :large_import_threshold, :integer, default: 500, env: 'SPREE_LARGE_IMPORT_THRESHOLD' # imports with more rows than this skip per-row UI broadcasts and use bulk processing
|
|
197
197
|
|
|
198
|
+
# maintenance tasks
|
|
199
|
+
# Turn off to run upgrade steps only from the command line: the dashboard
|
|
200
|
+
# page and its Admin API endpoints are refused. For hosts where the people
|
|
201
|
+
# holding a store's admin role are not the ones operating the installation,
|
|
202
|
+
# such as a platform serving many merchants from one database.
|
|
203
|
+
preference :maintenance_tasks_dashboard_enabled, :boolean, default: true, env: 'SPREE_MAINTENANCE_TASKS_DASHBOARD_ENABLED'
|
|
198
204
|
end
|
|
199
205
|
end
|
|
200
206
|
end
|
data/lib/spree/core/engine.rb
CHANGED
|
@@ -46,6 +46,7 @@ module Spree
|
|
|
46
46
|
:admin_agent_tools,
|
|
47
47
|
:store_agent_tools,
|
|
48
48
|
:integrations,
|
|
49
|
+
:maintenance_tasks,
|
|
49
50
|
:number_generators,
|
|
50
51
|
:subscribers,
|
|
51
52
|
:store_authentication_strategies,
|
|
@@ -77,7 +78,8 @@ module Spree
|
|
|
77
78
|
# Core's defaults are concatenated in after_initialize below.
|
|
78
79
|
%i[subscribers actor_classes commission_rules delivery_method_rules seller_requirements
|
|
79
80
|
delivery_rate_providers tax_providers pricing_providers inventory_providers
|
|
80
|
-
payout_providers digital_asset_providers delivery_profile_types pickup_point_providers
|
|
81
|
+
payout_providers digital_asset_providers delivery_profile_types pickup_point_providers
|
|
82
|
+
maintenance_tasks].each do |registry|
|
|
81
83
|
app.config.spree[registry] = []
|
|
82
84
|
end
|
|
83
85
|
app.config.spree.line_item_comparison_hooks = Set.new
|
|
@@ -540,6 +542,13 @@ module Spree
|
|
|
540
542
|
|
|
541
543
|
Rails.application.config.spree.integrations = []
|
|
542
544
|
|
|
545
|
+
# Upgrade steps backed by a task class are registered from the manifests
|
|
546
|
+
# that name them, so adding a step never means editing a second list.
|
|
547
|
+
Rails.application.config.spree.maintenance_tasks.concat [
|
|
548
|
+
'Spree::MaintenanceTasks::UpgradeStep',
|
|
549
|
+
*Spree::Upgrade.steps.filter_map { |step| step['task_class'] }
|
|
550
|
+
]
|
|
551
|
+
|
|
543
552
|
Rails.application.config.spree.validators.addresses = [
|
|
544
553
|
Spree::Addresses::PhoneValidator
|
|
545
554
|
]
|
|
@@ -201,6 +201,13 @@ module Spree
|
|
|
201
201
|
})
|
|
202
202
|
catalog.register_scope(:integrations, group: :settings, resources: -> { [Spree::Integration] })
|
|
203
203
|
|
|
204
|
+
# Sits with the other sensitive access grants rather than under
|
|
205
|
+
# settings: a task can rewrite any data in the install, so out of the
|
|
206
|
+
# box only the admin role holds it (docs/plans/6.0-maintenance-tasks.md).
|
|
207
|
+
catalog.register_scope(:maintenance_tasks, group: :access, resources: -> {
|
|
208
|
+
[Spree::MaintenanceTaskRun, :maintenance_task]
|
|
209
|
+
})
|
|
210
|
+
|
|
204
211
|
# The CORS allowlist is a credential as much as a key is: an origin on
|
|
205
212
|
# it may call the cookie-authenticated refresh endpoint from a browser
|
|
206
213
|
# and read back any signed-in admin's access token. So it sits with
|
data/lib/spree/core/version.rb
CHANGED
data/lib/spree/core.rb
CHANGED
|
@@ -135,7 +135,8 @@ module Spree
|
|
|
135
135
|
stock_reservations: :default,
|
|
136
136
|
tax_identifiers: :default,
|
|
137
137
|
data_requests: :default,
|
|
138
|
-
payouts: :default
|
|
138
|
+
payouts: :default,
|
|
139
|
+
maintenance_tasks: :default
|
|
139
140
|
).tap do |queues|
|
|
140
141
|
# @deprecated The taxons queue was renamed to categories in 6.0; removed in 6.1.
|
|
141
142
|
queues.define_singleton_method(:taxons) do
|
|
@@ -455,6 +456,19 @@ module Spree
|
|
|
455
456
|
custom_fields
|
|
456
457
|
end
|
|
457
458
|
|
|
459
|
+
# Registered maintenance task class names — the tasks an operator can run
|
|
460
|
+
# from the dashboard, the Admin API or the CLI
|
|
461
|
+
# (docs/plans/6.0-maintenance-tasks.md). Extensions append their own from an
|
|
462
|
+
# initializer:
|
|
463
|
+
#
|
|
464
|
+
# Spree.maintenance_tasks << 'SpreeReviews::MaintenanceTasks::RecountRatings'
|
|
465
|
+
#
|
|
466
|
+
# Class names rather than classes, so registration is safe at initializer
|
|
467
|
+
# time and survives code reloading in development.
|
|
468
|
+
#
|
|
469
|
+
# @return [Array<String, Class>]
|
|
470
|
+
singleton_class.delegate :maintenance_tasks, :maintenance_tasks=, to: :spree_config
|
|
471
|
+
|
|
458
472
|
# Registry mapping a numbered resource to the generator that produces its
|
|
459
473
|
# document numbers. With no entry, the store's `document_number_format`
|
|
460
474
|
# preference picks between the sequential and random strategies.
|
|
@@ -644,6 +658,7 @@ require 'spree/core/version'
|
|
|
644
658
|
require 'spree/core/number_generator'
|
|
645
659
|
require 'spree/number_generators/registry'
|
|
646
660
|
require 'spree/migrations'
|
|
661
|
+
require 'spree/upgrade'
|
|
647
662
|
require 'spree/validators'
|
|
648
663
|
require 'spree/core/engine'
|
|
649
664
|
|
|
@@ -651,6 +666,7 @@ require 'spree/i18n'
|
|
|
651
666
|
require 'spree/iso_data'
|
|
652
667
|
require 'spree/localized_number'
|
|
653
668
|
require 'spree/measurement'
|
|
669
|
+
require 'spree/time_zones'
|
|
654
670
|
require 'spree/translations'
|
|
655
671
|
require 'spree/money'
|
|
656
672
|
require 'spree/service_module'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Time zones are stored and exchanged as IANA identifiers (`Europe/Berlin`),
|
|
3
|
+
# the names browsers and every non-Ruby client understand. Rails' display
|
|
4
|
+
# names ("Eastern Time (US & Canada)") are converted to them on the way in.
|
|
5
|
+
module TimeZones
|
|
6
|
+
# The zones clients are offered: the IANA zones Rails gives a display name
|
|
7
|
+
# to, with plain `UTC` standing in for `Etc/UTC`. Any other valid IANA
|
|
8
|
+
# identifier is accepted too. The list comes with the Rails version rather
|
|
9
|
+
# than the machine's time zone database, so generated types match wherever
|
|
10
|
+
# the same Rails version runs.
|
|
11
|
+
SUGGESTED = (ActiveSupport::TimeZone::MAPPING.values - ['Etc/UTC'] + ['UTC']).uniq.sort.freeze
|
|
12
|
+
|
|
13
|
+
# @param identifier [String, nil]
|
|
14
|
+
# @return [Boolean] whether the value is an IANA time zone identifier
|
|
15
|
+
def self.valid?(identifier)
|
|
16
|
+
return false if identifier.blank?
|
|
17
|
+
|
|
18
|
+
TZInfo::Timezone.get(identifier.to_s)
|
|
19
|
+
true
|
|
20
|
+
rescue TZInfo::InvalidTimezoneIdentifier
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param value [String, nil] an IANA identifier or a Rails time zone name
|
|
25
|
+
# @return [String, nil] the IANA identifier, or the value unchanged when it
|
|
26
|
+
# names no zone the installed time zone database knows
|
|
27
|
+
def self.normalize(value)
|
|
28
|
+
return value if value.blank? || valid?(value)
|
|
29
|
+
|
|
30
|
+
identifier = ActiveSupport::TimeZone::MAPPING[value.to_s]
|
|
31
|
+
valid?(identifier) ? identifier : value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|