spree_core 5.4.0.rc1 → 5.4.0.rc3
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/assets/images/payment_icons/banktransfer.svg +1 -3
- data/app/models/concerns/spree/product_scopes.rb +10 -0
- data/app/models/concerns/spree/search_indexable.rb +1 -1
- data/app/models/spree/address.rb +15 -1
- data/app/models/spree/order/gift_card.rb +23 -3
- data/app/models/spree/order.rb +18 -1
- data/app/models/spree/price.rb +29 -0
- data/app/models/spree/price_history.rb +29 -0
- data/app/models/spree/promotion_handler/coupon.rb +9 -2
- data/app/models/spree/search_provider/meilisearch.rb +129 -110
- data/app/presenters/spree/search_provider/product_presenter.rb +0 -6
- data/app/services/spree/addresses/create.rb +2 -2
- data/app/services/spree/addresses/update.rb +2 -2
- data/app/services/spree/cart/remove_out_of_stock_items.rb +20 -5
- data/app/services/spree/carts/update.rb +9 -7
- data/app/services/spree/checkout/add_store_credit.rb +26 -2
- data/config/locales/en.yml +1 -0
- data/db/migrate/20260323000000_create_spree_price_histories.rb +20 -0
- data/lib/spree/core/configuration.rb +2 -0
- data/lib/spree/core/dependencies.rb +4 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/testing_support/factories/price_history_factory.rb +9 -0
- data/lib/tasks/price_history.rake +36 -0
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: feb2c702a17f0e10532d677c7c6cdb4a4092476ce33966db067ff095e0fd3d51
|
|
4
|
+
data.tar.gz: 6cfb1613127976496083e9d11d5a073e20c7d94b139d2df2f621e4c18ae52936
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8abff3fa43c5226f1eb1c3dd1a5d1bd84fedfd9ef62895da5df6402df1bd9e0fcb68a9fd646790d90ea5efa04292330aeab50e9269d1352a8970c2c8ff362d39
|
|
7
|
+
data.tar.gz: 2cde221d1575d9d48c633d4aacdc588d775aff1445c0bdc5c895ff82e77444e3a5341d5fc1677d6c5013accd73f182f1e2ea5bb5b9532470d13dae4a7e316061
|
|
@@ -1,3 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="
|
|
2
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z" />
|
|
3
|
-
</svg>
|
|
1
|
+
<svg width="38" height="24" viewBox="0 0 38 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="pi-banktransfer"><title id="pi-banktransfer">Bank Transfer</title><g clip-path="url(#pi-banktransfer-clip)"><path opacity=".07" d="M35 0H3C1.3 0 0 1.3 0 3v18c0 1.7 1.4 3 3 3h32c1.7 0 3-1.3 3-3V3c0-1.7-1.4-3-3-3z" fill="#000"/><path d="M35 1c1.1 0 2 .9 2 2v18c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V3c0-1.1.9-2 2-2h32z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M10 7l-4 2v1h8V9L10 7zM7 10.75v3.5h1v-3.5H7zm2 0v3.5h1v-3.5H9zm2 0v3.5h1.5v-3.5H11zM6 15v1h8v-1H6zM28 7l-4 2v1h8V9l-4-2zm-3 3.75v3.5h1v-3.5h-1zm2 0v3.5h1v-3.5h-1zm2 0v3.5h1.5v-3.5H29zM24 15v1h8v-1h-8z" fill="#4A4A4A"/><path d="M15.5 10h7M15.5 10l1.5-1.25M15.5 10l1.5 1.25" stroke="#4A4A4A" stroke-width=".9" stroke-linecap="round" stroke-linejoin="round"/><path d="M22.5 14h-7M22.5 14l-1.5-1.25M22.5 14l-1.5 1.25" stroke="#4A4A4A" stroke-width=".9" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="pi-banktransfer-clip"><path fill="#fff" d="M0 0h38v24H0z"/></clipPath></defs></svg>
|
|
@@ -3,6 +3,16 @@ module Spree
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
+
cattr_accessor :search_scopes do
|
|
7
|
+
[]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.add_search_scope(name, &block)
|
|
11
|
+
Spree::Deprecation.warn("add_search_scope is deprecated and will be removed in Spree 6.0. Use scope :#{name}, &block instead or use method instead")
|
|
12
|
+
singleton_class.send(:define_method, name.to_sym, &block)
|
|
13
|
+
search_scopes << name.to_sym
|
|
14
|
+
end
|
|
15
|
+
|
|
6
16
|
scope :ascend_by_updated_at, -> { order("#{Product.quoted_table_name}.updated_at ASC") }
|
|
7
17
|
scope :descend_by_updated_at, -> { order("#{Product.quoted_table_name}.updated_at DESC") }
|
|
8
18
|
scope :ascend_by_name, -> { order("#{Product.quoted_table_name}.name ASC") }
|
|
@@ -29,7 +29,7 @@ module Spree
|
|
|
29
29
|
# => { id: 1, name: "Shirt", price_USD: 19.99, ... }
|
|
30
30
|
def search_presentation(store = nil)
|
|
31
31
|
store ||= Spree::Current.store
|
|
32
|
-
Spree::
|
|
32
|
+
Spree::Dependencies.search_product_presenter_class.new(self, store).call
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Remove this record from search index synchronously (inline, no job).
|
data/app/models/spree/address.rb
CHANGED
|
@@ -106,12 +106,26 @@ module Spree
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def user_default_billing?
|
|
109
|
-
|
|
109
|
+
Spree::Deprecation.warn('Spree::Address#user_default_billing? is deprecated and will be removed in Spree 6.0. Use #is_default_billing? instead.')
|
|
110
|
+
is_default_billing?
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def user_default_shipping?
|
|
114
|
+
Spree::Deprecation.warn('Spree::Address#user_default_shipping? is deprecated and will be removed in Spree 6.0. Use #is_default_shipping? instead.')
|
|
115
|
+
is_default_shipping?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# In 6.0 these become real columns on Address, replacing User#bill_address_id / ship_address_id.
|
|
119
|
+
# For now they delegate to the User FK so the API shape is stable.
|
|
120
|
+
def is_default_billing?
|
|
121
|
+
user.present? && id == user.bill_address_id
|
|
122
|
+
end
|
|
123
|
+
alias_method :is_default_billing, :is_default_billing?
|
|
124
|
+
|
|
125
|
+
def is_default_shipping?
|
|
113
126
|
user.present? && id == user.ship_address_id
|
|
114
127
|
end
|
|
128
|
+
alias_method :is_default_shipping, :is_default_shipping?
|
|
115
129
|
|
|
116
130
|
# first_name / last_name aliases are defined via alias_attribute above
|
|
117
131
|
|
|
@@ -34,11 +34,31 @@ module Spree
|
|
|
34
34
|
Spree.gift_card_remove_service.call(order: self)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Recalculates the gift card payment amount based on the current order total.
|
|
38
|
+
# Updates the existing payment in place instead of remove + re-apply
|
|
39
|
+
# to avoid creating unnecessary invalid payment records.
|
|
37
40
|
def recalculate_gift_card
|
|
38
|
-
|
|
41
|
+
return unless gift_card.present?
|
|
42
|
+
|
|
43
|
+
payment = payments.checkout.store_credits.where(source: gift_card.store_credits).first
|
|
44
|
+
return unless payment
|
|
45
|
+
|
|
46
|
+
# with_lock acquires a row lock and wraps in a transaction.
|
|
47
|
+
# The entire read-compute-write must be inside the lock to prevent
|
|
48
|
+
# stale amount_remaining from concurrent requests.
|
|
49
|
+
gift_card.with_lock do
|
|
50
|
+
new_amount = [gift_card.amount_remaining + payment.amount, total].min
|
|
51
|
+
next if payment.amount == new_amount
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
difference = new_amount - payment.amount
|
|
54
|
+
# Uses update_column to bypass Payment#max_amount validation which
|
|
55
|
+
# can fail during recalculation due to stale in-memory order state.
|
|
56
|
+
# Bounds are enforced via min() above.
|
|
57
|
+
payment.update_column(:amount, new_amount)
|
|
58
|
+
payment.source.update_column(:amount, new_amount)
|
|
59
|
+
gift_card.amount_used += difference
|
|
60
|
+
gift_card.save!
|
|
61
|
+
end
|
|
42
62
|
end
|
|
43
63
|
|
|
44
64
|
def redeem_gift_card
|
data/app/models/spree/order.rb
CHANGED
|
@@ -49,10 +49,27 @@ module Spree
|
|
|
49
49
|
:included_tax_total, :additional_tax_total, :tax_total,
|
|
50
50
|
:shipment_total, :promo_total, :total,
|
|
51
51
|
:cart_promo_total, :pre_tax_item_amount, :pre_tax_total,
|
|
52
|
-
:payment_total
|
|
52
|
+
:payment_total, :amount_due
|
|
53
53
|
|
|
54
54
|
alias display_ship_total display_shipment_total
|
|
55
55
|
alias_attribute :ship_total, :shipment_total
|
|
56
|
+
def amount_due
|
|
57
|
+
outstanding_balance
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Transient warnings populated by remove_out_of_stock_items!
|
|
61
|
+
attribute :warnings, default: -> { [] }
|
|
62
|
+
|
|
63
|
+
# Removes out-of-stock/discontinued items and populates warnings.
|
|
64
|
+
# Returns self (reloaded if items were removed) with warnings set.
|
|
65
|
+
def remove_out_of_stock_items!
|
|
66
|
+
result = Spree::Cart::RemoveOutOfStockItems.call(order: self)
|
|
67
|
+
return self unless result.success?
|
|
68
|
+
|
|
69
|
+
order, _messages, warnings = result.value
|
|
70
|
+
order.warnings = warnings || []
|
|
71
|
+
order
|
|
72
|
+
end
|
|
56
73
|
|
|
57
74
|
# 5.5 API naming bridges (DB columns rename in 6.0)
|
|
58
75
|
alias_attribute :discount_total, :promo_total
|
data/app/models/spree/price.rb
CHANGED
|
@@ -13,8 +13,11 @@ module Spree
|
|
|
13
13
|
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: :prices, touch: true
|
|
14
14
|
belongs_to :price_list, class_name: 'Spree::PriceList', optional: true
|
|
15
15
|
|
|
16
|
+
has_many :price_histories, class_name: 'Spree::PriceHistory', dependent: :delete_all
|
|
17
|
+
|
|
16
18
|
before_validation :ensure_currency
|
|
17
19
|
before_save :remove_compare_at_amount_if_equals_amount
|
|
20
|
+
after_save :record_price_history, if: :should_record_price_history?
|
|
18
21
|
|
|
19
22
|
# legacy behavior
|
|
20
23
|
validates :amount, allow_nil: true, numericality: {
|
|
@@ -156,8 +159,34 @@ module Spree
|
|
|
156
159
|
!zero?
|
|
157
160
|
end
|
|
158
161
|
|
|
162
|
+
# Returns the price history record with the lowest amount in the last 30 days
|
|
163
|
+
# Used for EU Omnibus Directive compliance
|
|
164
|
+
#
|
|
165
|
+
# @return [Spree::PriceHistory, nil]
|
|
166
|
+
def prior_price
|
|
167
|
+
price_histories.where(recorded_at: 30.days.ago..).order(:amount).first
|
|
168
|
+
end
|
|
169
|
+
|
|
159
170
|
private
|
|
160
171
|
|
|
172
|
+
def should_record_price_history?
|
|
173
|
+
price_list_id.nil? &&
|
|
174
|
+
amount.present? &&
|
|
175
|
+
saved_change_to_amount? &&
|
|
176
|
+
Spree::Config[:track_price_history]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def record_price_history
|
|
180
|
+
Spree::PriceHistory.create!(
|
|
181
|
+
price: self,
|
|
182
|
+
variant_id: variant_id,
|
|
183
|
+
amount: amount,
|
|
184
|
+
compare_at_amount: compare_at_amount,
|
|
185
|
+
currency: currency,
|
|
186
|
+
recorded_at: Time.current
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
161
190
|
def ensure_currency
|
|
162
191
|
self.currency ||= Spree::Store.default.default_currency
|
|
163
192
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
class PriceHistory < Spree.base_class
|
|
5
|
+
belongs_to :price, class_name: 'Spree::Price'
|
|
6
|
+
belongs_to :variant, class_name: 'Spree::Variant'
|
|
7
|
+
|
|
8
|
+
validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
9
|
+
validates :currency, presence: true
|
|
10
|
+
validates :recorded_at, presence: true
|
|
11
|
+
|
|
12
|
+
scope :for_variant, ->(variant_id) { where(variant_id: variant_id) }
|
|
13
|
+
scope :for_currency, ->(currency) { where(currency: currency) }
|
|
14
|
+
scope :in_period, ->(from, to = Time.current) { where(recorded_at: from..to) }
|
|
15
|
+
scope :recent, ->(days = 30) { in_period(days.days.ago) }
|
|
16
|
+
|
|
17
|
+
def money
|
|
18
|
+
Spree::Money.new(amount || 0, currency: currency)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def display_amount
|
|
22
|
+
money.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def amount_in_cents
|
|
26
|
+
money.amount_in_cents
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -11,7 +11,7 @@ module Spree
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def apply
|
|
14
|
-
if load_gift_card_code
|
|
14
|
+
if gift_cards_enabled? && load_gift_card_code
|
|
15
15
|
|
|
16
16
|
if @gift_card.expired?
|
|
17
17
|
set_error_code :gift_card_expired
|
|
@@ -47,7 +47,7 @@ module Spree
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def remove(coupon_code)
|
|
50
|
-
if order.gift_card
|
|
50
|
+
if gift_cards_enabled? && order.gift_card
|
|
51
51
|
result = order.remove_gift_card
|
|
52
52
|
|
|
53
53
|
if result.success?
|
|
@@ -212,6 +212,13 @@ module Spree
|
|
|
212
212
|
Spree::CouponCode.unused.find_by(promotion_id: discount.source.promotion_id, code: coupon_code)&.apply_order!(order)
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
# Whether the coupon handler should also handle gift card codes.
|
|
216
|
+
# Defaults to true for backwards compatibility. Pass enable_gift_cards: false
|
|
217
|
+
# when using the dedicated gift card endpoint.
|
|
218
|
+
def gift_cards_enabled?
|
|
219
|
+
options.fetch(:enable_gift_cards, true)
|
|
220
|
+
end
|
|
221
|
+
|
|
215
222
|
def load_gift_card_code
|
|
216
223
|
return unless order.coupon_code.present?
|
|
217
224
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'pagy/toolbox/paginators/meilisearch'
|
|
2
|
+
|
|
1
3
|
module Spree
|
|
2
4
|
module SearchProvider
|
|
3
5
|
class Meilisearch < Base
|
|
@@ -66,7 +68,7 @@ module Spree
|
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
def index(product)
|
|
69
|
-
documents =
|
|
71
|
+
documents = presenter_class.new(product, store).call
|
|
70
72
|
client.index(index_name).add_documents(documents, 'prefixed_id')
|
|
71
73
|
end
|
|
72
74
|
|
|
@@ -91,9 +93,9 @@ module Spree
|
|
|
91
93
|
ensure_index_settings!
|
|
92
94
|
|
|
93
95
|
scope.reorder(id: :asc)
|
|
94
|
-
.
|
|
96
|
+
.preload_associations_lazily
|
|
95
97
|
.find_in_batches(batch_size: 500) do |batch|
|
|
96
|
-
documents = batch.flat_map { |product|
|
|
98
|
+
documents = batch.flat_map { |product| presenter_class.new(product, store).call }
|
|
97
99
|
index_batch(documents)
|
|
98
100
|
end
|
|
99
101
|
end
|
|
@@ -109,6 +111,10 @@ module Spree
|
|
|
109
111
|
|
|
110
112
|
private
|
|
111
113
|
|
|
114
|
+
def presenter_class
|
|
115
|
+
Spree::Dependencies.search_product_presenter_class
|
|
116
|
+
end
|
|
117
|
+
|
|
112
118
|
def client
|
|
113
119
|
@client ||= ::Meilisearch::Client.new(
|
|
114
120
|
ENV.fetch('MEILISEARCH_URL', 'http://localhost:7700'),
|
|
@@ -140,152 +146,165 @@ module Spree
|
|
|
140
146
|
%w[price -price name -name -available_on available_on best_selling]
|
|
141
147
|
end
|
|
142
148
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
# so that Meilisearch results match what the AR scope would return.
|
|
146
|
-
# All values are sanitized to prevent filter injection.
|
|
149
|
+
# Build Meilisearch filter conditions from API params.
|
|
150
|
+
# Combines system scoping (always applied) with user-facing filters.
|
|
147
151
|
def build_filters(filters)
|
|
148
|
-
conditions =
|
|
152
|
+
conditions = system_filter_conditions
|
|
153
|
+
conditions.concat(user_filter_conditions(filters))
|
|
154
|
+
conditions
|
|
155
|
+
end
|
|
149
156
|
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
# System scoping — always applied. Rarely overridden.
|
|
158
|
+
# Mirrors the AR scope: store.products.active(currency) with locale.
|
|
159
|
+
def system_filter_conditions
|
|
160
|
+
conditions = []
|
|
152
161
|
conditions << "store_ids = '#{store.id}'"
|
|
153
162
|
conditions << "status = 'active'"
|
|
154
163
|
conditions << "locale = '#{locale.to_s.gsub(/[^a-zA-Z_-]/, '')}'"
|
|
155
164
|
conditions << "currency = '#{currency.to_s.gsub(/[^A-Z]/, '')}'"
|
|
156
165
|
conditions << "(discontinue_on = 0 OR discontinue_on > #{Time.current.to_i})"
|
|
166
|
+
conditions
|
|
167
|
+
end
|
|
157
168
|
|
|
169
|
+
# User-facing filters — override to add custom filter pre/post processing.
|
|
170
|
+
def user_filter_conditions(filters)
|
|
171
|
+
conditions = []
|
|
158
172
|
filters = filters.to_unsafe_h if filters.respond_to?(:to_unsafe_h)
|
|
159
173
|
return conditions if filters.blank?
|
|
160
174
|
|
|
161
175
|
filters.each do |key, value|
|
|
162
176
|
next if value.blank?
|
|
163
177
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
conditions << "price <= #{value.to_f}"
|
|
170
|
-
when 'in_stock'
|
|
171
|
-
conditions << 'in_stock = true' if value.to_s != '0'
|
|
172
|
-
when 'out_of_stock'
|
|
173
|
-
conditions << 'in_stock = false' if value.to_s != '0'
|
|
174
|
-
when 'categories_id_eq'
|
|
175
|
-
conditions << "category_ids = '#{sanitize_prefixed_id(value)}'" if valid_prefixed_id?(value)
|
|
176
|
-
when 'with_option_value_ids'
|
|
177
|
-
Array(value).each do |ov|
|
|
178
|
-
conditions << "option_value_ids = '#{sanitize_prefixed_id(ov)}'" if valid_prefixed_id?(ov)
|
|
179
|
-
end
|
|
178
|
+
condition = build_filter_condition(key.to_s, value)
|
|
179
|
+
if condition.is_a?(Array)
|
|
180
|
+
conditions.concat(condition)
|
|
181
|
+
elsif condition
|
|
182
|
+
conditions << condition
|
|
180
183
|
end
|
|
181
184
|
end
|
|
182
185
|
|
|
183
186
|
conditions
|
|
184
187
|
end
|
|
185
188
|
|
|
189
|
+
# Translate a single Ransack-style filter param into Meilisearch filter syntax.
|
|
190
|
+
# Override in subclasses to handle custom filter keys — call super for built-in filters.
|
|
191
|
+
def build_filter_condition(key, value)
|
|
192
|
+
case key
|
|
193
|
+
when 'price_gte'
|
|
194
|
+
"price >= #{value.to_f}"
|
|
195
|
+
when 'price_lte'
|
|
196
|
+
"price <= #{value.to_f}"
|
|
197
|
+
when 'in_stock'
|
|
198
|
+
'in_stock = true' if value.to_s != '0'
|
|
199
|
+
when 'out_of_stock'
|
|
200
|
+
'in_stock = false' if value.to_s != '0'
|
|
201
|
+
when 'categories_id_eq'
|
|
202
|
+
"category_ids = '#{sanitize_prefixed_id(value)}'" if valid_prefixed_id?(value)
|
|
203
|
+
when 'with_option_value_ids'
|
|
204
|
+
Array(value).filter_map { |ov| "option_value_ids = '#{sanitize_prefixed_id(ov)}'" if valid_prefixed_id?(ov) }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Sort param to Meilisearch sort syntax.
|
|
209
|
+
# Override in subclasses to handle custom sort keys — call super for built-in sorts.
|
|
186
210
|
def build_sort(sort)
|
|
187
211
|
return nil if sort.blank?
|
|
188
212
|
|
|
213
|
+
sort_mapping(sort)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Map a sort param to Meilisearch sort syntax.
|
|
217
|
+
# Override in subclasses to add custom sorts — call super for built-in sorts.
|
|
218
|
+
def sort_mapping(sort)
|
|
189
219
|
case sort
|
|
190
|
-
when 'price'
|
|
191
|
-
|
|
192
|
-
when '
|
|
193
|
-
|
|
194
|
-
when '
|
|
195
|
-
|
|
196
|
-
when '
|
|
197
|
-
['name:desc']
|
|
198
|
-
when '-available_on'
|
|
199
|
-
['available_on:desc']
|
|
200
|
-
when 'available_on'
|
|
201
|
-
['available_on:asc']
|
|
202
|
-
when 'best_selling'
|
|
203
|
-
['units_sold_count:desc']
|
|
220
|
+
when 'price' then ['price:asc']
|
|
221
|
+
when '-price' then ['price:desc']
|
|
222
|
+
when 'name' then ['name:asc']
|
|
223
|
+
when '-name' then ['name:desc']
|
|
224
|
+
when '-available_on' then ['available_on:desc']
|
|
225
|
+
when 'available_on' then ['available_on:asc']
|
|
226
|
+
when 'best_selling' then ['units_sold_count:desc']
|
|
204
227
|
end
|
|
205
228
|
end
|
|
206
229
|
|
|
207
|
-
# Transform Meilisearch facetDistribution into
|
|
230
|
+
# Transform Meilisearch facetDistribution into standard filter response format.
|
|
231
|
+
# Override in subclasses to add custom facets — call super and append.
|
|
208
232
|
def build_facet_response(facet_distribution)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if facet_distribution['
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
type: 'price_range',
|
|
217
|
-
min: amounts.min,
|
|
218
|
-
max: amounts.max,
|
|
219
|
-
currency: currency
|
|
220
|
-
}
|
|
221
|
-
end
|
|
233
|
+
facets = []
|
|
234
|
+
facets << build_price_facet(facet_distribution['price']) if facet_distribution['price'].present?
|
|
235
|
+
facets << build_availability_facet(facet_distribution['in_stock']) if facet_distribution['in_stock'].present?
|
|
236
|
+
facets.concat(build_option_facets(facet_distribution['option_value_ids'])) if facet_distribution['option_value_ids'].present?
|
|
237
|
+
facets << build_category_facet(facet_distribution['category_ids']) if facet_distribution['category_ids'].present?
|
|
238
|
+
facets.compact
|
|
239
|
+
end
|
|
222
240
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
]
|
|
234
|
-
}
|
|
235
|
-
end
|
|
241
|
+
def build_price_facet(distribution)
|
|
242
|
+
amounts = distribution.keys.map(&:to_f)
|
|
243
|
+
{
|
|
244
|
+
id: 'price',
|
|
245
|
+
type: 'price_range',
|
|
246
|
+
min: amounts.min,
|
|
247
|
+
max: amounts.max,
|
|
248
|
+
currency: currency
|
|
249
|
+
}
|
|
250
|
+
end
|
|
236
251
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
next unless ov
|
|
248
|
-
|
|
249
|
-
ot = ov.option_type
|
|
250
|
-
by_option_type[ot] ||= []
|
|
251
|
-
by_option_type[ot] << { id: ov.prefixed_id, name: ov.name, label: ov.label, position: ov.position, count: count }
|
|
252
|
-
end
|
|
252
|
+
def build_availability_facet(distribution)
|
|
253
|
+
{
|
|
254
|
+
id: 'availability',
|
|
255
|
+
type: 'availability',
|
|
256
|
+
options: [
|
|
257
|
+
{ id: 'in_stock', count: distribution['true'] || 0 },
|
|
258
|
+
{ id: 'out_of_stock', count: distribution['false'] || 0 }
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
end
|
|
253
262
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
def build_option_facets(distribution)
|
|
264
|
+
prefixed_ids = distribution.keys
|
|
265
|
+
raw_ids = prefixed_ids.filter_map { |pid| Spree::OptionValue.decode_prefixed_id(pid) }
|
|
266
|
+
option_values = Spree::OptionValue.where(id: raw_ids).includes(:option_type).preload_associations_lazily.index_by(&:prefixed_id)
|
|
267
|
+
|
|
268
|
+
# Group by option type
|
|
269
|
+
by_option_type = {}
|
|
270
|
+
distribution.each do |ov_prefixed_id, count|
|
|
271
|
+
ov = option_values[ov_prefixed_id]
|
|
272
|
+
next unless ov
|
|
273
|
+
|
|
274
|
+
ot = ov.option_type
|
|
275
|
+
by_option_type[ot] ||= []
|
|
276
|
+
by_option_type[ot] << { id: ov.prefixed_id, name: ov.name, label: ov.label, position: ov.position, count: count }
|
|
263
277
|
end
|
|
264
278
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
id: 'categories',
|
|
273
|
-
type: 'category',
|
|
274
|
-
options: facet_distribution['category_ids'].filter_map do |prefixed_id, count|
|
|
275
|
-
cat = categories[prefixed_id]
|
|
276
|
-
next unless cat
|
|
277
|
-
|
|
278
|
-
{ id: cat.prefixed_id, name: cat.name, permalink: cat.permalink, count: count }
|
|
279
|
-
end
|
|
279
|
+
by_option_type.map do |option_type, values|
|
|
280
|
+
{
|
|
281
|
+
id: option_type.prefixed_id,
|
|
282
|
+
type: 'option',
|
|
283
|
+
name: option_type.name,
|
|
284
|
+
label: option_type.label,
|
|
285
|
+
options: values.sort_by { |o| o[:position] }
|
|
280
286
|
}
|
|
281
287
|
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def build_category_facet(distribution)
|
|
291
|
+
prefixed_ids = distribution.keys
|
|
292
|
+
raw_ids = prefixed_ids.filter_map { |pid| Spree::Taxon.decode_prefixed_id(pid) }
|
|
293
|
+
categories = Spree::Taxon.where(id: raw_ids).index_by(&:prefixed_id)
|
|
282
294
|
|
|
283
|
-
|
|
295
|
+
{
|
|
296
|
+
id: 'categories',
|
|
297
|
+
type: 'category',
|
|
298
|
+
options: distribution.filter_map do |prefixed_id, count|
|
|
299
|
+
cat = categories[prefixed_id]
|
|
300
|
+
next unless cat
|
|
301
|
+
|
|
302
|
+
{ id: cat.prefixed_id, name: cat.name, permalink: cat.permalink, count: count }
|
|
303
|
+
end
|
|
304
|
+
}
|
|
284
305
|
end
|
|
285
306
|
|
|
286
307
|
def build_pagy(ms_result, page, limit)
|
|
287
|
-
require 'pagy/toolbox/paginators/meilisearch'
|
|
288
|
-
|
|
289
308
|
fake_result = Struct.new(:raw_answer).new({
|
|
290
309
|
'totalHits' => ms_result['estimatedTotalHits'] || 0,
|
|
291
310
|
'hitsPerPage' => limit,
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module SearchProvider
|
|
3
3
|
class ProductPresenter
|
|
4
|
-
# Associations needed by this presenter — used by reindex and rake task for preloading
|
|
5
|
-
REQUIRED_PRELOADS = [
|
|
6
|
-
:taxons, :option_types, :primary_media, :store_products,
|
|
7
|
-
{ variants_including_master: [:prices, :option_values] }
|
|
8
|
-
].freeze
|
|
9
|
-
|
|
10
4
|
attr_reader :product, :store
|
|
11
5
|
|
|
12
6
|
def initialize(product, store)
|
|
@@ -8,8 +8,8 @@ module Spree
|
|
|
8
8
|
|
|
9
9
|
def call(address_params: {}, user: nil, **opts)
|
|
10
10
|
order = opts[:order]
|
|
11
|
-
default_billing = opts.fetch(:default_billing, false)
|
|
12
|
-
default_shipping = opts.fetch(:default_shipping, false)
|
|
11
|
+
default_billing = address_params.key?(:is_default_billing) ? address_params.delete(:is_default_billing) : opts.fetch(:default_billing, false)
|
|
12
|
+
default_shipping = address_params.key?(:is_default_shipping) ? address_params.delete(:is_default_shipping) : opts.fetch(:default_shipping, false)
|
|
13
13
|
|
|
14
14
|
address_params = fill_country_and_state_ids(address_params)
|
|
15
15
|
|
|
@@ -8,8 +8,8 @@ module Spree
|
|
|
8
8
|
|
|
9
9
|
def call(address:, address_params:, **opts)
|
|
10
10
|
order = opts[:order]
|
|
11
|
-
default_billing = opts.fetch(:default_billing, false)
|
|
12
|
-
default_shipping = opts.fetch(:default_shipping, false)
|
|
11
|
+
default_billing = address_params.key?(:is_default_billing) ? address_params.delete(:is_default_billing) : opts.fetch(:default_billing, false)
|
|
12
|
+
default_shipping = address_params.key?(:is_default_shipping) ? address_params.delete(:is_default_shipping) : opts.fetch(:default_shipping, false)
|
|
13
13
|
address_changes_except = opts.fetch(:address_changes_except, [])
|
|
14
14
|
create_new_address_on_update = opts.fetch(:create_new_address_on_update, false)
|
|
15
15
|
Spree::Deprecation.warn('Spree::Addresses::Update create_new_address_on_update parameter is deprecated and will be removed in Spree 5.5.') if create_new_address_on_update
|
|
@@ -5,8 +5,9 @@ module Spree
|
|
|
5
5
|
|
|
6
6
|
def call(order:)
|
|
7
7
|
@messages = []
|
|
8
|
+
@warnings = []
|
|
8
9
|
|
|
9
|
-
return success([order, @messages]) if order.item_count.zero? || order.line_items.none?
|
|
10
|
+
return success([order, @messages, @warnings]) if order.item_count.zero? || order.line_items.none?
|
|
10
11
|
|
|
11
12
|
line_items = order.line_items.includes(variant: [:product, :stock_items, :stock_locations, { stock_items: :stock_location }])
|
|
12
13
|
|
|
@@ -17,9 +18,9 @@ module Spree
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
if @messages.any? # If any line item was removed, reload the order
|
|
20
|
-
success([order.reload, @messages])
|
|
21
|
+
success([order.reload, @messages, @warnings])
|
|
21
22
|
else
|
|
22
|
-
success([order, @messages])
|
|
23
|
+
success([order, @messages, @warnings])
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -28,7 +29,14 @@ module Spree
|
|
|
28
29
|
def valid_status?(line_item)
|
|
29
30
|
product = line_item.product
|
|
30
31
|
if !product.active? || product.deleted? || product.discontinued? || line_item.variant.discontinued?
|
|
31
|
-
|
|
32
|
+
message = Spree.t('cart_line_item.discontinued', li_name: line_item.name)
|
|
33
|
+
@messages << message
|
|
34
|
+
@warnings << {
|
|
35
|
+
code: 'line_item_removed',
|
|
36
|
+
message: message,
|
|
37
|
+
line_item_id: line_item.prefixed_id,
|
|
38
|
+
variant_id: line_item.variant.prefixed_id
|
|
39
|
+
}
|
|
32
40
|
return false
|
|
33
41
|
end
|
|
34
42
|
true
|
|
@@ -36,7 +44,14 @@ module Spree
|
|
|
36
44
|
|
|
37
45
|
def stock_available?(line_item)
|
|
38
46
|
if line_item.insufficient_stock?
|
|
39
|
-
|
|
47
|
+
message = Spree.t('cart_line_item.out_of_stock', li_name: line_item.name)
|
|
48
|
+
@messages << message
|
|
49
|
+
@warnings << {
|
|
50
|
+
code: 'line_item_removed',
|
|
51
|
+
message: message,
|
|
52
|
+
line_item_id: line_item.prefixed_id,
|
|
53
|
+
variant_id: line_item.variant.prefixed_id
|
|
54
|
+
}
|
|
40
55
|
return false
|
|
41
56
|
end
|
|
42
57
|
true
|
|
@@ -89,18 +89,20 @@ module Spree
|
|
|
89
89
|
cart.state = 'address'
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Auto-advance as far as the checkout state machine allows
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# Failure is swallowed — the update itself already succeeded.
|
|
92
|
+
# Auto-advance as far as the checkout state machine allows, but never
|
|
93
|
+
# to complete. The complete transition must always be explicit via
|
|
94
|
+
# the /carts/:id/complete endpoint — otherwise gift cards or store
|
|
95
|
+
# credits that fully cover the order total would auto-complete the
|
|
96
|
+
# cart during address/delivery updates.
|
|
98
97
|
def try_advance
|
|
99
98
|
return if cart.complete? || cart.canceled?
|
|
100
99
|
|
|
100
|
+
steps = cart.checkout_steps
|
|
101
101
|
loop do
|
|
102
|
+
current_index = steps.index(cart.state).to_i
|
|
103
|
+
next_step = steps[current_index + 1]
|
|
104
|
+
break if next_step.nil? || next_step == 'complete'
|
|
102
105
|
break unless cart.next
|
|
103
|
-
break if cart.confirm? || cart.complete?
|
|
104
106
|
end
|
|
105
107
|
rescue StandardError => e
|
|
106
108
|
Rails.error.report(e, context: { order_id: cart.id, state: cart.state }, source: 'spree.checkout')
|
|
@@ -12,9 +12,13 @@ module Spree
|
|
|
12
12
|
return failure(nil, Spree.t(:error_user_does_not_have_any_store_credits)) unless @order.user&.store_credits&.any?
|
|
13
13
|
|
|
14
14
|
ApplicationRecord.transaction do
|
|
15
|
-
@order.payments.store_credits.where(state: :checkout)
|
|
15
|
+
existing = @order.payments.store_credits.where(state: :checkout)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
if existing.any?
|
|
18
|
+
update_existing_payments(existing, remaining_total)
|
|
19
|
+
else
|
|
20
|
+
apply_store_credits(remaining_total)
|
|
21
|
+
end
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
if @order.reload.payments.store_credits.valid.any?
|
|
@@ -27,6 +31,26 @@ module Spree
|
|
|
27
31
|
|
|
28
32
|
private
|
|
29
33
|
|
|
34
|
+
# Update existing checkout store credit payments in place to avoid
|
|
35
|
+
# creating unnecessary invalid payment records on every recalculation.
|
|
36
|
+
def update_existing_payments(payments, remaining_total)
|
|
37
|
+
payments.each do |payment|
|
|
38
|
+
credit = payment.source
|
|
39
|
+
available = credit.amount_remaining + payment.amount
|
|
40
|
+
new_amount = [available, remaining_total].min
|
|
41
|
+
|
|
42
|
+
if new_amount.positive?
|
|
43
|
+
payment.update_column(:amount, new_amount)
|
|
44
|
+
remaining_total -= new_amount
|
|
45
|
+
else
|
|
46
|
+
payment.invalidate!
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# If there's still remaining total, apply from additional store credits
|
|
51
|
+
apply_store_credits(remaining_total) if remaining_total.positive?
|
|
52
|
+
end
|
|
53
|
+
|
|
30
54
|
def apply_store_credits(remaining_total)
|
|
31
55
|
payment_method = Spree::PaymentMethod::StoreCredit.available.first
|
|
32
56
|
raise 'Store credit payment method could not be found' unless payment_method
|
data/config/locales/en.yml
CHANGED
|
@@ -1222,6 +1222,7 @@ en:
|
|
|
1222
1222
|
gift_card_expired: The Gift Card has expired.
|
|
1223
1223
|
gift_card_mismatched_currency: Gift Card cannot be applied with current currency.
|
|
1224
1224
|
gift_card_mismatched_customer: This gift card is associated with another user.
|
|
1225
|
+
gift_card_not_found: The Gift Card was not found.
|
|
1225
1226
|
gift_card_removed: The Gift Card was successfully removed from your order
|
|
1226
1227
|
gift_card_using_store_credit_error: You can't apply the Gift Card after you applied the store credit.
|
|
1227
1228
|
gift_cards: Gift Cards
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CreateSpreePriceHistories < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_price_histories do |t|
|
|
4
|
+
t.references :price, null: false, index: false
|
|
5
|
+
t.references :variant, null: false, index: false
|
|
6
|
+
t.decimal :amount, precision: 10, scale: 2, null: false
|
|
7
|
+
t.decimal :compare_at_amount, precision: 10, scale: 2
|
|
8
|
+
t.string :currency, null: false
|
|
9
|
+
t.datetime :recorded_at, null: false
|
|
10
|
+
t.datetime :created_at, null: false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :spree_price_histories, [:variant_id, :currency, :recorded_at],
|
|
14
|
+
name: 'idx_price_histories_variant_currency_recorded'
|
|
15
|
+
add_index :spree_price_histories, [:price_id, :recorded_at],
|
|
16
|
+
name: 'idx_price_histories_price_recorded'
|
|
17
|
+
add_index :spree_price_histories, :recorded_at,
|
|
18
|
+
name: 'idx_price_histories_recorded_at'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -96,6 +96,8 @@ module Spree
|
|
|
96
96
|
preference :tax_using_ship_address, :boolean, default: true
|
|
97
97
|
preference :title_site_name_separator, :string, deprecated: true
|
|
98
98
|
preference :track_inventory_levels, :boolean, default: true # Determines whether to track on_hand values for variants / products.
|
|
99
|
+
preference :track_price_history, :boolean, default: true # Records price changes for Omnibus Directive compliance. Disable for non-EU stores.
|
|
100
|
+
preference :price_history_retention_days, :integer, default: 30 # Days to retain price history records. Used by spree:price_history:prune rake task.
|
|
99
101
|
preference :use_user_locale, :boolean, default: true
|
|
100
102
|
|
|
101
103
|
# Sets the path used for products, taxons and pages.
|
|
@@ -103,7 +103,10 @@ module Spree
|
|
|
103
103
|
products_finder: 'Spree::Products::Find',
|
|
104
104
|
taxon_finder: 'Spree::Taxons::Find',
|
|
105
105
|
line_item_by_variant_finder: 'Spree::LineItems::FindByVariant',
|
|
106
|
-
variant_finder: 'Spree::Variants::Find'
|
|
106
|
+
variant_finder: 'Spree::Variants::Find',
|
|
107
|
+
|
|
108
|
+
# search
|
|
109
|
+
search_product_presenter: 'Spree::SearchProvider::ProductPresenter'
|
|
107
110
|
}.freeze
|
|
108
111
|
|
|
109
112
|
include Spree::DependenciesHelper
|
data/lib/spree/core/version.rb
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :spree do
|
|
4
|
+
namespace :price_history do
|
|
5
|
+
desc 'Seed price history from existing base prices (run once after migration)'
|
|
6
|
+
task seed: :environment do
|
|
7
|
+
count = 0
|
|
8
|
+
Spree::Price.where(deleted_at: nil, price_list_id: nil).find_each do |price|
|
|
9
|
+
next if price.amount.nil?
|
|
10
|
+
next if Spree::PriceHistory.exists?(price_id: price.id)
|
|
11
|
+
|
|
12
|
+
Spree::PriceHistory.create!(
|
|
13
|
+
price: price,
|
|
14
|
+
variant_id: price.variant_id,
|
|
15
|
+
amount: price.amount,
|
|
16
|
+
compare_at_amount: price.compare_at_amount,
|
|
17
|
+
currency: price.currency,
|
|
18
|
+
recorded_at: price.updated_at || Time.current
|
|
19
|
+
)
|
|
20
|
+
count += 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
puts "Seeded #{count} price history records"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc 'Prune price history older than retention period'
|
|
27
|
+
task prune: :environment do
|
|
28
|
+
retention_days = Spree::Config[:price_history_retention_days] || 30
|
|
29
|
+
deleted = Spree::PriceHistory
|
|
30
|
+
.where('recorded_at < ?', retention_days.days.ago)
|
|
31
|
+
.delete_all
|
|
32
|
+
|
|
33
|
+
puts "Pruned #{deleted} price history records older than #{retention_days} days"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.4.0.
|
|
4
|
+
version: 5.4.0.rc3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Schofield
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-03-
|
|
13
|
+
date: 2026-03-25 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: i18n-tasks
|
|
@@ -1052,6 +1052,7 @@ files:
|
|
|
1052
1052
|
- app/models/spree/policy.rb
|
|
1053
1053
|
- app/models/spree/preference.rb
|
|
1054
1054
|
- app/models/spree/price.rb
|
|
1055
|
+
- app/models/spree/price_history.rb
|
|
1055
1056
|
- app/models/spree/price_list.rb
|
|
1056
1057
|
- app/models/spree/price_rule.rb
|
|
1057
1058
|
- app/models/spree/price_rules/customer_group_rule.rb
|
|
@@ -1472,6 +1473,7 @@ files:
|
|
|
1472
1473
|
- db/migrate/20260315000000_create_spree_allowed_origins.rb
|
|
1473
1474
|
- db/migrate/20260315100000_add_product_media_support.rb
|
|
1474
1475
|
- db/migrate/20260317000000_create_spree_refresh_tokens.rb
|
|
1476
|
+
- db/migrate/20260323000000_create_spree_price_histories.rb
|
|
1475
1477
|
- db/sample_data/customers.csv
|
|
1476
1478
|
- db/sample_data/metafield_definitions.rb
|
|
1477
1479
|
- db/sample_data/orders.rb
|
|
@@ -1599,6 +1601,7 @@ files:
|
|
|
1599
1601
|
- lib/spree/testing_support/factories/payment_source_factory.rb
|
|
1600
1602
|
- lib/spree/testing_support/factories/policy_factory.rb
|
|
1601
1603
|
- lib/spree/testing_support/factories/price_factory.rb
|
|
1604
|
+
- lib/spree/testing_support/factories/price_history_factory.rb
|
|
1602
1605
|
- lib/spree/testing_support/factories/price_list_factory.rb
|
|
1603
1606
|
- lib/spree/testing_support/factories/price_rule_factory.rb
|
|
1604
1607
|
- lib/spree/testing_support/factories/product_factory.rb
|
|
@@ -1670,6 +1673,7 @@ files:
|
|
|
1670
1673
|
- lib/tasks/dependencies.rake
|
|
1671
1674
|
- lib/tasks/images.rake
|
|
1672
1675
|
- lib/tasks/markets.rake
|
|
1676
|
+
- lib/tasks/price_history.rake
|
|
1673
1677
|
- lib/tasks/products.rake
|
|
1674
1678
|
- lib/tasks/sample_data.rake
|
|
1675
1679
|
- lib/tasks/search.rake
|
|
@@ -1695,9 +1699,9 @@ licenses:
|
|
|
1695
1699
|
- BSD-3-Clause
|
|
1696
1700
|
metadata:
|
|
1697
1701
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
1698
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.
|
|
1702
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.rc3
|
|
1699
1703
|
documentation_uri: https://docs.spreecommerce.org/
|
|
1700
|
-
source_code_uri: https://github.com/spree/spree/tree/v5.4.0.
|
|
1704
|
+
source_code_uri: https://github.com/spree/spree/tree/v5.4.0.rc3
|
|
1701
1705
|
post_install_message:
|
|
1702
1706
|
rdoc_options: []
|
|
1703
1707
|
require_paths:
|