spree_core 6.0.0.beta1 → 6.0.0.beta2
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/sample_data/load_job.rb +13 -0
- data/app/models/concerns/spree/acted_by.rb +210 -0
- data/app/models/concerns/spree/actor.rb +51 -0
- data/app/models/concerns/spree/admin_user_methods.rb +48 -17
- data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
- data/app/models/spree/api_key.rb +5 -3
- data/app/models/spree/catalog.rb +50 -13
- data/app/models/spree/claim.rb +2 -1
- data/app/models/spree/current.rb +41 -13
- data/app/models/spree/exchange.rb +2 -1
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/order.rb +54 -6
- data/app/models/spree/pricing_provider/internal.rb +37 -15
- data/app/models/spree/refund.rb +2 -1
- data/app/models/spree/return.rb +2 -1
- data/app/models/spree/stock_receipt.rb +7 -2
- data/app/models/spree/store.rb +4 -0
- data/app/services/spree/orders/approve.rb +3 -2
- data/app/workflows/spree/orders/cancel.rb +3 -2
- data/app/workflows/spree/refunds/create.rb +1 -1
- data/app/workflows/spree/returns/approve.rb +1 -1
- data/app/workflows/spree/returns/create.rb +2 -2
- data/app/workflows/spree/returns/refund.rb +1 -1
- data/config/locales/en.yml +1 -0
- data/db/migrate/20260916000001_order_operation_actors_polymorphic.rb +30 -0
- data/lib/generators/spree/api_resource/api_resource_generator.rb +63 -5
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +5 -0
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +3 -0
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +4 -1
- data/lib/generators/spree/model/model_generator.rb +40 -1
- data/lib/generators/spree/model/templates/create_table_migration.rb.tt +9 -0
- data/lib/generators/spree/model/templates/model.rb.tt +10 -4
- data/lib/spree/core/configuration.rb +25 -25
- data/lib/spree/core/engine.rb +27 -0
- data/lib/spree/core/preferences/runtime_configuration.rb +76 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +27 -0
- data/lib/spree/testing_support/factories/user_factory.rb +0 -1
- data/lib/tasks/backfill_actor_types.rake +35 -0
- data/lib/tasks/store_settings.rake +1 -0
- metadata +14 -9
- /data/{app/models → lib}/spree/checkout/default_requirements.rb +0 -0
- /data/{app/models → lib}/spree/checkout/registry.rb +0 -0
- /data/{app/models → lib}/spree/checkout/requirement.rb +0 -0
- /data/{app/models → lib}/spree/checkout/requirements.rb +0 -0
- /data/{app/models → lib}/spree/checkout/step.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c2e491640b87a7c66d80915167c1b3e0e342ebc527f98ab6be473494850ed8c
|
|
4
|
+
data.tar.gz: 9507987225c9b98ea0676417742b9c22e338796d9d155f309f3d0f5262617f4d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25b1c9003b48955d08e1d26f0f64d630ae4eafb07043cd49bfb70b22736255b0315f85b6e6f22ab873fc2e24b64cb0a3a7c715ac77b522d58afe1190939f4b78
|
|
7
|
+
data.tar.gz: b7379fef995f651f02229b55260b8685a8e4318e7748be5f2a64aa0476a40577d877f1e32897a3d6542f47beee847e18a431bb027cc1237c189f2bde8e0c0493
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module SampleData
|
|
3
|
+
# Loads the demo catalog, customers and orders in the background. Enqueued
|
|
4
|
+
# by first-run setup when the merchant ticks "load sample data": the
|
|
5
|
+
# loader needs an admin to own its imports and downloads product images,
|
|
6
|
+
# so it can neither run before setup nor inside the setup request.
|
|
7
|
+
class LoadJob < Spree::BaseJob
|
|
8
|
+
def perform
|
|
9
|
+
Spree::SampleData::Loader.call
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Declares a "who performed this action" association — the canceler of an
|
|
3
|
+
# order, the receiver of a stock delivery, the opener of a return.
|
|
4
|
+
#
|
|
5
|
+
# class Spree::Order < Spree.base_class
|
|
6
|
+
# include Spree::ActedBy
|
|
7
|
+
# acted_by :created_by, :approver, :canceler
|
|
8
|
+
# end
|
|
9
|
+
#
|
|
10
|
+
# Each name becomes an optional polymorphic `belongs_to` whose type column is
|
|
11
|
+
# validated against `Spree.actor_classes`, so a row can only ever name a
|
|
12
|
+
# registered actor — an admin user, an API key, or a class an extension
|
|
13
|
+
# registered. See docs/plans/6.0-action-actors.md.
|
|
14
|
+
#
|
|
15
|
+
# Association names, readers and writers are the ones a plain `belongs_to`
|
|
16
|
+
# would give, so the workflows that assign these (`order.canceler = canceler`)
|
|
17
|
+
# are untouched by the conversion.
|
|
18
|
+
module ActedBy
|
|
19
|
+
extend ActiveSupport::Concern
|
|
20
|
+
|
|
21
|
+
# Every model that declares at least one `acted_by` association, so a
|
|
22
|
+
# caller sweeping actor columns — the backfill task, a user's erasure —
|
|
23
|
+
# walks all of them without being handed a list.
|
|
24
|
+
#
|
|
25
|
+
# The registry fills as models load, so it eager-loads first: in
|
|
26
|
+
# development nothing is loaded until something references it, and a
|
|
27
|
+
# caller that silently skipped an unloaded model would leave a deleted
|
|
28
|
+
# person's id behind or half-finish an upgrade.
|
|
29
|
+
#
|
|
30
|
+
# Names are held rather than classes, and resolved on read, so a code
|
|
31
|
+
# reload does not leave the registry pointing at superseded copies of the
|
|
32
|
+
# same model.
|
|
33
|
+
#
|
|
34
|
+
# @return [Array<Class>]
|
|
35
|
+
def self.models
|
|
36
|
+
Rails.application&.eager_load! unless Rails.application&.config&.eager_load
|
|
37
|
+
|
|
38
|
+
registered_model_names.filter_map(&:safe_constantize)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @api private
|
|
42
|
+
# @return [Set<String>]
|
|
43
|
+
def self.registered_model_names
|
|
44
|
+
@registered_model_names ||= Set.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
included do
|
|
48
|
+
# The names declared on this class, so the backfill task can ask a model
|
|
49
|
+
# what it records without being told. Inherited and extended per class,
|
|
50
|
+
# never assigned, so a subclass adding one does not blank its parent's.
|
|
51
|
+
class_attribute :acted_by_associations, default: [], instance_writer: false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The class an actor column names, answering what the reader would resolve
|
|
55
|
+
# rather than what the column literally holds: a row the backfill has not
|
|
56
|
+
# reached carries an id and no type, and the reader resolves it through
|
|
57
|
+
# the admin user class. Serializers read this so `<name>_id` and
|
|
58
|
+
# `<name>_type` never disagree about the same row.
|
|
59
|
+
#
|
|
60
|
+
# @param name [Symbol, String] the acted_by association
|
|
61
|
+
# @return [String, nil]
|
|
62
|
+
def acted_by_type(name)
|
|
63
|
+
stored = self[:"#{name}_type"]
|
|
64
|
+
return stored if stored.present?
|
|
65
|
+
|
|
66
|
+
Spree.admin_user_class.to_s if self[:"#{name}_id"].present?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The actor's prefixed id, encoded from the columns without loading the
|
|
70
|
+
# row — so a page of orders costs no query per actor it names. Paired with
|
|
71
|
+
# {#acted_by_type} so the id and the kind always describe the same row,
|
|
72
|
+
# including one the backfill has not reached.
|
|
73
|
+
#
|
|
74
|
+
# @param name [Symbol, String] the acted_by association
|
|
75
|
+
# @return [String, nil]
|
|
76
|
+
def acted_by_prefixed_id(name)
|
|
77
|
+
Spree::Base.polymorphic_prefixed_id(acted_by_type(name), self[:"#{name}_id"])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Names the admin user class on every un-backfilled row of `record`'s
|
|
81
|
+
# lazily-preloaded group, so the association preloads as one query rather
|
|
82
|
+
# than one per row. Writes the in-memory attribute only; nothing is saved.
|
|
83
|
+
#
|
|
84
|
+
# @api private
|
|
85
|
+
# @param record [ActiveRecord::Base]
|
|
86
|
+
# @param name [Symbol] the acted_by association
|
|
87
|
+
# @return [void]
|
|
88
|
+
def self.assume_admin_actor_type(record, name)
|
|
89
|
+
type_column = :"#{name}_type"
|
|
90
|
+
admin_type = Spree.admin_user_class.to_s
|
|
91
|
+
group = record.try(:lazy_preload_context)&.records.presence || [record]
|
|
92
|
+
|
|
93
|
+
group.each do |sibling|
|
|
94
|
+
next unless sibling.instance_of?(record.class)
|
|
95
|
+
next if sibling[type_column].present? || sibling[:"#{name}_id"].blank?
|
|
96
|
+
|
|
97
|
+
sibling[type_column] = admin_type
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Both halves of an actor column, for a caller writing through
|
|
102
|
+
# `update_columns` or `update_all` rather than through the association —
|
|
103
|
+
# an id without its type names nothing, so the pair is produced together
|
|
104
|
+
# and never by hand.
|
|
105
|
+
#
|
|
106
|
+
# changes.merge!(Spree::ActedBy.columns_for(:canceler, canceler))
|
|
107
|
+
#
|
|
108
|
+
# The actor is checked against the registry here because these writes go
|
|
109
|
+
# through `update_columns` / `update_all`, which skip validation — without
|
|
110
|
+
# it an unregistered actor persists as a row the model itself calls
|
|
111
|
+
# invalid.
|
|
112
|
+
#
|
|
113
|
+
# @param name [Symbol, String] the acted_by association
|
|
114
|
+
# @param actor [Object, nil]
|
|
115
|
+
# @raise [ArgumentError] when the actor's class is not registered
|
|
116
|
+
# @return [Hash{Symbol => Object, nil}]
|
|
117
|
+
def self.columns_for(name, actor)
|
|
118
|
+
type = actor && actor.class.polymorphic_name
|
|
119
|
+
|
|
120
|
+
if type.present? && Spree.actor_classes.exclude?(type)
|
|
121
|
+
raise ArgumentError, "#{type} is not a registered actor class — add it to Spree.actor_classes"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
{ :"#{name}_id" => actor&.id, :"#{name}_type" => type }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class_methods do
|
|
128
|
+
# @param names [Array<Symbol>] the association names to declare
|
|
129
|
+
# @return [void]
|
|
130
|
+
def acted_by(*names)
|
|
131
|
+
names = names.map(&:to_sym)
|
|
132
|
+
self.acted_by_associations = acted_by_associations + names
|
|
133
|
+
# An anonymous class — one built in a spec or a console — has no name
|
|
134
|
+
# to register, and a nil in the registry would break every later read.
|
|
135
|
+
Spree::ActedBy.registered_model_names << name_for_actor_registry if name_for_actor_registry
|
|
136
|
+
|
|
137
|
+
names.each do |name|
|
|
138
|
+
belongs_to name, polymorphic: true, optional: true
|
|
139
|
+
|
|
140
|
+
validate_actor_type(name)
|
|
141
|
+
define_transitional_actor_reader(name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# STI subclasses register under the base class, whose table the backfill
|
|
148
|
+
# updates; an anonymous class registers nothing.
|
|
149
|
+
def name_for_actor_registry
|
|
150
|
+
base_class.name
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_actor_type(name)
|
|
154
|
+
type_column = :"#{name}_type"
|
|
155
|
+
|
|
156
|
+
validate do
|
|
157
|
+
value = self[type_column]
|
|
158
|
+
next if value.blank? || Spree.actor_classes.include?(value)
|
|
159
|
+
|
|
160
|
+
errors.add(type_column, :unregistered_actor,
|
|
161
|
+
message: Spree.t('errors.messages.unregistered_actor'))
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Rows written before the type column existed carry an id and no type,
|
|
166
|
+
# which a polymorphic association reads as nil. Until the backfill task
|
|
167
|
+
# has run they resolve through the admin user class, the only thing they
|
|
168
|
+
# could ever have pointed at.
|
|
169
|
+
#
|
|
170
|
+
# The type is filled in on the in-memory attribute and the ordinary
|
|
171
|
+
# association reader does the rest, so the fallback keeps every benefit
|
|
172
|
+
# the association has — preloading included. Resolving it with a direct
|
|
173
|
+
# `find_by` instead would sidestep `ar_lazy_preload`, turning a page of
|
|
174
|
+
# 25 orders into 75 queries while the backfill is outstanding.
|
|
175
|
+
#
|
|
176
|
+
# Nothing is saved: `[]=` writes the attribute, and a record whose only
|
|
177
|
+
# change is this would still need an explicit save to persist it.
|
|
178
|
+
# Prepended rather than defined outright, so `super` reaches the
|
|
179
|
+
# generated reader.
|
|
180
|
+
#
|
|
181
|
+
# Removed in 6.1, once the backfill is a release behind.
|
|
182
|
+
def define_transitional_actor_reader(name)
|
|
183
|
+
transitional_actor_readers.module_eval do
|
|
184
|
+
define_method(name) do
|
|
185
|
+
return super() if self[:"#{name}_type"].present?
|
|
186
|
+
return super() if self[:"#{name}_id"].blank?
|
|
187
|
+
|
|
188
|
+
Spree::Deprecation.warn(
|
|
189
|
+
"#{self.class.name}##{name} resolved through #{Spree.admin_user_class} because " \
|
|
190
|
+
"#{name}_type is blank. Run `rake spree:upgrade:backfill_actor_types`; the fallback " \
|
|
191
|
+
'is removed in Spree 6.1.'
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Fill the type across the whole lazily-preloaded group, not just
|
|
195
|
+
# this record: the preloader batches on the type it finds, and
|
|
196
|
+
# filling one row at a time as each is read produces a query per
|
|
197
|
+
# row instead of one for the page.
|
|
198
|
+
Spree::ActedBy.assume_admin_actor_type(self, name)
|
|
199
|
+
super()
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The per-class module the transitional readers live in, prepended once.
|
|
205
|
+
def transitional_actor_readers
|
|
206
|
+
@transitional_actor_readers ||= Module.new.tap { |mod| prepend(mod) }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# What a model must answer to be recorded as the performer of an action.
|
|
3
|
+
#
|
|
4
|
+
# Included in the admin user class and in {Spree::ApiKey}; an extension's App
|
|
5
|
+
# or bot class includes it too and registers itself in
|
|
6
|
+
# `Spree.actor_classes` (see docs/plans/6.0-action-actors.md).
|
|
7
|
+
#
|
|
8
|
+
# Both methods have defaults that read whatever the including class already
|
|
9
|
+
# offers, so a class that names itself in one of the usual ways needs no
|
|
10
|
+
# override.
|
|
11
|
+
module Actor
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
# The kinds core itself registers, for the generated `ActorKind` type.
|
|
15
|
+
#
|
|
16
|
+
# A literal list rather than one derived from `Spree.actor_classes`: that
|
|
17
|
+
# registry is filled after serializer classes load, so deriving it would
|
|
18
|
+
# make the generated types depend on boot order and on which extensions
|
|
19
|
+
# are installed. The union stays open on the wire, so an extension's kind
|
|
20
|
+
# is still a valid value — it just does not autocomplete.
|
|
21
|
+
BUILT_IN_KINDS = %w[admin_user api_key].freeze
|
|
22
|
+
|
|
23
|
+
# How a timeline names this actor: a person's full name or email, an API
|
|
24
|
+
# key's name, an app's title.
|
|
25
|
+
#
|
|
26
|
+
# @return [String, nil]
|
|
27
|
+
def actor_label
|
|
28
|
+
%i[full_name name email].each do |method|
|
|
29
|
+
next unless respond_to?(method)
|
|
30
|
+
|
|
31
|
+
value = public_send(method)
|
|
32
|
+
value = value.to_s if value.present?
|
|
33
|
+
return value if value.present?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The wire shorthand for this actor's kind — `admin_user`, `api_key`, …
|
|
40
|
+
# The same derivation every polymorphic type column on the Admin API uses.
|
|
41
|
+
#
|
|
42
|
+
# Read from `polymorphic_name`, which is what the `*_type` column stores,
|
|
43
|
+
# so an STI subclass of an actor answers the same kind on the expansion
|
|
44
|
+
# as it does in the column beside it.
|
|
45
|
+
#
|
|
46
|
+
# @return [String]
|
|
47
|
+
def actor_kind
|
|
48
|
+
Spree::Base.polymorphic_api_type(self.class.polymorphic_name)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -2,6 +2,7 @@ module Spree
|
|
|
2
2
|
module AdminUserMethods
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
+
include Spree::Actor
|
|
5
6
|
include Spree::PrefixedId
|
|
6
7
|
include Spree::UserRoles
|
|
7
8
|
include Spree::RansackableAttributes
|
|
@@ -33,15 +34,20 @@ module Spree
|
|
|
33
34
|
|
|
34
35
|
# Associations
|
|
35
36
|
has_many :identities, class_name: 'Spree::UserIdentity', as: :user, dependent: :destroy
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# The order operations became polymorphic in 6.0, so these read through
|
|
38
|
+
# the association rather than a bare foreign key: matching on the id
|
|
39
|
+
# alone would hand this user the rows an API key with the same numeric
|
|
40
|
+
# id performed. The rest still point at admin users only and convert in
|
|
41
|
+
# 6.1 (see docs/plans/6.0-action-actors.md).
|
|
42
|
+
has_many :canceled_orders, class_name: 'Spree::Order', as: :canceler
|
|
43
|
+
has_many :created_orders, class_name: 'Spree::Order', as: :created_by
|
|
44
|
+
has_many :approved_orders, class_name: 'Spree::Order', as: :approver
|
|
45
|
+
has_many :refunded_refunds, class_name: 'Spree::Refund', as: :refunder
|
|
46
|
+
has_many :created_returns, class_name: 'Spree::Return', as: :created_by
|
|
47
|
+
has_many :created_exchanges, class_name: 'Spree::Exchange', as: :created_by
|
|
48
|
+
has_many :created_claims, class_name: 'Spree::Claim', as: :created_by
|
|
39
49
|
has_many :created_gift_cards, class_name: 'Spree::GiftCard', foreign_key: :created_by_id
|
|
40
50
|
has_many :created_gift_card_batches, class_name: 'Spree::GiftCardBatch', foreign_key: :created_by_id
|
|
41
|
-
has_many :refunded_refunds, class_name: 'Spree::Refund', foreign_key: :refunder_id
|
|
42
|
-
has_many :created_returns, class_name: 'Spree::Return', foreign_key: :created_by_id
|
|
43
|
-
has_many :created_exchanges, class_name: 'Spree::Exchange', foreign_key: :created_by_id
|
|
44
|
-
has_many :created_claims, class_name: 'Spree::Claim', foreign_key: :created_by_id
|
|
45
51
|
has_many :created_store_credits, class_name: 'Spree::StoreCredit', foreign_key: :created_by_id
|
|
46
52
|
has_many :exports, class_name: 'Spree::Export', foreign_key: :user_id
|
|
47
53
|
has_many :saved_reports, class_name: 'Spree::SavedReport', foreign_key: :user_id
|
|
@@ -79,28 +85,53 @@ module Spree
|
|
|
79
85
|
def nullify_approver_id_in_approved_orders
|
|
80
86
|
return if self.class != Spree.admin_user_class
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
clear_actor(Spree::Order, :approver)
|
|
83
89
|
end
|
|
84
90
|
|
|
85
91
|
def cleanup_admin_user_resources
|
|
86
92
|
return if self.class != Spree.admin_user_class
|
|
87
93
|
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
# Every actor column that names this user, derived from the `acted_by`
|
|
95
|
+
# declarations rather than listed here, so the 6.1 conversion of the
|
|
96
|
+
# remaining associations needs no edit in this file.
|
|
97
|
+
clear_every_actor
|
|
98
|
+
|
|
99
|
+
# The plain foreign keys that are still admin-user-only. They join the
|
|
100
|
+
# loop above once they convert in 6.1.
|
|
93
101
|
created_gift_cards.update_all(created_by_id: nil, updated_at: Time.current)
|
|
94
102
|
created_gift_card_batches.update_all(created_by_id: nil, updated_at: Time.current)
|
|
95
|
-
refunded_refunds.update_all(refunder_id: nil, updated_at: Time.current)
|
|
96
|
-
created_returns.update_all(created_by_id: nil, updated_at: Time.current)
|
|
97
|
-
created_exchanges.update_all(created_by_id: nil, updated_at: Time.current)
|
|
98
|
-
created_claims.update_all(created_by_id: nil, updated_at: Time.current)
|
|
99
103
|
created_store_credits.update_all(created_by_id: nil, updated_at: Time.current)
|
|
100
104
|
saved_reports.update_all(user_id: nil, updated_at: Time.current)
|
|
101
105
|
|
|
102
106
|
# resources to destroy
|
|
103
107
|
exports.destroy_all
|
|
104
108
|
end
|
|
109
|
+
|
|
110
|
+
# Erases this user from every actor column any model declares with
|
|
111
|
+
# `acted_by`.
|
|
112
|
+
def clear_every_actor
|
|
113
|
+
Spree::ActedBy.models.each do |model|
|
|
114
|
+
model.acted_by_associations.each { |name| clear_actor(model, name) }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Erases this user from one polymorphic actor column, clearing both halves
|
|
119
|
+
# together — a type left behind names a class no id points at.
|
|
120
|
+
#
|
|
121
|
+
# Matched on the id with either this class or no class at all, because a
|
|
122
|
+
# row written before the type column existed carries only the id until
|
|
123
|
+
# `spree:upgrade:backfill_actor_types` runs. Skipping those would leave a
|
|
124
|
+
# deleted person's id on the record through exactly the upgrade window the
|
|
125
|
+
# backfill exists for. The id alone cannot collide here: it is paired with
|
|
126
|
+
# a type that is ours or absent, and absent meant an admin user.
|
|
127
|
+
#
|
|
128
|
+
# @param model [Class]
|
|
129
|
+
# @param name [Symbol] the acted_by association
|
|
130
|
+
def clear_actor(model, name)
|
|
131
|
+
model.
|
|
132
|
+
where(:"#{name}_id" => id).
|
|
133
|
+
where(:"#{name}_type" => [self.class.polymorphic_name, nil]).
|
|
134
|
+
update_all(Spree::ActedBy.columns_for(name, nil).merge(updated_at: Time.current))
|
|
135
|
+
end
|
|
105
136
|
end
|
|
106
137
|
end
|
|
@@ -50,7 +50,7 @@ module Spree
|
|
|
50
50
|
#
|
|
51
51
|
# @return [Boolean]
|
|
52
52
|
def confirmation_required?
|
|
53
|
-
Spree::
|
|
53
|
+
Spree::StorePreferences.read(store, :always_include_confirm_step) ||
|
|
54
54
|
payments.valid.map(&:payment_method).compact.any?(&:confirmation_required?)
|
|
55
55
|
end
|
|
56
56
|
|
data/app/models/spree/api_key.rb
CHANGED
|
@@ -2,6 +2,7 @@ module Spree
|
|
|
2
2
|
class ApiKey < Spree.base_class
|
|
3
3
|
has_prefix_id :key # Spree-specific: api key
|
|
4
4
|
|
|
5
|
+
include Spree::Actor
|
|
5
6
|
include Spree::SingleStoreResource
|
|
6
7
|
|
|
7
8
|
KEY_TYPES = %w[publishable secret].freeze
|
|
@@ -124,10 +125,11 @@ module Spree
|
|
|
124
125
|
|
|
125
126
|
# Revokes this API key by setting +revoked_at+ to the current time.
|
|
126
127
|
#
|
|
127
|
-
# @param
|
|
128
|
+
# @param actor [Object, nil] who performed the revocation — an admin user
|
|
129
|
+
# or another API key (see Spree.actor_classes)
|
|
128
130
|
# @return [Boolean] true if the update succeeded
|
|
129
|
-
def revoke!(
|
|
130
|
-
update!(revoked_at: Time.current, revoked_by:
|
|
131
|
+
def revoke!(actor = nil)
|
|
132
|
+
update!(revoked_at: Time.current, revoked_by: actor)
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
# Whether this key carries the given scope. `write_*` implies the matching
|
data/app/models/spree/catalog.rb
CHANGED
|
@@ -14,8 +14,9 @@ module Spree
|
|
|
14
14
|
# catalog to those companies or divisions rather than to a group.
|
|
15
15
|
#
|
|
16
16
|
# Within whichever audience answered, visibility is the union of its
|
|
17
|
-
# catalogs' assortments
|
|
18
|
-
#
|
|
17
|
+
# catalogs' assortments; pricing walks the assigning nodes nearest first and
|
|
18
|
+
# within one node charges the best price any of its catalogs gives
|
|
19
|
+
# (docs/plans/6.0-b2b-companies-and-catalogs.md).
|
|
19
20
|
class Catalog < Spree.base_class
|
|
20
21
|
has_prefix_id :cat
|
|
21
22
|
|
|
@@ -97,6 +98,17 @@ module Spree
|
|
|
97
98
|
# @param company [Spree::Company, nil]
|
|
98
99
|
# @return [Array<Spree::Catalog>]
|
|
99
100
|
def self.for_company(company)
|
|
101
|
+
groups_for_company(company).flatten
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The same catalogs, grouped by the node that assigns them, nearest first.
|
|
105
|
+
# Only pricing needs the boundaries: catalogs sharing a node rank equally
|
|
106
|
+
# and the best price among them wins, while a nearer node outranks a
|
|
107
|
+
# further one. Everything else takes the flat {.for_company}.
|
|
108
|
+
#
|
|
109
|
+
# @param company [Spree::Company, nil]
|
|
110
|
+
# @return [Array<Array<Spree::Catalog>>]
|
|
111
|
+
def self.groups_for_company(company)
|
|
100
112
|
return [] if company.nil?
|
|
101
113
|
|
|
102
114
|
nodes = company.self_and_ancestors
|
|
@@ -110,9 +122,15 @@ module Spree
|
|
|
110
122
|
includes(:catalog).
|
|
111
123
|
group_by(&:assignable_id)
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
# A catalog assigned to a node and to an ancestor answers from the nearer.
|
|
126
|
+
seen = Set.new
|
|
127
|
+
|
|
128
|
+
nodes.filter_map do |node|
|
|
129
|
+
group = Array(assignments[node.id]).map(&:catalog).
|
|
130
|
+
select { |catalog| catalog.active? && seen.add?(catalog.id) }.
|
|
131
|
+
sort_by { |catalog| catalog.position.to_i }
|
|
132
|
+
group.presence
|
|
133
|
+
end
|
|
116
134
|
end
|
|
117
135
|
|
|
118
136
|
# The catalogs assigned to any of the given customer groups, by position.
|
|
@@ -151,18 +169,30 @@ module Spree
|
|
|
151
169
|
# @param channel [Spree::Channel, nil]
|
|
152
170
|
# @return [Array<Spree::Catalog>]
|
|
153
171
|
def self.for_context(store:, company: nil, user: nil, channel: nil)
|
|
172
|
+
groups_for_context(store: store, company: company, user: user, channel: channel).flatten
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The same resolution, grouped by precedence rank ({.groups_for_company}).
|
|
176
|
+
# Only the company axis has more than one rank.
|
|
177
|
+
#
|
|
178
|
+
# @return [Array<Array<Spree::Catalog>>]
|
|
179
|
+
def self.groups_for_context(store:, company: nil, user: nil, channel: nil)
|
|
154
180
|
return [] if store.nil?
|
|
155
181
|
|
|
156
|
-
|
|
182
|
+
groups = store.catalogs.groups_for_company(company)
|
|
183
|
+
|
|
157
184
|
# Only when the company axis found nothing: a buyer purchasing for a
|
|
158
185
|
# company is on that company's agreement, and adding their personal
|
|
159
186
|
# segment's catalogs on top would price one buyer under two agreements
|
|
160
187
|
# at once.
|
|
161
|
-
if
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
if groups.empty? && user
|
|
189
|
+
customer_groups = user.try(:customer_groups)&.where(store_id: store.id) || []
|
|
190
|
+
groups = [store.catalogs.for_customer_groups(customer_groups).presence].compact
|
|
164
191
|
end
|
|
165
|
-
|
|
192
|
+
|
|
193
|
+
groups = [[channel&.default_catalog].compact.select(&:active?).presence].compact if groups.empty?
|
|
194
|
+
|
|
195
|
+
catalogs = groups.flatten
|
|
166
196
|
# Pricing reads each catalog's price list, and the list's contextual
|
|
167
197
|
# rules decide whether it applies to the quantity being bought; without
|
|
168
198
|
# this the walk is one query per catalog again, plus one per list.
|
|
@@ -171,7 +201,7 @@ module Spree
|
|
|
171
201
|
records: catalogs, associations: { price_list: [:price_rules, :price_adjustment_tiers] }
|
|
172
202
|
).call
|
|
173
203
|
end
|
|
174
|
-
|
|
204
|
+
groups
|
|
175
205
|
end
|
|
176
206
|
|
|
177
207
|
# The catalogs applying to a buyer, resolving the company for them when
|
|
@@ -190,15 +220,22 @@ module Spree
|
|
|
190
220
|
# @param channel [Spree::Channel, nil]
|
|
191
221
|
# @return [Array<Spree::Catalog>]
|
|
192
222
|
def self.for_buyer(store:, customer: nil, company: nil, channel: nil)
|
|
223
|
+
groups_for_buyer(store: store, customer: customer, company: company, channel: channel).flatten
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# {.for_buyer}, grouped by precedence rank. What pricing walks.
|
|
227
|
+
#
|
|
228
|
+
# @return [Array<Array<Spree::Catalog>>]
|
|
229
|
+
def self.groups_for_buyer(store:, customer: nil, company: nil, channel: nil)
|
|
193
230
|
return [] if store.nil?
|
|
194
231
|
|
|
195
232
|
if store == Spree::Current.store
|
|
196
233
|
company ||= Spree::Current.standing_company_for(customer)
|
|
197
234
|
channel ||= Spree::Current.channel
|
|
198
|
-
Spree::Current.
|
|
235
|
+
Spree::Current.catalog_groups_for(company: company, user: customer, channel: channel)
|
|
199
236
|
else
|
|
200
237
|
company ||= Spree::Company.sole_standing_for(store: store, customer: customer)
|
|
201
|
-
|
|
238
|
+
groups_for_context(store: store, company: company, user: customer, channel: channel)
|
|
202
239
|
end
|
|
203
240
|
end
|
|
204
241
|
|
data/app/models/spree/claim.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Spree
|
|
|
10
10
|
has_prefix_id :claim
|
|
11
11
|
|
|
12
12
|
has_spree_number prefix: 'CLM'
|
|
13
|
+
include Spree::ActedBy
|
|
13
14
|
include Spree::NumberIdentifier
|
|
14
15
|
include Spree::SingleStoreResource
|
|
15
16
|
include Spree::HasStatus
|
|
@@ -27,7 +28,7 @@ module Spree
|
|
|
27
28
|
belongs_to :store, class_name: 'Spree::Store'
|
|
28
29
|
belongs_to :order, class_name: 'Spree::Order', inverse_of: :claims
|
|
29
30
|
belongs_to :reason, class_name: 'Spree::ClaimReason', optional: true, inverse_of: :claims
|
|
30
|
-
|
|
31
|
+
acted_by :created_by
|
|
31
32
|
|
|
32
33
|
has_many :claim_line_items, class_name: 'Spree::ClaimLineItem',
|
|
33
34
|
dependent: :destroy, inverse_of: :claim
|
data/app/models/spree/current.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Spree
|
|
|
4
4
|
# All attributes are automatically reset between requests by Rails.
|
|
5
5
|
# Fallback chains ensure sensible defaults when attributes are not explicitly set.
|
|
6
6
|
class Current < ::ActiveSupport::CurrentAttributes
|
|
7
|
-
attribute :store, :channel, :market, :currency, :locale, :content_locale, :tax_country, :price_lists, :applicable_catalogs, :quantity_rules_resolvers, :standing_companies, :global_pricing_context, :provider_cache, :integrations
|
|
7
|
+
attribute :store, :channel, :market, :currency, :locale, :content_locale, :tax_country, :price_lists, :applicable_catalogs, :applicable_catalog_groups, :quantity_rules_resolvers, :standing_companies, :global_pricing_context, :provider_cache, :integrations
|
|
8
8
|
|
|
9
9
|
# Scratch space for provider strategies to memoize a call across the
|
|
10
10
|
# request — part of the delivery rate provider contract (nothing in core
|
|
@@ -103,16 +103,22 @@ module Spree
|
|
|
103
103
|
# @param channel [Spree::Channel, nil]
|
|
104
104
|
# @return [Array<Spree::Catalog>]
|
|
105
105
|
def catalogs_for(company: nil, user: nil, channel: nil)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
key = catalog_memo_key(company: company, user: user, channel: channel)
|
|
107
|
+
applicable_catalogs[key] ||= catalog_groups_for(company: company, user: user, channel: channel).flatten
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The same catalogs grouped by precedence rank, which is what pricing
|
|
111
|
+
# walks ({Spree::Catalog.groups_for_company}). The resolved form — the
|
|
112
|
+
# flat set above is a cached flattening of it, never a second resolution.
|
|
113
|
+
#
|
|
114
|
+
# @param company [Spree::Company, nil]
|
|
115
|
+
# @param user [Object, nil]
|
|
116
|
+
# @param channel [Spree::Channel, nil]
|
|
117
|
+
# @return [Array<Array<Spree::Catalog>>]
|
|
118
|
+
def catalog_groups_for(company: nil, user: nil, channel: nil)
|
|
119
|
+
key = catalog_memo_key(company: company, user: user, channel: channel)
|
|
120
|
+
applicable_catalog_groups[key] ||= Spree::Catalog.groups_for_context(
|
|
121
|
+
store: store, company: company, user: normalized_catalog_user(user), channel: channel || self.channel
|
|
116
122
|
)
|
|
117
123
|
end
|
|
118
124
|
|
|
@@ -121,6 +127,11 @@ module Spree
|
|
|
121
127
|
super || (self.applicable_catalogs = {})
|
|
122
128
|
end
|
|
123
129
|
|
|
130
|
+
# @return [Hash]
|
|
131
|
+
def applicable_catalog_groups
|
|
132
|
+
super || (self.applicable_catalog_groups = {})
|
|
133
|
+
end
|
|
134
|
+
|
|
124
135
|
# The quantity-rule resolver for a buyer in this request, keyed the same
|
|
125
136
|
# way their catalogs are.
|
|
126
137
|
#
|
|
@@ -133,8 +144,7 @@ module Spree
|
|
|
133
144
|
# @param channel [Spree::Channel, nil]
|
|
134
145
|
# @return [Spree::Catalogs::ResolveQuantityRules]
|
|
135
146
|
def quantity_rules_resolver_for(company: nil, user: nil, channel: nil)
|
|
136
|
-
|
|
137
|
-
key = [store&.id, company&.id, user&.id, channel&.id]
|
|
147
|
+
key = catalog_memo_key(company: company, user: user, channel: channel)
|
|
138
148
|
quantity_rules_resolvers[key] ||= Spree::Catalogs::ResolveQuantityRules.new(
|
|
139
149
|
catalogs_for(company: company, user: user, channel: channel)
|
|
140
150
|
)
|
|
@@ -176,6 +186,7 @@ module Spree
|
|
|
176
186
|
# @return [void]
|
|
177
187
|
def reset_catalog_memos
|
|
178
188
|
self.applicable_catalogs = nil
|
|
189
|
+
self.applicable_catalog_groups = nil
|
|
179
190
|
self.quantity_rules_resolvers = nil
|
|
180
191
|
self.standing_companies = nil
|
|
181
192
|
self.price_lists = nil
|
|
@@ -194,5 +205,22 @@ module Spree
|
|
|
194
205
|
)
|
|
195
206
|
end
|
|
196
207
|
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
# One key for every buyer-derived memo, so they cannot drift apart.
|
|
212
|
+
#
|
|
213
|
+
# @return [Array]
|
|
214
|
+
def catalog_memo_key(company: nil, user: nil, channel: nil)
|
|
215
|
+
[store&.id, company&.id, normalized_catalog_user(user)&.id, (channel || self.channel)&.id]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Normalized before the key is built, not just before the query: the key
|
|
219
|
+
# is an id, so an admin and a customer sharing one would share an entry.
|
|
220
|
+
#
|
|
221
|
+
# @return [Object, nil]
|
|
222
|
+
def normalized_catalog_user(user)
|
|
223
|
+
user if user.is_a?(Spree.customer_class)
|
|
224
|
+
end
|
|
197
225
|
end
|
|
198
226
|
end
|
|
@@ -11,6 +11,7 @@ module Spree
|
|
|
11
11
|
has_prefix_id :exch
|
|
12
12
|
|
|
13
13
|
has_spree_number prefix: 'EX'
|
|
14
|
+
include Spree::ActedBy
|
|
14
15
|
include Spree::NumberIdentifier
|
|
15
16
|
include Spree::SingleStoreResource
|
|
16
17
|
include Spree::HasStatus
|
|
@@ -26,7 +27,7 @@ module Spree
|
|
|
26
27
|
belongs_to :order, class_name: 'Spree::Order', inverse_of: :exchanges
|
|
27
28
|
belongs_to :stock_location, class_name: 'Spree::StockLocation'
|
|
28
29
|
belongs_to :reason, class_name: 'Spree::ReturnReason', optional: true, inverse_of: :exchanges
|
|
29
|
-
|
|
30
|
+
acted_by :created_by
|
|
30
31
|
|
|
31
32
|
has_many :exchange_line_items, class_name: 'Spree::ExchangeLineItem',
|
|
32
33
|
dependent: :destroy, inverse_of: :exchange
|