bleroy / Nwazet.Commerce

Idiomatic commerce module for Orchard CMS.
BSD 3-Clause "New" or "Revised" License
26 stars 21 forks source link

SKUs' enhancements and multilanguage store #101

Closed MatteoPiovanelli-Laser closed 7 years ago

MatteoPiovanelli-Laser commented 7 years ago

Opening this for parts of what's in #99 I am keeping the skus with the multilanguage thing because they intersect

Multi-language store We will need to have localized versions of products, bundles and attributes. This opens up the field to several considerations not unlike those we did in Orchard for Taxonomies and ContentPicker: synchronization, limiting selection to same-language things only... On top of that, there is the issue of SKUs: the way I see this, a SKU should be a unique product identifier (even though there is currently no check for that, but I'll come to that later). However, if a ContentItem of type Product has localized versions, they should in general have the same SKU, because they represent the same product.

SKU As I mentioned above, to me a SKU should be a unique product identifier. However, this unicity is currently not enforced, and has some caveats (e.g. the case of multiple locales). I would create a feature to:

MatteoPiovanelli-Laser commented 7 years ago

Did anyone give a thought on advanced SKUs? I was thinking earlier that in some cases, variants may have slightly different skus, as they drive the inventory differently. e.g. the "Brown" variant for a show may have sku = "SHOE1_BROWN" and the "Navy" variant may have sku = "SHOE1_NAVY". Sku generation then becomes more complex

bleroy commented 7 years ago

Ideally, this could work like Autoroute, where you can configure how the slug gets generated, from tokens.

MatteoPiovanelli-Laser commented 7 years ago

I think that the ability for attributes to take part in SKU generation should be in a feature, probably the same feature that allows having separate inventories based on product attributes.

MatteoPiovanelli-Laser commented 7 years ago

I started doing something on this, in a branch. Let me know if it all makes sense or not:

Still thinking about:

bleroy commented 7 years ago

I agree attribute should take part in SKU generation, but this should be done using tokens. Instead of having providers that procedurally generate the SKU, do it the Orchard way and have a setting that uses tokens to format a string. This allows for all your scenarios and more, and doesn't require coding to change the format. Like for the slug, the SKU should still be editable after generation if settings allow for it. Really, Autoroute is the blueprint for this feature. It's pretty much the same design.

MatteoPiovanelli-Laser commented 7 years ago

OK, I agree with you that Autoroute is a great template for this.

The providers I mentioned above are not for SKU generation, but for the uniqueness check: they provide the cases where SKUs may actually not be unique (localization being the one case I could think of and wrote the provider for).

MatteoPiovanelli-Laser commented 7 years ago

The check for uniqueness is currently (in the branch I am working on) implemented as a query for ProductParts whose SKU is the same as the part being processed, . The providers for the exceptions have a method call that returns a list of the IDs of items that may have the same SKU as the product being processed.

var sameSKUProductIds = _contentManager.Query<ProductPart, ProductPartRecord>(VersionOptions.Latest)
    .Where(ppr => ppr.Id != productPart.Id && ppr.Sku == productPart.Sku)
    .List().Select(pp => pp.Id);
//Handle exceptions to uniqueness of SKUs
if (_SKUUniquenessExceptionProviders.Any()) {
    var exceptionIds = _SKUUniquenessExceptionProviders.SelectMany(provider =>
        provider.GetIdsOfValidSKUDuplicates(productPart));
    sameSKUProductIds = sameSKUProductIds.Where(ppid => !exceptionIds.Contains(ppid));
}
if (sameSKUProductIds.Any()) {
    context.Updater.AddModelError("", T("The SKU must be unique."));
}
MatteoPiovanelli-Laser commented 7 years ago

Considerations on the tokenization of the SKU using attributes:

Basically, where we want inventory and sku to be by-variant, I think we get:

I think I would in the end have an Inventory db table for that, that has:

bleroy commented 7 years ago

That is a big change. Not all products have attributes. I'd like to better understand what exactly will happen at various stages of the product creation and buying workflows: what are all the places where we use the SKU, and how do we use it with those changes?

MatteoPiovanelli-Laser commented 7 years ago

I am doing this in stages, because I am not 100% sold on the big changes I described in my post above, and I am not yet sure about the reworks those would require.

The first stage, for which I may be able to put a PR out tomorrow, sets up a feature that has:

That first stage doe not introduce any particular complexity.

The next stages would build on top of that feature and whatever we decide to use to handle an inventory-by-variant, and are for now postponed to whenever we feel that we found a solid design.

bleroy commented 7 years ago

Sounds good, except that I don't like the default pattern: nothing should expose the content id in principle. What about the slug?

MatteoPiovanelli-Laser commented 7 years ago

Sure

MatteoPiovanelli-Laser commented 7 years ago

With that PR merged, the next improvement I would do on SKUs would be to keep the inventory in synch between products with the same SKU.

The ISKUUniquenessHelper provided in the code there basically allows product translations to have the same SKU. I think they should also have the same inventory.

bleroy commented 7 years ago

Sounds reasonable.

MatteoPiovanelli-Laser commented 7 years ago

I am on this right now. I am setting this up so that when the specific feature is activated, the synchronization of the inventories based on the SKU is active by default.

I have a few points where I would like your input:

MatteoPiovanelli-Laser commented 7 years ago

also, something I noticed:

Say I have PRODUCT-A, with inventory 100. This is published. I Edit the inventory to 80 and save, creating a draft. The value of the inventory is changed also for the published PRODUCT-A. Same goes for all other product properties (price, sku ....).

I can see reasons for this being as it is, but I can also see cases in favor of versioning the ProductPart.

bleroy commented 7 years ago

If there's a way to coalesce warnings, that may alleviate the problem of too many products throwing them. Is the synchronization a manual operation or is it automatic when you activate the feature? Is it part of a migration? I don't think it can be a migration, because the feature could still be disabled later, products can go out of sync, and then you could turn it on again.

An inventory service sounds fine.

Better summary UI to modify inventory sounds great too. I know I would have used that feature. It should still have a one-click way to remove one. Maybe have up and down arrows next to the textbox.

Links to inventory-sharing products sounds good too.

Unrelated: we should really have a way to quickly find a product by SKU.

Yes, inventory is not versionable iirc, and probably shouldn't be. I'm not sure that's a problem. Price on the other hand, should probably be versionable. Maybe we should have an issue to track this and consider what should and shouldn't be versionable.

MatteoPiovanelli-Laser commented 7 years ago

If there's a way to coalesce warnings, that may alleviate the problem of too many products throwing them. Is the synchronization a manual operation or is it automatic when you activate the feature? Is it part of a migration? I don't think it can be a migration, because the feature could still be disabled later, products can go out of sync, and then you could turn it on again.

After the feature is enabled, the synchronization will happen automatically on inventory changes. At the moment the feature is enabled, I would run a check on products to verify that inventories that should be in synch (same SKU) actually are. If any are not, I would add a notification. My running idea right now is that I would have a single product for each "group" (linking to its editor view), up to a number (say 5). If there's more, I would have a link to a page where I am displaying the full list of issues. This page could be on principle the store settings page. Then, it should be possible to trigger manually the method verifying that the synchronization is fine, and it should also, I think, run every once in a while "by itself" to make sure nothing got messed up.

An inventory service sounds fine.

Doing it.

Better summary UI to modify inventory sounds great too. I know I would have used that feature. It should still have a one-click way to remove one. Maybe have up and down arrows next to the textbox.

Will do something. Up/down arrows should cover most cases, but I'll also think about something more complex. It's probably something that should be permission-ed, if it's not already (I don't remember)

