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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ac170eb6008053ca9eea6d6694bb98d49cc4e66e28826f4aac5bc97fad29e15
|
|
4
|
+
data.tar.gz: 8a883557c01ab2211232eab518443521435e1d6bc10dee6778f3f96119ea6334
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df3bff4364790fe3205baec9790c4fa123afc7945b68340afb583e56966e73acbd694f1c3016f11e9b880ba9a34bb1622620ee568970e3b625fca96da4807a09
|
|
7
|
+
data.tar.gz: 85bb8941d3648914ad037195c0e090b5e1df40ce3fb46b67aa6ca86a18784af3f1a0cb3ca1a9d656d54e52aead1050a2510fec3b1ccc5288e25d0f5f1f66a309
|
|
@@ -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.
|
|
@@ -3,8 +3,8 @@ module Spree
|
|
|
3
3
|
# The full address surface shared by Spree::Cart and Spree::Order:
|
|
4
4
|
# the bill/ship associations and their public aliases, default filling
|
|
5
5
|
# from the customer's saved addresses, address-row deduplication on
|
|
6
|
-
# write, ownership-guarded address_id assignment, promotion
|
|
7
|
-
# addresses to the customer's defaults, and the ship→bill cloning
|
|
6
|
+
# write, ownership-guarded address_id assignment and deletion, promotion
|
|
7
|
+
# of checkout addresses to the customer's defaults, and the ship→bill cloning
|
|
8
8
|
# predicate. The shipping address is canonical: use_shipping copies it
|
|
9
9
|
# onto the billing address; the reverse use_billing direction is a
|
|
10
10
|
# deprecated 6.1-removal bridge. Clone callbacks are wired per model.
|
|
@@ -12,8 +12,13 @@ module Spree
|
|
|
12
12
|
extend ActiveSupport::Concern
|
|
13
13
|
|
|
14
14
|
included do
|
|
15
|
-
belongs_to :bill_address, class_name: 'Spree::Address', optional: true
|
|
16
|
-
belongs_to :ship_address, class_name: 'Spree::Address', optional: true
|
|
15
|
+
belongs_to :bill_address, class_name: 'Spree::Address', optional: true
|
|
16
|
+
belongs_to :ship_address, class_name: 'Spree::Address', optional: true
|
|
17
|
+
|
|
18
|
+
# Stands in for `dependent: :destroy`, which would take any attached
|
|
19
|
+
# row down with the purchase. See {#owns_address?} for why only some
|
|
20
|
+
# of them may go.
|
|
21
|
+
after_destroy :destroy_own_addresses
|
|
17
22
|
|
|
18
23
|
# The writers these generate are shadowed by the hand-written ones
|
|
19
24
|
# below, which deduplicate address rows. The declarations still earn
|
|
@@ -40,9 +45,12 @@ module Spree
|
|
|
40
45
|
before_validation :clone_shipping_address, if: :use_shipping?
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
# Whether both addresses are present and match. With neither present
|
|
49
|
+
# there is nothing to call the same.
|
|
50
|
+
#
|
|
43
51
|
# @return [Boolean]
|
|
44
52
|
def shipping_eq_billing_address?
|
|
45
|
-
bill_address == ship_address
|
|
53
|
+
ship_address.present? && bill_address == ship_address
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
# Whether checkout must collect a customer shipping address. Selected
|
|
@@ -54,7 +62,11 @@ module Spree
|
|
|
54
62
|
#
|
|
55
63
|
# @return [Boolean]
|
|
56
64
|
def shipping_address_required?
|
|
57
|
-
|
|
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)
|
|
58
70
|
return selected_methods.any?(&:requires_address?) if selected_methods.any?
|
|
59
71
|
|
|
60
72
|
return false if preferred_stock_location_id.present?
|
|
@@ -90,6 +102,68 @@ module Spree
|
|
|
90
102
|
use_shipping.in?([true, 'true', '1'])
|
|
91
103
|
end
|
|
92
104
|
|
|
105
|
+
# The address behind an id a client may select, or nil when the id names
|
|
106
|
+
# one this purchase has no business shipping to. Takes a prefixed id or a
|
|
107
|
+
# raw primary key, so the services that resolve a client-supplied id and
|
|
108
|
+
# the writers below share one rule rather than each inventing their own.
|
|
109
|
+
#
|
|
110
|
+
# Two books qualify. The buyer's own is the ordinary case. The company's
|
|
111
|
+
# is the B2B one: a purchase for a node ships to that node's sites, and
|
|
112
|
+
# to its ancestors' — the same self-and-ancestors chain the node's
|
|
113
|
+
# default address is prefilled from, so a division can pick the
|
|
114
|
+
# headquarters address it already inherits.
|
|
115
|
+
#
|
|
116
|
+
# @param id [Integer, String, nil]
|
|
117
|
+
# @return [Spree::Address, nil]
|
|
118
|
+
def selectable_address(id)
|
|
119
|
+
address = ::Spree::Address.not_deleted.find_by_param(id) || ::Spree::Address.not_deleted.find_by(id: id)
|
|
120
|
+
return if address.nil?
|
|
121
|
+
|
|
122
|
+
return address if customer_id.present? && address.customer_owned? && address.owner_id == customer_id
|
|
123
|
+
|
|
124
|
+
company = readable_company
|
|
125
|
+
return if company.nil? || address.owner_type != 'Spree::Company'
|
|
126
|
+
|
|
127
|
+
company.self_and_ancestors.any? { |node| node.id == address.owner_id } ? address : nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Everything a checkout address picker may offer for this purchase: the
|
|
131
|
+
# buyer's own book plus the book of the company it names, that node's
|
|
132
|
+
# own entries and the ones it inherits, limited to the countries the
|
|
133
|
+
# purchase's market sells to. A subset of what {#selectable_address}
|
|
134
|
+
# accepts, so a picker built from it never offers an address the
|
|
135
|
+
# checkout cannot deliver to.
|
|
136
|
+
#
|
|
137
|
+
# @return [ActiveRecord::Relation<Spree::Address>]
|
|
138
|
+
def selectable_addresses
|
|
139
|
+
books = []
|
|
140
|
+
books << ::Spree::Address.not_deleted.where(owner_type: ::Spree.customer_class.to_s, owner_id: customer_id) if customer_id.present?
|
|
141
|
+
books << readable_company&.address_book
|
|
142
|
+
|
|
143
|
+
addresses = books.compact.reduce { |left, right| left.or(right) } || ::Spree::Address.none
|
|
144
|
+
return addresses if market.nil?
|
|
145
|
+
|
|
146
|
+
addresses.where(country_code: market.market_countries.select(:country_code))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Whether an attached address row is this purchase's own to delete.
|
|
150
|
+
#
|
|
151
|
+
# Only the ownerless rows are: the one a guest typed in, and the
|
|
152
|
+
# snapshot completion copies onto the order. Everything else is an entry
|
|
153
|
+
# in somebody's address book — the buyer's, a company node's, a seller's
|
|
154
|
+
# — which the purchase is merely pointing at. A cart may name a company
|
|
155
|
+
# site every other member ships from, so taking an attached row down
|
|
156
|
+
# with the cart would let one member empty the shared book.
|
|
157
|
+
#
|
|
158
|
+
# A business snapshot keeps its +owner_type+ to stay addressed to a
|
|
159
|
+
# company rather than to a person, so ownership is the id, not the type.
|
|
160
|
+
#
|
|
161
|
+
# @param address [Spree::Address, nil]
|
|
162
|
+
# @return [Boolean]
|
|
163
|
+
def owns_address?(address)
|
|
164
|
+
address.present? && address.owner_id.blank?
|
|
165
|
+
end
|
|
166
|
+
|
|
93
167
|
# Fills any blank bill/ship address from the customer's valid saved
|
|
94
168
|
# defaults. The ship address is skipped when no physical delivery is
|
|
95
169
|
# required, so shipping-address validations never fire on digital-only
|
|
@@ -174,46 +248,50 @@ module Spree
|
|
|
174
248
|
|
|
175
249
|
private
|
|
176
250
|
|
|
251
|
+
# The company whose book this purchase may read: only the one the
|
|
252
|
+
# client named, never one inferred from a sole membership. A cart asks
|
|
253
|
+
# again rather than trusting the column, which nothing clears when a
|
|
254
|
+
# membership is revoked; an order's authority is staff or a completed
|
|
255
|
+
# cart, so it keeps what it was given.
|
|
256
|
+
#
|
|
257
|
+
# @return [Spree::Company, nil]
|
|
258
|
+
def readable_company
|
|
259
|
+
return company if company.nil? || !is_a?(Spree::Cart)
|
|
260
|
+
|
|
261
|
+
company if customer&.standing_for?(company)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Both slots often hold the same row, so destroy it once.
|
|
265
|
+
def destroy_own_addresses
|
|
266
|
+
[bill_address, ship_address].compact.uniq.each do |address|
|
|
267
|
+
address.destroy if owns_address?(address)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
177
271
|
def items_require_shipping_address?
|
|
178
272
|
# Answered by the profile kind, resolved once per profile.
|
|
179
273
|
profile_requires_address = Hash.new do |cache, profile|
|
|
180
274
|
cache[profile] = profile.present? && profile.requires_shipping_address?
|
|
181
275
|
end
|
|
182
276
|
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
|
|
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|
|
|
186
291
|
profile_requires_address[line_item.variant&.resolved_delivery_profile]
|
|
187
292
|
end
|
|
188
293
|
end
|
|
189
294
|
|
|
190
|
-
# The address behind an id a client may select, or nil when the id names
|
|
191
|
-
# one this purchase has no business shipping to.
|
|
192
|
-
#
|
|
193
|
-
# Two books qualify. The buyer's own is the ordinary case. The company's
|
|
194
|
-
# is the B2B one: a purchase for a node ships to that node's sites, and
|
|
195
|
-
# to its ancestors' — the same self-and-ancestors chain the node's
|
|
196
|
-
# default address is prefilled from, so a division can pick the
|
|
197
|
-
# headquarters address it already inherits.
|
|
198
|
-
#
|
|
199
|
-
# Standing is not re-checked here: a cart cannot name a company its
|
|
200
|
-
# customer lacks standing over, so reaching this point already means the
|
|
201
|
-
# buyer may act for the node.
|
|
202
|
-
#
|
|
203
|
-
# @param id [Integer, String, nil]
|
|
204
|
-
# @return [Spree::Address, nil]
|
|
205
|
-
def selectable_address(id)
|
|
206
|
-
address = ::Spree::Address.find_by(id: id)
|
|
207
|
-
return nil if address.nil?
|
|
208
|
-
|
|
209
|
-
return address if customer_id.present? && address.customer_owned? && address.owner_id == customer_id
|
|
210
|
-
|
|
211
|
-
company = resolved_company
|
|
212
|
-
return nil if company.nil? || address.owner_type != 'Spree::Company'
|
|
213
|
-
|
|
214
|
-
company.self_and_ancestors.any? { |node| node.id == address.owner_id } ? address : nil
|
|
215
|
-
end
|
|
216
|
-
|
|
217
295
|
# Reuse never reaches past what the buyer already owns: a signed-in buyer
|
|
218
296
|
# may name any entry of their own address book, and a guest — who keeps
|
|
219
297
|
# no book — may only edit the row already in the slot being written.
|
|
@@ -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
|