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.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/app/jobs/spree/payments/handle_webhook_job.rb +1 -0
  3. data/app/jobs/spree/sample_data/load_job.rb +18 -0
  4. data/app/jobs/spree/seller_transfers/execute_pending_job.rb +1 -0
  5. data/app/mailers/spree/admin_user_mailer.rb +2 -2
  6. data/app/models/concerns/spree/acted_by.rb +210 -0
  7. data/app/models/concerns/spree/actor.rb +51 -0
  8. data/app/models/concerns/spree/admin_user_methods.rb +48 -17
  9. data/app/models/concerns/spree/customer_methods.rb +17 -1
  10. data/app/models/concerns/spree/has_number.rb +3 -2
  11. data/app/models/concerns/spree/prefixed_id.rb +4 -1
  12. data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
  13. data/app/models/concerns/spree/purchase/totals.rb +3 -1
  14. data/app/models/concerns/spree/ransackable_attributes.rb +38 -6
  15. data/app/models/concerns/spree/translatable_resource.rb +6 -7
  16. data/app/models/concerns/spree/webhook_payload_redaction.rb +132 -21
  17. data/app/models/spree/adjusters/promotion.rb +1 -1
  18. data/app/models/spree/api_key.rb +5 -3
  19. data/app/models/spree/authentication/lockout.rb +34 -0
  20. data/app/models/spree/authentication/strategies/email_password_strategy.rb +4 -12
  21. data/app/models/spree/authentication/strategies/oidc_strategy.rb +1 -1
  22. data/app/models/spree/catalog.rb +50 -13
  23. data/app/models/spree/claim.rb +3 -1
  24. data/app/models/spree/commission_line.rb +4 -2
  25. data/app/models/spree/commission_rule.rb +1 -1
  26. data/app/models/spree/coupon_code.rb +14 -2
  27. data/app/models/spree/current.rb +56 -13
  28. data/app/models/spree/exchange.rb +3 -1
  29. data/app/models/spree/export.rb +8 -5
  30. data/app/models/spree/exports/customers.rb +2 -2
  31. data/app/models/spree/fulfillment_group.rb +127 -0
  32. data/app/models/spree/gift_card.rb +9 -0
  33. data/app/models/spree/invitation.rb +18 -5
  34. data/app/models/spree/line_item.rb +1 -1
  35. data/app/models/spree/number_generators/sequential.rb +2 -2
  36. data/app/models/spree/option_value.rb +7 -0
  37. data/app/models/spree/order.rb +89 -18
  38. data/app/models/spree/order_group.rb +78 -6
  39. data/app/models/spree/payment_source.rb +1 -1
  40. data/app/models/spree/payment_split.rb +24 -0
  41. data/app/models/spree/price.rb +74 -0
  42. data/app/models/spree/pricing_provider/internal.rb +37 -15
  43. data/app/models/spree/product.rb +4 -0
  44. data/app/models/spree/product_option_type.rb +11 -0
  45. data/app/models/spree/promotion/actions/create_line_items.rb +92 -30
  46. data/app/models/spree/promotion/rules/category.rb +14 -9
  47. data/app/models/spree/promotion/rules/product.rb +17 -1
  48. data/app/models/spree/promotion.rb +1 -1
  49. data/app/models/spree/promotion_action.rb +12 -1
  50. data/app/models/spree/promotion_handler/coupon.rb +50 -5
  51. data/app/models/spree/promotion_rule.rb +14 -0
  52. data/app/models/spree/refund.rb +2 -1
  53. data/app/models/spree/return.rb +18 -7
  54. data/app/models/spree/return_line_item.rb +19 -0
  55. data/app/models/spree/search_provider/database.rb +10 -2
  56. data/app/models/spree/seller_payout.rb +3 -1
  57. data/app/models/spree/seller_transfer.rb +3 -1
  58. data/app/models/spree/stock_receipt.rb +7 -2
  59. data/app/models/spree/store.rb +4 -0
  60. data/app/models/spree/store_credit.rb +4 -0
  61. data/app/models/spree/translations/batch.rb +4 -2
  62. data/app/models/spree/user_identity.rb +10 -6
  63. data/app/models/spree/variant.rb +8 -3
  64. data/app/models/spree/webhook_delivery.rb +28 -0
  65. data/app/models/spree/webhook_endpoint.rb +14 -0
  66. data/app/presenters/spree/csv/order_line_item_presenter.rb +2 -2
  67. data/app/services/spree/carts/split_by_seller.rb +22 -12
  68. data/app/services/spree/companies/add_member.rb +8 -2
  69. data/app/services/spree/imports/row_processors/price_list_price.rb +6 -0
  70. data/app/services/spree/imports/row_processors/product_translation.rb +1 -1
  71. data/app/services/spree/imports/row_processors/product_variant.rb +17 -11
  72. data/app/services/spree/order_groups/allocate_payments.rb +3 -0
  73. data/app/services/spree/orders/approve.rb +3 -2
  74. data/app/services/spree/orders/build_fulfillments.rb +6 -0
  75. data/app/services/spree/orders/update_statuses.rb +8 -2
  76. data/app/services/spree/prices/bulk_upsert.rb +186 -47
  77. data/app/services/spree/sample_data/import_builder.rb +1 -1
  78. data/app/services/spree/sample_data/loader.rb +54 -40
  79. data/app/services/spree/seeds/all.rb +1 -26
  80. data/app/services/spree/seeds/allowed_origins.rb +5 -4
  81. data/app/services/spree/seeds/api_keys.rb +10 -9
  82. data/app/services/spree/seeds/channels.rb +9 -8
  83. data/app/services/spree/seeds/commission_rates.rb +21 -20
  84. data/app/services/spree/seeds/customer_groups.rb +5 -4
  85. data/app/services/spree/seeds/digital_delivery.rb +7 -8
  86. data/app/services/spree/seeds/payment_methods.rb +12 -11
  87. data/app/services/spree/seeds/product_types.rb +7 -8
  88. data/app/services/spree/seeds/returns_environment.rb +12 -11
  89. data/app/services/spree/seeds/roles.rb +8 -7
  90. data/app/services/spree/seeds/saved_reports.rb +19 -20
  91. data/app/services/spree/seeds/seller_requirements.rb +5 -4
  92. data/app/services/spree/seeds/store_resources.rb +41 -0
  93. data/app/services/spree/seeds/store_scoped.rb +15 -0
  94. data/app/services/spree/seeds/tax_categories.rb +10 -9
  95. data/app/services/spree/stock_locations/stock_items/create.rb +1 -1
  96. data/app/services/spree/stock_locations/stock_levels/create.rb +19 -3
  97. data/app/subscribers/spree/order_status_subscriber.rb +15 -7
  98. data/app/workflows/spree/carts/add_item.rb +5 -7
  99. data/app/workflows/spree/carts/complete.rb +20 -64
  100. data/app/workflows/spree/carts/recalculate_totals.rb +3 -1
  101. data/app/workflows/spree/claims/create.rb +31 -1
  102. data/app/workflows/spree/claims/resolve.rb +7 -11
  103. data/app/workflows/spree/exchanges/create.rb +11 -0
  104. data/app/workflows/spree/exchanges/fulfill.rb +21 -13
  105. data/app/workflows/spree/orders/cancel.rb +3 -2
  106. data/app/workflows/spree/orders/complete.rb +99 -2
  107. data/app/workflows/spree/price_lists/update.rb +17 -0
  108. data/app/workflows/spree/refunds/create.rb +1 -1
  109. data/app/workflows/spree/refunds/order_payments.rb +55 -1
  110. data/app/workflows/spree/returns/approve.rb +1 -1
  111. data/app/workflows/spree/returns/create.rb +11 -17
  112. data/app/workflows/spree/returns/refund.rb +16 -25
  113. data/app/workflows/spree/returns/returnable_quantity.rb +53 -0
  114. data/app/workflows/spree/sellers/invite.rb +1 -1
  115. data/config/locales/en.yml +8 -119
  116. data/db/migrate/20260916000001_add_email_delivery_flags_to_spree_order_groups.rb +14 -0
  117. data/db/migrate/20260916000001_order_operation_actors_polymorphic.rb +30 -0
  118. data/db/migrate/20260922000001_add_refunded_order_to_spree_store_credits.rb +5 -0
  119. data/db/migrate/20260923000001_change_spree_user_identities_tokens_to_text.rb +10 -0
  120. data/db/sample_data/channels.rb +1 -1
  121. data/db/sample_data/collections.rb +1 -1
  122. data/db/sample_data/custom_field_definitions.rb +1 -1
  123. data/db/sample_data/digital_products.rb +1 -1
  124. data/db/sample_data/fulfillment.rb +1 -1
  125. data/db/sample_data/options.rb +4 -2
  126. data/db/sample_data/orders.rb +9 -10
  127. data/db/sample_data/payment_methods.rb +18 -18
  128. data/db/sample_data/product_type_categories.rb +1 -1
  129. data/db/sample_data/product_types.rb +2 -2
  130. data/db/sample_data/promotions.rb +2 -4
  131. data/db/sample_data/wholesale.rb +1 -1
  132. data/lib/generators/spree/api_resource/api_resource_generator.rb +121 -6
  133. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +8 -2
  134. data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +3 -0
  135. data/lib/generators/spree/api_resource/templates/permissions.en.yml.tt +6 -0
  136. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +2 -2
  137. data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +4 -1
  138. data/lib/generators/spree/dummy/templates/rails/test.rb +7 -0
  139. data/lib/generators/spree/model/model_generator.rb +40 -1
  140. data/lib/generators/spree/model/templates/create_table_migration.rb.tt +9 -0
  141. data/lib/generators/spree/model/templates/model.rb.tt +10 -4
  142. data/lib/generators/spree/subscriber/subscriber_generator.rb +3 -3
  143. data/lib/spree/core/configuration.rb +25 -25
  144. data/lib/spree/core/engine.rb +35 -8
  145. data/lib/spree/core/preferences/runtime_configuration.rb +76 -2
  146. data/lib/spree/core/version.rb +1 -1
  147. data/lib/spree/core.rb +27 -0
  148. data/lib/spree/iso_data.rb +23 -4
  149. data/lib/spree/store_scope_guard.rb +16 -1
  150. data/lib/spree/testing_support/factories/invitation_factory.rb +1 -0
  151. data/lib/spree/testing_support/factories/order_group_factory.rb +49 -0
  152. data/lib/spree/testing_support/factories/payment_factory.rb +7 -2
  153. data/lib/spree/testing_support/factories/user_factory.rb +0 -1
  154. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
  155. data/lib/tasks/backfill_actor_types.rake +35 -0
  156. data/lib/tasks/sample_data.rake +8 -2
  157. data/lib/tasks/store_settings.rake +1 -0
  158. data/lib/tasks/upgrade.rake +6 -21
  159. metadata +23 -9
  160. /data/{app/models → lib}/spree/checkout/default_requirements.rb +0 -0
  161. /data/{app/models → lib}/spree/checkout/registry.rb +0 -0
  162. /data/{app/models → lib}/spree/checkout/requirement.rb +0 -0
  163. /data/{app/models → lib}/spree/checkout/requirements.rb +0 -0
  164. /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: d404e43490b356a04bf850e09a644143d8f399c7f39c147301184fd11c2cfed5
