spree_core 5.4.0.beta4 → 5.4.0.beta6
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 +47 -23
- data/app/models/concerns/spree/product_scopes.rb +65 -40
- data/app/models/concerns/spree/user_methods.rb +1 -0
- data/app/models/spree/category.rb +6 -0
- data/app/models/spree/payment/gateway_options.rb +5 -0
- data/app/models/spree/payment/processing.rb +2 -0
- data/app/models/spree/payment_method/check.rb +8 -0
- data/app/models/spree/product.rb +5 -34
- data/app/models/spree/store.rb +41 -80
- data/app/models/spree/taxon.rb +39 -34
- data/app/models/spree/webhook_endpoint.rb +17 -0
- data/app/services/spree/cart/create.rb +18 -2
- data/app/services/spree/cart/upsert_items.rb +80 -0
- data/app/services/spree/gift_cards/apply.rb +5 -4
- data/app/services/spree/orders/update.rb +121 -0
- data/app/services/spree/products/prepare_nested_attributes.rb +29 -3
- data/config/locales/en.yml +5 -0
- data/lib/spree/core/configuration.rb +1 -0
- data/lib/spree/core/controller_helpers/strong_parameters.rb +1 -1
- data/lib/spree/core/dependencies.rb +2 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/permitted_attributes.rb +3 -5
- data/lib/spree/testing_support/factories/store_factory.rb +0 -9
- data/lib/tasks/core.rake +0 -28
- metadata +21 -5
- data/app/models/concerns/spree/stores/socials.rb +0 -72
data/app/models/spree/taxon.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'stringex'
|
|
2
4
|
|
|
3
5
|
module Spree
|
|
4
6
|
class Taxon < Spree.base_class
|
|
5
|
-
has_prefix_id :
|
|
7
|
+
has_prefix_id :ctg
|
|
6
8
|
|
|
7
9
|
RULES_MATCH_POLICIES = %w[all any].freeze
|
|
8
10
|
SORT_ORDERS = [
|
|
@@ -54,9 +56,9 @@ module Spree
|
|
|
54
56
|
#
|
|
55
57
|
# Validations
|
|
56
58
|
#
|
|
57
|
-
validates :name, presence: true, uniqueness: { scope: [
|
|
59
|
+
validates :name, presence: true, uniqueness: { scope: %i[parent_id taxonomy_id], case_sensitive: false }
|
|
58
60
|
validates :taxonomy, presence: true
|
|
59
|
-
validates :permalink, uniqueness: { case_sensitive: false, scope: [
|
|
61
|
+
validates :permalink, uniqueness: { case_sensitive: false, scope: %i[parent_id taxonomy_id] }
|
|
60
62
|
validates :hide_from_nav, inclusion: { in: [true, false] }
|
|
61
63
|
validate :check_for_root, on: :create
|
|
62
64
|
validate :parent_belongs_to_same_taxonomy
|
|
@@ -88,9 +90,9 @@ module Spree
|
|
|
88
90
|
scope :for_stores, ->(stores) { joins(:taxonomy).where(spree_taxonomies: { store_id: stores.ids }) }
|
|
89
91
|
scope :for_taxonomy, lambda { |taxonomy_name|
|
|
90
92
|
if Spree.use_translations?
|
|
91
|
-
joins(:taxonomy)
|
|
92
|
-
join_translation_table(Taxonomy)
|
|
93
|
-
where(
|
|
93
|
+
joins(:taxonomy)
|
|
94
|
+
.join_translation_table(Taxonomy)
|
|
95
|
+
.where(
|
|
94
96
|
Taxonomy.arel_table_alias[:name].lower.matches(taxonomy_name.downcase.strip)
|
|
95
97
|
)
|
|
96
98
|
else
|
|
@@ -110,7 +112,7 @@ module Spree
|
|
|
110
112
|
end
|
|
111
113
|
end
|
|
112
114
|
|
|
113
|
-
scope :with_matching_name,
|
|
115
|
+
scope :with_matching_name, lambda { |name_to_match|
|
|
114
116
|
value = name_to_match.to_s.strip.downcase
|
|
115
117
|
|
|
116
118
|
if Spree.use_translations?
|
|
@@ -118,13 +120,14 @@ module Spree
|
|
|
118
120
|
else
|
|
119
121
|
where(arel_table[:name].lower.eq(value))
|
|
120
122
|
end
|
|
121
|
-
|
|
123
|
+
}
|
|
122
124
|
|
|
123
125
|
#
|
|
124
126
|
# Ransack
|
|
125
127
|
#
|
|
126
|
-
self.whitelisted_ransackable_associations = %w[taxonomy]
|
|
127
|
-
self.whitelisted_ransackable_attributes = %w[name permalink automatic
|
|
128
|
+
self.whitelisted_ransackable_associations = %w[taxonomy parent]
|
|
129
|
+
self.whitelisted_ransackable_attributes = %w[name permalink automatic depth is_root children_count
|
|
130
|
+
classification_count hide_from_nav parent_id]
|
|
128
131
|
|
|
129
132
|
#
|
|
130
133
|
# Translations
|
|
@@ -142,7 +145,9 @@ module Spree
|
|
|
142
145
|
validates :sort_order, inclusion: { in: SORT_ORDERS }, presence: true
|
|
143
146
|
|
|
144
147
|
has_many :taxon_rules, class_name: 'Spree::TaxonRule', dependent: :destroy
|
|
145
|
-
accepts_nested_attributes_for :taxon_rules, allow_destroy: true, reject_if: proc { |attributes|
|
|
148
|
+
accepts_nested_attributes_for :taxon_rules, allow_destroy: true, reject_if: proc { |attributes|
|
|
149
|
+
attributes['value'].blank?
|
|
150
|
+
}
|
|
146
151
|
alias rules taxon_rules
|
|
147
152
|
|
|
148
153
|
scope :manual, -> { where.not(automatic: true) }
|
|
@@ -160,14 +165,14 @@ module Spree
|
|
|
160
165
|
end
|
|
161
166
|
|
|
162
167
|
def active_products_with_descendants
|
|
163
|
-
@active_products_with_descendants ||= store.products
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
@active_products_with_descendants ||= store.products
|
|
169
|
+
.joins(:classifications)
|
|
170
|
+
.active
|
|
171
|
+
.where(
|
|
172
|
+
Spree::Classification.table_name => {
|
|
173
|
+
taxon_id: descendants.ids + [id]
|
|
174
|
+
}
|
|
175
|
+
)
|
|
171
176
|
end
|
|
172
177
|
|
|
173
178
|
def products_matching_rules(opts = {})
|
|
@@ -209,10 +214,10 @@ module Spree
|
|
|
209
214
|
# so we can later use them for product filtering and so on
|
|
210
215
|
# if we want to fire the service once during object lifecycle - pass only_once: true
|
|
211
216
|
def regenerate_taxon_products(only_once: false)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
return unless marked_for_regenerate_taxon_products?
|
|
218
|
+
|
|
219
|
+
Spree::Taxons::RegenerateProducts.call(taxon: self)
|
|
220
|
+
self.marked_for_regenerate_taxon_products = false if !frozen? && only_once
|
|
216
221
|
end
|
|
217
222
|
|
|
218
223
|
def slug
|
|
@@ -283,7 +288,8 @@ module Spree
|
|
|
283
288
|
end
|
|
284
289
|
|
|
285
290
|
def generate_permalink_including_parent
|
|
286
|
-
[parent_permalink_with_fallback,
|
|
291
|
+
[parent_permalink_with_fallback,
|
|
292
|
+
(permalink.blank? ? name_with_fallback.to_url : permalink.split('/').last.to_url)].join('/')
|
|
287
293
|
end
|
|
288
294
|
|
|
289
295
|
def generate_pretty_name_including_parent
|
|
@@ -384,11 +390,10 @@ module Spree
|
|
|
384
390
|
end
|
|
385
391
|
|
|
386
392
|
def sync_taxonomy_name
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
return unless saved_changes.key?(:name) && root?
|
|
394
|
+
return if taxonomy.name.to_s == name.to_s
|
|
389
395
|
|
|
390
|
-
|
|
391
|
-
end
|
|
396
|
+
taxonomy.update(name: name)
|
|
392
397
|
end
|
|
393
398
|
|
|
394
399
|
def touch_ancestors_and_taxonomy
|
|
@@ -399,15 +404,15 @@ module Spree
|
|
|
399
404
|
end
|
|
400
405
|
|
|
401
406
|
def check_for_root
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
407
|
+
return unless taxonomy.try(:root).present? && parent_id.nil?
|
|
408
|
+
|
|
409
|
+
errors.add(:root_conflict, 'this taxonomy already has a root taxon')
|
|
405
410
|
end
|
|
406
411
|
|
|
407
412
|
def parent_belongs_to_same_taxonomy
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
413
|
+
return unless parent.present? && parent.taxonomy_id != taxonomy_id
|
|
414
|
+
|
|
415
|
+
errors.add(:parent, 'must belong to the same taxonomy')
|
|
411
416
|
end
|
|
412
417
|
|
|
413
418
|
def copy_taxonomy_from_parent
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ssrf_filter'
|
|
4
|
+
require 'resolv'
|
|
5
|
+
|
|
3
6
|
module Spree
|
|
4
7
|
class WebhookEndpoint < Spree.base_class
|
|
5
8
|
has_prefix_id :whe # Stripe: we_
|
|
@@ -8,12 +11,15 @@ module Spree
|
|
|
8
11
|
|
|
9
12
|
include Spree::SingleStoreResource
|
|
10
13
|
|
|
14
|
+
encrypts :secret_key, deterministic: true if Rails.configuration.active_record.encryption.include?(:primary_key)
|
|
15
|
+
|
|
11
16
|
belongs_to :store, class_name: 'Spree::Store'
|
|
12
17
|
has_many :webhook_deliveries, class_name: 'Spree::WebhookDelivery', dependent: :destroy_async
|
|
13
18
|
|
|
14
19
|
validates :store, :url, presence: true
|
|
15
20
|
validates :url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: :invalid_url }
|
|
16
21
|
validates :active, inclusion: { in: [true, false] }
|
|
22
|
+
validate :url_must_not_resolve_to_private_ip, if: -> { url.present? && url_changed? }
|
|
17
23
|
|
|
18
24
|
before_create :generate_secret_key
|
|
19
25
|
|
|
@@ -51,5 +57,16 @@ module Spree
|
|
|
51
57
|
def generate_secret_key
|
|
52
58
|
self.secret_key ||= SecureRandom.hex(32)
|
|
53
59
|
end
|
|
60
|
+
|
|
61
|
+
def url_must_not_resolve_to_private_ip
|
|
62
|
+
uri = URI.parse(url)
|
|
63
|
+
blacklist = SsrfFilter::IPV4_BLACKLIST + SsrfFilter::IPV6_BLACKLIST
|
|
64
|
+
addresses = Resolv.getaddresses(uri.host)
|
|
65
|
+
if addresses.any? { |addr| blacklist.any? { |range| range.include?(IPAddr.new(addr)) } }
|
|
66
|
+
errors.add(:url, :internal_address_not_allowed)
|
|
67
|
+
end
|
|
68
|
+
rescue URI::InvalidURIError, Resolv::ResolvError, IPAddr::InvalidAddressError, ArgumentError
|
|
69
|
+
# URI format validation handles invalid URLs; DNS failures are not SSRF
|
|
70
|
+
end
|
|
54
71
|
end
|
|
55
72
|
end
|
|
@@ -10,9 +10,11 @@ module Spree
|
|
|
10
10
|
# @param public_metadata [Hash] public metadata for the order
|
|
11
11
|
# @param private_metadata [Hash] private metadata for the order
|
|
12
12
|
# @param order_params [Hash] additional order attributes
|
|
13
|
+
# @param line_items [Array<Hash>] line items to add, each with :variant_id (prefixed) and :quantity
|
|
13
14
|
# @return [Spree::ServiceModule::Result]
|
|
14
|
-
def call(user:, store:, currency:, locale: nil, metadata: {}, public_metadata: {}, private_metadata: {}, order_params: {})
|
|
15
|
+
def call(user:, store:, currency:, locale: nil, metadata: {}, public_metadata: {}, private_metadata: {}, order_params: {}, line_items: [])
|
|
15
16
|
order_params ||= {}
|
|
17
|
+
line_items ||= []
|
|
16
18
|
|
|
17
19
|
# we cannot create an order without store
|
|
18
20
|
return failure(:store_is_required) if store.nil?
|
|
@@ -28,8 +30,22 @@ module Spree
|
|
|
28
30
|
private_metadata: resolved_metadata.to_h
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
order =
|
|
33
|
+
order = nil
|
|
34
|
+
|
|
35
|
+
ApplicationRecord.transaction do
|
|
36
|
+
order = store.orders.create!(default_params.merge(order_params))
|
|
37
|
+
|
|
38
|
+
if line_items.present?
|
|
39
|
+
result = Spree.cart_upsert_items_service.call(order: order, line_items: line_items)
|
|
40
|
+
raise StandardError, result.error.to_s if result.failure?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
32
44
|
success(order)
|
|
45
|
+
rescue ActiveRecord::RecordNotFound
|
|
46
|
+
raise
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
failure(order, e.message)
|
|
33
49
|
end
|
|
34
50
|
end
|
|
35
51
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Cart
|
|
3
|
+
# Bulk upsert line items on an order.
|
|
4
|
+
#
|
|
5
|
+
# For each entry in +line_items+:
|
|
6
|
+
# - If a line item for the variant already exists → sets its quantity
|
|
7
|
+
# - If no line item exists → creates one with the given quantity
|
|
8
|
+
#
|
|
9
|
+
# After all items are processed the order is recalculated once.
|
|
10
|
+
#
|
|
11
|
+
# Price calculation and tax adjustments are handled by LineItem model callbacks
|
|
12
|
+
# (copy_price, update_adjustments, update_tax_charge), so we only need to
|
|
13
|
+
# save each item and run a single order recalculation at the end.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# Spree::Cart::UpsertItems.call(
|
|
17
|
+
# order: order,
|
|
18
|
+
# line_items: [
|
|
19
|
+
# { variant_id: "variant_k5nR8xLq", quantity: 2 },
|
|
20
|
+
# { variant_id: "variant_m3Rp9wXz", quantity: 1 }
|
|
21
|
+
# ]
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
class UpsertItems
|
|
25
|
+
prepend Spree::ServiceModule::Base
|
|
26
|
+
|
|
27
|
+
def call(order:, line_items:)
|
|
28
|
+
line_items = Array(line_items)
|
|
29
|
+
return success(order) if line_items.empty?
|
|
30
|
+
|
|
31
|
+
store = order.store || Spree::Current.store
|
|
32
|
+
|
|
33
|
+
ApplicationRecord.transaction do
|
|
34
|
+
line_items.each do |item_params|
|
|
35
|
+
item_params = item_params.to_h.deep_symbolize_keys
|
|
36
|
+
variant = resolve_variant(store, item_params[:variant_id])
|
|
37
|
+
next unless variant
|
|
38
|
+
|
|
39
|
+
quantity = (item_params[:quantity] || 1).to_i
|
|
40
|
+
|
|
41
|
+
return failure(variant, "#{variant.name} is not available in #{order.currency}") if variant.amount_in(order.currency).nil?
|
|
42
|
+
|
|
43
|
+
line_item = Spree.line_item_by_variant_finder.new.execute(order: order, variant: variant)
|
|
44
|
+
|
|
45
|
+
if line_item
|
|
46
|
+
line_item.quantity = quantity
|
|
47
|
+
line_item.metadata = line_item.metadata.merge(item_params[:metadata].to_h) if item_params[:metadata].present?
|
|
48
|
+
else
|
|
49
|
+
line_item = order.line_items.new(quantity: quantity, variant: variant, options: { currency: order.currency })
|
|
50
|
+
line_item.metadata = item_params[:metadata].to_h if item_params[:metadata].present?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return failure(line_item) unless line_item.save
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
order.update_with_updater!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
success(order)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def resolve_variant(store, prefixed_id)
|
|
65
|
+
return nil if prefixed_id.blank?
|
|
66
|
+
|
|
67
|
+
variant = store.variants.find_by_prefix_id(prefixed_id)
|
|
68
|
+
|
|
69
|
+
raise ActiveRecord::RecordNotFound.new(
|
|
70
|
+
"Variant '#{prefixed_id}' not found in this store",
|
|
71
|
+
'Spree::Variant',
|
|
72
|
+
'prefix_id',
|
|
73
|
+
prefixed_id
|
|
74
|
+
) unless variant
|
|
75
|
+
|
|
76
|
+
variant
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -25,15 +25,16 @@ module Spree
|
|
|
25
25
|
return failure(:gift_card_mismatched_customer) if gift_card.user != order.user
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
amount = [gift_card.amount_remaining, order.total].min
|
|
29
28
|
store = order.store
|
|
30
29
|
|
|
31
|
-
return failure(:gift_card_no_amount_remaining) unless amount.positive? || order.total.zero?
|
|
32
|
-
|
|
33
30
|
payment_method = ensure_store_credit_payment_method!(store)
|
|
34
31
|
|
|
35
|
-
gift_card.lock!
|
|
36
32
|
order.with_lock do
|
|
33
|
+
gift_card.lock!
|
|
34
|
+
amount = [gift_card.amount_remaining, order.total].min
|
|
35
|
+
|
|
36
|
+
return failure(:gift_card_no_amount_remaining) unless amount.positive? || order.total.zero?
|
|
37
|
+
|
|
37
38
|
store_credit = gift_card.store_credits.create!(
|
|
38
39
|
store: store,
|
|
39
40
|
user: order.user,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Orders
|
|
3
|
+
# Core order update service with modern conventions:
|
|
4
|
+
# - Flat parameter structure (no wrapping in "order" key)
|
|
5
|
+
# - snake_case field names without "_attributes" suffix
|
|
6
|
+
# - Automatic state management based on what's being updated
|
|
7
|
+
# - Support for line_items with prefixed variant IDs
|
|
8
|
+
#
|
|
9
|
+
# @example Update order with line items
|
|
10
|
+
# Spree::Orders::Update.call(
|
|
11
|
+
# order: order,
|
|
12
|
+
# params: {
|
|
13
|
+
# email: "customer@example.com",
|
|
14
|
+
# line_items: [
|
|
15
|
+
# { variant_id: "variant_123", quantity: 2 },
|
|
16
|
+
# { variant_id: "variant_456", quantity: 1 }
|
|
17
|
+
# ],
|
|
18
|
+
# ship_address: {
|
|
19
|
+
# firstname: "John",
|
|
20
|
+
# lastname: "Doe",
|
|
21
|
+
# address1: "123 Main St",
|
|
22
|
+
# city: "New York",
|
|
23
|
+
# zipcode: "10001",
|
|
24
|
+
# country_iso: "US",
|
|
25
|
+
# state_abbr: "NY"
|
|
26
|
+
# }
|
|
27
|
+
# }
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
class Update
|
|
31
|
+
prepend Spree::ServiceModule::Base
|
|
32
|
+
|
|
33
|
+
def call(order:, params:)
|
|
34
|
+
@order = order
|
|
35
|
+
@params = params.to_h.deep_symbolize_keys
|
|
36
|
+
|
|
37
|
+
ApplicationRecord.transaction do
|
|
38
|
+
assign_order_attributes
|
|
39
|
+
assign_address(:ship_address)
|
|
40
|
+
assign_address(:bill_address)
|
|
41
|
+
|
|
42
|
+
order.save!
|
|
43
|
+
|
|
44
|
+
process_line_items
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
success(order.reload)
|
|
48
|
+
rescue ActiveRecord::RecordNotFound
|
|
49
|
+
raise
|
|
50
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
51
|
+
failure(order, e.record.errors.full_messages.to_sentence)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
failure(order, e.message)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :order, :params
|
|
59
|
+
|
|
60
|
+
def assign_order_attributes
|
|
61
|
+
order.email = params[:email] if params[:email].present?
|
|
62
|
+
order.special_instructions = params[:special_instructions] if params.key?(:special_instructions)
|
|
63
|
+
order.currency = params[:currency].upcase if params[:currency].present?
|
|
64
|
+
order.locale = params[:locale] if params[:locale].present?
|
|
65
|
+
order.metadata = order.metadata.merge(params[:metadata].to_h) if params[:metadata].present?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def assign_address(address_type)
|
|
69
|
+
address_id_param = params[:"#{address_type}_id"]
|
|
70
|
+
address_params = params[address_type]
|
|
71
|
+
|
|
72
|
+
# Priority 1: Direct address ID reference (ship_address_id / bill_address_id)
|
|
73
|
+
if address_id_param.present?
|
|
74
|
+
address_id = resolve_address_id(address_id_param)
|
|
75
|
+
order.public_send(:"#{address_type}_id=", address_id) if address_id
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Priority 2: Nested address params (ship_address / bill_address)
|
|
80
|
+
return unless address_params.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
if address_params[:id].present?
|
|
83
|
+
# Using existing address by ID within nested params
|
|
84
|
+
address_id = resolve_address_id(address_params[:id])
|
|
85
|
+
order.public_send(:"#{address_type}_id=", address_id) if address_id
|
|
86
|
+
else
|
|
87
|
+
# Creating/updating address with provided attributes
|
|
88
|
+
revert_to_address_state if order.has_checkout_step?('address')
|
|
89
|
+
order.public_send(:"#{address_type}_attributes=", address_params)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def process_line_items
|
|
94
|
+
return unless params[:line_items].is_a?(Array)
|
|
95
|
+
|
|
96
|
+
result = Spree.cart_upsert_items_service.call(
|
|
97
|
+
order: order,
|
|
98
|
+
line_items: params[:line_items]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
raise StandardError, result.error.to_s if result.failure?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Translate prefixed ID to internal id
|
|
105
|
+
def resolve_address_id(prefixed_id)
|
|
106
|
+
return unless order.user
|
|
107
|
+
|
|
108
|
+
decoded = Spree::Address.decode_prefixed_id(prefixed_id)
|
|
109
|
+
decoded ? order.user.addresses.find_by(id: decoded)&.id : nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Revert order state to 'address' when address changes
|
|
113
|
+
# This ensures shipments are recreated when transitioning back to delivery
|
|
114
|
+
def revert_to_address_state
|
|
115
|
+
return if ['cart', 'address'].include?(order.state)
|
|
116
|
+
|
|
117
|
+
order.state = 'address'
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -17,11 +17,14 @@ module Spree
|
|
|
17
17
|
# prepared_params = service.call
|
|
18
18
|
#
|
|
19
19
|
class PrepareNestedAttributes
|
|
20
|
+
attr_reader :variants_to_discontinue
|
|
21
|
+
|
|
20
22
|
def initialize(product, store, params, ability)
|
|
21
23
|
@product = product
|
|
22
24
|
@store = store
|
|
23
25
|
@params = params
|
|
24
26
|
@ability = ability
|
|
27
|
+
@variants_to_discontinue = []
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def call
|
|
@@ -75,7 +78,8 @@ module Spree
|
|
|
75
78
|
params[:option_type_ids] = []
|
|
76
79
|
params[:variants_attributes] = {}
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
populate_variants_to_discontinue
|
|
82
|
+
variant_ids_to_destroy.each_with_index do |variant_id, index|
|
|
79
83
|
params[:variants_attributes][index.to_s] = { id: variant_id, _destroy: '1' }
|
|
80
84
|
end
|
|
81
85
|
|
|
@@ -122,15 +126,37 @@ module Spree
|
|
|
122
126
|
def removed_variants_attributes
|
|
123
127
|
return {} unless can_remove_variants?
|
|
124
128
|
|
|
129
|
+
populate_variants_to_discontinue
|
|
130
|
+
|
|
125
131
|
attributes = {}
|
|
126
132
|
last_index = params[:variants_attributes].keys.map(&:to_i).max
|
|
127
|
-
|
|
128
|
-
attributes[(
|
|
133
|
+
variant_ids_to_destroy.each_with_index do |variant_id, index|
|
|
134
|
+
attributes[(last_index + 1 + index).to_s] = { id: variant_id, _destroy: '1' }
|
|
129
135
|
end
|
|
130
136
|
|
|
131
137
|
attributes
|
|
132
138
|
end
|
|
133
139
|
|
|
140
|
+
def populate_variants_to_discontinue
|
|
141
|
+
ids = variants_to_remove.select { |vid| variant_ids_with_completed_orders.include?(vid) }
|
|
142
|
+
@variants_to_discontinue = product.variants.where(id: ids).to_a if ids.any?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def variant_ids_to_destroy
|
|
146
|
+
variants_to_remove - variant_ids_with_completed_orders
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def variant_ids_with_completed_orders
|
|
150
|
+
@variant_ids_with_completed_orders ||=
|
|
151
|
+
product.variants
|
|
152
|
+
.joins(:orders)
|
|
153
|
+
.merge(Spree::Order.complete)
|
|
154
|
+
.reorder(nil)
|
|
155
|
+
.distinct
|
|
156
|
+
.pluck(:id)
|
|
157
|
+
.map(&:to_s)
|
|
158
|
+
end
|
|
159
|
+
|
|
134
160
|
def removed_product_option_types_attributes
|
|
135
161
|
return {} unless can_manage_option_types?
|
|
136
162
|
|
data/config/locales/en.yml
CHANGED
|
@@ -363,6 +363,10 @@ en:
|
|
|
363
363
|
cannot_destroy_if_attached_to_line_items: Cannot delete Variants that are added to placed Orders. In such cases, please discontinue them.
|
|
364
364
|
must_supply_price_for_variant_or_master: Must supply price for variant or master price for product.
|
|
365
365
|
no_master_variant_found_to_infer_price: No master variant found to infer price
|
|
366
|
+
spree/webhook_endpoint:
|
|
367
|
+
attributes:
|
|
368
|
+
url:
|
|
369
|
+
internal_address_not_allowed: must not point to an internal or private network address
|
|
366
370
|
spree/wished_item:
|
|
367
371
|
attributes:
|
|
368
372
|
variant:
|
|
@@ -1231,6 +1235,7 @@ en:
|
|
|
1231
1235
|
this_file_language: English (US)
|
|
1232
1236
|
translations: Translations
|
|
1233
1237
|
icon: Icon
|
|
1238
|
+
idempotency_key_reused: This Idempotency-Key has already been used with different request parameters.
|
|
1234
1239
|
image: Image
|
|
1235
1240
|
image_alt_text: Add Alt text to your image
|
|
1236
1241
|
images: Images
|
|
@@ -47,6 +47,7 @@ module Spree
|
|
|
47
47
|
preference :expedited_exchanges_days_window, :integer, default: 14 # the amount of days the customer has to return their item after the expedited exchange is shipped in order to avoid being charged
|
|
48
48
|
preference :geocode_addresses, :boolean, default: true
|
|
49
49
|
preference :images_save_from_url_job_attempts, :integer, default: 5
|
|
50
|
+
preference :max_image_download_size, :integer, default: 20_971_520 # 20 MB in bytes
|
|
50
51
|
|
|
51
52
|
# Preprocessed product image variant sizes at 2x retina resolution.
|
|
52
53
|
# These variants are generated on upload to reduce runtime processing.
|
|
@@ -16,6 +16,7 @@ module Spree
|
|
|
16
16
|
cart_remove_item_service: 'Spree::Cart::RemoveItem',
|
|
17
17
|
cart_remove_line_item_service: 'Spree::Cart::RemoveLineItem',
|
|
18
18
|
cart_set_item_quantity_service: 'Spree::Cart::SetQuantity',
|
|
19
|
+
cart_upsert_items_service: 'Spree::Cart::UpsertItems',
|
|
19
20
|
cart_estimate_shipping_rates_service: 'Spree::Cart::EstimateShippingRates',
|
|
20
21
|
cart_empty_service: 'Spree::Cart::Empty',
|
|
21
22
|
cart_destroy_service: 'Spree::Cart::Destroy',
|
|
@@ -41,6 +42,7 @@ module Spree
|
|
|
41
42
|
# order
|
|
42
43
|
order_approve_service: 'Spree::Orders::Approve',
|
|
43
44
|
order_cancel_service: 'Spree::Orders::Cancel',
|
|
45
|
+
order_update_service: 'Spree::Orders::Update',
|
|
44
46
|
order_updater: 'Spree::OrderUpdater',
|
|
45
47
|
|
|
46
48
|
# shipment
|
data/lib/spree/core/version.rb
CHANGED
|
@@ -266,15 +266,13 @@ module Spree
|
|
|
266
266
|
:meta_description, :default_currency, :default_country_iso, :mail_from_address,
|
|
267
267
|
:customer_support_email, :description, :address, :contact_phone,
|
|
268
268
|
:supported_locales, :default_locale, :supported_currencies,
|
|
269
|
-
:new_order_notifications_email,
|
|
269
|
+
:new_order_notifications_email,
|
|
270
270
|
:preferred_admin_locale, :preferred_timezone, :preferred_weight_unit, :preferred_unit_system,
|
|
271
271
|
:preferred_digital_asset_authorized_clicks, :preferred_digital_asset_authorized_days,
|
|
272
272
|
:preferred_limit_digital_download_count, :preferred_limit_digital_download_days,
|
|
273
273
|
:preferred_digital_asset_link_expire_time,
|
|
274
|
-
:logo, :mailer_logo,
|
|
275
|
-
:
|
|
276
|
-
:customer_terms_of_service, :customer_privacy_policy,
|
|
277
|
-
:customer_returns_policy, :customer_shipping_policy]
|
|
274
|
+
:logo, :mailer_logo,
|
|
275
|
+
:preferred_guest_checkout]
|
|
278
276
|
|
|
279
277
|
@@store_credit_attributes = %i[amount currency category_id memo]
|
|
280
278
|
|
|
@@ -14,14 +14,5 @@ FactoryBot.define do
|
|
|
14
14
|
instagram { 'spreecommerce' }
|
|
15
15
|
meta_description { 'Sample store description.' }
|
|
16
16
|
|
|
17
|
-
trait :with_favicon do
|
|
18
|
-
after(:build) do |store|
|
|
19
|
-
store.favicon_image.attach(
|
|
20
|
-
io: File.open(Spree::Core::Engine.root.join('spec', 'fixtures', 'thinking-cat.jpg')),
|
|
21
|
-
filename: 'thinking-cat.jpg',
|
|
22
|
-
content_type: 'image/jpeg'
|
|
23
|
-
)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
17
|
end
|
|
27
18
|
end
|
data/lib/tasks/core.rake
CHANGED
|
@@ -8,34 +8,6 @@ namespace :db do
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
desc 'Migrate policies to store policies'
|
|
12
|
-
task migrate_policies: :environment do |_t, _args|
|
|
13
|
-
# Helper to consistently derive policy name
|
|
14
|
-
derive_policy_name = lambda do |name_str|
|
|
15
|
-
name_str.to_s.gsub(/customer_/, '').gsub(/_policy$/, '')
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Check if migration has already been run
|
|
19
|
-
if Spree::Policy.any?
|
|
20
|
-
puts "Policies already exist. Skipping migration to prevent duplicates."
|
|
21
|
-
exit
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
Spree::Store.all.each do |store|
|
|
25
|
-
%w[customer_terms_of_service customer_privacy_policy customer_returns_policy customer_shipping_policy].each do |policy_slug|
|
|
26
|
-
policy = store.send(policy_slug)
|
|
27
|
-
next unless policy.present?
|
|
28
|
-
|
|
29
|
-
store.policies.create(
|
|
30
|
-
name: Spree.t(derive_policy_name.call(policy_slug)),
|
|
31
|
-
body: policy.body
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
puts "Migrated #{policy_slug} to store #{store.id}"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
11
|
end
|
|
40
12
|
|
|
41
13
|
namespace :core do
|