spree_core 6.0.0.rc7 → 6.0.0
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/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/users_to_customers.rb +28 -0
- data/app/maintenance_tasks/spree/maintenance_tasks/upgrade_step.rb +44 -0
- data/app/models/spree/ability.rb +9 -0
- data/app/models/spree/maintenance_task.rb +221 -0
- data/app/models/spree/maintenance_task_run.rb +78 -0
- data/app/models/spree/upgrade_record.rb +62 -0
- 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/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/lib/generators/spree/api_resource/api_resource_generator.rb +2 -2
- 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 +16 -1
- data/lib/spree/testing_support/factories/maintenance_task_run_factory.rb +7 -0
- data/lib/spree/upgrade.rb +189 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +17 -10
- data/lib/tasks/maintenance_tasks.rake +42 -0
- data/lib/tasks/upgrade.rake +81 -60
- metadata +25 -6
- data/lib/tasks/capture_methods_migration.rake +0 -30
- data/lib/tasks/order_market_backfill.rake +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 579b3e333d378aec89721e12502e86fe2f529d99e18f087ecaf29cebe6e6b772
|
|
4
|
+
data.tar.gz: 29411570d9588634cfa2a4f559bea3bf6622c8672b90415a38ee1372a996494f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38eef7cff0fce557dd3b14460d3bbdf50e410fc28ae7e9c74fa59f92767078cc74531f221d7a615ba53af0297c3dd8bd9e72ab15a5f09dd5ab88ceff20b0e653
|
|
7
|
+
data.tar.gz: dd2f38e2a6c8b179610ae41ded85992b051081ed71ac110e3a078ebd595b4ea273bbdabf6b64226af1831ce7e93eda1e32591dda4f5da9969dc700aaa3d6f587
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
# Executes one Spree::MaintenanceTaskRun on Active Job Continuations
|
|
4
|
+
# (docs/plans/6.0-maintenance-tasks.md).
|
|
5
|
+
#
|
|
6
|
+
# Three things can stop a run mid-collection, and all three resolve to the
|
|
7
|
+
# same checkpoint: the worker is shutting down (Continuations re-queue the
|
|
8
|
+
# job), the execution has used its time slice (the runner re-queues
|
|
9
|
+
# itself so no single task holds a worker), or an operator asked to
|
|
10
|
+
# cancel (the Cancel service writes the request, the runner observes it
|
|
11
|
+
# here). The run row's cursor is written before each checkpoint, so
|
|
12
|
+
# whichever path fires, the next execution restarts from the same record.
|
|
13
|
+
class RunJob < ::Spree::BaseJob
|
|
14
|
+
include ActiveJob::Continuable
|
|
15
|
+
|
|
16
|
+
queue_as Spree.queues.maintenance_tasks
|
|
17
|
+
|
|
18
|
+
# A run that fails is recorded as failed; it must not be replayed by the
|
|
19
|
+
# backend behind the operator's back. Running it again is an explicit
|
|
20
|
+
# action.
|
|
21
|
+
#
|
|
22
|
+
# `inline` is a run executing in the caller's own process — the
|
|
23
|
+
# upgrade walk and the command line. It holds no worker, so it is not
|
|
24
|
+
# sliced: handing the remainder to a worker would let the caller report
|
|
25
|
+
# a step done that has not finished, on a deploy where no worker runs.
|
|
26
|
+
def perform(run_id, inline: false)
|
|
27
|
+
# Outside the steps on purpose — runs on every execution, resumes
|
|
28
|
+
# included.
|
|
29
|
+
@run = Spree::MaintenanceTaskRun.find_by(id: run_id)
|
|
30
|
+
return if @run.nil?
|
|
31
|
+
# `cancelling` counts as active: a job re-queued after its worker
|
|
32
|
+
# stopped is the only execution that will ever answer it.
|
|
33
|
+
return unless @run.active?
|
|
34
|
+
|
|
35
|
+
@task_class = @run.task_class
|
|
36
|
+
return unfindable_task if @task_class.nil?
|
|
37
|
+
|
|
38
|
+
@inline = inline
|
|
39
|
+
@execution_started_at = Time.current
|
|
40
|
+
@time_running_before = @run.time_running.to_f
|
|
41
|
+
prepare
|
|
42
|
+
return if @halted
|
|
43
|
+
|
|
44
|
+
check_operator_request
|
|
45
|
+
return if @halted
|
|
46
|
+
return refuse_duplicate if duplicate_run?
|
|
47
|
+
|
|
48
|
+
step :start
|
|
49
|
+
# Seeded from the row rather than from the continuation: the row
|
|
50
|
+
# survives a dropped job, which a re-queued execution then picks up
|
|
51
|
+
# with no continuation at all.
|
|
52
|
+
step :process_collection, start: @run.cursor
|
|
53
|
+
step :finish
|
|
54
|
+
rescue SignalException => e
|
|
55
|
+
# Ctrl-C or a deploy stopping an inline run. No job was queued for it,
|
|
56
|
+
# so nothing would ever come back to a run left `running`, and it
|
|
57
|
+
# would block every later run of its task.
|
|
58
|
+
record_error(e) if @inline && run&.reload&.active?
|
|
59
|
+
raise
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
attr_reader :run, :task
|
|
65
|
+
|
|
66
|
+
def start
|
|
67
|
+
return if run.running?
|
|
68
|
+
|
|
69
|
+
started = write_status('running', from: %w[enqueued interrupted],
|
|
70
|
+
started_at: run.started_at || Time.current,
|
|
71
|
+
job_id: job_id,
|
|
72
|
+
tick_total: run.tick_total || safe_count)
|
|
73
|
+
# An operator cancelled the run after the check above.
|
|
74
|
+
return check_operator_request unless started
|
|
75
|
+
|
|
76
|
+
publish('started')
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
record_error(e)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Outside the steps, so an error here would never reach the rescues
|
|
82
|
+
# below: a run whose arguments no longer fit its task (a deploy removed
|
|
83
|
+
# a parameter) would stay queued forever, or be resumed by Continuations
|
|
84
|
+
# without end. Recorded as a failure instead.
|
|
85
|
+
def prepare
|
|
86
|
+
@task = @task_class.new(@run.arguments.presence || {})
|
|
87
|
+
@task.run = @run
|
|
88
|
+
resume_after_time_slice
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
record_error(e)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# One run per task is checked when a run is created, but two requests
|
|
94
|
+
# can both pass that check before either is saved. The older run wins,
|
|
95
|
+
# so the outcome does not depend on which worker picks a job up first.
|
|
96
|
+
def duplicate_run?
|
|
97
|
+
return false unless run.enqueued?
|
|
98
|
+
|
|
99
|
+
run.class.active.where(task_name: run.task_name).where(run.class.arel_table[:id].lt(run.id)).exists?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def refuse_duplicate
|
|
103
|
+
close_run('cancelled', 'Spree::MaintenanceTasks::AlreadyRunning',
|
|
104
|
+
"#{run.task_api_type} was already running, so this run did not start")
|
|
105
|
+
publish('cancelled')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Continuations skip `start` once it has completed, so an execution
|
|
109
|
+
# picking up after its time slice would otherwise keep reporting
|
|
110
|
+
# `interrupted` while it works — and the Cancel service would treat it as
|
|
111
|
+
# idle. Conditional, so a cancel written while the job sat
|
|
112
|
+
# in the queue is not overwritten.
|
|
113
|
+
def resume_after_time_slice
|
|
114
|
+
return unless run.interrupted?
|
|
115
|
+
|
|
116
|
+
run.class.where(id: run.id, status: 'interrupted').update_all(status: 'running', updated_at: Time.current)
|
|
117
|
+
run.reload
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The collection walk. Batches are fetched by cursor rather than by
|
|
121
|
+
# offset so a resumed execution reads nothing it has already processed.
|
|
122
|
+
def process_collection(step)
|
|
123
|
+
return if @halted
|
|
124
|
+
|
|
125
|
+
if task.no_collection?
|
|
126
|
+
process_single
|
|
127
|
+
return
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
loop do
|
|
131
|
+
break if @halted
|
|
132
|
+
|
|
133
|
+
items = next_batch(step.cursor)
|
|
134
|
+
break if items.empty?
|
|
135
|
+
|
|
136
|
+
process_batch(items)
|
|
137
|
+
break if @halted
|
|
138
|
+
|
|
139
|
+
# One cursor value, the last id seen, written to the row and to the
|
|
140
|
+
# continuation. Persisted before the checkpoint — an interruption
|
|
141
|
+
# between these two lines must not lose the batch already applied.
|
|
142
|
+
cursor = items.last.id.to_s
|
|
143
|
+
persist_progress(cursor: cursor, count: items.size)
|
|
144
|
+
step.set!(cursor)
|
|
145
|
+
|
|
146
|
+
check_operator_request
|
|
147
|
+
check_time_slice(step)
|
|
148
|
+
end
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
# Not discard_on: Continuations resume errors raised after progress,
|
|
151
|
+
# so a discard handler would never see them and the run would loop.
|
|
152
|
+
record_error(e)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The callback runs before the row is written so a task can summarise in
|
|
156
|
+
# `after_complete` — tallying there and persisting first would silently
|
|
157
|
+
# drop whatever it recorded.
|
|
158
|
+
def finish
|
|
159
|
+
return if @halted
|
|
160
|
+
|
|
161
|
+
task.after_complete
|
|
162
|
+
return check_operator_request unless write_status('succeeded', from: 'running',
|
|
163
|
+
ended_at: Time.current,
|
|
164
|
+
tallies: merged_tallies)
|
|
165
|
+
|
|
166
|
+
publish('succeeded')
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
record_error(e)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A no_collection task is one unit of work: no cursor, no partial
|
|
172
|
+
# progress, and an error fails the whole run.
|
|
173
|
+
def process_single
|
|
174
|
+
task.process
|
|
175
|
+
persist_progress(cursor: nil, count: 1)
|
|
176
|
+
rescue StandardError => e
|
|
177
|
+
record_error(e)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def process_batch(items)
|
|
181
|
+
items.each { |item| task.process(item) }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Cursor-paged rather than find_each: the cursor has to survive between
|
|
185
|
+
# executions, which an in-memory batch enumerator cannot do.
|
|
186
|
+
def next_batch(cursor)
|
|
187
|
+
scope = task.collection
|
|
188
|
+
scope = scope.where(scope.arel_table[scope.primary_key].gt(cursor)) if cursor.present?
|
|
189
|
+
scope.limit(task.class.batch_size).to_a
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# update_columns rather than update!: progress is written on every batch
|
|
193
|
+
# and carries no validation of its own.
|
|
194
|
+
def persist_progress(cursor:, count:)
|
|
195
|
+
run.update_columns(
|
|
196
|
+
cursor: cursor,
|
|
197
|
+
tick_count: run.tick_count + count,
|
|
198
|
+
tallies: merged_tallies,
|
|
199
|
+
time_running: accumulated_time,
|
|
200
|
+
updated_at: Time.current
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Cancel is a request written to the row by the Cancel service; the
|
|
205
|
+
# runner is the only writer of the resulting state, so a killed worker
|
|
206
|
+
# can never leave `cancelled` on a run that is still moving.
|
|
207
|
+
#
|
|
208
|
+
# Read once per batch rather than on a timer: a primary-key read costs
|
|
209
|
+
# nothing beside the batch of writes it follows, and a time-based
|
|
210
|
+
# throttle would make a fast task ignore a cancel altogether — the
|
|
211
|
+
# batches would all land inside one interval.
|
|
212
|
+
def check_operator_request
|
|
213
|
+
requested = run.class.where(id: run.id).pick(:status)
|
|
214
|
+
return unless requested.in?(%w[cancelling cancelled])
|
|
215
|
+
|
|
216
|
+
write_status('cancelled', from: requested, ended_at: Time.current, time_running: accumulated_time)
|
|
217
|
+
publish('cancelled')
|
|
218
|
+
@halted = true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The control services write to the same row this job holds in memory,
|
|
222
|
+
# so the write only lands while the row is still in the status the
|
|
223
|
+
# runner expects. Otherwise an operator's cancel, written
|
|
224
|
+
# between the runner's read and its write, would be overwritten.
|
|
225
|
+
#
|
|
226
|
+
# @return [Boolean] whether the row was in an expected status
|
|
227
|
+
def write_status(status, from:, **attributes)
|
|
228
|
+
written = run.class.where(id: run.id, status: from).
|
|
229
|
+
update_all(status: status, updated_at: Time.current, **attributes)
|
|
230
|
+
run.reload
|
|
231
|
+
written.positive?
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def check_time_slice(step)
|
|
235
|
+
return if @inline
|
|
236
|
+
return if Time.current - @execution_started_at < Spree::MaintenanceTask::MAX_RUN_TIME
|
|
237
|
+
|
|
238
|
+
return check_operator_request unless write_status('interrupted', from: 'running',
|
|
239
|
+
time_running: accumulated_time)
|
|
240
|
+
|
|
241
|
+
publish('interrupted')
|
|
242
|
+
# Raises Continuation::Interrupt, which inherits Exception and is
|
|
243
|
+
# never caught by the rescues above.
|
|
244
|
+
interrupt!(reason: :run_time_limit)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Recording a failure must never fail. This is the last thing standing
|
|
248
|
+
# between a crashed task and a run stuck in `running` forever, so it
|
|
249
|
+
# writes columns directly rather than going through validations that
|
|
250
|
+
# could reject the very state that describes the problem.
|
|
251
|
+
def record_error(error)
|
|
252
|
+
run.reload
|
|
253
|
+
run.update_columns(
|
|
254
|
+
status: 'errored',
|
|
255
|
+
ended_at: Time.current,
|
|
256
|
+
time_running: accumulated_time,
|
|
257
|
+
tallies: merged_tallies,
|
|
258
|
+
error_class: error.class.name,
|
|
259
|
+
error_message: error.message.to_s.truncate(1000),
|
|
260
|
+
error_backtrace: cleaned_backtrace(error),
|
|
261
|
+
updated_at: Time.current
|
|
262
|
+
)
|
|
263
|
+
publish('errored')
|
|
264
|
+
Rails.error.report(error, handled: true)
|
|
265
|
+
@halted = true
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def cleaned_backtrace(error)
|
|
269
|
+
return nil if error.backtrace.blank?
|
|
270
|
+
|
|
271
|
+
Rails.backtrace_cleaner.clean(error.backtrace).first(50).join("\n")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# The task was removed from the registry between enqueue and execution —
|
|
275
|
+
# a deploy that dropped it. Recorded rather than raised: retrying cannot
|
|
276
|
+
# bring the class back.
|
|
277
|
+
def unfindable_task
|
|
278
|
+
close_run('errored', 'Spree::MaintenanceTasks::TaskNotRegistered',
|
|
279
|
+
"#{run.task_name} is not a registered maintenance task")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Ends a run that never started work, so there is nothing to tally.
|
|
283
|
+
def close_run(status, error_class, error_message)
|
|
284
|
+
run.update_columns(status: status, ended_at: Time.current, error_class: error_class,
|
|
285
|
+
error_message: error_message, updated_at: Time.current)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Summed, not replaced: a task instance counts only what the current
|
|
289
|
+
# execution did, so an interrupted run that merged would report its last
|
|
290
|
+
# slice as the whole total. Baselined at the first checkpoint of each
|
|
291
|
+
# execution so repeated writes within one execution stay accurate.
|
|
292
|
+
def merged_tallies
|
|
293
|
+
@tally_baseline ||= (run.tallies || {}).dup
|
|
294
|
+
return @tally_baseline.dup if task.nil?
|
|
295
|
+
|
|
296
|
+
task.tallies.each_with_object(@tally_baseline.dup) do |(key, value), result|
|
|
297
|
+
result[key] = result.fetch(key, 0) + value
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# From the total this execution started with, never the row's current
|
|
302
|
+
# value: progress rewrites the row every batch, so adding elapsed time
|
|
303
|
+
# to it would count the same seconds again on each write.
|
|
304
|
+
def accumulated_time
|
|
305
|
+
return run.time_running if @execution_started_at.nil?
|
|
306
|
+
|
|
307
|
+
@time_running_before + (Time.current - @execution_started_at)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def safe_count
|
|
311
|
+
task.count
|
|
312
|
+
rescue StandardError
|
|
313
|
+
nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def publish(event)
|
|
317
|
+
run.publish_event("maintenance_task_run.#{event}")
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# 5.6 → 6.0: market is mandatory on orders in Spree 6, and orders placed
|
|
5
|
+
# before markets existed carry NULLs. Each order takes its own store's
|
|
6
|
+
# default market, so the walk is per order rather than one UPDATE per
|
|
7
|
+
# store — that is what gives the run a cursor to resume from and a
|
|
8
|
+
# progress bar to watch on a catalog-sized table.
|
|
9
|
+
#
|
|
10
|
+
# Orders whose store has no default market are counted and left alone;
|
|
11
|
+
# there is nothing to assign, and failing the run would strand every
|
|
12
|
+
# other store's orders behind them.
|
|
13
|
+
class BackfillOrderMarkets < Spree::MaintenanceTask
|
|
14
|
+
supports_dry_run
|
|
15
|
+
collection_batch_size 500
|
|
16
|
+
|
|
17
|
+
def collection
|
|
18
|
+
Spree::Order.unscoped.where(market_id: nil).order(:id)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def process(order)
|
|
22
|
+
market_id = default_market_id_for(order.store_id)
|
|
23
|
+
|
|
24
|
+
if market_id.nil?
|
|
25
|
+
tally(:skipped_no_default_market)
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return tally(:would_update) if dry_run?
|
|
30
|
+
|
|
31
|
+
order.update_columns(market_id: market_id, updated_at: Time.current)
|
|
32
|
+
tally(:updated)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# One lookup per store rather than per order: a backfill of this size
|
|
38
|
+
# walks the same handful of stores over and over.
|
|
39
|
+
def default_market_id_for(store_id)
|
|
40
|
+
@default_market_ids ||= {}
|
|
41
|
+
return @default_market_ids[store_id] if @default_market_ids.key?(store_id)
|
|
42
|
+
|
|
43
|
+
@default_market_ids[store_id] = Spree::Store.find_by(id: store_id)&.default_market&.id
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# 5.6 → 6.0 conversion of the per-payment-method auto_capture boolean onto
|
|
5
|
+
# the capture_method vocabulary (docs/plans/6.0-store-scoped-configuration.md).
|
|
6
|
+
#
|
|
7
|
+
# A method that captured on authorization charges at checkout. One that
|
|
8
|
+
# did not only recorded "not at checkout" — the old column could not say
|
|
9
|
+
# whether dispatch or staff was meant to take the money, and the store's
|
|
10
|
+
# own setting is what decided. Leaving those rows empty is what makes them
|
|
11
|
+
# keep inheriting it, so only the true case is written here, and the
|
|
12
|
+
# collection is the true case alone: any other scope would never reach
|
|
13
|
+
# zero remaining.
|
|
14
|
+
class CaptureMethods < Spree::MaintenanceTask
|
|
15
|
+
supports_dry_run
|
|
16
|
+
collection_batch_size 5_000
|
|
17
|
+
|
|
18
|
+
def collection
|
|
19
|
+
Spree::PaymentMethod.unscoped.where(capture_method: nil, auto_capture: true).order(:id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def process(payment_method)
|
|
23
|
+
return tally(:would_convert) if dry_run?
|
|
24
|
+
|
|
25
|
+
payment_method.update_columns(capture_method: 'checkout', updated_at: Time.current)
|
|
26
|
+
tally(:converted)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Rows the conversion deliberately leaves alone, reported once so an
|
|
30
|
+
# operator can see they were considered rather than missed.
|
|
31
|
+
def after_complete
|
|
32
|
+
return if dry_run?
|
|
33
|
+
|
|
34
|
+
inheriting = Spree::PaymentMethod.unscoped.where(capture_method: nil, auto_capture: false).count
|
|
35
|
+
tally(:left_inheriting_store_setting, inheriting) if inheriting.positive?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# Creates a delivery for every fulfillment that carries a 5.6 tracking
|
|
5
|
+
# number, moving carrier tracking onto its own record.
|
|
6
|
+
class Deliveries < RakeStep
|
|
7
|
+
runs_rake_task 'spree:migrate_deliveries'
|
|
8
|
+
|
|
9
|
+
attribute :batch_size, :integer, default: 1_000
|
|
10
|
+
|
|
11
|
+
passes_to_environment :batch_size, as: 'BATCH_SIZE'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# Copies the coupon code a placed order was bought with onto the order
|
|
5
|
+
# itself.
|
|
6
|
+
class OrderCouponCodes < RakeStep
|
|
7
|
+
runs_rake_task 'spree:backfill_order_coupon_codes'
|
|
8
|
+
|
|
9
|
+
attribute :batch_size, :integer, default: 500
|
|
10
|
+
|
|
11
|
+
passes_to_environment :batch_size, as: 'BATCH_SIZE'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# Rewrites Option Value promotion rules so they reference option values
|
|
5
|
+
# directly rather than the join rows they used to point at.
|
|
6
|
+
class PromotionOptionValueRules < RakeStep
|
|
7
|
+
runs_rake_task 'spree:upgrade:migrate_promotion_option_value_rules'
|
|
8
|
+
|
|
9
|
+
attribute :skip_failed_rows, :boolean, default: false
|
|
10
|
+
|
|
11
|
+
passes_to_environment :skip_failed_rows, as: 'SKIP_FAILED_ROWS'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# Base for the one-pass upgrade steps that ask the operator for parameters
|
|
5
|
+
# (docs/plans/6.0-maintenance-tasks.md). The rake body stays the
|
|
6
|
+
# implementation; this class only maps its parameters onto the environment
|
|
7
|
+
# variables the body reads. A step without parameters runs through
|
|
8
|
+
# `UpgradeStep` and needs no class at all.
|
|
9
|
+
class RakeStep < Spree::MaintenanceTask
|
|
10
|
+
ENVIRONMENT_LOCK = Mutex.new
|
|
11
|
+
|
|
12
|
+
no_collection
|
|
13
|
+
|
|
14
|
+
class_attribute :rake_task_name, instance_writer: false
|
|
15
|
+
class_attribute :environment_parameters, instance_writer: false, default: {}.freeze
|
|
16
|
+
|
|
17
|
+
# @param name [String] the rake task this step runs
|
|
18
|
+
def self.runs_rake_task(name)
|
|
19
|
+
self.rake_task_name = name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Declares a parameter the rake body reads from the environment.
|
|
23
|
+
#
|
|
24
|
+
# These bodies were written for a shell, so their answers arrive as
|
|
25
|
+
# environment variables. Mapping them to attributes records the answer
|
|
26
|
+
# on the run, and lets `ARGS` supply it when the step is run on its own.
|
|
27
|
+
#
|
|
28
|
+
# @param attribute_name [Symbol]
|
|
29
|
+
# @param as [String] the environment variable the body reads
|
|
30
|
+
def self.passes_to_environment(attribute_name, as:)
|
|
31
|
+
self.environment_parameters = environment_parameters.merge(attribute_name.to_sym => as).freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The declared parameters an operator exported before running the
|
|
35
|
+
# upgrade walk, so the walk passes them on rather than letting each
|
|
36
|
+
# attribute's default overwrite them.
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash{String => String}]
|
|
39
|
+
def self.arguments_from_environment
|
|
40
|
+
environment_parameters.each_with_object({}) do |(attribute_name, variable), arguments|
|
|
41
|
+
arguments[attribute_name.to_s] = ENV[variable] if ENV.key?(variable)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def process
|
|
46
|
+
with_environment { invoke_rake_task(self.class.rake_task_name) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Restored afterwards so one run never leaks its answers into the next.
|
|
52
|
+
# ENV is shared by the whole process, so two steps running at once in a
|
|
53
|
+
# threaded worker take turns rather than reading each other's answers.
|
|
54
|
+
def with_environment(&block)
|
|
55
|
+
return yield if self.class.environment_parameters.empty?
|
|
56
|
+
|
|
57
|
+
ENVIRONMENT_LOCK.synchronize { with_variables_set(&block) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def with_variables_set
|
|
61
|
+
keys = self.class.environment_parameters.values
|
|
62
|
+
previous = ENV.to_h.slice(*keys)
|
|
63
|
+
|
|
64
|
+
self.class.environment_parameters.each do |attribute_name, variable|
|
|
65
|
+
ENV[variable] = environment_value(public_send(attribute_name))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
keys&.each { |key| ENV[key] = previous[key] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def environment_value(value)
|
|
74
|
+
case value
|
|
75
|
+
when true then 'true'
|
|
76
|
+
when false, nil then nil
|
|
77
|
+
else value.to_s.presence
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# Moves return authorizations onto the 6.0 Return and Exchange models.
|
|
5
|
+
#
|
|
6
|
+
# Resumable by construction: the legacy number is copied onto the new
|
|
7
|
+
# record and uniquely indexed there, so what is still outstanding is a
|
|
8
|
+
# query rather than tracked state.
|
|
9
|
+
class Returns < RakeStep
|
|
10
|
+
runs_rake_task 'spree:upgrade:migrate_returns'
|
|
11
|
+
|
|
12
|
+
attribute :skip_failed_rows, :boolean, default: false
|
|
13
|
+
attribute :batch_size, :integer, default: 500
|
|
14
|
+
|
|
15
|
+
passes_to_environment :skip_failed_rows, as: 'SKIP_FAILED_ROWS'
|
|
16
|
+
passes_to_environment :batch_size, as: 'BATCH_SIZE'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
module Upgrade
|
|
4
|
+
# Copies the legacy Devise user table onto the gem-owned customer model
|
|
5
|
+
# and re-points every polymorphic reference that named the old class.
|
|
6
|
+
#
|
|
7
|
+
# Two of its parameters are questions only an operator can answer: whether
|
|
8
|
+
# a Devise pepper was ever configured (an encrypted password cannot be
|
|
9
|
+
# read without knowing, and Devise is gone by the time this runs), and
|
|
10
|
+
# what to do with source rows that cannot be copied.
|
|
11
|
+
class UsersToCustomers < RakeStep
|
|
12
|
+
runs_rake_task 'spree:upgrade:migrate_users_to_customers'
|
|
13
|
+
|
|
14
|
+
attribute :confirm_no_pepper, :boolean, default: false
|
|
15
|
+
attribute :skip_invalid_rows, :boolean, default: false
|
|
16
|
+
attribute :source_user_table, :string
|
|
17
|
+
attribute :source_user_type, :string
|
|
18
|
+
attribute :batch_size, :integer, default: 1_000
|
|
19
|
+
|
|
20
|
+
passes_to_environment :confirm_no_pepper, as: 'CONFIRM_NO_PEPPER'
|
|
21
|
+
passes_to_environment :skip_invalid_rows, as: 'SKIP_INVALID_ROWS'
|
|
22
|
+
passes_to_environment :source_user_table, as: 'SOURCE_USER_TABLE'
|
|
23
|
+
passes_to_environment :source_user_type, as: 'SOURCE_USER_TYPE'
|
|
24
|
+
passes_to_environment :batch_size, as: 'BATCH_SIZE'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
# Runs one rake-backed step of an upgrade manifest, so a shell walk and a
|
|
4
|
+
# dashboard run leave the same audit record
|
|
5
|
+
# (docs/plans/6.0-maintenance-tasks.md).
|
|
6
|
+
#
|
|
7
|
+
# Every one-pass step names its rake task in the manifest and runs here, so
|
|
8
|
+
# the rake body stays the only implementation. A step gets a task class of
|
|
9
|
+
# its own only when it walks rows (progress, resume, dry run) or asks the
|
|
10
|
+
# operator for parameters.
|
|
11
|
+
class UpgradeStep < Spree::MaintenanceTask
|
|
12
|
+
no_collection
|
|
13
|
+
|
|
14
|
+
attribute :step_id, :string
|
|
15
|
+
validates :step_id, presence: true
|
|
16
|
+
|
|
17
|
+
validate :step_must_exist
|
|
18
|
+
|
|
19
|
+
def process
|
|
20
|
+
invoke_rake_task(manifest_step.fetch('task'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The manifest entry this run is executing, resolved across every
|
|
24
|
+
# manifest rather than a fixed one — the walker may be part way through a
|
|
25
|
+
# multi-release upgrade.
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash, nil]
|
|
28
|
+
def manifest_step
|
|
29
|
+
@manifest_step ||= Spree::Upgrade.find_step(step_id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# A step with a task class of its own runs as that class, so only a
|
|
35
|
+
# step that names a rake task can be run here.
|
|
36
|
+
def step_must_exist
|
|
37
|
+
return if step_id.blank?
|
|
38
|
+
return if manifest_step&.dig('task').present?
|
|
39
|
+
|
|
40
|
+
errors.add(:step_id, "no upgrade step with id #{step_id} runs a rake task")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/app/models/spree/ability.rb
CHANGED
|
@@ -95,6 +95,15 @@ module Spree
|
|
|
95
95
|
apply_staff_baseline
|
|
96
96
|
grantable_keys_for(roles).each { |key| activate_permission(key) }
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
withhold_maintenance_tasks unless Spree::Config.maintenance_tasks_dashboard_enabled
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Applied last so it overrides the full-access grant, and travels to the
|
|
103
|
+
# dashboard with the other rules, which then hides the page.
|
|
104
|
+
def withhold_maintenance_tasks
|
|
105
|
+
cannot :manage, [Spree::MaintenanceTaskRun, :maintenance_task]
|
|
106
|
+
@permission_keys -= %w[read_maintenance_tasks write_maintenance_tasks]
|
|
98
107
|
end
|
|
99
108
|
|
|
100
109
|
# Every key the roles ask for, bounded at activation by what each role's
|