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
@@ -9,6 +9,7 @@ module Spree
9
9
  class StockReceipt < Spree.base_class
10
10
  has_prefix_id :sr
11
11
 
12
+ include Spree::ActedBy
12
13
  include Spree::SingleStoreResource
13
14
  has_spree_number prefix: 'SR'
14
15
  include Spree::NumberIdentifier
@@ -17,7 +18,7 @@ module Spree
17
18
  publishes_lifecycle_events
18
19
 
19
20
  belongs_to :receivable, polymorphic: true, inverse_of: :stock_receipts
20
- belongs_to :received_by, class_name: Spree.admin_user_class.to_s, optional: true
21
+ acted_by :received_by
21
22
 
22
23
  has_many :items, class_name: 'Spree::StockReceiptItem', inverse_of: :stock_receipt,
23
24
  dependent: :destroy
@@ -29,8 +30,12 @@ module Spree
29
30
 
30
31
  normalizes :reference, with: ->(value) { value.strip.presence }
31
32
 
33
+ # `received_by_type` sits beside its id because ids are per-table: filtering
34
+ # on the id alone would match the admin user and the API key that happen to
35
+ # share that number.
32
36
  self.whitelisted_ransackable_attributes = %w[number reference received_at receivable_type
33
- receivable_id received_by_id created_at]
37
+ receivable_id received_by_id received_by_type
38
+ created_at]
34
39
 
35
40
  # @return [Integer] units this delivery put on the shelf
36
41
  def quantity_accepted_total
@@ -95,6 +95,10 @@ module Spree
95
95
  # e.g. "https://myshop.com" — see #storefront_url for the fallback chain.
96
96
  preference :storefront_url, :string
97
97
  preference :special_instructions_enabled, :boolean, default: false
98
+ # Advertises a confirm/review step for every checkout, even when no
99
+ # payment method asks for one. Payment methods that require confirmation
100
+ # get it regardless of this setting.
101
+ preference :always_include_confirm_step, :boolean, default: false
98
102
  preference :stock_reservation_ttl_minutes, :integer, default: 10
99
103
  # Store-wide default for when a customer is charged rather than only
100
104
  # authorized. A payment method's own capture_method wins when set.
@@ -25,6 +25,8 @@ module Spree
25
25
  include Spree::DeprecatedCustomerAlias
26
26
  belongs_to :created_by, class_name: Spree.admin_user_class.to_s, foreign_key: 'created_by_id', optional: true
27
27
  belongs_to :originator, polymorphic: true, optional: true
28
+ belongs_to :refunded_order, class_name: 'Spree::Order', optional: true,
29
+ inverse_of: :store_credit_refunds
28
30
 
29
31
  has_many :store_credit_events, class_name: 'Spree::StoreCreditEvent'
30
32
  has_many :payments, as: :source, class_name: 'Spree::Payment'
@@ -111,6 +113,8 @@ module Spree
111
113
  self.whitelisted_ransackable_attributes = %w[customer_id created_by_id amount currency memo]
112
114
  self.whitelisted_ransackable_associations = %w[customer created_by]
113
115
  self.whitelisted_ransackable_scopes = %w[outstanding from_gift_card]
116
+ # Staff notes and who issued the credit are back-office data.
117
+ self.private_ransackable_attributes = { store: %w[memo created_by_id] }
114
118
 
115
119
  # Two-state scopes: see Spree::Base.ransack_flag? for why the cast is
116
120
  # opted out of here and done inside each scope instead.
@@ -38,8 +38,10 @@ module Spree
38
38
  attr_reader :records
39
39
 
40
40
  # @param entries [Array<Hash>] [{ resource_type:, resource_id:, values: { locale => { field => value } } }]
41
- def initialize(entries)
41
+ # @param store [Spree::Store] the store every record must belong to
42
+ def initialize(entries, store: Spree::Store.current)
42
43
  @entries = Array(entries)
44
+ @store = store
43
45
  @records = []
44
46
  end
45
47
 
@@ -86,7 +88,7 @@ module Spree
86
88
  klass = resource_class(entry[:resource_type])
87
89
  raise EntryError.new("Unknown translatable resource type: #{entry[:resource_type]}", index) if klass.nil?
88
90
 
89
- relation = klass.respond_to?(:for_store) ? klass.for_store(Spree::Store.current) : klass
91
+ relation = klass.respond_to?(:translatable_scope) ? klass.translatable_scope(@store) : klass.for_store(@store)
90
92
  relation.find_by_prefix_id!(entry[:resource_id])
91
93
  rescue ActiveRecord::RecordNotFound
92
94
  raise EntryError.new("Resource not found: #{entry[:resource_id]}", index)
@@ -9,15 +9,19 @@ module Spree
9
9
 
10
10
  validates :provider, inclusion: {
11
11
  in: lambda { |_record|
12
- (Spree.store_authentication_strategies.keys + Spree.admin_authentication_strategies.keys).uniq.map(&:to_s)
12
+ (
13
+ Spree.store_authentication_strategies.keys +
14
+ Spree.admin_authentication_strategies.keys +
15
+ Spree.seller_authentication_strategies.keys
16
+ ).uniq.map(&:to_s)
13
17
  }
14
18
  }
15
19
 
16
- # Store provider-specific data
17
- # info: JSON field with provider-specific data (name, avatar, etc)
18
- # access_token: encrypted OAuth access token
19
- # refresh_token: encrypted OAuth refresh token
20
- # expires_at: token expiration timestamp
20
+ # Keys may come from config or from encrypted credentials. Rows written before
21
+ # encryption was enabled stay readable and are encrypted on their next write.
22
+ if ActiveRecord::Encryption.config.has_primary_key?
23
+ encrypts :access_token, :refresh_token, support_unencrypted_data: true
24
+ end
21
25
 
22
26
  # Find or create user from OAuth data
23
27
  def self.find_or_create_from_oauth(provider:, uid:, info:, tokens: {}, user_class: nil)
@@ -330,6 +330,7 @@ module Spree
330
330
  carton_package_type_id carton_weight cartons_per_pallet]
331
331
  self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku search
332
332
  available_at_stock_location)
333
+ self.private_ransackable_attributes = { store: %w[cost_price cost_currency deleted_at] }
333
334
 
334
335
  def self.product_name_or_sku_cont(query)
335
336
  sanitized_query = ActiveRecord::Base.sanitize_sql_like(query.to_s.downcase.strip)
@@ -604,8 +605,11 @@ module Spree
604
605
  # @param opt_type_position [Integer] the position of the option type
605
606
  # @return [void]
606
607
  def set_option_value(opt_name, opt_value, opt_type_position = nil)
607
- option_type = Spree::OptionType.where(name: opt_name.parameterize).first_or_initialize do |o|
608
+ # The product's own store: option types are store-owned, and matching one
609
+ # by name elsewhere would attach and extend another store's type.
610
+ option_type = Spree::OptionType.for_store(product.store).where(name: opt_name.parameterize).first_or_initialize do |o|
608
611
  o.name = o.label = opt_name
612
+ o.store = product.store
609
613
  o.save!
610
614
  end
611
615
 
@@ -1190,10 +1194,11 @@ module Spree
1190
1194
  end
1191
1195
 
1192
1196
  # Only the product's own store's locations — a new variant must not grow
1193
- # stock items in every other store's warehouses.
1197
+ # stock items in every other store's warehouses — and of those, a seller's
1198
+ # location only stocks what that seller sells.
1194
1199
  def create_stock_levels
1195
1200
  locations = product&.store ? product.store.stock_locations : StockLocation.all
1196
- locations.where(propagate_all_variants: true).each do |stock_location|
1201
+ locations.where(propagate_all_variants: true, seller_id: [nil, resolved_seller_id].uniq).each do |stock_location|
1197
1202
  stock_location.propagate_variant(self)
1198
1203
  end
1199
1204
  end
@@ -26,6 +26,34 @@ module Spree
26
26
  # Ransack configuration
27
27
  self.whitelisted_ransackable_attributes = %w[event_name response_code execution_time success delivered_at created_at]
28
28
 
29
+ # Event subjects whose permission is not found by class name alone.
30
+ PAYLOAD_SUBJECT_CLASSES = {
31
+ 'cart' => 'Spree::Order',
32
+ 'customer' => -> { Spree.customer_class },
33
+ 'newsletter_subscriber' => -> { Spree.customer_class }
34
+ }.freeze
35
+
36
+ # Event subjects whose payload holds no record of anyone's data.
37
+ PUBLIC_PAYLOAD_SUBJECTS = %w[webhook].freeze
38
+
39
+ # The catalog key a caller needs to read this delivery's payload. The
40
+ # payload is the full record the event is about — an order's addresses, a
41
+ # customer's email — so reading the log is no wider than reading the
42
+ # record. A subject no permission scope covers falls back to customer
43
+ # access, since an unknown payload may hold personal data.
44
+ #
45
+ # @return [String, nil] nil when anyone who can read the log may see it
46
+ def payload_permission_key
47
+ subject = event_name.to_s.split('.').first.to_s
48
+ return if PUBLIC_PAYLOAD_SUBJECTS.include?(subject)
49
+
50
+ source = PAYLOAD_SUBJECT_CLASSES[subject]
51
+ klass = source.respond_to?(:call) ? source.call : (source || "Spree::#{subject.camelize}").to_s.safe_constantize
52
+ scope = Spree.permissions.scope_for_resource(klass)
53
+
54
+ scope ? "read_#{scope.name}" : 'read_customers'
55
+ end
56
+
29
57
  # Check if the delivery was successful
30
58
  #
31
59
  # @return [Boolean]
@@ -50,11 +50,18 @@ module Spree
50
50
  # Number of consecutive failed deliveries before auto-disabling
51
51
  AUTO_DISABLE_THRESHOLD = 15
52
52
 
53
+ # Events whose payload carries a live customer credential (a password
54
+ # reset token). Receiving one is as good as holding the customer's account,
55
+ # so they reach only an endpoint that names them — never through `*` or a
56
+ # pattern — and the API asks for customer write access to point one at them.
57
+ CREDENTIAL_EVENTS = %w[customer.password_reset_requested].freeze
58
+
53
59
  # Check if this endpoint is subscribed to a specific event
54
60
  #
55
61
  # @param event_name [String] the event name to check
56
62
  # @return [Boolean]
57
63
  def subscribed_to?(event_name)
64
+ return subscriptions.to_a.include?(event_name) if CREDENTIAL_EVENTS.include?(event_name)
58
65
  return true if subscriptions.blank? || subscriptions.include?('*')
59
66
 
60
67
  subscriptions.any? do |subscription|
@@ -67,6 +74,13 @@ module Spree
67
74
  end
68
75
  end
69
76
 
77
+ # Whether this endpoint receives customer credentials.
78
+ #
79
+ # @return [Boolean]
80
+ def receives_credentials?
81
+ (subscriptions.to_a & CREDENTIAL_EVENTS).any?
82
+ end
83
+
70
84
  # Returns all events this endpoint is subscribed to
71
85
  #
72
86
  # @return [Array<String>]
@@ -130,7 +130,7 @@ module Spree
130
130
  index.zero? ? order.bill_address&.company : nil,
131
131
  index.zero? ? order.bill_address&.city : nil,
132
132
  index.zero? ? order.bill_address&.postal_code : nil,
133
- index.zero? ? order.bill_address&.state_name : nil,
133
+ index.zero? ? order.bill_address&.state_text : nil,
134
134
  index.zero? ? order.bill_address&.country&.name : nil,
135
135
  index.zero? ? order.bill_address&.phone : nil,
136
136
  index.zero? ? order.ship_address&.full_name : nil,
@@ -139,7 +139,7 @@ module Spree
139
139
  index.zero? ? order.ship_address&.company : nil,
140
140
  index.zero? ? order.ship_address&.city : nil,
141
141
  index.zero? ? order.ship_address&.postal_code : nil,
142
- index.zero? ? order.ship_address&.state_name : nil,
142
+ index.zero? ? order.ship_address&.state_text : nil,
143
143
  index.zero? ? order.ship_address&.country&.name : nil,
144
144
  index.zero? ? order.ship_address&.phone : nil,
