spree_core 6.0.0.rc5 → 6.0.0.rc7
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/images/save_from_url_job.rb +24 -5
- data/app/models/concerns/spree/account_deactivation.rb +54 -0
- data/app/models/concerns/spree/calculator/currency_amounts.rb +19 -6
- data/app/models/concerns/spree/derived_number.rb +1 -1
- data/app/models/concerns/spree/has_external_references.rb +24 -16
- data/app/models/concerns/spree/prefixed_id.rb +15 -5
- data/app/models/concerns/spree/publishable.rb +47 -1
- data/app/models/concerns/spree/purchase/addresses.rb +19 -4
- data/app/models/concerns/spree/purchase/digital_items.rb +17 -3
- data/app/models/concerns/spree/purchase/freight.rb +2 -1
- data/app/models/concerns/spree/purchase/payment_processing.rb +7 -1
- data/app/models/concerns/spree/purchase/quantity_rules.rb +2 -0
- data/app/models/concerns/spree/stores/channels.rb +3 -1
- data/app/models/concerns/spree/translatable_resource.rb +6 -1
- data/app/models/spree/admin_user.rb +1 -0
- data/app/models/spree/base.rb +33 -1
- data/app/models/spree/calculator/shipping/digital_delivery.rb +3 -1
- data/app/models/spree/calculator/shipping/flat_rate.rb +3 -1
- data/app/models/spree/calculator/shipping/per_item.rb +3 -1
- data/app/models/spree/cart.rb +4 -1
- data/app/models/spree/category.rb +76 -5
- data/app/models/spree/channel.rb +79 -40
- data/app/models/spree/company.rb +45 -2
- data/app/models/spree/customer.rb +1 -1
- data/app/models/spree/customer_group.rb +59 -41
- data/app/models/spree/delivery_method.rb +18 -8
- data/app/models/spree/delivery_profile.rb +18 -0
- data/app/models/spree/digital_asset.rb +14 -0
- data/app/models/spree/event.rb +20 -5
- data/app/models/spree/fulfillment.rb +8 -1
- data/app/models/spree/fulfillment_changer.rb +3 -1
- data/app/models/spree/fulfillment_item.rb +11 -0
- data/app/models/spree/import.rb +20 -2
- data/app/models/spree/order_routing/strategy/rules.rb +4 -1
- data/app/models/spree/price_list.rb +15 -0
- data/app/models/spree/product/channels.rb +16 -8
- data/app/models/spree/product.rb +7 -2
- data/app/models/spree/return.rb +8 -1
- data/app/models/spree/search_provider/base.rb +8 -3
- data/app/models/spree/search_provider/database.rb +2 -10
- data/app/models/spree/seller.rb +35 -5
- data/app/models/spree/seller_balance.rb +28 -0
- data/app/models/spree/seller_requirements/delivery_method.rb +20 -2
- data/app/models/spree/stock/coordinator.rb +7 -4
- data/app/models/spree/stock/estimator.rb +6 -0
- data/app/models/spree/stock/inventory_unit_builder.rb +14 -2
- data/app/models/spree/stock/packer.rb +39 -5
- data/app/models/spree/stock/quantifier.rb +16 -0
- data/app/models/spree/stock_location.rb +15 -8
- data/app/models/spree/store.rb +31 -2
- data/app/models/spree/store_credit.rb +4 -4
- data/app/models/spree/variant.rb +23 -6
- data/app/models/spree/webhook_endpoint.rb +45 -11
- data/app/services/spree/carts/price_items.rb +3 -0
- data/app/services/spree/carts/split_by_seller.rb +3 -1
- data/app/services/spree/catalogs/set_product_terms.rb +8 -3
- data/app/services/spree/collections/add_products.rb +1 -1
- data/app/services/spree/collections/regenerate_products.rb +1 -3
- data/app/services/spree/collections/remove_products.rb +1 -1
- data/app/services/spree/newsletter/subscribe.rb +9 -1
- data/app/services/spree/order_groups/allocate_payments.rb +7 -5
- data/app/services/spree/orders/update.rb +19 -0
- data/app/services/spree/product_memberships.rb +16 -6
- data/app/services/spree/products/duplicator.rb +12 -4
- data/app/services/spree/search_provider/custom_field_schema.rb +10 -2
- data/app/services/spree/stock_levels/correct.rb +11 -4
- data/app/services/spree/stock_reservations/reserve.rb +1 -1
- data/app/services/spree/stores/provision_defaults.rb +11 -7
- data/app/services/spree/tags/bulk_add.rb +16 -6
- data/app/workflows/spree/carts/complete.rb +25 -9
- data/app/workflows/spree/carts/upsert_items.rb +31 -4
- data/app/workflows/spree/customers/anonymize.rb +2 -2
- data/app/workflows/spree/digital_assets/create.rb +56 -0
- data/app/workflows/spree/digital_assets/update.rb +44 -0
- data/app/workflows/spree/digital_assets/writing.rb +108 -0
- data/app/workflows/spree/imports/start_mapping.rb +3 -1
- data/app/workflows/spree/orders/complete.rb +12 -5
- data/app/workflows/spree/payments/capture.rb +12 -0
- data/app/workflows/spree/products/nested_attributes.rb +12 -28
- data/app/workflows/spree/products/propose.rb +9 -4
- data/app/workflows/spree/stock_levels/bulk_upsert.rb +28 -13
- data/app/workflows/spree/variants/nested_attributes.rb +35 -10
- data/config/locales/en.yml +10 -0
- data/db/migrate/20261022000001_store_credit_actor_polymorphic.rb +13 -0
- data/db/migrate/20261023100001_add_deactivated_at_to_spree_admin_users.rb +7 -0
- data/lib/generators/spree/dummy/templates/rails/test.rb +87 -0
- data/lib/spree/checkout/default_requirements.rb +12 -2
- data/lib/spree/core/dependencies.rb +5 -0
- data/lib/spree/core/permissions/default_catalog.rb +7 -2
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/reporting/default_vocabulary.rb +1 -1
- data/lib/spree/testing_support/factories/product_factory.rb +10 -4
- data/lib/spree/testing_support/factories/store_factory.rb +4 -1
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +10 -0
- data/lib/tasks/backfill_tagging_tenants.rake +35 -0
- data/lib/tasks/seller_sample_data.rake +114 -112
- metadata +13 -6
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
has_prefix_id :credit
|
|
4
4
|
|
|
5
5
|
include Spree::SingleStoreResource
|
|
6
|
+
include Spree::ActedBy
|
|
6
7
|
include Spree::HasCustomFields
|
|
7
8
|
include Spree::Metadata
|
|
8
9
|
include Spree::CanonicalDecimals
|
|
@@ -25,7 +26,7 @@ module Spree
|
|
|
25
26
|
# to attach it to.
|
|
26
27
|
belongs_to :customer, class_name: "::#{Spree.customer_class}", optional: true
|
|
27
28
|
include Spree::DeprecatedCustomerAlias
|
|
28
|
-
|
|
29
|
+
acted_by :created_by
|
|
29
30
|
belongs_to :originator, polymorphic: true, optional: true
|
|
30
31
|
belongs_to :refunded_order, class_name: 'Spree::Order', optional: true,
|
|
31
32
|
inverse_of: :store_credit_refunds
|
|
@@ -40,8 +41,6 @@ module Spree
|
|
|
40
41
|
validate :amount_used_less_than_or_equal_to_amount
|
|
41
42
|
validate :amount_authorized_less_than_or_equal_to_amount
|
|
42
43
|
|
|
43
|
-
delegate :email, to: :created_by, prefix: true, allow_nil: true
|
|
44
|
-
|
|
45
44
|
# Redemption order: the oldest credit is spent first.
|
|
46
45
|
scope :oldest_first, -> { order(:created_at, :id) }
|
|
47
46
|
# @deprecated Removed in Spree 6.1 — store credit types are gone; use {.oldest_first}.
|
|
@@ -110,7 +109,7 @@ module Spree
|
|
|
110
109
|
|
|
111
110
|
self.whitelisted_ransackable_attributes = %w[customer_id created_by_id amount currency memo]
|
|
112
111
|
search_by :memo
|
|
113
|
-
self.whitelisted_ransackable_associations = %w[customer
|
|
112
|
+
self.whitelisted_ransackable_associations = %w[customer]
|
|
114
113
|
self.whitelisted_ransackable_scopes = %w[outstanding from_gift_card]
|
|
115
114
|
self.ransackable_scope_types = %w[outstanding from_gift_card].index_with('boolean')
|
|
116
115
|
# Staff notes and who issued the credit are back-office data.
|
|
@@ -283,6 +282,7 @@ module Spree
|
|
|
283
282
|
amount: amount,
|
|
284
283
|
customer_id: customer_id,
|
|
285
284
|
created_by_id: created_by_id,
|
|
285
|
+
created_by_type: created_by_type,
|
|
286
286
|
currency: currency,
|
|
287
287
|
memo: credit_allocation_memo,
|
|
288
288
|
store: store
|
data/app/models/spree/variant.rb
CHANGED
|
@@ -393,7 +393,11 @@ module Spree
|
|
|
393
393
|
@tax_category ||= if self[:tax_category_id].nil?
|
|
394
394
|
product.tax_category
|
|
395
395
|
else
|
|
396
|
-
|
|
396
|
+
# The association, so a page of variants loads its
|
|
397
|
+
# categories together; one deleted since it was
|
|
398
|
+
# assigned no longer counts.
|
|
399
|
+
own = super
|
|
400
|
+
own.nil? || own.destroyed? || own.try(:deleted?) ? product.tax_category : own
|
|
397
401
|
end
|
|
398
402
|
end
|
|
399
403
|
|
|
@@ -466,7 +470,7 @@ module Spree
|
|
|
466
470
|
# Returns the options text of the variant.
|
|
467
471
|
# @return [String] the options text of the variant
|
|
468
472
|
def options_text
|
|
469
|
-
@options_text ||= if option_values
|
|
473
|
+
@options_text ||= if read_association_in_memory?(:option_values)
|
|
470
474
|
option_values.sort_by do |ov|
|
|
471
475
|
ov.option_type.position
|
|
472
476
|
end.map { |ov| "#{ov.option_type.label}: #{ov.label}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
|
|
@@ -663,7 +667,7 @@ module Spree
|
|
|
663
667
|
def price_in(currency)
|
|
664
668
|
currency = currency&.upcase
|
|
665
669
|
|
|
666
|
-
price = if read_association_in_memory?(:prices)
|
|
670
|
+
price = if read_association_in_memory?(:prices)
|
|
667
671
|
prices.detect { |p| p.currency == currency && p.price_list_id.nil? }
|
|
668
672
|
else
|
|
669
673
|
prices.base_prices.find_by(currency: currency)
|
|
@@ -864,7 +868,12 @@ module Spree
|
|
|
864
868
|
# @return [void]
|
|
865
869
|
def set_stock(count_on_hand, backorderable = nil, stock_location = nil)
|
|
866
870
|
stock_location ||= default_stock_location
|
|
867
|
-
stock_level = stock_levels.
|
|
871
|
+
stock_level = if stock_levels.loaded?
|
|
872
|
+
stock_levels.detect { |level| level.stock_location_id == stock_location.id } ||
|
|
873
|
+
stock_levels.build(stock_location: stock_location)
|
|
874
|
+
else
|
|
875
|
+
stock_levels.find_or_initialize_by(stock_location: stock_location)
|
|
876
|
+
end
|
|
868
877
|
# `unless nil?`, not `if present?` — a caller passing false means it.
|
|
869
878
|
stock_level.backorderable = backorderable unless backorderable.nil?
|
|
870
879
|
|
|
@@ -880,7 +889,10 @@ module Spree
|
|
|
880
889
|
delta = count_on_hand.to_i - stock_level.count_on_hand.to_i
|
|
881
890
|
return if delta.zero?
|
|
882
891
|
|
|
883
|
-
|
|
892
|
+
# Through the level already in hand rather than StockLocation#adjust,
|
|
893
|
+
# which would look the same row up again.
|
|
894
|
+
stock_level.stock_movements.create!(quantity: delta, kind: 'adjusted',
|
|
895
|
+
reason: Spree::StockMovement.default_adjustment_reason)
|
|
884
896
|
end
|
|
885
897
|
|
|
886
898
|
def default_stock_location
|
|
@@ -967,6 +979,11 @@ module Spree
|
|
|
967
979
|
# Variants carry no store of their own; the product owns it.
|
|
968
980
|
# @return [Spree::Store, nil]
|
|
969
981
|
def preference_store
|
|
982
|
+
# The request's store instance when it is this one, so a page of
|
|
983
|
+
# variants reads its settings from one record rather than a copy each.
|
|
984
|
+
current_store = Spree::Current.store
|
|
985
|
+
return current_store if current_store && product&.store_id == current_store.id
|
|
986
|
+
|
|
970
987
|
product&.store
|
|
971
988
|
end
|
|
972
989
|
|
|
@@ -1188,7 +1205,7 @@ module Spree
|
|
|
1188
1205
|
end
|
|
1189
1206
|
|
|
1190
1207
|
def set_cost_currency
|
|
1191
|
-
self.cost_currency = Spree::Store.default.default_currency if cost_currency.blank?
|
|
1208
|
+
self.cost_currency = (product&.store || Spree::Store.default).default_currency if cost_currency.blank?
|
|
1192
1209
|
end
|
|
1193
1210
|
|
|
1194
1211
|
# Only the product's own store's locations — a new variant must not grow
|
|
@@ -20,6 +20,10 @@ module Spree
|
|
|
20
20
|
validates :url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: :invalid_url }
|
|
21
21
|
validates :active, inclusion: { in: [true, false] }
|
|
22
22
|
validate :url_must_not_resolve_to_private_ip, if: -> { !Rails.env.development? && url.present? && url_changed? }
|
|
23
|
+
validate :subscriptions_must_be_event_patterns, if: :will_save_change_to_subscriptions?
|
|
24
|
+
|
|
25
|
+
attribute :subscriptions, default: -> { [] }
|
|
26
|
+
normalizes :subscriptions, apply_to_nil: true, with: ->(value) { Array(value).map { |entry| entry.to_s.strip }.reject(&:empty?).uniq }
|
|
23
27
|
|
|
24
28
|
before_create :generate_secret_key
|
|
25
29
|
after_create { @reveal_secret_in_response = true }
|
|
@@ -83,6 +87,23 @@ module Spree
|
|
|
83
87
|
# Number of consecutive failed deliveries before auto-disabling
|
|
84
88
|
AUTO_DISABLE_THRESHOLD = 15
|
|
85
89
|
|
|
90
|
+
MAX_SUBSCRIPTIONS = 200
|
|
91
|
+
MAX_SUBSCRIPTION_LENGTH = 100
|
|
92
|
+
SUBSCRIPTION_SEGMENT = /\A[a-zA-Z0-9_-]+\z/
|
|
93
|
+
|
|
94
|
+
# Whether +value+ is a shape `subscriptions` accepts: `*`, an event name,
|
|
95
|
+
# or a name with `*` in place of whole segments (`order.*`, `*.created`).
|
|
96
|
+
# Anything else — regexp syntax above all — is refused before it is
|
|
97
|
+
# stored, so the matcher only ever sees names and segment wildcards.
|
|
98
|
+
#
|
|
99
|
+
# @param value [Object]
|
|
100
|
+
# @return [Boolean]
|
|
101
|
+
def self.event_pattern?(value)
|
|
102
|
+
return false unless value.is_a?(String) && value.length.between?(1, MAX_SUBSCRIPTION_LENGTH)
|
|
103
|
+
|
|
104
|
+
value.split('.', -1).all? { |segment| segment == '*' || segment.match?(SUBSCRIPTION_SEGMENT) }
|
|
105
|
+
end
|
|
106
|
+
|
|
86
107
|
# Check if this endpoint is subscribed to a specific event.
|
|
87
108
|
#
|
|
88
109
|
# Events whose payload carries a live credential (a password reset token)
|
|
@@ -93,16 +114,9 @@ module Spree
|
|
|
93
114
|
# @return [Boolean]
|
|
94
115
|
def subscribed_to?(event_name)
|
|
95
116
|
return subscriptions.to_a.include?(event_name) if Spree::Events.catalog.credential?(event_name)
|
|
96
|
-
return true if subscriptions.blank?
|
|
97
|
-
|
|
98
|
-
subscriptions.any?
|
|
99
|
-
if subscription.include?('*')
|
|
100
|
-
pattern = Regexp.escape(subscription).gsub('\*', '.*')
|
|
101
|
-
event_name.match?(Regexp.new("^#{pattern}$"))
|
|
102
|
-
else
|
|
103
|
-
subscription == event_name
|
|
104
|
-
end
|
|
105
|
-
end
|
|
117
|
+
return true if subscriptions.blank?
|
|
118
|
+
|
|
119
|
+
subscriptions.any? { |subscription| Spree::Event.matches?(event_name, subscription) }
|
|
106
120
|
end
|
|
107
121
|
|
|
108
122
|
# The credential-carrying events this endpoint names.
|
|
@@ -191,6 +205,16 @@ module Spree
|
|
|
191
205
|
self.secret_key ||= SecureRandom.hex(32)
|
|
192
206
|
end
|
|
193
207
|
|
|
208
|
+
def subscriptions_must_be_event_patterns
|
|
209
|
+
entries = subscriptions.to_a
|
|
210
|
+
errors.add(:subscriptions, :too_many, count: MAX_SUBSCRIPTIONS) if entries.size > MAX_SUBSCRIPTIONS
|
|
211
|
+
|
|
212
|
+
rejected = entries.reject { |entry| self.class.event_pattern?(entry) }
|
|
213
|
+
return if rejected.empty?
|
|
214
|
+
|
|
215
|
+
errors.add(:subscriptions, :invalid_pattern, entries: rejected.first(5).map { |entry| entry.truncate(40) }.join(', '))
|
|
216
|
+
end
|
|
217
|
+
|
|
194
218
|
def clear_disabled_state_when_reactivated
|
|
195
219
|
return unless will_save_change_to_active? && active
|
|
196
220
|
|
|
@@ -203,12 +227,22 @@ module Spree
|
|
|
203
227
|
|
|
204
228
|
uri = URI.parse(url)
|
|
205
229
|
blacklist = SsrfFilter::IPV4_BLACKLIST + SsrfFilter::IPV6_BLACKLIST
|
|
206
|
-
addresses =
|
|
230
|
+
addresses = host_addresses(uri.host)
|
|
207
231
|
if addresses.any? { |addr| blacklist.any? { |range| range.include?(IPAddr.new(addr)) } }
|
|
208
232
|
errors.add(:url, :internal_address_not_allowed)
|
|
209
233
|
end
|
|
210
234
|
rescue URI::InvalidURIError, Resolv::ResolvError, IPAddr::InvalidAddressError, ArgumentError
|
|
211
235
|
# URI format validation handles invalid URLs; DNS failures are not SSRF
|
|
212
236
|
end
|
|
237
|
+
|
|
238
|
+
# A literal address first — including the forms a resolver answers
|
|
239
|
+
# nothing for, such as a bracketed IPv6 address, `127.1`, or a decimal or
|
|
240
|
+
# hex IPv4 — then DNS for a hostname.
|
|
241
|
+
def host_addresses(host)
|
|
242
|
+
literal = host.to_s.delete_prefix('[').delete_suffix(']')
|
|
243
|
+
Socket.getaddrinfo(literal, nil, nil, Socket::SOCK_STREAM, nil, Socket::AI_NUMERICHOST).map { |entry| entry[3] }.uniq
|
|
244
|
+
rescue SocketError
|
|
245
|
+
Resolv.getaddresses(host)
|
|
246
|
+
end
|
|
213
247
|
end
|
|
214
248
|
end
|
|
@@ -31,6 +31,9 @@ module Spree
|
|
|
31
31
|
items = items.reject(&:manual_price?)
|
|
32
32
|
return success([]) if items.empty?
|
|
33
33
|
|
|
34
|
+
# One read of every line's prices rather than one per variant.
|
|
35
|
+
ActiveRecord::Associations::Preloader.new(records: items.filter_map(&:variant).uniq, associations: :prices).call
|
|
36
|
+
|
|
34
37
|
resolutions = items.filter_map do |line_item|
|
|
35
38
|
price = resolve_price(line_item, cart)
|
|
36
39
|
next if price.blank?
|
|
@@ -270,8 +270,10 @@ module Spree
|
|
|
270
270
|
# describe what they held before the split, and the new one holds nothing
|
|
271
271
|
# at all.
|
|
272
272
|
def restate_freight_summaries(*fulfillments)
|
|
273
|
+
selected_rates = Spree::DeliveryRate.where(fulfillment_id: fulfillments.map(&:id), selected: true).order(:cost).to_a
|
|
274
|
+
|
|
273
275
|
fulfillments.each do |fulfillment|
|
|
274
|
-
rate = fulfillment.
|
|
276
|
+
rate = selected_rates.find { |selected_rate| selected_rate.fulfillment_id == fulfillment.id }
|
|
275
277
|
next if rate.nil?
|
|
276
278
|
|
|
277
279
|
metadata = rate.metadata
|
|
@@ -50,7 +50,8 @@ module Spree
|
|
|
50
50
|
def apply(catalog, product, values)
|
|
51
51
|
minimum = normalize(values[:minimum_order_quantity])
|
|
52
52
|
multiple = normalize(values[:order_multiple])
|
|
53
|
-
|
|
53
|
+
variants = product.variants.to_a
|
|
54
|
+
variant_ids = variants.map(&:id)
|
|
54
55
|
|
|
55
56
|
# Both blank is a deletion: the product falls back to the catalog's
|
|
56
57
|
# own default, which is what an empty pair means on the row.
|
|
@@ -59,8 +60,12 @@ module Spree
|
|
|
59
60
|
return nil
|
|
60
61
|
end
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
existing_rules = catalog.quantity_rules.where(variant_id: variant_ids).index_by(&:variant_id)
|
|
64
|
+
|
|
65
|
+
variants.each do |variant|
|
|
66
|
+
rule = existing_rules[variant.id] || catalog.quantity_rules.build
|
|
67
|
+
# The loaded variant, so the store check reads its product in memory.
|
|
68
|
+
rule.variant = variant
|
|
64
69
|
rule.assign_attributes(minimum_order_quantity: minimum, order_multiple: multiple)
|
|
65
70
|
return rule unless rule.save
|
|
66
71
|
end
|
|
@@ -22,7 +22,7 @@ module Spree
|
|
|
22
22
|
def group_class = Spree::Collection
|
|
23
23
|
|
|
24
24
|
def refresh_group_counters(collection_ids)
|
|
25
|
-
|
|
25
|
+
Spree::Collection.reset_counters_in_bulk(collection_ids, :product_collections)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -44,9 +44,7 @@ module Spree
|
|
|
44
44
|
|
|
45
45
|
# counter caches — bulk insert/delete bypass the ProductCollection counter_cache callbacks
|
|
46
46
|
Spree::Collection.reset_counters(collection.id, :product_collections)
|
|
47
|
-
|
|
48
|
-
existing_product_ids = Spree::Product.where(id: all_product_ids).pluck(:id)
|
|
49
|
-
existing_product_ids.each { |id| Spree::Product.reset_counters(id, :product_collections) }
|
|
47
|
+
Spree::Product.reset_counters_in_bulk(previous_products_ids + product_ids_to_insert, :product_collections)
|
|
50
48
|
|
|
51
49
|
success(collection)
|
|
52
50
|
end
|
|
@@ -22,7 +22,7 @@ module Spree
|
|
|
22
22
|
def group_class = Spree::Collection
|
|
23
23
|
|
|
24
24
|
def refresh_group_counters(collection_ids)
|
|
25
|
-
|
|
25
|
+
Spree::Collection.reset_counters_in_bulk(collection_ids, :product_collections)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -54,8 +54,16 @@ module Spree
|
|
|
54
54
|
@existed_subscription ||= Spree::NewsletterSubscriber.verified.find_by(email: email, store: current_store)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# The account the subscription belongs to: the signed-in customer only
|
|
58
|
+
# when the address is theirs. Linking it to whoever is signed in would
|
|
59
|
+
# hand them another person's subscription — and with it the right to
|
|
60
|
+
# cancel it.
|
|
57
61
|
def known_user
|
|
58
|
-
@known_user ||= current_user
|
|
62
|
+
@known_user ||= if current_user&.email.to_s.casecmp?(email.to_s)
|
|
63
|
+
current_user
|
|
64
|
+
else
|
|
65
|
+
Spree.customer_class.find_by(email: email)
|
|
66
|
+
end
|
|
59
67
|
end
|
|
60
68
|
end
|
|
61
69
|
end
|
|
@@ -82,14 +82,16 @@ module Spree
|
|
|
82
82
|
)
|
|
83
83
|
captured = payment.completed?
|
|
84
84
|
|
|
85
|
+
allocated_order_ids = Spree::PaymentSplit.where(payment_id: payment.id, order_id: orders.map(&:id)).pluck(:order_id).to_set
|
|
86
|
+
|
|
85
87
|
orders.each_with_index do |order, index|
|
|
86
|
-
|
|
87
|
-
next if split.persisted?
|
|
88
|
+
next if allocated_order_ids.include?(order.id)
|
|
88
89
|
|
|
89
90
|
share = Spree::Money::Rounding.from_minor_units(shares[index], currency)
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
|
|
91
|
+
# The payment and order are handed over rather than looked up, so
|
|
92
|
+
# validating and re-summing each share reuses these instances
|
|
93
|
+
# instead of loading a copy per share.
|
|
94
|
+
split = Spree::PaymentSplit.new(payment: payment, order: order)
|
|
93
95
|
split.currency = payment.currency
|
|
94
96
|
split.authorized_amount = share
|
|
95
97
|
split.captured_amount = captured ? share : 0
|
|
@@ -14,6 +14,7 @@ module Spree
|
|
|
14
14
|
|
|
15
15
|
items_param = @params.delete(:items)
|
|
16
16
|
address_params = extract_address_params
|
|
17
|
+
scope_store_references
|
|
17
18
|
|
|
18
19
|
# requires_new: the rescue below sits outside the block, so joining a
|
|
19
20
|
# caller's open transaction (the API's order lock) would let it commit
|
|
@@ -63,6 +64,24 @@ module Spree
|
|
|
63
64
|
end.compact_blank
|
|
64
65
|
end
|
|
65
66
|
|
|
67
|
+
# Named by id in the payload and read through the order's own store, as
|
|
68
|
+
# Orders::Create does: the model only checks that the record exists, so
|
|
69
|
+
# another store's market, channel or warehouse would otherwise be
|
|
70
|
+
# accepted.
|
|
71
|
+
STORE_REFERENCES = {
|
|
72
|
+
market_id: :markets,
|
|
73
|
+
channel_id: :channels,
|
|
74
|
+
preferred_stock_location_id: :stock_locations
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
def scope_store_references
|
|
78
|
+
STORE_REFERENCES.each do |key, association|
|
|
79
|
+
next if @params[key].blank?
|
|
80
|
+
|
|
81
|
+
@params[key] = @order.store.public_send(association).find_by_param!(@params[key]).id
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
66
85
|
def assign_addresses(address_params)
|
|
67
86
|
address_params.each do |association, attributes|
|
|
68
87
|
@order.public_send(:"#{association}_attributes=", attributes)
|
|
@@ -7,9 +7,15 @@ module Spree
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def add_memberships(groups, products)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
group_ids = groups.pluck(:id)
|
|
11
|
+
product_ids = products.pluck(:id)
|
|
12
|
+
# Every group's current size in one grouped count, rather than a probe
|
|
13
|
+
# per group for the position to append at.
|
|
14
|
+
sizes = membership_class.where(group_key => group_ids).group(group_key).count
|
|
15
|
+
|
|
16
|
+
rows = group_ids.flat_map do |group_id|
|
|
17
|
+
position = sizes.fetch(group_id, 0)
|
|
18
|
+
product_ids.map do |product_id|
|
|
13
19
|
{
|
|
14
20
|
group_key => group_id, product_id: product_id, position: (position += 1),
|
|
15
21
|
created_at: Time.current, updated_at: Time.current
|
|
@@ -18,7 +24,7 @@ module Spree
|
|
|
18
24
|
end
|
|
19
25
|
membership_class.insert_all(rows)
|
|
20
26
|
|
|
21
|
-
refresh_after_membership_change(
|
|
27
|
+
refresh_after_membership_change(group_ids, products)
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
# Removes the products, then re-packs the remaining positions from 1.
|
|
@@ -28,8 +34,12 @@ module Spree
|
|
|
28
34
|
ApplicationRecord.transaction do
|
|
29
35
|
membership_class.where(group_key => group_ids, product_id: products.pluck(:id)).delete_all
|
|
30
36
|
|
|
37
|
+
# What survived the delete, for every group in one read.
|
|
38
|
+
remaining = membership_class.where(group_key => group_ids).order(:position).pluck(group_key, :product_id).
|
|
39
|
+
group_by(&:first).transform_values { |pairs| pairs.map(&:last) }
|
|
40
|
+
|
|
31
41
|
rows = group_ids.flat_map do |group_id|
|
|
32
|
-
|
|
42
|
+
remaining.fetch(group_id, []).map.with_index(1) do |product_id, position|
|
|
33
43
|
{
|
|
34
44
|
group_key => group_id, product_id: product_id, position: position,
|
|
35
45
|
created_at: Time.current, updated_at: Time.current
|
|
@@ -46,7 +56,7 @@ module Spree
|
|
|
46
56
|
# search index are refreshed here.
|
|
47
57
|
def refresh_after_membership_change(group_ids, products)
|
|
48
58
|
product_ids = products.pluck(:id)
|
|
49
|
-
|
|
59
|
+
Spree::Product.reset_counters_in_bulk(product_ids, membership_class.model_name.element.pluralize.to_sym)
|
|
50
60
|
refresh_group_counters(group_ids)
|
|
51
61
|
|
|
52
62
|
Spree::Product.where(id: product_ids).touch_all
|
|
@@ -42,7 +42,7 @@ module Spree
|
|
|
42
42
|
|
|
43
43
|
def duplicate_variant(variant, include_images)
|
|
44
44
|
new_variant = variant.dup
|
|
45
|
-
new_variant.sku = sku_generator(
|
|
45
|
+
new_variant.sku = sku_generator(variant)
|
|
46
46
|
new_variant.deleted_at = nil
|
|
47
47
|
new_variant.option_values = variant.option_values.map { |option_value| option_value }
|
|
48
48
|
new_variant.prices = duplicate_prices(variant.prices)
|
|
@@ -82,10 +82,18 @@ module Spree
|
|
|
82
82
|
new_image
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
def sku_generator(
|
|
86
|
-
return '' if sku.blank?
|
|
85
|
+
def sku_generator(variant)
|
|
86
|
+
return '' if variant.sku.blank?
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
store_product_ids = Product.with_deleted.where(store_id: variant.store_id).select(:id)
|
|
89
|
+
# Matched the way the SKU uniqueness check compares, without regard
|
|
90
|
+
# to case, and within the same seller scope it reads, so the name taken
|
|
91
|
+
# from the latest match is one this seller may use.
|
|
92
|
+
sku_pattern = Variant.arel_table[:sku].matches("%#{Variant.sanitize_sql_like(variant.sku)}", '\\', false)
|
|
93
|
+
latest_match = Variant.with_deleted.for_seller(variant.resolved_seller_id).
|
|
94
|
+
where(product_id: store_product_ids).where(sku_pattern).order(:created_at, :id).last
|
|
95
|
+
|
|
96
|
+
"COPY OF #{(latest_match || variant).sku}"
|
|
89
97
|
end
|
|
90
98
|
|
|
91
99
|
def duplicate_error_message(new_product)
|
|
@@ -15,13 +15,16 @@ module Spree
|
|
|
15
15
|
ORDERED_FILTER_PREDICATES = (TEXT_FILTER_PREDICATES | NUMBER_FILTER_PREDICATES).
|
|
16
16
|
sort_by { |predicate| -predicate.length }.freeze
|
|
17
17
|
|
|
18
|
-
attr_reader :store
|
|
18
|
+
attr_reader :store, :audience
|
|
19
19
|
|
|
20
20
|
# @param store [Spree::Store] the store whose custom-field schema this
|
|
21
21
|
# describes — definitions are store-owned, so two stores have their own
|
|
22
22
|
# filter vocabularies.
|
|
23
|
-
|
|
23
|
+
# @param audience [Symbol, nil] who filters: `:store` (shoppers) sees only
|
|
24
|
+
# storefront-visible definitions; `nil` is the back office
|
|
25
|
+
def initialize(store, audience: nil)
|
|
24
26
|
@store = store
|
|
27
|
+
@audience = audience
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
# @return [Hash{String => Spree::CustomFieldDefinition}]
|
|
@@ -134,8 +137,13 @@ module Spree
|
|
|
134
137
|
|
|
135
138
|
private
|
|
136
139
|
|
|
140
|
+
# A definition hidden from the storefront stays out of its filter and
|
|
141
|
+
# sort vocabulary too: the serializer withholds the value, and a sort or
|
|
142
|
+
# a range filter on it would hand that value back one comparison at a
|
|
143
|
+
# time.
|
|
137
144
|
def product_definitions
|
|
138
145
|
scope = store.custom_field_definitions.for_resource_type('Spree::Product')
|
|
146
|
+
scope = scope.storefront_visible if audience == :store
|
|
139
147
|
scope.where(searchable: true).or(scope.where(sortable: true))
|
|
140
148
|
end
|
|
141
149
|
|
|
@@ -32,6 +32,11 @@ module Spree
|
|
|
32
32
|
return failure(stock_level, I18n.t('spree.stock_level.errors.count_on_hand_not_an_integer')) if count_on_hand.present? && target.nil?
|
|
33
33
|
return failure(stock_level, I18n.t('spree.stock_level.errors.adjustment_not_an_integer')) if adjustment.present? && delta.nil?
|
|
34
34
|
|
|
35
|
+
# Read before locking, which reloads the level and with it drops the
|
|
36
|
+
# variant and location a bulk caller loaded for every row at once.
|
|
37
|
+
stock_location = stock_level.stock_location
|
|
38
|
+
variant = stock_level.variant
|
|
39
|
+
|
|
35
40
|
# Locked around the read: the delta is worked out from the count this
|
|
36
41
|
# caller first saw, so two admins correcting the same level at once
|
|
37
42
|
# would otherwise each measure against a shelf the other had already
|
|
@@ -42,12 +47,14 @@ module Spree
|
|
|
42
47
|
|
|
43
48
|
# Through the location's own mover rather than building a movement
|
|
44
49
|
# by hand: it types the row as an adjustment and records the reason,
|
|
45
|
-
# so a correction reads like every other change in the history.
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
# so a correction reads like every other change in the history. The
|
|
51
|
+
# movement writes through this locked instance, which leaves it
|
|
52
|
+
# current without another reload.
|
|
53
|
+
stock_location.adjust(variant, move, reason: Spree::StockMovement.adjustment_reason_text(reason),
|
|
54
|
+
stock_level: stock_level)
|
|
48
55
|
end
|
|
49
56
|
|
|
50
|
-
success(stock_level
|
|
57
|
+
success(stock_level)
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
private
|
|
@@ -69,7 +69,7 @@ module Spree
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
71
|
def build_targets(cart)
|
|
72
|
-
cart.line_items.includes(variant: { stock_levels: :stock_location }).filter_map do |line_item|
|
|
72
|
+
cart.line_items.includes(variant: [:product, { stock_levels: :stock_location }]).filter_map do |line_item|
|
|
73
73
|
variant = line_item.variant
|
|
74
74
|
next unless variant&.should_track_inventory?
|
|
75
75
|
|
|
@@ -31,13 +31,17 @@ module Spree
|
|
|
31
31
|
@currency = resolve_currency(currency, country)
|
|
32
32
|
@locale = resolve_locale(locale, country)
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
# Defaults are bookkeeping, not merchant actions — webhooks and
|
|
35
|
+
# subscribers should not hear about each zone and method created here.
|
|
36
|
+
Spree::Events.disable do
|
|
37
|
+
ApplicationRecord.transaction do
|
|
38
|
+
update_default_market
|
|
39
|
+
provision_stock_location
|
|
40
|
+
provision_delivery_zones
|
|
41
|
+
provision_package_type
|
|
42
|
+
provision_pickup
|
|
43
|
+
restate_seeded_calculators
|
|
44
|
+
end
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
store.reload
|
|
@@ -10,8 +10,15 @@ module Spree
|
|
|
10
10
|
# @param context [String] default: 'tags'
|
|
11
11
|
# @return [Spree::ServiceModule::Base::Result]
|
|
12
12
|
def call(tag_names: [], records: [], context: 'tags')
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
names = tag_names.map(&:strip).reject(&:blank?).uniq
|
|
14
|
+
return if names.empty?
|
|
15
|
+
|
|
16
|
+
# One read for the names that already exist, then a create only for
|
|
17
|
+
# the ones that do not. `find_or_create_by` per name is a query per
|
|
18
|
+
# tag, which a bulk tagging pays on every name in the payload.
|
|
19
|
+
existing = ActsAsTaggableOn::Tag.where(name: names).index_by(&:name)
|
|
20
|
+
tags = names.map do |name|
|
|
21
|
+
existing[name] || ActsAsTaggableOn::Tag.create_or_find_by(name: name)
|
|
15
22
|
end
|
|
16
23
|
|
|
17
24
|
record_class = records.first.class
|
|
@@ -27,17 +34,20 @@ module Spree
|
|
|
27
34
|
|
|
28
35
|
private
|
|
29
36
|
|
|
37
|
+
# Mirrors the gem's own tagging save, which stores the taggable's tenant
|
|
38
|
+
# (its `store_id` for store-owned records) as a string on each tagging.
|
|
30
39
|
def taggings_attributes(tags, records, context:, record_class:)
|
|
31
|
-
records.
|
|
40
|
+
records.flat_map do |record|
|
|
32
41
|
tags.map do |tag|
|
|
33
42
|
{
|
|
34
|
-
taggable_id:
|
|
43
|
+
taggable_id: record.id,
|
|
35
44
|
taggable_type: record_class.to_s,
|
|
36
45
|
context: context,
|
|
37
|
-
tag_id: tag.id
|
|
46
|
+
tag_id: tag.id,
|
|
47
|
+
tenant: record.taggable_tenant&.to_s
|
|
38
48
|
}
|
|
39
49
|
end
|
|
40
|
-
end
|
|
50
|
+
end
|
|
41
51
|
end
|
|
42
52
|
end
|
|
43
53
|
end
|