Links to inventory-sharing products sounds good too.

Done it, I think.

Unrelated: we should really have a way to quickly find a product by SKU.

I agree. A feature based on Orchard.Search that adds a search box to the Product's list and sets the search up? I am honestly not familiar with Search. I would also like things like "products with inventory less than N", "Sort products by price" and so on...

MatteoPiovanelli-Laser commented 7 years ago

As a side note, to make the inventory synchronization work I am also fixing a few weird things with bundles: basically, when a ContentItem has both BundlePart and ProductPart, the inventory is not always being updated correctly. For example, when clicking the "remove one" link for the bundle, only the inventory of the products it contains are being updated. Moreover, when updating a product's inventory form its editor view, bundles containing that product are not being updated.

Since I am doing the IProductInventoryService I am cleaning that up as well.

MatteoPiovanelli-Laser commented 7 years ago

Better summary UI to modify inventory sounds great too. I know I would have used that feature. It should still have a one-click way to remove one. Maybe have up and down arrows next to the textbox.

2 things:

bleroy commented 7 years ago

I would also like things like "products with inventory less than N", "Sort products by price" and so on...

That sounds more like reports to me, for which there's a feature in the module already.

What did you use to minify inventory.js?

I don't remember. Probably a manual operation using Mads Kristensen's VS extension. Ideally, we'd have an automated task to do that.

I am not adding the "Add One" link for bundles, because it sounds wrong to have it.

Sounds reasonable.

Then, it should be possible to trigger manually the method verifying that the synchronization is fine, and it should also, I think, run every once in a while "by itself" to make sure nothing got messed up.

I'm uncomfortable with all this. Having code lingering around, and even running regularly, that deals with what should be a point-in-time state doesn't seem right. I'd much prefer a manual upgrade step, maybe even in a separate feature. The reasoning here is that if I'm a user who started with the actual feature on, I have no use for that code and shouldn't be affected by it in any way.

MatteoPiovanelli-Laser commented 7 years ago

How about this:

I add to InventoryBySKUSiteSettingsPart a InventoriesAreAllInSynch flag that is set to true when we checked all inventories and verified that they are fine. I add a method CheckForInventoryConflicts that gives me the list of product groups whose inventories don't match. If InventoriesAreAllInSynch == false, an INotificationProvider tells the back-end user there may be issues and prompts them to go to the ecommerce settings page. At the time the feature is enabled, the flag is set to false, and CheckForInventoryConflicts is run once. If no conflicts are found, the flag is set to true. In the ecommerce settings page, I have a text telling that the feature is on (this is already in the code I have). I'll add a button "Check for inventory conflicts" that will cause the CheckForInventoryConflicts method to be called and its results displayed:

So the method will run only once at the time the feature is enabled, and then can be re-run manually whenever the store administrator wishes. Ideally, it will find no conflicts to begin with. Less ideally, it will be run once automatically, and it will be run again manually as the back-end user goes to fix things (twice here: once to find the list of issues and a second time once they are all fixed to set the flag).

If ways are found to put things out of synch after that, the back-end user will not be notified automatically.

bleroy commented 7 years ago

Sounds reasonable.

MatteoPiovanelli-Laser commented 7 years ago

See #113 where I implemented what is described above.

MatteoPiovanelli-Laser commented 7 years ago

Since #113 is in, I'll close this issue as well.