Skip to content
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

Unit test variant stock #2638

Merged

Conversation

sauloperez
Copy link
Contributor

@sauloperez sauloperez commented Sep 3, 2018

What? Why?

Closes #2627

As explained in the issue, this adds unit tests, documentation and deprecation warnings to the new VariantStock module. The one that provides backward compatibility for the current stock methods such as #on_hand or #on_demand.

You can read the commits to understand minor details of the decisions taken, like 5c14a8c.

What should we test?

The upgrade is not ready to be tested yet.

Release notes

Changelog Category: Changed

@sauloperez sauloperez changed the base branch from master to 2-0-stable September 3, 2018 16:13
@daniellemoorhead
Copy link
Contributor

@sauloperez

Please, please, #assignyourself to PRs. Please 🙏

raise_error_if_no_stock_item_available
raise_error_if_multiple_stock_items
Copy link
Contributor Author

@sauloperez sauloperez Sep 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to convince you this deserves to become a DB-level constraint although opinions (See https://openfoodnetwork.slack.com/threads/convo/C4NDJT3FY-1535723508.000100/) were against it.

First, at this point, it is too late. The data is already inconsistent, violating data integrity and cleaning it manually is the most we can do. And that is hard and painful.

Second, nothing stops the data to become inconsistent by any other means. Adding this sort of barrier, doesn't prevent this from happening from a race condition, rails console, a controller or even a psql session. It's like poner puertas al campo aka. trying to stop the unstoppable.

As a result, unconfident programming comes up; the lines of code consuming stock_items become paranoid checking there's a single stock item throughout the app.

That is why, I believe this would be better off replaced by adding add_index :spree_stock_items, :variant_id, unique: true from a migration. I know we don't like messing with Spree's schema but this doesn't modify the tables and the data integrity it provides well outweighs it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I dont like the approach because we should know who's doing what and fix when it's not happening.

"become paranoid checking there's a single stock item throughout the app"
The place from where code is "consuming stock_items" should be one and only one: it's the model and service layer of this part of the app. But I know we don't have that kind of isolation... So, lets do it!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm in favour of a constraint. That does make some of my earlier comments redundant.

@sauloperez sauloperez removed the pr-wip label Sep 4, 2018
@sauloperez sauloperez force-pushed the unit-test-variant-stock branch 2 times, most recently from 0402200 to 8edff73 Compare September 4, 2018 14:32
# These methods were available in Spree 1, but were removed in Spree 2. We
# would still like to use them so that we still give support to the consumers
# of this methods, making the upgrade backward compatible.
#
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this module should be called Adapter, VariantStockAdapter.
And the documentation could become more simple:

-# This module is an adapter for OFN to work with Spree v2 code.
-#
-# This adapter simplifies the Spree 2 data model by using only a single stock item per variant.
-# Each stock item is associated to a single stock location per instance (default stock location).
-# The stock items are used to track the count_on_hand value (previously a database column on -variants).
-# See https://github.com/openfoodfoundation/openfoodnetwork/wiki/Spree-Upgrade%3A-Stock-locations
-# for details.

# track the `count_on_hand` value that was previously a database column on
# variants. See
# https://github.com/openfoodfoundation/openfoodnetwork/wiki/Spree-Upgrade%3A-Stock-locations
# for details.
#
# We may decide to deprecate these methods after we designed the Network feature.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have the deprecation warning here, for example, "Some methods are or may become deprecated." but I dont think we should mention roadmap/future items in code comments ever. I'd remove the reference to Network feature.

# Returns the number of items of the variant available in the
# stock. When allowing on demand, it returns infinite.
#
# Note this method relies on Spree's 2.0 #total_on_hand.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this comment. The code is self explanatory.

@@ -22,39 +34,65 @@ def on_hand
end
end

# Alias for Spree's 2.0 #total_on_hand
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of referring to the method used below. It would be more useful to explain what this method does, probably just "Number of items available in the stock for this variant."

def on_hand=(new_level)
error = 'Cannot set on_hand value when Spree::Config[:track_inventory_levels] is false'
raise error unless Spree::Config.track_inventory_levels

self.count_on_hand = new_level
end

# Sets the stock level. As opposed to #on_hand= it does not check `track_inventory_levels`'s value. It does ensure there's a stock item for the variant however raising otherwise. See #raise_error_if_no_stock_item_available for details.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More then explaining what's already in the code, I'd prefer to see here the why it is doing so. Why is it not checking "track_inventory_levels" here? same for on_hand=, why is it not checking for the existence of stock_item?

def count_on_hand=(new_level)
raise_error_if_no_stock_item_available
overwrite_stock_levels new_level
end

def on_demand
stock_items.any?(&:backorderable?)
warn_deprecation(__method__)
stock_items.first.backorderable?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!!

end

# Sets whether the variant can be ordered on demand or not. Note
# that although this modifies the stock item, it is not
# persisted.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by "it is not persisted"? why would you use it then?

raise_error_if_no_stock_item_available
raise_error_if_multiple_stock_items
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I dont like the approach because we should know who's doing what and fix when it's not happening.

"become paranoid checking there's a single stock item throughout the app"
The place from where code is "consuming stock_items" should be one and only one: it's the model and service layer of this part of the app. But I know we don't have that kind of isolation... So, lets do it!

Copy link
Contributor

@luisramos0 luisramos0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work Pau!
I just added some very ignorable comments about comments :-)