145
145
  index.zero? ? format_date(order.completed_at) : nil,
@@ -29,20 +29,26 @@ module Spree
29
29
  # blocklist would carry every future column onto siblings by default.
30
30
  # Adding a column here is a decision that it describes the checkout
31
31
  # rather than one seller's part of it.
32
+ #
33
+ # +metadata+ is here because at this point it holds what the checkout
34
+ # recorded about the purchase (copied from the cart), so every child gets
35
+ # the same hash — and so does the group. Once split, each order's
36
+ # metadata is its own: edits never propagate between siblings.
32
37
  CARRIED_TO_SIBLING = %w[
33
38
  email currency locale market_id channel_id company_id
34
39
  customer_id token accept_marketing preferred_stock_location_id
35
- customer_note last_ip_address po_number
40
+ customer_note last_ip_address po_number metadata
36
41
  ].freeze
37
42
 
38
- # @param cart [Spree::Cart]
39
43
  # @param order [Spree::Order] the paid draft order, adopted as the
40
44
  # group's first child rather than replaced — it is the record the
41
45
  # payment was taken against
42
46
  # @param partitions [Array<Spree::Carts::SellerPartition>] ordered, first
43
47
  # party first
48
+ # @param cart [Spree::Cart, nil] nil for an order raised from the admin,
49
+ # which never had one
44
50
  # @return [Spree::ServiceModule::Result] value is the Spree::OrderGroup
45
- def call(cart:, order:, partitions:)
51
+ def call(order:, partitions:, cart: nil)
46
52
  group = nil
47
53
  # Every apportionment here divides money in this one currency.
48
54
  @currency = order.currency
@@ -76,20 +82,24 @@ module Spree
76
82
  # keeps the reference they saw at checkout, and the children number off
77
83
  # it: R1001-1, R1001-2. The cart_id moves up too — the group is what that
78
84
  # cart completed into, and it becomes the replay anchor.
85
+ #
86
+ # The facts come off the order rather than the cart because the order
87
+ # carries every one of them and an admin-raised order has no cart; the
88
+ # cart is only linked, as the replay anchor, when there is one.
79
89
  def build_group(cart, order)
80
90
  order.update_columns(cart_id: nil)
81
91
 
82
- group = cart.store.order_groups.create!(
92
+ order.store.order_groups.create!(
83
93
  cart: cart,
84
- customer: cart.customer,
94
+ customer: order.customer,
85
95
  number: order.number,
86
- currency: cart.currency,
87
- email: cart.email,
88
- token: cart.token,
89
- ship_address: cart.ship_address&.snapshot,
90
- bill_address: cart.bill_address&.snapshot
96
+ currency: order.currency,
97
+ email: order.email,
98
+ token: order.token,
99
+ metadata: order.metadata.to_h.deep_dup,
100
+ ship_address: order.ship_address&.snapshot,
101
+ bill_address: order.bill_address&.snapshot
91
102
  )
92
- group
93
103
  end
94
104
 
95
105
  # Files the paid order under the group as its first child. Its own
@@ -109,7 +119,7 @@ module Spree
109
119
  # settled during completion.
110
120
  def hand_off(source_order, group, partition, index)
111
121
  sibling = source_order.store.orders.create!(
112
- source_order.attributes.slice(*CARRIED_TO_SIBLING).merge(
122
+ source_order.attributes.slice(*CARRIED_TO_SIBLING).deep_dup.merge(
113
123
  'order_group_id' => group.id,
114
124
  'seller_id' => partition.seller_id,
115
125
  'number' => "#{group.number}-#{index}",
@@ -18,11 +18,17 @@ module Spree
18
18
  # (memberships have no metadata column; a direct-membership payload is
19
19
  # applied by the caller through the `company_membership.form_fields`
20
20
  # contract, not stored here)
21
+ # @param require_acceptance [Boolean] always invite, even an existing
22
+ # customer — for callers who cannot vouch that the email is the
23
+ # person they mean (a storefront member typing an address). The
24
+ # emailed token is what proves the invitee owns the address and
25
+ # agrees to join; a direct membership would hand the company to
26
+ # whoever happens to hold that email.
21
27
  # @return [Spree::ServiceModule::Result] value is the created
22
28
  # Spree::CompanyMembership or Spree::CompanyInvitation
23
- def call(company:, email:, inviter: nil, metadata: nil)
29
+ def call(company:, email:, inviter: nil, metadata: nil, require_acceptance: false)
24
30
  normalized_email = email.to_s.strip.downcase
25
- customer = Spree.customer_class.find_by(email: normalized_email)
31
+ customer = Spree.customer_class.find_by(email: normalized_email) unless require_acceptance
26
32
 
27
33
  record =
28
34
  if customer
@@ -154,6 +154,12 @@ module Spree
154
154
  Spree.t(:price_list_import_too_many_breaks, count: Spree::Price::MAXIMUM_BREAKS_PER_VARIANT)
155
155
  elsif value[:invalid_quantities].present?
156
156
  Spree.t(:price_list_import_invalid_quantity, value: attributes['min_quantity'])
157
+ elsif (rising = value[:rising_ladders].presence)
158
+ # Named rung and floor, like the sibling branches: a file is written
159
+ # one rung per row, so the merchant needs to know which one and what
160
+ # it had to beat.
161
+ Spree.t(:price_list_import_price_rises_with_quantity,
162
+ quantity: rising.first[:min_quantity], floor: rising.first[:floor])
157
163
  else
158
164
  result.error.to_s.presence || Spree.t(:price_list_import_failed)
159
165
  end
@@ -37,7 +37,7 @@ module Spree
37
37
  private
38
38
 
39
39
  def product_scope
40
- Spree::Product.accessible_by(import.current_ability, :manage)
40
+ store.products.accessible_by(import.current_ability, :manage)
41
41
  end
42
42
  end
43
43
  end
@@ -125,15 +125,17 @@ module Spree
125
125
 
126
126
  # Which products this import may resolve an existing row onto.
127
127
  #
128
- # A seller's import reaches only that seller's own products. The
129
- # ability cannot answer this: it grants capability, never tenancy —
130
- # a seller holding `write_products` gets `can :manage, Spree::Product`,
131
- # the whole class (see Spree::Ability). On the API that is safe because
132
- # the controller scope-fetches through `current_seller`; a background
133
- # row processor has no controller, so the narrowing has to happen here
134
- # or a CSV naming another seller's slug would edit their product.
128
+ # The import's own store's products, and for a seller's import only
129
+ # that seller's. The ability cannot answer either: it grants
130
+ # capability, never tenancy — a seller holding `write_products` gets
131
+ # `can :manage, Spree::Product`, the whole class (see Spree::Ability),
132
+ # and an admin of two stores reaches both. On the API that is safe because
133
+ # the controller scope-fetches through `current_store` and
134
+ # `current_seller`; a background row processor has no controller, so
135
+ # the narrowing has to happen here or a CSV naming another store's or
136
+ # seller's slug would edit their product.
135
137
  def product_scope
136
- scope = Spree::Product.accessible_by(import.current_ability, :manage)
138
+ scope = store.products.accessible_by(import.current_ability, :manage)
137
139
  seller.present? ? scope.where(seller_id: seller.id) : scope
138
140
  end
139
141
 
@@ -237,7 +239,7 @@ module Spree
237
239
  def prepare_tax_category
238
240
  tax_category_name = attributes['tax_category'].strip
239
241
  cached_lookup(:tax_category, tax_category_name) do
240
- Spree::TaxCategory.find_by(name: tax_category_name)
242
+ store.tax_categories.find_by(name: tax_category_name)
241
243
  end
242
244
  end
243
245
 
@@ -302,15 +304,19 @@ module Spree
302
304
  def find_or_create_option_type!(label)
303
305
  cached_lookup(:option_type, label) do
304
306
  begin
305
- Spree::OptionType.search_by_name(label).first || Spree::OptionType.create!(label: label)
307
+ store_option_types.search_by_name(label).first || store_option_types.create!(label: label)
306
308
  rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
307
309
  raise unless uniqueness_conflict?(e, :name)
308
310
 
309
- Spree::OptionType.search_by_name(label).first!
311
+ store_option_types.search_by_name(label).first!
310
312
  end
311
313
  end
312
314
  end
313
315
 
316
+ def store_option_types
317
+ Spree::OptionType.for_store(store)
318
+ end
319
+
314
320
  def find_or_create_option_value!(option_type, label)
315
321
  cached_lookup(:option_value, option_type.id, label) do
316
322
  begin
@@ -87,6 +87,9 @@ module Spree
87
87
  next if split.persisted?
88
88
 
89
89
  share = Spree::Money::Rounding.from_minor_units(shares[index], currency)
90
+ # So re-summing its payment total on save reuses this instance rather
91
+ # than loading a second copy of the row per share.
92
+ split.order = order
90
93
  split.currency = payment.currency
91
94
  split.authorized_amount = share
92
95
  split.captured_amount = captured ? share : 0
@@ -6,7 +6,8 @@ module Spree
6
6
  # Approves an order: clears the risk flag and records who approved it and when.
7
7
  #
8
8
  # @param order [Spree::Order]
9
- # @param approver [Object, nil] the user/admin who approved
9
+ # @param approver [Object, nil] who approved it — an admin user or an
10
+ # API key (see Spree.actor_classes)
10
11
  # @param level [String, nil] deprecated and ignored — removed in Spree 6.1
11
12
  # @param note [String, nil] deprecated and ignored — removed in Spree 6.1
12
13
  # @return [Spree::ServiceModule::Result]
@@ -16,7 +17,7 @@ module Spree
16
17
  end
17
18
 
18
19
  changes = { considered_risky: false, approved_at: Time.current }
19
- changes[:approver_id] = approver.id if approver.present?
20
+ changes.merge!(Spree::ActedBy.columns_for(:approver, approver)) if approver.present?
20
21
  order.update_columns(changes)
21
22
 
22
23
  order.publish_event('order.approved')
@@ -9,10 +9,16 @@ module Spree
9
9
  #
10
10
  # No-op when the order has no shipping address, no line items, or does
11
11
  # not require delivery (digital orders, etc.).
12
+ #
13
+ # Also a no-op once the order is placed. Its fulfillments hold the stock
14
+ # promised at placement, and an edit adjusts them in place through
15
+ # Spree::OrderInventory. Rebuilding would delete them and leave that
16
+ # promise on rows nothing can cancel or ship, so the stock stays held.
12
17
  class BuildFulfillments
13
18
  prepend Spree::ServiceModule::Base
14
19
 
15
20
  def call(order:)
21
+ return success(order) if order.completed?
16
22
  return success(order) unless order.ship_address_id.present?
17
23
  return success(order) unless order.line_items.any?
18
24
  return success(order) unless order.delivery_step_required?
@@ -60,14 +60,20 @@ module Spree
60
60
  # shipped and been captured while another has not, no proportion of the
61
61
  # group's totals can describe either.
62
62
  #
63
+ # Refunded spans both ledgers. A refund paid as store credit writes no
64
+ # {Spree::Refund} row, and counting only those rows left an order that
65
+ # had given every penny back still reading `paid`.
66
+ #
63
67
  # @return [Array(BigDecimal, BigDecimal, BigDecimal)] captured,
64
68
  # authorized and refunded, in that order
65
69
  def money_for(order)
70
+ credited = order.store_credit_refunds.sum(:amount)
71
+
66
72
  unless order.order_group_id.present?
67
73
  return [
68
74
  order.payments.valid.completed.sum(:amount),
69
75
  order.payments.valid.pending.sum(:amount),
70
- order.refunds.sum(:amount)
76
+ order.refunds.sum(:amount) + credited
71
77
  ]
72
78
  end
73
79
 
@@ -76,7 +82,7 @@ module Spree
76
82
 
77
83
  # Authorized means still to draw: what the shares allow, less what has
78
84
  # already been taken against them.
79
- [captured, splits.sum(&:authorized_amount) - captured, splits.sum(&:refunded_amount)]
85
+ [captured, splits.sum(&:authorized_amount) - captured, splits.sum(&:refunded_amount) + credited]
80
86
  end
81
87
 
82
88
  # Rolls the fulfillments up into one word for filtering and display.