spree_core 5.4.0.beta3 → 5.4.0.beta5
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/exports/generate_job.rb +1 -1
- data/app/jobs/spree/images/save_from_url_job.rb +47 -23
- data/app/jobs/spree/reports/generate_job.rb +1 -1
- data/app/models/concerns/spree/product_scopes.rb +78 -42
- data/app/models/concerns/spree/user_methods.rb +1 -0
- data/app/models/spree/address.rb +0 -14
- data/app/models/spree/category.rb +6 -0
- data/app/models/spree/country.rb +2 -23
- data/app/models/spree/coupon_code.rb +6 -1
- data/app/models/spree/current.rb +3 -2
- data/app/models/spree/exports/coupon_codes.rb +18 -0
- data/app/models/spree/market.rb +8 -0
- data/app/models/spree/payment/gateway_options.rb +5 -0
- data/app/models/spree/product.rb +7 -34
- data/app/models/spree/store.rb +41 -80
- data/app/models/spree/taxon.rb +49 -44
- data/app/models/spree/variant.rb +1 -1
- data/app/models/spree/webhook_endpoint.rb +17 -0
- data/app/models/spree/zone.rb +21 -9
- data/app/presenters/spree/csv/coupon_code_presenter.rb +31 -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/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/engine.rb +2 -1
- 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/spree_core.rb +0 -1
- data/lib/tasks/core.rake +0 -28
- metadata +23 -20
- data/app/models/concerns/spree/stores/socials.rb +0 -72
- data/lib/normalize_string.rb +0 -18
data/app/models/spree/store.rb
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'uri'
|
|
2
4
|
|
|
3
5
|
module Spree
|
|
4
6
|
class Store < Spree.base_class
|
|
5
|
-
has_prefix_id :store
|
|
7
|
+
has_prefix_id :store # Spree-specific: store
|
|
6
8
|
|
|
7
9
|
include FriendlyId
|
|
8
10
|
include Spree::TranslatableResource
|
|
9
11
|
include Spree::Metafields
|
|
10
12
|
include Spree::Metadata
|
|
11
13
|
include Spree::Stores::Setup
|
|
12
|
-
include Spree::Stores::Socials
|
|
13
14
|
include Spree::Stores::Markets
|
|
14
15
|
include Spree::Security::Stores if defined?(Spree::Security::Stores)
|
|
15
16
|
include Spree::UserManagement
|
|
@@ -23,8 +24,7 @@ module Spree
|
|
|
23
24
|
#
|
|
24
25
|
# Translations
|
|
25
26
|
#
|
|
26
|
-
TRANSLATABLE_FIELDS = %i[name meta_description meta_keywords seo_title
|
|
27
|
-
twitter instagram customer_support_email
|
|
27
|
+
TRANSLATABLE_FIELDS = %i[name meta_description meta_keywords seo_title customer_support_email
|
|
28
28
|
address contact_phone].freeze
|
|
29
29
|
translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
|
|
30
30
|
self::Translation.class_eval do
|
|
@@ -43,9 +43,6 @@ module Spree
|
|
|
43
43
|
preference :unit_system, :string, default: 'imperial'
|
|
44
44
|
# email preferences
|
|
45
45
|
preference :send_consumer_transactional_emails, :boolean, default: true
|
|
46
|
-
# SEO preferences
|
|
47
|
-
preference :index_in_search_engines, :boolean, default: false
|
|
48
|
-
preference :password_protected, :boolean, default: false
|
|
49
46
|
# Checkout preferences
|
|
50
47
|
preference :guest_checkout, :boolean, default: true
|
|
51
48
|
preference :special_instructions_enabled, :boolean, default: false
|
|
@@ -86,6 +83,7 @@ module Spree
|
|
|
86
83
|
|
|
87
84
|
has_many :taxonomies, class_name: 'Spree::Taxonomy'
|
|
88
85
|
has_many :taxons, through: :taxonomies, class_name: 'Spree::Taxon'
|
|
86
|
+
has_many :categories, through: :taxonomies, class_name: 'Spree::Category', source: :taxons
|
|
89
87
|
|
|
90
88
|
has_many :store_promotions, class_name: 'Spree::StorePromotion'
|
|
91
89
|
has_many :promotions, through: :store_promotions, class_name: 'Spree::Promotion'
|
|
@@ -111,21 +109,6 @@ module Spree
|
|
|
111
109
|
|
|
112
110
|
has_many :api_keys, class_name: 'Spree::ApiKey', dependent: :destroy
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
# ActionText
|
|
117
|
-
#
|
|
118
|
-
has_rich_text :checkout_message
|
|
119
|
-
has_rich_text :customer_terms_of_service
|
|
120
|
-
has_rich_text :customer_privacy_policy
|
|
121
|
-
has_rich_text :customer_returns_policy
|
|
122
|
-
has_rich_text :customer_shipping_policy
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
# Virtual attributes
|
|
126
|
-
#
|
|
127
|
-
store_accessor :private_metadata, :storefront_password
|
|
128
|
-
|
|
129
112
|
#
|
|
130
113
|
# Validations
|
|
131
114
|
#
|
|
@@ -137,19 +120,17 @@ module Spree
|
|
|
137
120
|
validates :mail_from_address, email: { allow_blank: false }
|
|
138
121
|
# FIXME: we should remove this condition in v5
|
|
139
122
|
if !ENV['SPREE_DISABLE_DB_CONNECTION'] &&
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
123
|
+
connected? &&
|
|
124
|
+
table_exists? &&
|
|
125
|
+
connection.column_exists?(:spree_stores, :new_order_notifications_email)
|
|
143
126
|
validates :new_order_notifications_email, email: { allow_blank: true }
|
|
144
127
|
end
|
|
145
|
-
validates :
|
|
128
|
+
validates :mailer_logo, content_type: Rails.application.config.active_storage.web_image_content_types
|
|
146
129
|
|
|
147
130
|
#
|
|
148
131
|
# Attachments
|
|
149
132
|
#
|
|
150
133
|
has_one_attached :logo, service: Spree.public_storage_service_name
|
|
151
|
-
has_one_attached :favicon_image, service: Spree.public_storage_service_name
|
|
152
|
-
has_one_attached :social_image, service: Spree.public_storage_service_name
|
|
153
134
|
has_one_attached :mailer_logo, service: Spree.public_storage_service_name
|
|
154
135
|
|
|
155
136
|
#
|
|
@@ -157,8 +138,6 @@ module Spree
|
|
|
157
138
|
before_validation :set_default_code, on: :create
|
|
158
139
|
before_save :ensure_default_exists_and_is_unique
|
|
159
140
|
after_create :ensure_default_market
|
|
160
|
-
after_create :ensure_default_taxonomies_are_created
|
|
161
|
-
after_create :ensure_default_automatic_taxons
|
|
162
141
|
after_create :create_default_policies
|
|
163
142
|
|
|
164
143
|
#
|
|
@@ -233,16 +212,6 @@ module Spree
|
|
|
233
212
|
@default_country_for_market = country
|
|
234
213
|
end
|
|
235
214
|
|
|
236
|
-
def seo_meta_description
|
|
237
|
-
if meta_description.present?
|
|
238
|
-
meta_description
|
|
239
|
-
elsif seo_title.present?
|
|
240
|
-
seo_title
|
|
241
|
-
else
|
|
242
|
-
name
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
215
|
def unique_name
|
|
247
216
|
@unique_name ||= "#{name} (#{code})"
|
|
248
217
|
end
|
|
@@ -312,18 +281,18 @@ module Spree
|
|
|
312
281
|
# @return [ActiveRecord::Relation<Spree::Country>]
|
|
313
282
|
def countries_with_shipping_coverage
|
|
314
283
|
zone_ids = Spree::Zone
|
|
315
|
-
|
|
316
|
-
|
|
284
|
+
.joins(:shipping_methods)
|
|
285
|
+
.select(:id)
|
|
317
286
|
|
|
318
287
|
country_zone_country_ids = Spree::ZoneMember
|
|
319
|
-
|
|
320
|
-
|
|
288
|
+
.where(zone_id: zone_ids, zoneable_type: 'Spree::Country')
|
|
289
|
+
.select(:zoneable_id)
|
|
321
290
|
|
|
322
291
|
state_zone_country_ids = Spree::State
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
292
|
+
.where(id: Spree::ZoneMember
|
|
293
|
+
.where(zone_id: zone_ids, zoneable_type: 'Spree::State')
|
|
294
|
+
.select(:zoneable_id))
|
|
295
|
+
.select(:country_id)
|
|
327
296
|
|
|
328
297
|
Spree::Country
|
|
329
298
|
.where(id: country_zone_country_ids)
|
|
@@ -337,7 +306,8 @@ module Spree
|
|
|
337
306
|
@default_stock_location ||= begin
|
|
338
307
|
stock_location_scope = Spree::StockLocation.where(default: true)
|
|
339
308
|
stock_location_scope.first || ActiveRecord::Base.connected_to(role: :writing) do
|
|
340
|
-
stock_location_scope.create(default: true, name: Spree.t(:default_stock_location_name),
|
|
309
|
+
stock_location_scope.create(default: true, name: Spree.t(:default_stock_location_name),
|
|
310
|
+
country: default_country)
|
|
341
311
|
end
|
|
342
312
|
end
|
|
343
313
|
end
|
|
@@ -348,12 +318,6 @@ module Spree
|
|
|
348
318
|
users
|
|
349
319
|
end
|
|
350
320
|
|
|
351
|
-
def favicon
|
|
352
|
-
return unless favicon_image.attached? && favicon_image.variable?
|
|
353
|
-
|
|
354
|
-
favicon_image.variant(resize_to_limit: [32, 32])
|
|
355
|
-
end
|
|
356
|
-
|
|
357
321
|
def metric_unit_system?
|
|
358
322
|
preferred_unit_system == 'metric'
|
|
359
323
|
end
|
|
@@ -366,14 +330,6 @@ module Spree
|
|
|
366
330
|
@digital_shipping_category ||= ShippingCategory.find_or_create_by(name: 'Digital')
|
|
367
331
|
end
|
|
368
332
|
|
|
369
|
-
%w[customer_terms_of_service customer_privacy_policy customer_returns_policy customer_shipping_policy].each do |policy_method|
|
|
370
|
-
define_method policy_method do
|
|
371
|
-
Spree::Deprecation.warn("Store##{policy_method} is deprecated and will be removed in Spree 5.5. Please use Store#policies instead.")
|
|
372
|
-
|
|
373
|
-
ActionText::RichText.find_by(name: policy_method, record: self)
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
|
|
377
333
|
private
|
|
378
334
|
|
|
379
335
|
def ensure_default_market
|
|
@@ -395,7 +351,25 @@ module Spree
|
|
|
395
351
|
end
|
|
396
352
|
end
|
|
397
353
|
|
|
354
|
+
def create_default_policies
|
|
355
|
+
Spree::Events.disable do
|
|
356
|
+
[
|
|
357
|
+
translate_with_store_locale_fallback('spree.terms_of_service'),
|
|
358
|
+
translate_with_store_locale_fallback('spree.privacy_policy'),
|
|
359
|
+
translate_with_store_locale_fallback('spree.returns_policy'),
|
|
360
|
+
translate_with_store_locale_fallback('spree.shipping_policy')
|
|
361
|
+
].each do |policy_name|
|
|
362
|
+
# Manual exists?/create to work around Mobility bug with find_or_create_by
|
|
363
|
+
next if policies.with_matching_name(policy_name).exists?
|
|
364
|
+
|
|
365
|
+
policies.create(name: policy_name)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
398
370
|
def ensure_default_taxonomies_are_created
|
|
371
|
+
Spree::Deprecation.warn('Store#ensure_default_taxonomies_are_created is deprecated and will be removed in Spree 5.5. Please remove it from your codebase')
|
|
372
|
+
|
|
399
373
|
Spree::Events.disable do
|
|
400
374
|
[
|
|
401
375
|
translate_with_store_locale_fallback('spree.taxonomy_categories_name'),
|
|
@@ -411,13 +385,16 @@ module Spree
|
|
|
411
385
|
end
|
|
412
386
|
|
|
413
387
|
def ensure_default_automatic_taxons
|
|
388
|
+
Spree::Deprecation.warn('Store#ensure_default_automatic_taxons is deprecated and will be removed in Spree 5.5. Please remove it from your codebase')
|
|
389
|
+
|
|
414
390
|
Spree::Events.disable do
|
|
415
391
|
# Use Mobility-safe lookup for taxonomy
|
|
416
392
|
collections_taxonomy = taxonomies.with_matching_name(translate_with_store_locale_fallback('spree.taxonomy_collections_name')).first
|
|
417
393
|
return unless collections_taxonomy.present?
|
|
418
394
|
|
|
419
395
|
automatic_taxons_config = [
|
|
420
|
-
{ name: translate_with_store_locale_fallback('spree.automatic_taxon_names.on_sale'),
|
|
396
|
+
{ name: translate_with_store_locale_fallback('spree.automatic_taxon_names.on_sale'),
|
|
397
|
+
rule_type: 'Spree::TaxonRules::Sale', rule_value: 'true' },
|
|
421
398
|
{ name: translate_with_store_locale_fallback('spree.automatic_taxon_names.new_arrivals'), rule_type: 'Spree::TaxonRules::AvailableOn', rule_value: 30 }
|
|
422
399
|
]
|
|
423
400
|
|
|
@@ -439,22 +416,6 @@ module Spree
|
|
|
439
416
|
end
|
|
440
417
|
end
|
|
441
418
|
|
|
442
|
-
def create_default_policies
|
|
443
|
-
Spree::Events.disable do
|
|
444
|
-
[
|
|
445
|
-
translate_with_store_locale_fallback('spree.terms_of_service'),
|
|
446
|
-
translate_with_store_locale_fallback('spree.privacy_policy'),
|
|
447
|
-
translate_with_store_locale_fallback('spree.returns_policy'),
|
|
448
|
-
translate_with_store_locale_fallback('spree.shipping_policy')
|
|
449
|
-
].each do |policy_name|
|
|
450
|
-
# Manual exists?/create to work around Mobility bug with find_or_create_by
|
|
451
|
-
next if policies.with_matching_name(policy_name).exists?
|
|
452
|
-
|
|
453
|
-
policies.create(name: policy_name)
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
end
|
|
457
|
-
|
|
458
419
|
# Translates a key using the store's default locale with fallback to :en
|
|
459
420
|
def translate_with_store_locale_fallback(key)
|
|
460
421
|
locale = default_locale.presence&.to_sym || :en
|
data/app/models/spree/taxon.rb
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
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
|
-
SORT_ORDERS =
|
|
9
|
-
manual
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
]
|
|
10
|
+
SORT_ORDERS = [
|
|
11
|
+
'manual',
|
|
12
|
+
'best_selling',
|
|
13
|
+
'price asc',
|
|
14
|
+
'price desc',
|
|
15
|
+
'available_on desc',
|
|
16
|
+
'available_on asc',
|
|
17
|
+
'name asc',
|
|
18
|
+
'name desc'
|
|
19
|
+
].freeze
|
|
18
20
|
|
|
19
21
|
include Spree::TranslatableResource
|
|
20
22
|
include Spree::TranslatableResourceSlug
|
|
@@ -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
|
data/app/models/spree/variant.rb
CHANGED
|
@@ -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
|
data/app/models/spree/zone.rb
CHANGED
|
@@ -52,15 +52,27 @@ module Spree
|
|
|
52
52
|
|
|
53
53
|
# Returns the matching zone with the highest priority zone type (State, Country, Zone.)
|
|
54
54
|
# Returns nil in the case of no matches.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
# Accepts either an address (with country_id/state_id) or a Spree::Country directly.
|
|
56
|
+
def self.match(address_or_country)
|
|
57
|
+
return unless address_or_country
|
|
58
|
+
|
|
59
|
+
if address_or_country.is_a?(Spree::Country)
|
|
60
|
+
country_id = address_or_country.id
|
|
61
|
+
state_id = nil
|
|
62
|
+
else
|
|
63
|
+
country_id = address_or_country.country_id
|
|
64
|
+
state_id = address_or_country.state_id
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
matches = includes(:zone_members).
|
|
68
|
+
order('spree_zones.zone_members_count', 'spree_zones.created_at').
|
|
69
|
+
where("(spree_zone_members.zoneable_type = 'Spree::Country' AND " \
|
|
70
|
+
'spree_zone_members.zoneable_id = ?) OR ' \
|
|
71
|
+
"(spree_zone_members.zoneable_type = 'Spree::State' AND " \
|
|
72
|
+
'spree_zone_members.zoneable_id = ?)', country_id, state_id).
|
|
73
|
+
references(:zones)
|
|
74
|
+
|
|
75
|
+
return if matches.empty?
|
|
64
76
|
|
|
65
77
|
%w[state country].each do |zone_kind|
|
|
66
78
|
if match = matches.detect { |zone| zone_kind == zone.kind }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module CSV
|
|
3
|
+
class CouponCodePresenter
|
|
4
|
+
HEADERS = [
|
|
5
|
+
'Code',
|
|
6
|
+
'State',
|
|
7
|
+
'Promotion Name',
|
|
8
|
+
'Order Number',
|
|
9
|
+
'Created At',
|
|
10
|
+
'Updated At'
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(coupon_code)
|
|
14
|
+
@coupon_code = coupon_code
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_accessor :coupon_code
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
[
|
|
21
|
+
coupon_code.display_code,
|
|
22
|
+
coupon_code.state,
|
|
23
|
+
coupon_code.promotion&.name,
|
|
24
|
+
coupon_code.order&.number,
|
|
25
|
+
coupon_code.created_at&.strftime('%Y-%m-%d %H:%M:%S'),
|
|
26
|
+
coupon_code.updated_at&.strftime('%Y-%m-%d %H:%M:%S')
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
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
|