-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP Superseding historical prices #987
Conversation
@@ -12,6 +13,13 @@ class Price < Spree::Base | |||
less_than_or_equal_to: MAXIMUM_AMOUNT | |||
} | |||
|
|||
scope :valid_before, -> (date) { where("#{self.table_name}.valid_from <= ?", date).order(valid_from: :desc) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant self
detected.
An example of where we have caching that will be affected by this:
The product show page is currently cached on Locale, Currency, and Product and we rely upon Price touching Variant and Variant touching Product to bust the cache. |
That should work, though: Whenever I update, create or destroy a price belonging to a variant, that variant and in turn the associated product will be touched. Alternatively, we could create a more sophisticated cache key for products; something along the lines of class Product
def cache_key
associated_object_timestamps = [updated_at] + variants.map(&:updated_at) + prices.map(&:updated_at)
hashed_timestamps = Digest::MD5.hexdigest associated_object_timestamps.map(&:to_s).join(",")
super + hashed_timestamps
end
end Right now, that should bust the cache quite reliably whenever we update, add or remove any variants or prices. In the future, we'll also have to look at modifying the |
@gmacdougall and I talked IRL (well, on the interwebs) and he explained that time itself should expire the cache key. I've cooked something up by looking up |
@gmacdougall @jhawthorn Please have a look at this, there is a caching strategy and a migration. |
@@ -274,6 +274,10 @@ def find_variant_property_rule(option_value_ids) | |||
end | |||
end | |||
|
|||
def cache_key | |||
super + "/prices/" + prices.currently_valid.pluck(:id).join("/") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be best to digest the prices. I don't know if we'll encounter an issue with key length if we have thousands of prices, but it's better to err on the side of safety.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this but went for "the simplest thing that could possibly work". I do agree with digesting we'd be safe from key length issues.
Something that's crossed my mind though: Should we maybe ask the currently used pricer make sure the correct cache key is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think that's makes sense. I'm not sure which PR you want to do that in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can absolutely be this one. I'm thinking after #994 is in, which gives us a first go at how a pricer would be structured, it could be quite neatly tucked into a new pricer that's less stupid than the ConservativeLineItemPricer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, coming to think of it: This PR should really just introduce this feature. Variant#prices.currently_valid
will return all prices (all currencies and so on), and custom pricers will just add more attributes to select prices from - this caching strategy would still work in most cases. I'll work on your suggestions (use timecop
and use the change
matcher).
Wow, this is much more of a pain than I thought, mostly because saving a new item on a |
it "should not change updated_at" do | ||
expect { subject }.not_to change{ product.updated_at } | ||
it "should not change the cache key" do | ||
expect { subject }.not_to change{ product.cache_key } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space missing to the left of {.
@gmacdougall @jhawthorn This is done, as far as I'm concerned. Would you have a look? :) |
@@ -0,0 +1,34 @@ | |||
class MigrateDefaultToCurrentPrices < ActiveRecord::Migration | |||
def up | |||
say_with_time 'Setting default prices to be valid from now' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the say_with_time
is necessary when all it's wrapping is an execute
. But it doesn't really hurt to have it.
scope :valid_before, | ||
-> (date) { latest_valid_from_first.where("#{Spree::Price.table_name}.valid_from <= ?", date) } | ||
scope :valid_before_now, -> { valid_before(Time.current) } | ||
scope :with_default_currency, -> { where(currency: Spree::Config.currency) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in_currency(Spree::Config.currency)
Perfect. Thanks for adding more docs and considering my feedback. Someone please have a look into the SQL in the migrations as I'm not able to review it. @jhawthorn @gmacdougall 👍 |
The idea here is to introduce a valid_from column to prices that determines which price is taken if there is more prices that fit the given search scope. For example, if you have two prices for currency JPY: ``` a = Spree::Price(currency: "JPY", amount: 13, valid_from: 2015-12-31) b = Spree::Price(currency: "JPY", amount: 13, valid_from: 2014-12-31) c = Spree::Price(currency: "JPY", amount: 13, valid_from: 2016-12-31) ``` And you want to find the price currently valid on Feb 2nd, 2016, you would obtain price b - the latest price that is not valid in the future. If you wanted to find out which price *was* valid on Feb 2nd, 2015, that would be price a. You can even find out a valid price far in the future, although that's more brittle, as is the case with most telling the future things.
Spree::Price.valid_before(date) will return all prices valid before certain date. Spree::Price.valid_before_now will return all prices before the current date. Spree::Price.latest_valid_from_first will return the prices sorted by valid_from, in descending order. Also makes sure that a price is always initialized with the current date and time as a valid from date, if none is given.
Modifies the scope in the `default_price` `has_one` association such that it returns the prices that are valid before the current time.
Without this commit, the product's cache key method would not expire the cache for the product when the currently valid prices change. This attaches the currently valid price ids to the end of the cache key, making it know when to invalidate the cache. Introduces a class method on `Spree::Price` to generate a cache key for a collection of prices that never changes in length.
Set the timestamp for `valid_from` on default prices (not in the future, most recent) to true. This migration respects time zones as well as different ways of saying "true" or "false" in your database dialect.
Solidus re-implements `autosave: true` for prices, even though it already tells ActiveRecord to autosave.
We want prices to be essentially immutable, and we want a history of prices for our variant. This commits makes it such that the present default price is replaced instead of modified. Unfortunately, the old price is also deleted in the process. ActiveRecord does that, and there does not seem to be any way of stopping it! Fixed spec setup for product helper spec. Do not set currency in product duplicator (the master price will take care of that itself) Do not set variants default price currency, as the price object will handle that. Fix another spec so that the validation for price (which isn't tested) does not barf. Use the correct scope instead of unscoping `default_price` `acts_as_paranoid` offers a `with_deleted` scope for paranoid models. This commit uses that scope in the `scope` argument of the `has_one` declaration instead of using `unscoped` and overriding the generated method.
It does not make any sense to set a currency on a variant - if it's the default currency, it does not change what prices do by themselves, and if it's not, you're changing the default price to something that can not be the default as it would have the wrong currency. However, there was a number of places in the old code that still tried to do that, so I expect custom code to also have that code. This commit re-introduces the setter with a Deprecation warning.
Before this commit, the concern would always create an association that has `:variant` as its inverse. This commit changes that for an association that has the symbolized underscored class name as inverse. When using this concern, be aware that you'll need a `your_model_id` column on prices.
I've been a bit stumped about what `default_price` actually does, so here goes some documentation. Also moved the scope for a default price from the default price module into the price class.
We want to know all prices for a variant. When building a new `default_price` as we do every time `Spree::Variant#price=` is called, ActiveRecord will silently `destroy` the old price. `Acts_as_paranoid` mitigates this behaviour, and the `with_deleted` scope here appears like it never happened. I briefly considered overwriting `Spree::Price#destroy` to be a noop, but that would be an introduction of where uncommon behaviour for a model.
This is now very out-of-date. Much of it has been taken in in other PRs. I'm closing this for the time being. |
This is to replace the
is_default
boolean on prices.That boolean governed what price would be currently valid. I'd like to replace that with logic that can tell you what price a variant was at any given point. In order to do that, I introduce a
valid_from
field, which defaults to the time the price is created and indefinitely valid afterwards. There is novalid_until
field because the product hasavailable_until
and we don't want to run into a product ever not having a price.@gmacdougall mentioned that this might have caching implications. @jhawthorn Please also have a look at this.