@sauloperez sauloperez force-pushed the unit-test-variant-stock branch from 8edff73 to 2cdcd22 Compare September 6, 2018 06:56
@sauloperez
Copy link
Contributor Author

sauloperez commented Sep 6, 2018

Thanks for your review @luisramos0 ! I tried to share more knowledge and be concise. Let me know if it's all clear. Future devs must have no doubts when reading it.

I move on to add a DB constraint and rename it now.

@sauloperez sauloperez force-pushed the unit-test-variant-stock branch from 2cdcd22 to 3066913 Compare September 6, 2018 09:28
This also replaces the after_save callback to after_update.

We enforce a stock_item exists when modifying the variant and the
stock_item gets created on variant's after_create. This means it's not
possible to use any of the VariantStock's setters before the variant is
persisted so executing the callback on creation is pointless.
This will prevent other devs from relying on this methods and will tell
us the exact lines that call this methods, which will come in handy when
removing this module.
This means we violated the business rules of having a single stock
location for the OFN instance and hence a single stock item per variant.

Although it is too late to preserve the data integrity we can know data
needs to be cleaned up.
By forbidding more than a row per variant in the spree_stock_items we
can ensure all variants have a single stock_item associated.
@sauloperez sauloperez force-pushed the unit-test-variant-stock branch from d4cfd51 to cd53ec1 Compare September 6, 2018 11:21
@@ -961,6 +961,7 @@

add_index "spree_stock_items", ["stock_location_id", "variant_id"], :name => "stock_item_by_loc_and_var_id"
add_index "spree_stock_items", ["stock_location_id"], :name => "index_spree_stock_items_on_stock_location_id"
add_index "spree_stock_items", ["variant_id"], :name => "index_spree_stock_items_on_variant_id", :unique => true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this now raises

ActiveRecord::RecordNotUnique:
  PG::Error: ERROR:  duplicate key value violates unique constraint "index_spree_stock_items_on_variant_id"
  DETAIL:  Key (variant_id)=(9) already exists.
  : INSERT INTO "spree_stock_items" ("backorderable", "count_on_hand", "created_at", "stock_location_id", "updated_at", "variant_id") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"

when saving a stock_item for a variant that already exists in the table.

@luisramos0
Copy link
Contributor

Awesome! Comments are better now, thanks!
I know it's probably the normal thing to do in Rails but I have to say again, I dont like the fact the name of the module is not stating clearly that it is an Adapter or a Concern. This VariantStock will never be used outside of Variant, it's an add on, it is not a standalone service/module/model, so I'd call it VariantStockConcern.

@sauloperez
Copy link
Contributor Author

Yep, I still need to take a look at that

Copy link
Member

@mkllnk mkllnk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice pull request. But I would like to discuss the deprecation approach before approving this.

def on_hand=(new_level)
error = 'Cannot set on_hand value when Spree::Config[:track_inventory_levels] is false'
raise error unless Spree::Config.track_inventory_levels

self.count_on_hand = new_level
end

# Sets the stock level. As opposed to #on_hand= it does not check
# `track_inventory_levels`'s value as it was previously an ActiveModel
# setter of te database column of the `spree_variants` table. That is why
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny typo:

te database

# #on_hand= is more widely used in Spree's codebase using #count_on_hand=
# underneath.
#
# So, if #count_on_hand= is used, `track_inventory_levels` won't be tacken
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tacken

def save_stock
stock_items.each(&:save)
stock_items.first.save
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In on_demand= we change all stock items, but here we save only the first one. Even though there should be only one item, I kept the each to make it correct in case there is more than one item.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, just take a look at https://github.com/openfoodfoundation/openfoodnetwork/pull/2638/files#diff-1acd2e7e27a227829d5d14a91c863bb6R964. With the DB-level constraint it will only be one item so no need for iterations.

