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
|
@@ -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
|
-
|
|
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
|
|
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
|
data/app/models/spree/store.rb
CHANGED
|
@@ -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
|
-
|
|
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?(:
|
|
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
|
-
(
|
|
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
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
data/app/models/spree/variant.rb
CHANGED
|
@@ -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
|
-
|
|
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&.
|
|
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&.
|
|
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(
|
|
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
|
-
|
|
92
|
+
order.store.order_groups.create!(
|
|
83
93
|
cart: cart,
|
|
84
|
-
customer:
|
|
94
|
+
customer: order.customer,
|
|
85
95
|
number: order.number,
|
|
86
|
-
currency:
|
|
87
|
-
email:
|
|
88
|
-
token:
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
@@ -125,15 +125,17 @@ module Spree
|
|
|
125
125
|
|
|
126
126
|
# Which products this import may resolve an existing row onto.
|
|
127
127
|
#
|
|
128
|
-
#
|
|
129
|
-
# ability cannot answer
|
|
130
|
-
# a seller holding `write_products` gets
|
|
131
|
-
# the whole class (see Spree::Ability)
|
|
132
|
-
# the
|
|
133
|
-
#
|
|
134
|
-
#
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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.
|