4
- data.tar.gz: 2b1a720b211b09a061c26b5bf1761510fdde20bf2d50ee9451fbab4a67939b0c
3
+ metadata.gz: fb19660bc9b7017b74df8b6b92fc2522cb84ef62a6a015f817484528b7e8c992
4
+ data.tar.gz: bac9e0d41a47af2e5452406f1c0d1ac268b687ebdf1ade714742aa689e5cae0a
5
5
  SHA512:
6
- metadata.gz: 62dd3e3663ddf4b540127ae6bd526027adc294004e05f6069bf9041f36f5542c7daead8f747937094ddd73d8e19da153ff6ae214cc0c98ca6d7b80a6a8d2b8e8
7
- data.tar.gz: b8c49e3df74336ea44676d186c1b376979541c85610baf04b575f82d80886c2850ab4245d4274eb887535c4081e028631766a1b2fa31788c302116de4aca596e
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 store URL.
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.formatted_url, token)
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
- has_many :canceled_orders, class_name: 'Spree::Order', foreign_key: :canceler_id
37
- has_many :created_orders, class_name: 'Spree::Order', foreign_key: :created_by_id
38
- has_many :approved_orders, class_name: 'Spree::Order', foreign_key: :approver_id
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
- approved_orders.update_all(approver_id: nil, updated_at: Time.current)
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
- # resources to nullify
89
- # TODO: we should change these associations to polymorphic and resolve this via standard rails association
90
- # declarations with dependent: :nullify
91
- canceled_orders.update_all(canceler_id: nil, updated_at: Time.current)
92
- created_orders.update_all(created_by_id: nil, updated_at: Time.current)
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
- has_many :carts, -> { incomplete }, foreign_key: :customer_id, class_name: 'Spree::Order'
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. Unscoped because the index is global.
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
- Spree::PrefixedId.decode_prefixed_id(prefixed_id_string)
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::Config[:always_include_confirm_step] ||
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(*_args)
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(*_args)
40
+ def self.ransackable_attributes(auth_object = nil)
21
41
  base = default_ransackable_attributes | (whitelisted_ransackable_attributes || [])
22
- base | Spree.ransack.custom_attributes_for(self)
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(*_args)
26
- base = whitelisted_ransackable_scopes || []
27
- base | Spree.ransack.custom_scopes_for(self)
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, so it is
52
- # right for a store column (Product), a polymorphic owner (Policy) and
53
- # genuinely global reference data alike (OptionType, which has no store
54
- # dimension and returns everything by design). Its one blind spot is
55
- # +Spree::Store+: with no `stores` association to follow it falls back to
56
- # the UNSCOPED class, which would answer with every store in 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]