spree_core 6.0.0.rc4 → 6.0.0.rc6
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 +114 -36
- 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/concerns/spree/webhook_payload_redaction.rb +1 -0
- data/app/models/spree/ability.rb +8 -0
- data/app/models/spree/address.rb +6 -0
- 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 +7 -7
- data/app/models/spree/category.rb +76 -5
- data/app/models/spree/channel.rb +79 -40
- data/app/models/spree/company.rb +46 -3
- data/app/models/spree/customer.rb +1 -1
- data/app/models/spree/customer_group.rb +59 -41
- data/app/models/spree/data_request.rb +34 -1
- 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.rb +1 -1
- 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/sales_context.rb +162 -0
- 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/tax_line.rb +5 -4
- data/app/models/spree/variant.rb +23 -6
- data/app/models/spree/webhook_endpoint.rb +45 -11
- data/app/services/spree/carts/destroy.rb +7 -2
- 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/carts/update.rb +1 -4
- 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/data_requests/confirm.rb +37 -0
- data/app/services/spree/data_requests/create.rb +61 -13
- 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 +40 -19
- data/app/workflows/spree/data_requests/fulfill.rb +20 -6
- 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/orders/create.rb +41 -17
- 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 +14 -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/engine.rb +2 -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/data_request_factory.rb +4 -0
- 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 +15 -6
|
@@ -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
|
|
@@ -52,9 +52,14 @@ module Spree
|
|
|
52
52
|
success(cart: cart)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Lets go of everything the cart was only borrowing, so the cascade that
|
|
56
|
+
# follows reaches nothing but the cart's own rows. An entry in somebody's
|
|
57
|
+
# address book stays in it, and a row a fulfillment or a completed order
|
|
58
|
+
# still points at is detached rather than soft-deleted out from under
|
|
59
|
+
# them.
|
|
55
60
|
def clear_addresses(cart:)
|
|
56
|
-
cart.ship_address = nil unless cart.ship_address
|
|
57
|
-
cart.bill_address = nil unless cart.bill_address
|
|
61
|
+
cart.ship_address = nil unless cart.owns_address?(cart.ship_address) && cart.ship_address.can_be_deleted?
|
|
62
|
+
cart.bill_address = nil unless cart.owns_address?(cart.bill_address) && cart.bill_address.can_be_deleted?
|
|
58
63
|
|
|
59
64
|
success(cart: cart)
|
|
60
65
|
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
|
|
@@ -206,10 +206,7 @@ module Spree
|
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
def resolve_address_id(prefixed_id)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
decoded = Spree::Address.decode_prefixed_id(prefixed_id)
|
|
212
|
-
decoded ? cart.customer.addresses.find_by(id: decoded)&.id : nil
|
|
209
|
+
cart.selectable_address(prefixed_id)&.id
|
|
213
210
|
end
|
|
214
211
|
|
|
215
212
|
def assign_market
|
|
@@ -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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module DataRequests
|
|
3
|
+
# Confirms a request the customer raised, from the link emailed to the
|
|
4
|
+
# address it names, and queues the work.
|
|
5
|
+
#
|
|
6
|
+
# Confirming again is answered with the request as it stands, so a second
|
|
7
|
+
# click on the same link changes nothing.
|
|
8
|
+
class Confirm
|
|
9
|
+
prepend Spree::ServiceModule::Base
|
|
10
|
+
|
|
11
|
+
# @param data_request [Spree::DataRequest]
|
|
12
|
+
# @return [Spree::ServiceModule::Result] the confirmed request
|
|
13
|
+
def call(data_request:)
|
|
14
|
+
newly_confirmed = false
|
|
15
|
+
|
|
16
|
+
data_request.with_lock do
|
|
17
|
+
newly_confirmed = data_request.confirmable?
|
|
18
|
+
|
|
19
|
+
if newly_confirmed
|
|
20
|
+
data_request.update!(status: 'pending')
|
|
21
|
+
elsif data_request.awaiting_confirmation?
|
|
22
|
+
data_request.update!(status: 'lapsed')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if data_request.lapsed?
|
|
27
|
+
data_request.errors.add(:base, I18n.t('spree.data_request_errors.confirmation_invalid'))
|
|
28
|
+
return failure(data_request)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Spree::DataRequests::ProcessJob.perform_later(data_request.prefixed_id) if newly_confirmed
|
|
32
|
+
|
|
33
|
+
success(data_request)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -7,9 +7,15 @@ module Spree
|
|
|
7
7
|
# whole history, and a customer holding the button down should not be able
|
|
8
8
|
# to queue a hundred of them. It is also the honest answer — the request
|
|
9
9
|
# they already made is still being worked on.
|
|
10
|
+
#
|
|
11
|
+
# A request the customer raised themselves waits for confirmation from its
|
|
12
|
+
# email address instead of being queued, because it reaches guest orders
|
|
13
|
+
# placed at that address. Asking again while it waits sends the email again.
|
|
10
14
|
class Create
|
|
11
15
|
prepend Spree::ServiceModule::Base
|
|
12
16
|
|
|
17
|
+
CONFIRMATION_EVENT = 'data_request.confirmation_requested'.freeze
|
|
18
|
+
|
|
13
19
|
# @param store [Spree::Store]
|
|
14
20
|
# @param customer [Spree::Customer]
|
|
15
21
|
# @param kind [String] Spree::DataRequest::ACCESS or ERASURE
|
|
@@ -19,8 +25,12 @@ module Spree
|
|
|
19
25
|
# caller fulfils the request itself — the admin export renders the file
|
|
20
26
|
# inline for a waiting staffer, and queueing would additionally email
|
|
21
27
|
# the customer a copy of something they did not ask for
|
|
28
|
+
# @param confirm_by_email [Boolean] hold the request until it is
|
|
29
|
+
# confirmed from its email address; true for the customer's own requests
|
|
30
|
+
# @param redirect_url [String, nil] the storefront page the confirmation
|
|
31
|
+
# link opens, already checked against the store's allowed origins
|
|
22
32
|
# @return [Spree::ServiceModule::Result]
|
|
23
|
-
def call(store:, customer:, kind:, requested_by: nil, process: true)
|
|
33
|
+
def call(store:, customer:, kind:, requested_by: nil, process: true, confirm_by_email: false, redirect_url: nil)
|
|
24
34
|
# Nothing useful is left to answer with: an export would return the
|
|
25
35
|
# tombstone and email a link to an address that no longer receives,
|
|
26
36
|
# and a second erasure has nothing to erase.
|
|
@@ -38,7 +48,9 @@ module Spree
|
|
|
38
48
|
# of the same person's whole history; taking the lock closes it without
|
|
39
49
|
# a partial unique index, which MySQL does not support.
|
|
40
50
|
customer.with_lock do
|
|
41
|
-
|
|
51
|
+
lapse_unconfirmed_requests(store: store, customer: customer, kind: kind, staff_request: !confirm_by_email)
|
|
52
|
+
existing = in_flight_request(store: store, customer: customer, kind: kind,
|
|
53
|
+
include_unconfirmed: confirm_by_email)
|
|
42
54
|
|
|
43
55
|
unless existing
|
|
44
56
|
data_request = Spree::DataRequest.new(
|
|
@@ -46,20 +58,27 @@ module Spree
|
|
|
46
58
|
customer: customer,
|
|
47
59
|
kind: kind.to_s,
|
|
48
60
|
email: customer.email,
|
|
49
|
-
requested_by: requested_by
|
|
61
|
+
requested_by: requested_by,
|
|
62
|
+
status: confirm_by_email ? 'awaiting_confirmation' : 'pending'
|
|
50
63
|
)
|
|
51
64
|
data_request.save
|
|
52
65
|
end
|
|
53
66
|
end
|
|
54
67
|
|
|
55
|
-
return
|
|
56
|
-
return failure(data_request) unless data_request.persisted?
|
|
68
|
+
return failure(data_request) if data_request && !data_request.persisted?
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
# and it should not contend for the row this flow still holds.
|
|
60
|
-
Spree::DataRequests::ProcessJob.perform_later(data_request.prefixed_id) if process
|
|
70
|
+
request = existing || data_request
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
# Both happen after the lock is released: the job may start at once and
|
|
73
|
+
# should not contend for the row this flow holds, and the email must
|
|
74
|
+
# not go out before the row is committed.
|
|
75
|
+
if request.awaiting_confirmation?
|
|
76
|
+
request_confirmation(request, redirect_url: redirect_url)
|
|
77
|
+
elsif data_request && process
|
|
78
|
+
Spree::DataRequests::ProcessJob.perform_later(data_request.prefixed_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
success(request)
|
|
63
82
|
end
|
|
64
83
|
|
|
65
84
|
private
|
|
@@ -67,10 +86,39 @@ module Spree
|
|
|
67
86
|
# Only the person's own requests. A staff member exporting from the admin
|
|
68
87
|
# opens their own row and answers it inline, and handing that back here
|
|
69
88
|
# would close the customer's request with a file they can never download.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
# A request still waiting for confirmation is returned only to the
|
|
90
|
+
# customer: staff have checked who they are dealing with, so theirs
|
|
91
|
+
# proceeds on its own row (see lapse_unconfirmed_requests).
|
|
92
|
+
def in_flight_request(store:, customer:, kind:, include_unconfirmed:)
|
|
93
|
+
requests = Spree::DataRequest.where(store_id: store.id, customer_id: customer.id, kind: kind.to_s,
|
|
94
|
+
requested_by_id: nil)
|
|
95
|
+
open_requests = include_unconfirmed ? requests.in_progress.or(requests.confirmable) : requests.in_progress
|
|
96
|
+
|
|
97
|
+
open_requests.recent_first.first
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Lapses what can no longer be confirmed: requests past the window, and
|
|
101
|
+
# requests naming an address the account has since moved away from, so
|
|
102
|
+
# asking again emails the current address. A staff request of the same
|
|
103
|
+
# kind replaces the customer's unconfirmed one, which would otherwise
|
|
104
|
+
# run the same work a second time once confirmed.
|
|
105
|
+
def lapse_unconfirmed_requests(store:, customer:, kind:, staff_request:)
|
|
106
|
+
unconfirmed = Spree::DataRequest.where(store_id: store.id, customer_id: customer.id).awaiting_confirmation
|
|
107
|
+
stale = unconfirmed.past_confirmation_window.or(unconfirmed.where.not(email: customer.email))
|
|
108
|
+
stale = stale.or(unconfirmed.where(kind: kind.to_s)) if staff_request
|
|
109
|
+
|
|
110
|
+
stale.find_each { |data_request| data_request.update!(status: 'lapsed') }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The token travels in a credential event, which reaches only webhook
|
|
114
|
+
# endpoints that name it and keeps the token out of the delivery log.
|
|
115
|
+
def request_confirmation(data_request, redirect_url:)
|
|
116
|
+
payload = data_request.event_payload_for(
|
|
117
|
+
CONFIRMATION_EVENT,
|
|
118
|
+
confirmation_token: data_request.generate_token_for(:confirmation),
|
|
119
|
+
redirect_url: redirect_url.presence
|
|
120
|
+
)
|
|
121
|
+
data_request.publish_event(CONFIRMATION_EVENT, payload)
|
|
74
122
|
end
|
|
75
123
|
end
|
|
76
124
|
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
|