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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 820fa04cc9e7d880ff2a324afaf5fa2dce0f197c809b7d0af3aa59b69b65f8b6
|
|
4
|
+
data.tar.gz: be3a6fac21d4524551c479cd02b8256f34ee9957e838f382bd37555fa40e9689
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8941ff574c4ba3455b1e67d90d500a61180d2567e6c5f5fa74e3db43aefb62d79d720753565a0ef4c29a6628f9e02a92385b6af7345548c8074815f6d212fa17
|
|
7
|
+
data.tar.gz: 9e0ac49d08274a0cc82a1aea9fb8ea74bfb093b49d276401e62e34ef57b0fee0e30b3f814d465718d2d758ab855d35a36f16b31b281245a377e7d6ced174af9f
|
|
@@ -75,6 +75,7 @@ module Spree
|
|
|
75
75
|
|
|
76
76
|
def download_and_attach_image(external_url, image)
|
|
77
77
|
max_size = Spree::Config.max_image_download_size
|
|
78
|
+
streamed_body = nil
|
|
78
79
|
|
|
79
80
|
response = SsrfFilter.get(
|
|
80
81
|
external_url,
|
|
@@ -90,12 +91,12 @@ module Spree
|
|
|
90
91
|
read_timeout: 60,
|
|
91
92
|
open_timeout: 30
|
|
92
93
|
}
|
|
93
|
-
)
|
|
94
|
+
) { |fetched| streamed_body = read_within(fetched, max_size) }
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
# A fetch that did not stream (a test double, say) is read whole and
|
|
97
|
+
# held to the same limit.
|
|
98
|
+
body = streamed_body || response.body
|
|
99
|
+
raise_too_large(max_size) if body.bytesize > max_size
|
|
99
100
|
|
|
100
101
|
uri = URI.parse(external_url)
|
|
101
102
|
filename = File.basename(uri.path)
|
|
@@ -114,6 +115,24 @@ module Spree
|
|
|
114
115
|
end
|
|
115
116
|
end
|
|
116
117
|
|
|
118
|
+
# Reads the body as it arrives and stops at the limit, so an oversized
|
|
119
|
+
# or endless response is refused mid-stream rather than held in memory
|
|
120
|
+
# first; a declared length over the limit is refused before any of it.
|
|
121
|
+
def read_within(response, max_size)
|
|
122
|
+
raise_too_large(max_size) if response['Content-Length'].to_i > max_size
|
|
123
|
+
|
|
124
|
+
buffer = +''
|
|
125
|
+
response.read_body do |chunk|
|
|
126
|
+
buffer << chunk
|
|
127
|
+
raise_too_large(max_size) if buffer.bytesize > max_size
|
|
128
|
+
end
|
|
129
|
+
buffer
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def raise_too_large(max_size)
|
|
133
|
+
raise StandardError, "Image file size exceeds the maximum allowed size of #{max_size} bytes"
|
|
134
|
+
end
|
|
135
|
+
|
|
117
136
|
def image_already_saved?(image, external_url)
|
|
118
137
|
image.persisted? && image.attachment.attached? && image.external_url.present? && external_url == image.external_url
|
|
119
138
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Switches a staff account off without deleting it. Deleting a staff member
|
|
3
|
+
# clears them from every record that names them as its actor, so the history
|
|
4
|
+
# of who approved, refunded or created what is lost; deactivating keeps the
|
|
5
|
+
# account and refuses every way back in.
|
|
6
|
+
#
|
|
7
|
+
# Enforcement lives in the API: a deactivated account cannot sign in by any
|
|
8
|
+
# strategy, accept an invitation, finish a password reset or refresh a
|
|
9
|
+
# session, and its access tokens stop working on the next request. Including
|
|
10
|
+
# this concern opts a model in; the checks are duck-typed, so a custom admin
|
|
11
|
+
# user class without it is never refused.
|
|
12
|
+
#
|
|
13
|
+
# Secret API keys are left alone: they belong to the store, not to the person
|
|
14
|
+
# who created them, and revoking them would break the store's integrations.
|
|
15
|
+
module AccountDeactivation
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def deactivated?
|
|
18
|
+
deactivated_at.present?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Deactivates the account and ends every session it holds: refresh tokens
|
|
22
|
+
# are deleted, and OAuth tokens and grants issued to apps on its behalf are
|
|
23
|
+
# revoked. Access tokens already issued are refused from the next request.
|
|
24
|
+
#
|
|
25
|
+
# Safe to call again. The first deactivation time is kept, but sessions
|
|
26
|
+
# are ended every time, so an account whose column was set some other way
|
|
27
|
+
# still loses the credentials it holds.
|
|
28
|
+
#
|
|
29
|
+
# Skips validations: switching an account off must not fail because some
|
|
30
|
+
# unrelated attribute no longer passes today's rules.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def deactivate!
|
|
34
|
+
now = Time.current
|
|
35
|
+
transaction do
|
|
36
|
+
update_columns(deactivated_at: now, updated_at: now) unless deactivated?
|
|
37
|
+
Spree::RefreshToken.revoke_all_for(self)
|
|
38
|
+
oauth_owner = { resource_owner_type: self.class.polymorphic_name, resource_owner_id: id, revoked_at: nil }
|
|
39
|
+
Spree::OauthAccessToken.where(oauth_owner).update_all(revoked_at: now)
|
|
40
|
+
Spree::OauthAccessGrant.where(oauth_owner).update_all(revoked_at: now)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Lets the account sign in again. Sessions ended by deactivation stay
|
|
45
|
+
# ended; the person signs in afresh.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
def reactivate!
|
|
49
|
+
return unless deactivated?
|
|
50
|
+
|
|
51
|
+
update_columns(deactivated_at: nil, updated_at: Time.current)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -6,9 +6,12 @@ module Spree
|
|
|
6
6
|
# currency directly, so one method serves every currency it has an
|
|
7
7
|
# amount for — no more method-per-currency duplication.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Once the hash holds an amount it alone decides, and a currency it leaves
|
|
10
|
+
# out is not offered. The 5.x single `amount` + `currency` pair quotes its
|
|
11
|
+
# own currency only while the hash is empty, so upgraded stores quote
|
|
12
|
+
# unchanged until they fill it in. The pair's untouched default (0 in the
|
|
13
|
+
# store's currency) can never be read as a price someone set beside the
|
|
14
|
+
# hash.
|
|
12
15
|
module CurrencyAmounts
|
|
13
16
|
extend ActiveSupport::Concern
|
|
14
17
|
|
|
@@ -17,14 +20,15 @@ module Spree
|
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
# The amount this calculator quotes for a currency: the per-currency
|
|
20
|
-
# hash
|
|
23
|
+
# hash when it holds any amount, otherwise the legacy single amount for
|
|
24
|
+
# its own currency.
|
|
21
25
|
#
|
|
22
26
|
# @param currency [String]
|
|
23
27
|
# @return [BigDecimal, nil] nil when the currency has no amount — the
|
|
24
28
|
# method is not offered for that cart
|
|
25
29
|
def amount_for(currency)
|
|
26
|
-
|
|
27
|
-
return
|
|
30
|
+
amounts = currency_amounts
|
|
31
|
+
return amounts[currency.to_s.upcase] if amounts.any?
|
|
28
32
|
|
|
29
33
|
legacy_currency = respond_to?(:preferred_currency) ? preferred_currency : nil
|
|
30
34
|
return preferred_amount if legacy_currency.blank? || legacy_currency.casecmp(currency.to_s).zero?
|
|
@@ -38,6 +42,15 @@ module Spree
|
|
|
38
42
|
def supports_currency?(currency)
|
|
39
43
|
amount_for(currency).present?
|
|
40
44
|
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# The hash's amounts by upper-case currency, without blank entries.
|
|
49
|
+
def currency_amounts
|
|
50
|
+
(preferred_amounts || {}).each_with_object({}) do |(key, value), amounts|
|
|
51
|
+
amounts[key.to_s.upcase] = BigDecimal(value.to_s) if value.present?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
41
54
|
end
|
|
42
55
|
end
|
|
43
56
|
end
|
|
@@ -43,8 +43,31 @@ module Spree
|
|
|
43
43
|
# @return [Spree::ExternalReference, nil] nil when the reference was removed
|
|
44
44
|
def set_external_id(system, external_id, metadata: nil)
|
|
45
45
|
key = Spree::ExternalReference.normalize_system(system)
|
|
46
|
-
|
|
46
|
+
write_external_reference(key, external_references.for_system(key).first, external_id, metadata)
|
|
47
|
+
end
|
|
47
48
|
|
|
49
|
+
# Writes several references at once, leaving systems the payload does not
|
|
50
|
+
# mention untouched — a connector states its own key without disturbing
|
|
51
|
+
# another system's. Accepts the `{ system => external_id }` map the admin
|
|
52
|
+
# serializers render, or a list of `{ system:, external_id: }` entries.
|
|
53
|
+
#
|
|
54
|
+
# @param references [Hash{String,Symbol=>String}, Array<Hash>]
|
|
55
|
+
# @return [void]
|
|
56
|
+
def assign_external_references(references)
|
|
57
|
+
entries = Spree::ExternalReference.normalize_references(references)
|
|
58
|
+
return if entries.empty?
|
|
59
|
+
|
|
60
|
+
keys = entries.map { |entry| Spree::ExternalReference.normalize_system(entry[:system]) }
|
|
61
|
+
existing = external_references.where(system: keys).order(:id).group_by(&:system).transform_values(&:first)
|
|
62
|
+
|
|
63
|
+
entries.zip(keys).each do |entry, key|
|
|
64
|
+
existing[key] = write_external_reference(key, existing[key], entry[:external_id], entry[:metadata])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def write_external_reference(key, reference, external_id, metadata)
|
|
48
71
|
if external_id.to_s.strip.blank?
|
|
49
72
|
reference&.destroy
|
|
50
73
|
external_references.reset
|
|
@@ -64,21 +87,6 @@ module Spree
|
|
|
64
87
|
reference
|
|
65
88
|
end
|
|
66
89
|
|
|
67
|
-
# Writes several references at once, leaving systems the payload does not
|
|
68
|
-
# mention untouched — a connector states its own key without disturbing
|
|
69
|
-
# another system's. Accepts the `{ system => external_id }` map the admin
|
|
70
|
-
# serializers render, or a list of `{ system:, external_id: }` entries.
|
|
71
|
-
#
|
|
72
|
-
# @param references [Hash{String,Symbol=>String}, Array<Hash>]
|
|
73
|
-
# @return [void]
|
|
74
|
-
def assign_external_references(references)
|
|
75
|
-
Spree::ExternalReference.normalize_references(references).each do |reference|
|
|
76
|
-
set_external_id(reference[:system], reference[:external_id], metadata: reference[:metadata])
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
82
90
|
# A reference belongs to the same store as the record it identifies.
|
|
83
91
|
#
|
|
84
92
|
# Store-owning models answer directly;
|
|
@@ -123,12 +123,22 @@ module Spree
|
|
|
123
123
|
|
|
124
124
|
return values unless klass
|
|
125
125
|
|
|
126
|
+
# A model with a lookup of its own (Order) resolves each id through it.
|
|
127
|
+
unless klass.method(:find_by_param).owner == Spree::PrefixedId::ClassMethods
|
|
128
|
+
return values.map { |v| Spree::PrefixedId.prefixed_id?(v) ? klass.find_by_param!(v).id : v }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# One lookup for the whole list rather than one per id.
|
|
132
|
+
decoded = values.select { |v| Spree::PrefixedId.prefixed_id?(v) }.index_with { |v| klass.decode_own_prefixed_id(v) }
|
|
133
|
+
existing_ids = decoded.empty? ? [] : klass.where(id: decoded.values.compact).pluck(:id)
|
|
134
|
+
|
|
126
135
|
values.map do |v|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
next v unless decoded.key?(v)
|
|
137
|
+
|
|
138
|
+
id = decoded[v]
|
|
139
|
+
raise ActiveRecord::RecordNotFound.new("Couldn't find #{klass.name} with param=#{v}", klass.name) unless id && existing_ids.include?(id)
|
|
140
|
+
|
|
141
|
+
id
|
|
132
142
|
end
|
|
133
143
|
end
|
|
134
144
|
|
|
@@ -91,9 +91,14 @@ module Spree
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
if events.include?(:delete)
|
|
94
|
-
before_destroy :capture_pre_destroy_payload, if: :
|
|
94
|
+
before_destroy :capture_pre_destroy_payload, if: :lifecycle_publishing_enabled?
|
|
95
95
|
after_commit :publish_delete_event, on: :destroy, if: :should_publish_events?
|
|
96
96
|
end
|
|
97
|
+
|
|
98
|
+
after_save :note_lifecycle_write
|
|
99
|
+
after_destroy :note_lifecycle_destroy
|
|
100
|
+
after_commit :close_lifecycle_write
|
|
101
|
+
after_rollback :close_lifecycle_write
|
|
97
102
|
end
|
|
98
103
|
|
|
99
104
|
# A subclass publishes its lifecycle events under its own prefix, which may
|
|
@@ -308,9 +313,50 @@ module Spree
|
|
|
308
313
|
end
|
|
309
314
|
|
|
310
315
|
def should_publish_events?
|
|
316
|
+
!silenced_lifecycle_write? && lifecycle_publishing_enabled?
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def lifecycle_publishing_enabled?
|
|
311
320
|
self.class.publish_events && Spree::Events.lifecycle_enabled?
|
|
312
321
|
end
|
|
313
322
|
|
|
323
|
+
# Lifecycle events publish at commit, which can come after the block that
|
|
324
|
+
# silenced the write ended — a default record created in its parent's
|
|
325
|
+
# `after_create` commits together with the parent. Each write records
|
|
326
|
+
# whether events were on, so a record written only while they were off
|
|
327
|
+
# stays silent at commit; one written with them on still publishes.
|
|
328
|
+
#
|
|
329
|
+
# The marker is read, never cleared, at commit: Rails may check several
|
|
330
|
+
# commit callbacks for one commit (a soft delete is also an update), and
|
|
331
|
+
# the order commit callbacks run in is the host app's setting. A commit or
|
|
332
|
+
# rollback only closes it, and the next write starts a fresh one.
|
|
333
|
+
def note_lifecycle_write
|
|
334
|
+
reopen_lifecycle_write
|
|
335
|
+
@_spree_lifecycle_write_enabled ||= Spree::Events.lifecycle_enabled?
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# A destroy commits as the delete event alone, so an earlier update made
|
|
339
|
+
# with events on must not carry it through.
|
|
340
|
+
def note_lifecycle_destroy
|
|
341
|
+
reopen_lifecycle_write
|
|
342
|
+
@_spree_lifecycle_write_enabled = Spree::Events.lifecycle_enabled?
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def silenced_lifecycle_write?
|
|
346
|
+
@_spree_lifecycle_write_enabled == false
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def close_lifecycle_write
|
|
350
|
+
@_spree_lifecycle_write_closed = true
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def reopen_lifecycle_write
|
|
354
|
+
return unless @_spree_lifecycle_write_closed
|
|
355
|
+
|
|
356
|
+
@_spree_lifecycle_write_closed = false
|
|
357
|
+
@_spree_lifecycle_write_enabled = nil
|
|
358
|
+
end
|
|
359
|
+
|
|
314
360
|
# True when this commit was caused only by `touch` — no real save ran
|
|
315
361
|
# (`after_update` fires for saves, never for touches). Flags reset every
|
|
316
362
|
# commit so a later genuine update publishes normally.
|
|
@@ -62,7 +62,11 @@ module Spree
|
|
|
62
62
|
#
|
|
63
63
|
# @return [Boolean]
|
|
64
64
|
def shipping_address_required?
|
|
65
|
-
|
|
65
|
+
candidates = fulfillments.to_a
|
|
66
|
+
if candidates.many? && candidates.all?(ActiveRecord::Base)
|
|
67
|
+
ActiveRecord::Associations::Preloader.new(records: candidates, associations: { selected_delivery_rate: :delivery_method }).call
|
|
68
|
+
end
|
|
69
|
+
selected_methods = candidates.filter_map(&:delivery_method)
|
|
66
70
|
return selected_methods.any?(&:requires_address?) if selected_methods.any?
|
|
67
71
|
|
|
68
72
|
return false if preferred_stock_location_id.present?
|
|
@@ -270,9 +274,20 @@ module Spree
|
|
|
270
274
|
cache[profile] = profile.present? && profile.requires_shipping_address?
|
|
271
275
|
end
|
|
272
276
|
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
|
|
277
|
+
# On a list the association batches across the page, so a fresh
|
|
278
|
+
# `includes` query would load every cart's items separately. Read
|
|
279
|
+
# alone, both profile paths are preloaded: the variant's own override,
|
|
280
|
+
# and the product's for the variants that inherit it.
|
|
281
|
+
profile_paths = { variant: [:delivery_profile, { product: :delivery_profile }] }
|
|
282
|
+
items = if !read_association_in_memory?(:line_items)
|
|
283
|
+
line_items.includes(profile_paths)
|
|
284
|
+
elsif lazy_preload_context.nil?
|
|
285
|
+
line_items.tap { |loaded| ActiveRecord::Associations::Preloader.new(records: loaded.to_a, associations: profile_paths).call }
|
|
286
|
+
else
|
|
287
|
+
line_items
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
items.any? do |line_item|
|
|
276
291
|
profile_requires_address[line_item.variant&.resolved_delivery_profile]
|
|
277
292
|
end
|
|
278
293
|
end
|
|
@@ -9,7 +9,7 @@ module Spree
|
|
|
9
9
|
if total_quantity.zero? || line_items.empty?
|
|
10
10
|
false
|
|
11
11
|
else
|
|
12
|
-
|
|
12
|
+
line_items_with_variants(variant: :product).all?(&:digital?)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ module Spree
|
|
|
18
18
|
if total_quantity.zero? || line_items.empty?
|
|
19
19
|
false
|
|
20
20
|
else
|
|
21
|
-
|
|
21
|
+
line_items_with_variants(variant: :product).any?(&:digital?)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ module Spree
|
|
|
27
27
|
if total_quantity.zero? || line_items.empty?
|
|
28
28
|
false
|
|
29
29
|
else
|
|
30
|
-
|
|
30
|
+
line_items_with_variants(variant: :digital_assets).any?(&:with_digital_assets?)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -55,6 +55,20 @@ module Spree
|
|
|
55
55
|
def delivery_step_required?
|
|
56
56
|
line_items.any? && !digital?
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Lines already in memory get the associations preloaded onto them —
|
|
62
|
+
# onto every line of their preload context when they sit in a list, so
|
|
63
|
+
# the list batches them instead of querying once per purchase.
|
|
64
|
+
def line_items_with_variants(associations)
|
|
65
|
+
return line_items.includes(associations) unless read_association_in_memory?(:line_items)
|
|
66
|
+
|
|
67
|
+
loaded = line_items.to_a
|
|
68
|
+
records = loaded.first&.lazy_preload_context&.records || loaded
|
|
69
|
+
ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
|
|
70
|
+
loaded
|
|
71
|
+
end
|
|
58
72
|
end
|
|
59
73
|
end
|
|
60
74
|
end
|
|
@@ -42,7 +42,8 @@ module Spree
|
|
|
42
42
|
# twice. Two freight consignments are still one load to the forwarder,
|
|
43
43
|
# so they are merged rather than listed.
|
|
44
44
|
def build_freight_summary
|
|
45
|
-
|
|
45
|
+
shipping = read_association_in_memory?(:fulfillments) ? fulfillments.reject(&:canceled?) : fulfillments.valid
|
|
46
|
+
summaries = shipping.filter_map { |fulfillment| fulfillment.selected_delivery_rate&.freight_summary }
|
|
46
47
|
return if summaries.empty?
|
|
47
48
|
|
|
48
49
|
Spree::FreightSummary.merge(summaries)
|
|
@@ -55,7 +55,7 @@ module Spree
|
|
|
55
55
|
# @return [Boolean]
|
|
56
56
|
def confirmation_required?
|
|
57
57
|
Spree::StorePreferences.read(store, :always_include_confirm_step) ||
|
|
58
|
-
|
|
58
|
+
valid_payments.filter_map(&:payment_method).any?(&:confirmation_required?)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def unprocessed_payments
|
|
@@ -64,6 +64,12 @@ module Spree
|
|
|
64
64
|
|
|
65
65
|
private
|
|
66
66
|
|
|
67
|
+
# On a list the association batches across the page, so the loaded rows
|
|
68
|
+
# are filtered rather than queried through `valid` once per record.
|
|
69
|
+
def valid_payments
|
|
70
|
+
read_association_in_memory?(:payments) ? payments.reject(&:has_invalid_status?) : payments.valid
|
|
71
|
+
end
|
|
72
|
+
|
|
67
73
|
def process_payments_with(action)
|
|
68
74
|
# Don't run if there is nothing to pay. Read from the database, not
|
|
69
75
|
# the in-memory attribute: settlements land through the order status
|
|
@@ -104,6 +104,8 @@ module Spree
|
|
|
104
104
|
# accuse a completed sale of breaking a rule written after it.
|
|
105
105
|
return [] if staff_initiated? || (is_a?(Spree::Order) && completed?)
|
|
106
106
|
|
|
107
|
+
ActiveRecord::Associations::Preloader.new(records: line_items.to_a, associations: { variant: :product }).call
|
|
108
|
+
|
|
107
109
|
line_items.filter_map do |line_item|
|
|
108
110
|
variant = line_item.variant
|
|
109
111
|
next if variant.nil?
|
|
@@ -13,7 +13,9 @@ module Spree
|
|
|
13
13
|
def ensure_default_channel
|
|
14
14
|
return if default_channel
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Spree::Events.disable do
|
|
17
|
+
channels.create!(name: 'Online Store', code: Spree::Channel::DEFAULT_CODE)
|
|
18
|
+
end
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -168,7 +168,12 @@ module Spree
|
|
|
168
168
|
def translatable_store
|
|
169
169
|
return self if is_a?(Spree::Store)
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
current_store = Spree::Current.store
|
|
172
|
+
# The request's store instance already carries its markets, so a batch
|
|
173
|
+
# of records does not re-read them once per record's own store copy.
|
|
174
|
+
return current_store if current_store && try(:store_id) == current_store.id
|
|
175
|
+
|
|
176
|
+
try(:store) || current_store
|
|
172
177
|
end
|
|
173
178
|
|
|
174
179
|
private
|
data/app/models/spree/base.rb
CHANGED
|
@@ -46,13 +46,21 @@ class Spree::Base < ApplicationRecord
|
|
|
46
46
|
select("#{klass.table_name}.*").select(*(fields || klass::TRANSLATABLE_FIELDS))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
# The store's own rows of this model, through the association named after
|
|
50
|
+
# it. A model the store has no association for answers nothing: a tenant
|
|
51
|
+
# boundary that quietly widened to every store's rows is how a cross-store
|
|
52
|
+
# read ships unnoticed. Global reference data (countries, roles) and
|
|
53
|
+
# records reached through a parent are fetched as such, never through here.
|
|
54
|
+
#
|
|
55
|
+
# @param store [Spree::Store]
|
|
56
|
+
# @return [ActiveRecord::Relation]
|
|
49
57
|
def self.for_store(store)
|
|
50
58
|
plural_model_name = model_name.plural.gsub(/spree_/, '').to_sym
|
|
51
59
|
|
|
52
60
|
if store.respond_to?(plural_model_name)
|
|
53
61
|
store.send(plural_model_name)
|
|
54
62
|
else
|
|
55
|
-
|
|
63
|
+
none
|
|
56
64
|
end
|
|
57
65
|
end
|
|
58
66
|
|
|
@@ -81,6 +89,30 @@ class Spree::Base < ApplicationRecord
|
|
|
81
89
|
association(name).loaded? || lazy_preload_context.present?
|
|
82
90
|
end
|
|
83
91
|
|
|
92
|
+
# Recomputes a counter cache for many records at once. Rails'
|
|
93
|
+
# +reset_counters+ spends two queries per record, which a bulk write pays
|
|
94
|
+
# for every row it touched; this is one grouped count plus one UPDATE per
|
|
95
|
+
# distinct value.
|
|
96
|
+
#
|
|
97
|
+
# @param ids [Array] the records whose counter is recomputed
|
|
98
|
+
# @param association [Symbol] the has_many whose rows are counted
|
|
99
|
+
# @return [void]
|
|
100
|
+
def self.reset_counters_in_bulk(ids, association)
|
|
101
|
+
ids = Array(ids).compact.uniq
|
|
102
|
+
return if ids.empty?
|
|
103
|
+
|
|
104
|
+
reflection = reflect_on_association(association)
|
|
105
|
+
counter = reflection.klass.reflect_on_all_associations(:belongs_to).find do |belongs_to|
|
|
106
|
+
belongs_to.foreign_key.to_s == reflection.foreign_key.to_s && belongs_to.options[:counter_cache]
|
|
107
|
+
end.counter_cache_column
|
|
108
|
+
|
|
109
|
+
counts = reflection.klass.where(reflection.foreign_key => ids).group(reflection.foreign_key).count
|
|
110
|
+
|
|
111
|
+
ids.group_by { |id| counts.fetch(id, 0) }.each do |count, grouped_ids|
|
|
112
|
+
unscoped.where(id: grouped_ids).where.not(counter => count).update_all(counter => count)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
84
116
|
# @see Spree.mysql? — the single definition; this reads it so a model can
|
|
85
117
|
# branch without reaching for the connection itself.
|
|
86
118
|
def mysql_adapter?
|
|
@@ -3,7 +3,9 @@ module Spree
|
|
|
3
3
|
class DigitalDelivery < ShippingCalculator
|
|
4
4
|
include Spree::Calculator::CurrencyAmounts
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# Blank once its figure has moved into `amounts`, so the method quotes
|
|
7
|
+
# nothing outside the hash.
|
|
8
|
+
preference :amount, :money, default: 0, nullable: true
|
|
7
9
|
preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
|
|
8
10
|
|
|
9
11
|
def self.description
|
|
@@ -3,7 +3,9 @@ module Spree
|
|
|
3
3
|
class FlatRate < ShippingCalculator
|
|
4
4
|
include Spree::Calculator::CurrencyAmounts
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# Blank once its figure has moved into `amounts`, so the method quotes
|
|
7
|
+
# nothing outside the hash.
|
|
8
|
+
preference :amount, :money, default: 0, nullable: true
|
|
7
9
|
# @deprecated Together with the single `amount`, superseded by the
|
|
8
10
|
# per-currency `amounts` hash; kept as the fallback for its own
|
|
9
11
|
# currency so upgraded stores quote unchanged.
|
|
@@ -3,7 +3,9 @@ module Spree
|
|
|
3
3
|
class PerItem < ShippingCalculator
|
|
4
4
|
include Spree::Calculator::CurrencyAmounts
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# Blank once its figure has moved into `amounts`, so the method quotes
|
|
7
|
+
# nothing outside the hash.
|
|
8
|
+
preference :amount, :money, default: 0, nullable: true
|
|
7
9
|
preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
|
|
8
10
|
|
|
9
11
|
def self.description
|
data/app/models/spree/cart.rb
CHANGED
|
@@ -203,10 +203,13 @@ module Spree
|
|
|
203
203
|
# delivery_unavailable warning per affected line item (parity with the
|
|
204
204
|
# order-side ensure_available_delivery_rates).
|
|
205
205
|
def prune_undeliverable_fulfillments!
|
|
206
|
-
|
|
206
|
+
candidates = fulfillments.reload.to_a
|
|
207
|
+
ActiveRecord::Associations::Preloader.new(records: candidates, associations: :delivery_rates).call
|
|
208
|
+
undeliverable = candidates.select { |fulfillment| fulfillment.delivery_rates.empty? }
|
|
207
209
|
return if undeliverable.empty?
|
|
208
210
|
|
|
209
211
|
affected_line_items = undeliverable.flat_map(&:line_items).uniq
|
|
212
|
+
ActiveRecord::Associations::Preloader.new(records: affected_line_items, associations: { variant: :product }).call
|
|
210
213
|
undeliverable.each(&:destroy)
|
|
211
214
|
self.warnings |= affected_line_items.map do |line_item|
|
|
212
215
|
{
|