spree_core 6.0.0.beta1 → 6.0.0.beta3
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/payments/handle_webhook_job.rb +1 -0
- data/app/jobs/spree/sample_data/load_job.rb +18 -0
- data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
- data/app/mailers/spree/admin_user_mailer.rb +2 -2
- 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/customer_methods.rb +17 -1
- data/app/models/concerns/spree/has_number.rb +3 -2
- data/app/models/concerns/spree/prefixed_id.rb +4 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
- data/app/models/concerns/spree/purchase/totals.rb +3 -1
- data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
- data/app/models/concerns/spree/translatable_resource.rb +6 -7
- data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
- data/app/models/spree/adjusters/promotion.rb +1 -1
- data/app/models/spree/api_key.rb +5 -3
- data/app/models/spree/authentication/lockout.rb +34 -0
- data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
- data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
- data/app/models/spree/catalog.rb +50 -13
- data/app/models/spree/claim.rb +3 -1
- data/app/models/spree/commission_line.rb +4 -2
- data/app/models/spree/commission_rule.rb +1 -1
- data/app/models/spree/coupon_code.rb +14 -2
- data/app/models/spree/current.rb +56 -13
- data/app/models/spree/exchange.rb +3 -1
- data/app/models/spree/export.rb +8 -5
- data/app/models/spree/exports/customers.rb +2 -2
- data/app/models/spree/fulfillment_group.rb +127 -0
- data/app/models/spree/gift_card.rb +9 -0
- data/app/models/spree/invitation.rb +18 -5
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/number_generators/sequential.rb +2 -2
- data/app/models/spree/option_value.rb +7 -0
- data/app/models/spree/order.rb +89 -18
- data/app/models/spree/order_group.rb +78 -6
- data/app/models/spree/payment_source.rb +1 -1
- data/app/models/spree/payment_split.rb +24 -0
- data/app/models/spree/price.rb +74 -0
- data/app/models/spree/pricing_provider/internal.rb +37 -15
- data/app/models/spree/product.rb +4 -0
- data/app/models/spree/product_option_type.rb +11 -0
- data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
- data/app/models/spree/promotion/rules/category.rb +14 -9
- data/app/models/spree/promotion/rules/product.rb +17 -1
- data/app/models/spree/promotion.rb +1 -1
- data/app/models/spree/promotion_action.rb +12 -1
- data/app/models/spree/promotion_handler/coupon.rb +50 -5
- data/app/models/spree/promotion_rule.rb +14 -0
- data/app/models/spree/refund.rb +2 -1
- data/app/models/spree/return.rb +18 -7
- data/app/models/spree/return_line_item.rb +19 -0
- data/app/models/spree/search_provider/database.rb +10 -2
- data/app/models/spree/seller_payout.rb +3 -1
- data/app/models/spree/seller_transfer.rb +3 -1
- data/app/models/spree/stock_receipt.rb +7 -2
- data/app/models/spree/store.rb +4 -0
- data/app/models/spree/store_credit.rb +4 -0
- data/app/models/spree/translations/batch.rb +4 -2
- data/app/models/spree/user_identity.rb +10 -6
- data/app/models/spree/variant.rb +8 -3
- data/app/models/spree/webhook_delivery.rb +28 -0
- data/app/models/spree/webhook_endpoint.rb +14 -0
- data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
- data/app/services/spree/carts/split_by_seller.rb +22 -12
- data/app/services/spree/companies/add_member.rb +8 -2
- data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
- data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
- data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
- data/app/services/spree/order_groups/allocate_payments.rb +3 -0
- data/app/services/spree/orders/approve.rb +3 -2
- data/app/services/spree/orders/build_fulfillments.rb +6 -0
- data/app/services/spree/orders/update_statuses.rb +8 -2
- data/app/services/spree/prices/bulk_upsert.rb +186 -47
- data/app/services/spree/sample_data/import_builder.rb +1 -1
- data/app/services/spree/sample_data/loader.rb +54 -40
- data/app/services/spree/seeds/all.rb +1 -26
- data/app/services/spree/seeds/allowed_origins.rb +5 -4
- data/app/services/spree/seeds/api_keys.rb +10 -9
- data/app/services/spree/seeds/channels.rb +9 -8
- data/app/services/spree/seeds/commission_rates.rb +21 -20
- data/app/services/spree/seeds/customer_groups.rb +5 -4
- data/app/services/spree/seeds/digital_delivery.rb +7 -8
- data/app/services/spree/seeds/payment_methods.rb +12 -11
- data/app/services/spree/seeds/product_types.rb +7 -8
- data/app/services/spree/seeds/returns_environment.rb +12 -11
- data/app/services/spree/seeds/roles.rb +8 -7
- data/app/services/spree/seeds/saved_reports.rb +19 -20
- data/app/services/spree/seeds/seller_requirements.rb +5 -4
- data/app/services/spree/seeds/store_resources.rb +41 -0
- data/app/services/spree/seeds/store_scoped.rb +15 -0
- data/app/services/spree/seeds/tax_categories.rb +10 -9
- data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
- data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
- data/app/subscribers/spree/order_status_subscriber.rb +15 -7
- data/app/workflows/spree/carts/add_item.rb +5 -7
- data/app/workflows/spree/carts/complete.rb +20 -64
- data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
- data/app/workflows/spree/claims/create.rb +31 -1
- data/app/workflows/spree/claims/resolve.rb +7 -11
- data/app/workflows/spree/exchanges/create.rb +11 -0
- data/app/workflows/spree/exchanges/fulfill.rb +21 -13
- data/app/workflows/spree/orders/cancel.rb +3 -2
- data/app/workflows/spree/orders/complete.rb +99 -2
- data/app/workflows/spree/price_lists/update.rb +17 -0
- data/app/workflows/spree/refunds/create.rb +1 -1
- data/app/workflows/spree/refunds/order_payments.rb +55 -1
- data/app/workflows/spree/returns/approve.rb +1 -1
- data/app/workflows/spree/returns/create.rb +11 -17
- data/app/workflows/spree/returns/refund.rb +16 -25
- data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
- data/app/workflows/spree/sellers/invite.rb +1 -1
- data/config/locales/en.yml +8 -119
- data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
- data/db/migrate/20260916000001_order_operation_actors_polymorphic.rb +30 -0
- data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
- data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
- data/db/sample_data/channels.rb +1 -1
- data/db/sample_data/collections.rb +1 -1
- data/db/sample_data/custom_field_definitions.rb +1 -1
- data/db/sample_data/digital_products.rb +1 -1
- data/db/sample_data/fulfillment.rb +1 -1
- data/db/sample_data/options.rb +4 -2
- data/db/sample_data/orders.rb +9 -10
- data/db/sample_data/payment_methods.rb +18 -18
- data/db/sample_data/product_type_categories.rb +1 -1
- data/db/sample_data/product_types.rb +2 -2
- data/db/sample_data/promotions.rb +2 -4
- data/db/sample_data/wholesale.rb +1 -1
- data/lib/generators/spree/api_resource/api_resource_generator.rb +121 -6
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +8 -2
- data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +3 -0
- data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
- data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +4 -1
- data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
- 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/generators/spree/subscriber/subscriber_generator.rb +3 -3
- data/lib/spree/core/configuration.rb +25 -25
- data/lib/spree/core/engine.rb +35 -8
- 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/iso_data.rb +23 -4
- data/lib/spree/store_scope_guard.rb +16 -1
- data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
- data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
- data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
- data/lib/spree/testing_support/factories/user_factory.rb +0 -1
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
- data/lib/tasks/backfill_actor_types.rake +35 -0
- data/lib/tasks/sample_data.rake +8 -2
- data/lib/tasks/store_settings.rake +1 -0
- data/lib/tasks/upgrade.rake +6 -21
- metadata +23 -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: fb19660bc9b7017b74df8b6b92fc2522cb84ef62a6a015f817484528b7e8c992
|
|
4
|
+
data.tar.gz: bac9e0d41a47af2e5452406f1c0d1ac268b687ebdf1ade714742aa689e5cae0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0a322ff461f40b5d0a8eb5522fb5f6b45f0a1f7526e9e797f255a0779ddfeabf0db9bfe6629b341d4ed03f71591c76dc3435928cdf28b7d98029c1164c0efe6
|
|
7
|
+
data.tar.gz: bd3ef61d467f2b481120080350fbf0f7c661e9fe81123c24869978a5177240766454fe248fb35cfc967445fbf049aa2fc483a06884573c13e6c4a33b9fda0b4e
|
|
@@ -9,6 +9,7 @@ module Spree
|
|
|
9
9
|
|
|
10
10
|
def perform(payment_method_id:, action:, payment_session_id:)
|
|
11
11
|
payment_method = Spree::PaymentMethod.find(payment_method_id)
|
|
12
|
+
Spree::Current.store = payment_method.store
|
|
12
13
|
payment_session = Spree::PaymentSession.find(payment_session_id)
|
|
13
14
|
|
|
14
15
|
Spree.payments_handle_webhook_workflow.call(
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
# @param store_id [String, Integer, nil] the store to load into; the
|
|
9
|
+
# default store when omitted
|
|
10
|
+
def perform(store_id = nil)
|
|
11
|
+
return Spree::SampleData::Loader.call if store_id.nil?
|
|
12
|
+
|
|
13
|
+
store = Spree::Store.find_by(id: store_id)
|
|
14
|
+
Spree::SampleData::Loader.call(store: store) if store
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -26,6 +26,7 @@ module Spree
|
|
|
26
26
|
seller = Spree::Seller.find_by(id: seller_id)
|
|
27
27
|
return if seller.nil? || !seller.payouts_enabled?
|
|
28
28
|
|
|
29
|
+
Spree::Current.store = seller.store
|
|
29
30
|
provider = seller.store.payout_provider_instance
|
|
30
31
|
|
|
31
32
|
# Earnings only — see the scope. A reversal is retryable the same way
|
|
@@ -34,9 +34,9 @@ module Spree
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# The dashboard SPA passes a validated redirect URL; the token is appended as
|
|
37
|
-
# a query param, falling back to the
|
|
37
|
+
# a query param, falling back to the dashboard's own reset page.
|
|
38
38
|
def password_reset_url(token, store, redirect_url)
|
|
39
|
-
append_token(redirect_url.presence || store
|
|
39
|
+
append_token(redirect_url.presence || "#{Spree::Stores::DashboardUrl.call(store: store)}/reset-password", token)
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
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
|
|
@@ -64,7 +64,14 @@ module Spree
|
|
|
64
64
|
has_many :promotion_rule_users, class_name: 'Spree::PromotionRuleUser', foreign_key: :customer_id, dependent: :destroy
|
|
65
65
|
has_many :promotion_rules, through: :promotion_rule_users, class_name: 'Spree::PromotionRule'
|
|
66
66
|
has_many :orders, foreign_key: :customer_id, class_name: 'Spree::Order'
|
|
67
|
-
|
|
67
|
+
# Open carts only. Since the Cart/Order split a cart lives on its own
|
|
68
|
+
# table and never becomes an order row, so an incomplete Order is a
|
|
69
|
+
# backoffice draft, not a cart. Completed carts stay behind as read-only
|
|
70
|
+
# audit records of a checkout and are reached through the order.
|
|
71
|
+
# No cascade on destroy, like #orders: a cart can be mid-checkout with
|
|
72
|
+
# money authorized against it, and erasure goes through
|
|
73
|
+
# Spree::Customers::Anonymize, which scrubs carts explicitly.
|
|
74
|
+
has_many :carts, -> { incomplete }, foreign_key: :customer_id, class_name: 'Spree::Cart', dependent: nil
|
|
68
75
|
has_many :completed_orders, -> { complete }, foreign_key: :customer_id, class_name: 'Spree::Order'
|
|
69
76
|
has_many :store_credits, class_name: 'Spree::StoreCredit', foreign_key: :customer_id, dependent: :destroy
|
|
70
77
|
# Everything the customer has bought and can download. Scoped to completed
|
|
@@ -272,11 +279,20 @@ module Spree
|
|
|
272
279
|
end
|
|
273
280
|
|
|
274
281
|
# Returns the last incomplete spree order for the current store
|
|
282
|
+
# @deprecated Carts are {Spree::Cart} since 6.0, so an incomplete order is
|
|
283
|
+
# a backoffice draft rather than the customer's cart. Use
|
|
284
|
+
# +carts.where(store: store)+ for carts, or +orders.drafts+ for drafts.
|
|
285
|
+
# Removed in Spree 6.1.
|
|
275
286
|
# @param [Spree::Store] store
|
|
276
287
|
# @param [Hash] options
|
|
277
288
|
# @option options [Array<Symbol>] :includes
|
|
278
289
|
# @return [Spree::Order]
|
|
279
290
|
def last_incomplete_spree_order(store, options = {})
|
|
291
|
+
Spree::Deprecation.warn(
|
|
292
|
+
'Spree::CustomerMethods#last_incomplete_spree_order is deprecated and will be removed in Spree 6.1. ' \
|
|
293
|
+
'Carts are Spree::Cart since 6.0 and this returns draft orders; use #carts for the customer\'s carts.'
|
|
294
|
+
)
|
|
295
|
+
|
|
280
296
|
orders.where(store: store).incomplete.not_canceled.
|
|
281
297
|
includes(options[:includes]).
|
|
282
298
|
order('created_at DESC').
|
|
@@ -99,9 +99,10 @@ module Spree
|
|
|
99
99
|
|
|
100
100
|
# Advisory only — a concurrent writer can take the number between this
|
|
101
101
|
# check and the insert, which is what the unique-index rescue below is
|
|
102
|
-
# for.
|
|
102
|
+
# for. Checked on the base class, unscoped, because the index is global —
|
|
103
|
+
# it spans every STI subclass and soft-deleted row.
|
|
103
104
|
def number_taken?(candidate)
|
|
104
|
-
self.class.unscoped.exists?(number: candidate)
|
|
105
|
+
self.class.base_class.unscoped.exists?(number: candidate)
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
# Last line of defence. The pre-check and the uniqueness validation both
|
|
@@ -168,8 +168,11 @@ module Spree
|
|
|
168
168
|
Spree::PrefixedId.decode_prefixed_id(prefixed_id)
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
# Model-scoped decode: answers nil for another model's prefixed ID, like
|
|
172
|
+
# +find_by_prefix_id+. Use +Spree::PrefixedId.decode_prefixed_id+ when
|
|
173
|
+
# any model's ID is acceptable.
|
|
171
174
|
def decode_prefixed_id(prefixed_id_string)
|
|
172
|
-
|
|
175
|
+
decode_own_prefixed_id(prefixed_id_string)
|
|
173
176
|
end
|
|
174
177
|
|
|
175
178
|
# Find by prefixed ID first, falling back to integer id for backwards compatibility.
|
|
@@ -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
|
|
|
@@ -35,8 +35,10 @@ module Spree
|
|
|
35
35
|
# unsaved changes — callers legitimately hold dirty attributes while
|
|
36
36
|
# a payment is destroyed (cart teardown, order merging), and this
|
|
37
37
|
# must never disturb their in-memory state.
|
|
38
|
+
# updated_at moves with it, or the API validates a cached response
|
|
39
|
+
# against an unchanged timestamp and serves the pre-payment figure.
|
|
38
40
|
settled = settled_payments_arel
|
|
39
|
-
self.class.where(id: id).update_all(payment_total: settled)
|
|
41
|
+
self.class.where(id: id).update_all(payment_total: settled, updated_at: Time.current)
|
|
40
42
|
self.payment_total = self.class.where(id: id).pick(:payment_total)
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
module Spree::RansackableAttributes
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
# Ransack `auth_object` values for callers outside the back office. The
|
|
5
|
+
# allowlists below were written for staff; a shopper or a seller filtering
|
|
6
|
+
# on them would get a yes/no answer about data their responses never show.
|
|
7
|
+
RESTRICTED_AUDIENCES = %i[store seller].freeze
|
|
8
|
+
|
|
3
9
|
included do
|
|
4
10
|
class_attribute :whitelisted_ransackable_associations
|
|
5
11
|
class_attribute :whitelisted_ransackable_attributes
|
|
6
12
|
class_attribute :whitelisted_ransackable_scopes
|
|
7
13
|
|
|
14
|
+
# Associations the Store API may filter through. Empty by default: every
|
|
15
|
+
# hop is a join into data the storefront was never meant to query, and a
|
|
16
|
+
# join back to the model it came from lets one filter chain them forever.
|
|
17
|
+
class_attribute :storefront_ransackable_associations, default: []
|
|
18
|
+
|
|
19
|
+
# `{ store: [...], seller: [...] }` — attributes and scopes a restricted
|
|
20
|
+
# audience may not filter on, although staff may.
|
|
21
|
+
class_attribute :private_ransackable_attributes, default: {}
|
|
22
|
+
class_attribute :private_ransackable_scopes, default: {}
|
|
23
|
+
|
|
8
24
|
class_attribute :default_ransackable_attributes
|
|
9
25
|
# `position` is included so any `acts_as_list` model is sortable by it
|
|
10
26
|
# without each subclass having to opt in. Ransack ignores attributes
|
|
@@ -12,19 +28,35 @@ module Spree::RansackableAttributes
|
|
|
12
28
|
# without a position column.
|
|
13
29
|
self.default_ransackable_attributes = %w[id name updated_at created_at position]
|
|
14
30
|
|
|
15
|
-
def self.ransackable_associations(
|
|
31
|
+
def self.ransackable_associations(auth_object = nil)
|
|
32
|
+
if Spree::RansackableAttributes.restricted_audience?(auth_object)
|
|
33
|
+
return auth_object.to_sym == :store ? storefront_ransackable_associations : []
|
|
34
|
+
end
|
|
35
|
+
|
|
16
36
|
base = whitelisted_ransackable_associations || []
|
|
17
37
|
base | Spree.ransack.custom_associations_for(self)
|
|
18
38
|
end
|
|
19
39
|
|
|
20
|
-
def self.ransackable_attributes(
|
|
40
|
+
def self.ransackable_attributes(auth_object = nil)
|
|
21
41
|
base = default_ransackable_attributes | (whitelisted_ransackable_attributes || [])
|
|
22
|
-
base
|
|
42
|
+
base |= Spree.ransack.custom_attributes_for(self)
|
|
43
|
+
return base unless Spree::RansackableAttributes.restricted_audience?(auth_object)
|
|
44
|
+
|
|
45
|
+
base - Array(private_ransackable_attributes[auth_object.to_sym])
|
|
23
46
|
end
|
|
24
47
|
|
|
25
|
-
def self.ransackable_scopes(
|
|
26
|
-
base = whitelisted_ransackable_scopes || []
|
|
27
|
-
base
|
|
48
|
+
def self.ransackable_scopes(auth_object = nil)
|
|
49
|
+
base = (whitelisted_ransackable_scopes || []).map(&:to_s)
|
|
50
|
+
base |= Spree.ransack.custom_scopes_for(self).map(&:to_s)
|
|
51
|
+
return base unless Spree::RansackableAttributes.restricted_audience?(auth_object)
|
|
52
|
+
|
|
53
|
+
base - Array(private_ransackable_scopes[auth_object.to_sym])
|
|
28
54
|
end
|
|
29
55
|
end
|
|
56
|
+
|
|
57
|
+
# @param auth_object [Symbol, nil] the Ransack auth object a search ran with
|
|
58
|
+
# @return [Boolean] whether it names a caller outside the back office
|
|
59
|
+
def self.restricted_audience?(auth_object)
|
|
60
|
+
auth_object.respond_to?(:to_sym) && RESTRICTED_AUDIENCES.include?(auth_object.to_sym)
|
|
61
|
+
end
|
|
30
62
|
end
|
|
@@ -48,13 +48,12 @@ module Spree
|
|
|
48
48
|
# Every record of this type belonging to +store+, for surfaces that read
|
|
49
49
|
# a whole translatable resource type at once (the coverage grid).
|
|
50
50
|
#
|
|
51
|
-
# +for_store+ resolves through the store's own association,
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
# the
|
|
57
|
-
# installation, so that case is narrowed here.
|
|
51
|
+
# +for_store+ resolves through the store's own association, or through a
|
|
52
|
+
# model's own +for_store+ scope where there is none to follow (OptionType
|
|
53
|
+
# by its store column, OptionValue through its option type). Its one
|
|
54
|
+
# blind spot is +Spree::Store+: with no `stores` association to follow it
|
|
55
|
+
# falls back to the UNSCOPED class, which would answer with every store in
|
|
56
|
+
# the installation, so that case is narrowed here.
|
|
58
57
|
#
|
|
59
58
|
# @param store [Spree::Store]
|
|
60
59
|
# @return [ActiveRecord::Relation]
|