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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2a4eae0b4e102ba4160e435fe81d656ced27b014ed013670226bb8a522b5996
|
|
4
|
+
data.tar.gz: c8c6737253472a4456f29b06f1ae028b2c4144732fba6dae7d8dd8c5e2b92d8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec9479d52b6bcbd036354511897d0ee360da7784aef051f20affe23991b1eaebe8525b7a0173fbc7cdc1310a977909e4b78a33b0cd677869d579d1666bbada7a
|
|
7
|
+
data.tar.gz: 44b40ddca311ed24ae26cd99b5455a7259a0aa986cb8a9a76598101e4c42ea48042b8f1256365b2734c7e43218337ec7963c78b0bcffc5ab70f62e0f794d4b9e
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
require 'open-uri'
|
|
2
2
|
require 'openssl'
|
|
3
|
+
require 'ssrf_filter'
|
|
4
|
+
require 'tempfile'
|
|
3
5
|
|
|
4
6
|
module Spree
|
|
5
7
|
module Images
|
|
6
8
|
class SaveFromUrlJob < ::Spree::BaseJob
|
|
7
9
|
queue_as Spree.queues.images
|
|
8
|
-
retry_on ActiveRecord::RecordInvalid,
|
|
10
|
+
retry_on ActiveRecord::RecordInvalid, wait: :polynomially_longer, attempts: Spree::Config.images_save_from_url_job_attempts.to_i
|
|
9
11
|
discard_on URI::InvalidURIError
|
|
12
|
+
discard_on SsrfFilter::Error
|
|
10
13
|
|
|
11
14
|
def perform(viewable_id, viewable_type, external_url, external_id = nil, position = nil)
|
|
12
15
|
viewable = viewable_type.safe_constantize.find(viewable_id)
|
|
@@ -29,34 +32,55 @@ module Spree
|
|
|
29
32
|
# still trigger save! if position has changed
|
|
30
33
|
image.save! and return if image_already_saved?(image, external_url)
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
unless %w[http https].include?(uri.scheme)
|
|
34
|
-
raise URI::InvalidURIError, "Invalid URL scheme: #{uri.scheme}. Only http and https are allowed."
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
file = uri.open(
|
|
38
|
-
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
39
|
-
'Accept' => 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
|
40
|
-
'Accept-Language' => 'en-US,en;q=0.9',
|
|
41
|
-
'Accept-Encoding' => 'gzip, deflate, br',
|
|
42
|
-
'Cache-Control' => 'no-cache',
|
|
43
|
-
'Pragma' => 'no-cache',
|
|
44
|
-
read_timeout: 60,
|
|
45
|
-
ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
46
|
-
redirect: true
|
|
47
|
-
)
|
|
48
|
-
filename = File.basename(uri.path)
|
|
49
|
-
|
|
50
|
-
image.attachment.attach(io: file, filename: filename)
|
|
51
|
-
image.external_url = external_url
|
|
52
|
-
image.external_id = external_id if external_id.present? && image.respond_to?(:external_id)
|
|
53
|
-
image.save!
|
|
35
|
+
download_and_attach_image(external_url, image, external_id)
|
|
54
36
|
rescue ActiveStorage::IntegrityError => e
|
|
55
37
|
raise e unless Rails.env.test?
|
|
56
38
|
end
|
|
57
39
|
|
|
58
40
|
private
|
|
59
41
|
|
|
42
|
+
def download_and_attach_image(external_url, image, external_id)
|
|
43
|
+
max_size = Spree::Config.max_image_download_size
|
|
44
|
+
|
|
45
|
+
response = SsrfFilter.get(
|
|
46
|
+
external_url,
|
|
47
|
+
headers: {
|
|
48
|
+
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
49
|
+
'Accept' => 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
|
50
|
+
'Accept-Language' => 'en-US,en;q=0.9',
|
|
51
|
+
'Accept-Encoding' => 'gzip, deflate, br',
|
|
52
|
+
'Cache-Control' => 'no-cache',
|
|
53
|
+
'Pragma' => 'no-cache'
|
|
54
|
+
},
|
|
55
|
+
http_options: {
|
|
56
|
+
read_timeout: 60,
|
|
57
|
+
open_timeout: 30
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
body = response.body
|
|
62
|
+
if body.bytesize > max_size
|
|
63
|
+
raise StandardError, "Image file size exceeds the maximum allowed size of #{max_size} bytes"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
uri = URI.parse(external_url)
|
|
67
|
+
filename = File.basename(uri.path)
|
|
68
|
+
tempfile = Tempfile.new(['spree_image', File.extname(uri.path)], binmode: true)
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
tempfile.write(body)
|
|
72
|
+
tempfile.rewind
|
|
73
|
+
|
|
74
|
+
image.attachment.attach(io: tempfile, filename: filename)
|
|
75
|
+
image.external_url = external_url
|
|
76
|
+
image.external_id = external_id if external_id.present? && image.respond_to?(:external_id)
|
|
77
|
+
image.save!
|
|
78
|
+
ensure
|
|
79
|
+
tempfile.close
|
|
80
|
+
tempfile.unlink
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
60
84
|
def image_already_saved?(image, external_url)
|
|
61
85
|
image.persisted? && image.attachment.attached? && image.external_url.present? && external_url == image.external_url
|
|
62
86
|
end
|
|
@@ -41,32 +41,38 @@ module Spree
|
|
|
41
41
|
order(price_table_name => { amount: :desc })
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# Price sorting scopes that use
|
|
45
|
-
# These ensure products with only variant prices (no master price) are included in results
|
|
44
|
+
# Price sorting scopes that use a derived table JOIN to get prices across all variants.
|
|
45
|
+
# These ensure products with only variant prices (no master price) are included in results.
|
|
46
|
+
#
|
|
47
|
+
# Uses Arel::Nodes::As for select expressions so that:
|
|
48
|
+
# - PG allows ORDER BY with DISTINCT (expressions must appear in SELECT list)
|
|
49
|
+
# - Mobility's select_for_count can safely call .right on all select_values
|
|
46
50
|
add_search_scope :ascend_by_price do
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.select('MIN(amount)')
|
|
51
|
+
price_agg_sql = Price.non_zero.joins(:variant)
|
|
52
|
+
.select("#{Variant.table_name}.product_id AS product_id, MIN(#{Price.table_name}.amount) AS agg_price")
|
|
53
|
+
.group("#{Variant.table_name}.product_id")
|
|
54
|
+
.to_sql
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
price_expr = Arel.sql('COALESCE(price_agg.agg_price, 999999999)')
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
joins("LEFT JOIN (#{price_agg_sql}) AS price_agg ON price_agg.product_id = #{Product.table_name}.id").
|
|
59
|
+
select("#{Product.table_name}.*").
|
|
60
|
+
select(Arel::Nodes::As.new(price_expr, Arel.sql('min_price'))).
|
|
61
|
+
order(price_expr.asc)
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
add_search_scope :descend_by_price do
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.select('MAX(amount)')
|
|
65
|
+
price_agg_sql = Price.non_zero.joins(:variant)
|
|
66
|
+
.select("#{Variant.table_name}.product_id AS product_id, MAX(#{Price.table_name}.amount) AS agg_price")
|
|
67
|
+
.group("#{Variant.table_name}.product_id")
|
|
68
|
+
.to_sql
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
price_expr = Arel.sql('COALESCE(price_agg.agg_price, 0)')
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
joins("LEFT JOIN (#{price_agg_sql}) AS price_agg ON price_agg.product_id = #{Product.table_name}.id").
|
|
73
|
+
select("#{Product.table_name}.*").
|
|
74
|
+
select(Arel::Nodes::As.new(price_expr, Arel.sql('max_price'))).
|
|
75
|
+
order(price_expr.desc)
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
add_search_scope :price_between do |low, high|
|
|
@@ -81,6 +87,25 @@ module Spree
|
|
|
81
87
|
where(Price.table_name => { amount: price.. })
|
|
82
88
|
end
|
|
83
89
|
|
|
90
|
+
# Joins spree_variants and spree_stock_items directly (without association
|
|
91
|
+
# aliases) so that the table names stay as-is. This avoids alias conflicts
|
|
92
|
+
# when combined with other scopes (e.g., price sorting) that also join
|
|
93
|
+
# spree_variants through associations which generate aliases.
|
|
94
|
+
def self.join_variants_and_stock_items
|
|
95
|
+
joins("INNER JOIN #{Variant.table_name} ON #{Variant.table_name}.deleted_at IS NULL AND #{Variant.table_name}.product_id = #{Product.table_name}.id").
|
|
96
|
+
joins("LEFT OUTER JOIN #{StockItem.table_name} ON #{StockItem.table_name}.deleted_at IS NULL AND #{StockItem.table_name}.variant_id = #{Variant.table_name}.id")
|
|
97
|
+
end
|
|
98
|
+
private_class_method :join_variants_and_stock_items
|
|
99
|
+
|
|
100
|
+
# Mirrors Spree::Variant.in_stock_or_backorderable logic using raw table
|
|
101
|
+
# names (to pair with join_variants_and_stock_items).
|
|
102
|
+
scope :in_stock_or_backorderable_condition, -> {
|
|
103
|
+
where(
|
|
104
|
+
"#{Variant.table_name}.track_inventory = ? OR #{StockItem.table_name}.count_on_hand > ? OR #{StockItem.table_name}.backorderable = ?",
|
|
105
|
+
false, 0, true
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
# Can't use add_search_scope for this as it needs a default argument
|
|
85
110
|
# Ransack calls with '1' to activate, '0' or nil to skip
|
|
86
111
|
# In Ruby code: in_stock(true) for in-stock, in_stock(false) for out-of-stock
|
|
@@ -88,26 +113,34 @@ module Spree
|
|
|
88
113
|
if in_stock == '0' || !in_stock
|
|
89
114
|
all
|
|
90
115
|
else
|
|
91
|
-
|
|
116
|
+
join_variants_and_stock_items.in_stock_or_backorderable_condition
|
|
92
117
|
end
|
|
93
118
|
end
|
|
119
|
+
|
|
120
|
+
add_search_scope :price_lte do |price|
|
|
121
|
+
where(Price.table_name => { amount: ..price })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
add_search_scope :price_gte do |price|
|
|
125
|
+
where(Price.table_name => { amount: price.. })
|
|
126
|
+
end
|
|
94
127
|
search_scopes << :in_stock
|
|
95
128
|
|
|
96
129
|
def self.out_of_stock(out_of_stock = true)
|
|
97
130
|
if out_of_stock == '0' || !out_of_stock
|
|
98
131
|
all
|
|
99
132
|
else
|
|
100
|
-
where.not(id:
|
|
133
|
+
where.not(id: join_variants_and_stock_items.in_stock_or_backorderable_condition)
|
|
101
134
|
end
|
|
102
135
|
end
|
|
103
136
|
search_scopes << :out_of_stock
|
|
104
137
|
|
|
105
138
|
add_search_scope :backorderable do
|
|
106
|
-
|
|
139
|
+
join_variants_and_stock_items.where(StockItem.table_name => { backorderable: true })
|
|
107
140
|
end
|
|
108
141
|
|
|
109
142
|
add_search_scope :in_stock_or_backorderable do
|
|
110
|
-
|
|
143
|
+
join_variants_and_stock_items.in_stock_or_backorderable_condition
|
|
111
144
|
end
|
|
112
145
|
|
|
113
146
|
# This scope selects products in taxon AND all its descendants
|
|
@@ -132,6 +165,11 @@ module Spree
|
|
|
132
165
|
where("#{Classification.table_name}.taxon_id" => taxon.cached_self_and_descendants_ids).distinct
|
|
133
166
|
end
|
|
134
167
|
|
|
168
|
+
# Alias for in_taxon — public API name
|
|
169
|
+
add_search_scope :in_category do |category|
|
|
170
|
+
in_taxon(category)
|
|
171
|
+
end
|
|
172
|
+
|
|
135
173
|
# This scope selects products in all taxons AND all its descendants
|
|
136
174
|
# If you need products only within one taxon use
|
|
137
175
|
#
|
|
@@ -196,9 +234,7 @@ module Spree
|
|
|
196
234
|
|
|
197
235
|
return none if actual_ids.empty?
|
|
198
236
|
|
|
199
|
-
|
|
200
|
-
joins(variants: :option_values).
|
|
201
|
-
where(Spree::OptionValue.table_name => { id: actual_ids })
|
|
237
|
+
joins(variants: :option_values).where(Spree::OptionValue.table_name => { id: actual_ids })
|
|
202
238
|
end
|
|
203
239
|
|
|
204
240
|
# Finds all products which have an option value with the name matching the one given
|
|
@@ -260,7 +296,7 @@ module Spree
|
|
|
260
296
|
|
|
261
297
|
def self.not_discontinued(only_not_discontinued = true)
|
|
262
298
|
if only_not_discontinued != '0' && only_not_discontinued
|
|
263
|
-
where(discontinue_on: [nil, Time.current..])
|
|
299
|
+
where(discontinue_on: [nil, Time.current.beginning_of_minute..])
|
|
264
300
|
else
|
|
265
301
|
all
|
|
266
302
|
end
|
|
@@ -278,7 +314,10 @@ module Spree
|
|
|
278
314
|
# Can't use add_search_scope for this as it needs a default argument
|
|
279
315
|
def self.available(available_on = nil, currency = nil)
|
|
280
316
|
scope = not_discontinued.where(status: 'active')
|
|
281
|
-
|
|
317
|
+
if available_on
|
|
318
|
+
available_on = available_on.beginning_of_minute if available_on.respond_to?(:beginning_of_minute)
|
|
319
|
+
scope = scope.where("#{Product.quoted_table_name}.available_on <= ?", available_on)
|
|
320
|
+
end
|
|
282
321
|
|
|
283
322
|
unless Spree::Config.show_products_without_price
|
|
284
323
|
currency ||= Spree::Store.default.default_currency
|
|
@@ -314,25 +353,22 @@ module Spree
|
|
|
314
353
|
end
|
|
315
354
|
|
|
316
355
|
# Orders products by best selling based on units_sold_count and revenue
|
|
317
|
-
#
|
|
356
|
+
# from spree_products_stores (already joined via store.products).
|
|
318
357
|
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
# @param order_direction [Symbol] :desc (default) or :asc
|
|
323
|
-
# @return [ActiveRecord::Relation]
|
|
358
|
+
# Uses Arel::Nodes::As so that ORDER BY expressions appear in SELECT
|
|
359
|
+
# and work with DISTINCT (same pattern as the price sorting scopes).
|
|
324
360
|
add_search_scope :by_best_selling do |order_direction = :desc|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
conditions = sp_table[:product_id].eq(products_table[:id]).and(sp_table[:store_id].eq(store_id))
|
|
330
|
-
|
|
331
|
-
units_sold = Arel::Nodes::NamedFunction.new('COALESCE', [sp_table.project(sp_table[:units_sold_count]).where(conditions), 0])
|
|
332
|
-
revenue = Arel::Nodes::NamedFunction.new('COALESCE', [sp_table.project(sp_table[:revenue]).where(conditions), 0])
|
|
361
|
+
sp_table = StoreProduct.table_name
|
|
362
|
+
units_expr = Arel.sql("COALESCE(#{sp_table}.units_sold_count, 0)")
|
|
363
|
+
revenue_expr = Arel.sql("COALESCE(#{sp_table}.revenue, 0)")
|
|
333
364
|
|
|
334
365
|
order_dir = order_direction == :desc ? :desc : :asc
|
|
335
|
-
|
|
366
|
+
|
|
367
|
+
select("#{Product.table_name}.*").
|
|
368
|
+
select(Arel::Nodes::As.new(units_expr, Arel.sql('best_selling_units'))).
|
|
369
|
+
select(Arel::Nodes::As.new(revenue_expr, Arel.sql('best_selling_revenue'))).
|
|
370
|
+
order(units_expr.send(order_dir)).
|
|
371
|
+
order(revenue_expr.send(order_dir))
|
|
336
372
|
end
|
|
337
373
|
|
|
338
374
|
# .search_by_name
|
data/app/models/spree/address.rb
CHANGED
|
@@ -27,8 +27,6 @@ module Spree
|
|
|
27
27
|
# those attributes depending of the logic of their applications
|
|
28
28
|
ADDRESS_FIELDS = %w(firstname lastname company address1 address2 city state zipcode country phone)
|
|
29
29
|
EXCLUDED_KEYS_FOR_COMPARISON = %w(id updated_at created_at deleted_at label user_id public_metadata private_metadata)
|
|
30
|
-
FIELDS_TO_NORMALIZE = %w(firstname lastname phone alternative_phone company address1 address2 city zipcode)
|
|
31
|
-
|
|
32
30
|
if defined?(Spree::Security::Addresses)
|
|
33
31
|
include Spree::Security::Addresses
|
|
34
32
|
end
|
|
@@ -53,7 +51,6 @@ module Spree
|
|
|
53
51
|
before_validation :normalize_country
|
|
54
52
|
before_validation :normalize_state
|
|
55
53
|
before_validation :clear_invalid_state_entities, if: -> { country.present? }, on: :update
|
|
56
|
-
before_validation :remove_emoji_and_normalize
|
|
57
54
|
|
|
58
55
|
after_create :set_user_attributes, if: -> { user.present? }
|
|
59
56
|
|
|
@@ -294,17 +291,6 @@ module Spree
|
|
|
294
291
|
end
|
|
295
292
|
end
|
|
296
293
|
|
|
297
|
-
def remove_emoji_and_normalize
|
|
298
|
-
attributes_to_normalize = attributes.slice(*FIELDS_TO_NORMALIZE)
|
|
299
|
-
normalized_attributes = attributes_to_normalize.compact_blank.deep_transform_values do |value|
|
|
300
|
-
NormalizeString.remove_emoji_and_normalize(value.to_s).strip
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
normalized_attributes.transform_keys! { |key| key.gsub('original_', '') } if defined?(Spree::Security::Addresses)
|
|
304
|
-
|
|
305
|
-
assign_attributes(normalized_attributes)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
294
|
def set_user_attributes
|
|
309
295
|
if user.name.blank?
|
|
310
296
|
user.first_name = firstname
|
data/app/models/spree/country.rb
CHANGED
|
@@ -38,29 +38,8 @@ module Spree
|
|
|
38
38
|
iso.upcase.chars.map { |c| (c.ord + 127397).chr(Encoding::UTF_8) }.join
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# @return [String, nil] currency code (e.g., 'USD', 'EUR') or nil if no market found
|
|
45
|
-
def market_currency
|
|
46
|
-
current_market&.currency
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Returns the default locale for this country from its market in the current store.
|
|
50
|
-
# Looks up which market contains this country and returns that market's default locale.
|
|
51
|
-
#
|
|
52
|
-
# @return [String, nil] locale code (e.g., 'en', 'de') or nil if no market found
|
|
53
|
-
def market_locale
|
|
54
|
-
current_market&.default_locale
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Returns the supported locales for this country from its market in the current store.
|
|
58
|
-
#
|
|
59
|
-
# @return [Array<String>] locale codes (e.g., ['en', 'fr']) or empty array if no market found
|
|
60
|
-
def market_supported_locales
|
|
61
|
-
current_market&.supported_locales_list || []
|
|
62
|
-
end
|
|
63
|
-
|
|
41
|
+
# Lookups Market for Country for the current Store
|
|
42
|
+
# @return [Spree::Market, nil]
|
|
64
43
|
def current_market
|
|
65
44
|
@current_market ||= Spree::Current.store&.market_for_country(self)
|
|
66
45
|
end
|
|
@@ -19,7 +19,8 @@ module Spree
|
|
|
19
19
|
validates :code, presence: true, uniqueness: { scope: spree_base_uniqueness_scope, conditions: -> { where(deleted_at: nil) } }
|
|
20
20
|
validates :state, :promotion, presence: true
|
|
21
21
|
|
|
22
|
-
self.whitelisted_ransackable_attributes = %w[state code]
|
|
22
|
+
self.whitelisted_ransackable_attributes = %w[state code promotion_id]
|
|
23
|
+
self.whitelisted_ransackable_associations = %w[promotion]
|
|
23
24
|
|
|
24
25
|
def self.used?(code)
|
|
25
26
|
used_with_code(code).any?
|
|
@@ -36,5 +37,9 @@ module Spree
|
|
|
36
37
|
def display_code
|
|
37
38
|
code.upcase
|
|
38
39
|
end
|
|
40
|
+
|
|
41
|
+
def to_csv(_store = nil)
|
|
42
|
+
Spree::CSV::CouponCodePresenter.new(self).call
|
|
43
|
+
end
|
|
39
44
|
end
|
|
40
45
|
end
|
data/app/models/spree/current.rb
CHANGED
|
@@ -34,10 +34,11 @@ module Spree
|
|
|
34
34
|
super || market&.default_locale || store&.default_locale
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Returns the current tax zone
|
|
37
|
+
# Returns the current tax zone.
|
|
38
|
+
# Fallback: market's tax zone (from default country) -> global default tax zone.
|
|
38
39
|
# @return [Spree::Zone, nil]
|
|
39
40
|
def zone
|
|
40
|
-
super || default_tax_zone
|
|
41
|
+
super || market&.tax_zone || default_tax_zone
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# Returns the default tax zone (memoized per request).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Exports
|
|
3
|
+
class CouponCodes < Spree::Export
|
|
4
|
+
def csv_headers
|
|
5
|
+
Spree::CSV::CouponCodePresenter::HEADERS
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def scope_includes
|
|
9
|
+
[:promotion, :order]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def scope
|
|
13
|
+
model_class.joins(promotion: :stores).where(spree_stores: { id: store.id })
|
|
14
|
+
.accessible_by(current_ability)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/app/models/spree/market.rb
CHANGED
|
@@ -64,6 +64,14 @@ module Spree
|
|
|
64
64
|
countries.order(:name).first
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Returns the tax zone matching this market's default country.
|
|
68
|
+
# Used by Spree::Current to determine the browsing tax zone before a customer enters an address.
|
|
69
|
+
#
|
|
70
|
+
# @return [Spree::Zone, nil]
|
|
71
|
+
def tax_zone
|
|
72
|
+
@tax_zone ||= Spree::Zone.match(default_country)
|
|
73
|
+
end
|
|
74
|
+
|
|
67
75
|
# Returns supported locales as an array, always including default_locale
|
|
68
76
|
#
|
|
69
77
|
# @return [Array<String>]
|
|
@@ -34,6 +34,10 @@ module Spree
|
|
|
34
34
|
payment.number
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def idempotency_key
|
|
38
|
+
"spree-#{payment.number}"
|
|
39
|
+
end
|
|
40
|
+
|
|
37
41
|
def shipping
|
|
38
42
|
order.ship_total * exchange_multiplier
|
|
39
43
|
end
|
|
@@ -66,6 +70,7 @@ module Spree
|
|
|
66
70
|
:ip,
|
|
67
71
|
:order_id,
|
|
68
72
|
:payment_id,
|
|
73
|
+
:idempotency_key,
|
|
69
74
|
:shipping,
|
|
70
75
|
:tax,
|
|
71
76
|
:subtotal,
|
data/app/models/spree/product.rb
CHANGED
|
@@ -40,9 +40,9 @@ module Spree
|
|
|
40
40
|
|
|
41
41
|
publishes_lifecycle_events
|
|
42
42
|
|
|
43
|
-
MEMOIZED_METHODS = %w[total_on_hand taxonomy_ids taxon_and_ancestors
|
|
43
|
+
MEMOIZED_METHODS = %w[total_on_hand taxonomy_ids taxon_and_ancestors
|
|
44
44
|
default_variant_id tax_category default_variant variant_for_images
|
|
45
|
-
|
|
45
|
+
brand_taxon main_taxon
|
|
46
46
|
purchasable? in_stock? backorderable? digital?]
|
|
47
47
|
|
|
48
48
|
STATUSES = %w[draft active archived].freeze
|
|
@@ -74,6 +74,7 @@ module Spree
|
|
|
74
74
|
has_many :option_types, through: :product_option_types
|
|
75
75
|
has_many :classifications, -> { order(created_at: :asc) }, dependent: :delete_all, inverse_of: :product
|
|
76
76
|
has_many :taxons, through: :classifications, before_remove: :remove_taxon
|
|
77
|
+
has_many :categories, through: :classifications, class_name: 'Spree::Category', source: :taxon
|
|
77
78
|
has_many :taxonomies, through: :taxons
|
|
78
79
|
|
|
79
80
|
has_many :product_promotion_rules, class_name: 'Spree::ProductPromotionRule'
|
|
@@ -217,10 +218,12 @@ module Spree
|
|
|
217
218
|
alias options product_option_types
|
|
218
219
|
|
|
219
220
|
self.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status available_on created_at updated_at]
|
|
220
|
-
self.whitelisted_ransackable_associations = %w[taxons stores variants_including_master master variants tags labels
|
|
221
|
+
self.whitelisted_ransackable_associations = %w[taxons categories stores variants_including_master master variants tags labels
|
|
221
222
|
shipping_category classifications option_types]
|
|
222
223
|
self.whitelisted_ransackable_scopes = %w[not_discontinued search_by_name in_taxon price_between
|
|
224
|
+
price_lte price_gte
|
|
223
225
|
multi_search in_stock out_of_stock with_option_value_ids
|
|
226
|
+
|
|
224
227
|
ascend_by_price descend_by_price]
|
|
225
228
|
|
|
226
229
|
[
|
|
@@ -554,40 +557,10 @@ module Spree
|
|
|
554
557
|
brand&.name
|
|
555
558
|
end
|
|
556
559
|
|
|
557
|
-
# Returns the category for the product
|
|
558
|
-
# If a category association is defined (e.g., belongs_to :category), it will be used
|
|
559
|
-
# Otherwise, falls back to category_taxon for compatibility
|
|
560
|
-
# @return [Spree::Category, Spree::Taxon]
|
|
561
|
-
def category
|
|
562
|
-
if self.class.reflect_on_association(:category)
|
|
563
|
-
super
|
|
564
|
-
else
|
|
565
|
-
Spree::Deprecation.warn('Spree::Product#category is deprecated and will be removed in Spree 5.5. Please use Spree::Product#category_taxon instead.')
|
|
566
|
-
category_taxon
|
|
567
|
-
end
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
# Returns the category taxon for the product
|
|
571
|
-
# @return [Spree::Taxon]
|
|
572
|
-
def category_taxon
|
|
573
|
-
@category_taxon ||= if classification_count.zero?
|
|
574
|
-
nil
|
|
575
|
-
elsif Spree.use_translations?
|
|
576
|
-
taxons.joins(:taxonomy).
|
|
577
|
-
join_translation_table(Taxonomy).
|
|
578
|
-
order(depth: :desc).
|
|
579
|
-
find_by(Taxonomy.translation_table_alias => { name: Spree.t(:taxonomy_categories_name) })
|
|
580
|
-
elsif taxons.loaded?
|
|
581
|
-
taxons.find { |taxon| taxon.taxonomy.name == Spree.t(:taxonomy_categories_name) }
|
|
582
|
-
else
|
|
583
|
-
taxons.joins(:taxonomy).order(depth: :desc).find_by(Taxonomy.table_name => { name: Spree.t(:taxonomy_categories_name) })
|
|
584
|
-
end
|
|
585
|
-
end
|
|
586
|
-
|
|
587
560
|
def main_taxon
|
|
588
561
|
return if classification_count.zero?
|
|
589
562
|
|
|
590
|
-
@main_taxon ||=
|
|
563
|
+
@main_taxon ||= taxons.first
|
|
591
564
|
end
|
|
592
565
|
|
|
593
566
|
def taxons_for_store(store)
|