In on_demand= however, I couldn't make the test work and I still don't know why... I can investigate it more in-depth after we merge it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Spree config option allow_backorders affects this attribute in various ways, if that helps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea how? I'll take a look anyway.

Copy link
Contributor Author

@sauloperez sauloperez Sep 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that config is gone @Matt-Yorkley from Spree.

end
# There shouldn't be any other stock items, because we should
# have only one stock location.
stock_items.first.__send__(:count_on_hand=, new_level)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. If there would be an accidental stock item, we would see weird bugs.

def on_demand
stock_items.any?(&:backorderable?)
warn_deprecation(__method__)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line should have been in the next commit. The method warn_deprecation is not defined yet.

@@ -77,7 +84,7 @@ def count_on_hand=(new_level)
# track_inventory_levels only. It was initially introduced in
# https://github.com/openfoodfoundation/spree/commit/20b5ad9835dca7f41a40ad16c7b45f987eea6dcc
def on_demand
warn_deprecation(__method__)
warn_deprecation(__method__, 'Spree::Config[:track_inventory_levels]')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not right. Track inventory levels is a global setting that we don't use at all. Alternatives are in_stock? and stock_items.first.backorderable?.

@@ -125,5 +134,12 @@ def overwrite_stock_levels(new_level)
# have only one stock location.
stock_items.first.__send__(:count_on_hand=, new_level)
end

def warn_deprecation(method_name, new_method_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm feeling a bit uneasy to introduce deprecation warnings without having a clear plan what else to use. We can refer to the stock items, but that may change again when we detailed the network feature. I would only deprecate the methods we are sure how to replace them.

Copy link
Contributor Author

@sauloperez sauloperez Sep 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, stock items may change again in upcoming versions, but no network feature can come without us being at a stable version of Spree at least. So we will forsee changes before that feature comes.

I would only deprecate the methods we are sure how to replace them.

It's more or less clear now and we'll get more insights as we move forward with the upgrade. The only thing we know is that using methods that no longer exist in Spree for new features is not an option.

raise_error_if_no_stock_item_available
raise_error_if_multiple_stock_items
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm in favour of a constraint. That does make some of my earlier comments redundant.

@sauloperez
Copy link
Contributor Author

sauloperez commented Sep 10, 2018

Ok, so I renamed and moved it @luisramos0 . Aligned with what you suggested I moved it to concerns/ to follow Rails 4 convention (In Rails 4 there's a models/concern/ by default).

It's an old misconception that all stuff in models/ needs to be AR's models and all other abstractions we come up with should go into lib/. Any of these abstractions are also part of our domain logic and as such, it makes sense they also live under app/. This CodeClimate's blog post is a nice and short explanation of the topic.

Thoughts?

@luisramos0
Copy link
Contributor

Awesome! I will do the same on OrderShippingMethod.
For me it makes perfect sense, /lib for generic not-ofn specific stuff and /app for ofn stuff.

What I am concerned about is the difficulty of creating a service culture: business logic like this should not be in models or concerns (models are for ORM and data), it should be in services. I believe the best place for this would be /app/services, but it would have to be a service, not a concern.

@sauloperez
Copy link
Contributor Author

To me, it is also less than ideal to have a concern (or name it mixin) rather than a service but this is here to 🔥 asap, so it feels good enough. Don't worry, the service culture is going to come sooner or later. I also have to convince myself sometimes 😅 .

@sauloperez sauloperez force-pushed the unit-test-variant-stock branch from cd648b0 to 26a988d Compare September 18, 2018 09:48
@sauloperez sauloperez force-pushed the unit-test-variant-stock branch from 26a988d to 314ad54 Compare September 18, 2018 10:19
@sauloperez
Copy link
Contributor Author

Track inventory levels is a global setting that we don't use at all

is that explained/documented somewhere @mkllnk ?

Also, #in_stock?'s definition uses exactly that config.

def in_stock?
return true unless Spree::Config[:track_inventory_levels]
on_demand || (count_on_hand > 0)
end

@luisramos0
Copy link
Contributor

2 approvals. moving to test ready.
I think @Matt-Yorkley just offered some advise at one point, he was not planning to review the PR.
correct me if I am wrong.

@luisramos0
Copy link
Contributor

moving to ready to go actually, no tests here.

@sauloperez
Copy link
Contributor Author

sauloperez commented Sep 24, 2018

I followed @Matt-Yorkley 's advice but nothing relevant came out of it, at least for now.

@sauloperez sauloperez merged commit 52a412a into openfoodfoundation:2-0-stable Sep 24, 2018
@sauloperez sauloperez deleted the unit-test-variant-stock branch September 24, 2018 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants