November Happy Hour will be moved to Thursday December 5th.

Filter product search results on inventory

Vote:
 

Similar to the issue described in "Filter Item search results on inventory?", I am trying to filter products based on whether any related variants are in stock. I attempted the following filter, where warehouses is a list of strings:

     query.Filter(x => x.Stock(), x => x.WarehouseCode.In(warehouses) & x.Quantity.GreaterThan(0));

The Stock() method is an extension method on Products that returns a list of the following type:

public class VariantStockDetails(string sku, string warehouseCode, int quantity)
{
    public string Sku { get; set; }
    public string WarehouseCode { get; set; } = warehouseCode;
    public int Quantity { get; set; } = quantity;
}

I have also attempted to include this in our find initialization module:

client.Conventions.ForInstancesOf<BaseProduct>()
      .IncludeField(x => x.Stock());

client.Conventions.NestedConventions.ForInstancesOf<BaseProduct>().Add(c => c.Stock());
client.Conventions.NestedConventions.ForType<BaseProduct>().Add(c => c.Stock());

I can see entries in the index like this:

 "Stock$$nested": [
        {
            "Quantity$$number": 35,
            "___types": [
                "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails",
                "System.Object"
            ],
            "WarehouseCode$$string": "100",
            "$type": "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails, XXX.Foundation.Domain"
        },
        {
            "Quantity$$number": 36,
            "___types": [
                "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails",
                "System.Object"
            ],
            "WarehouseCode$$string": "106",
            "$type": "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails, XXX.Foundation.Domain"
        }
    ],

However, it appears that the filter isn't being applied correctly, as I am still receiving results where none of the WarehouseCodes on stocks on the products entries matches any of the values in the provided warehouses list.



#332746
Nov 12, 2024 12:25
Vote:
 

I don't think you can filter it like that. Nested filter is a bit more tricky, you might need to create something like this

new NestedFilterExpression<BaseProduct, VariantStockDetails>(x => x.Stocks(),  x => x.WarehouseCode.In(warehouses) & x.Quantity.GreaterThan(0)), client.Conventions);

disclaimer: I'm not a Find expert

#332751
Nov 12, 2024 17:04
Christian Morup - Nov 12, 2024 21:20
I am afraid that I get a "MaxSerializationDepthExceededException: Max serialization depth exceeded while serializing object of type SearchRequestBody. The depth was 26. This indicates that there might be a circular reference in the object. Ensure that the serialized object does not return itself or has a property that returns itself or the serialized object." with this approach.
Quan Mai - Nov 12, 2024 21:39
can you share what you've done?
Christian Morup - Nov 12, 2024 21:41
Edit: I will answer in a new comment. For some reason parts are missing.
Vote:
 

I did the following: 

var nestedFilter = new NestedFilterExpression<BaseProduct, VariantStockDetails>(x => x.Stock(), x => x.WarehouseCode.In(warehouses) & x.Quantity.GreaterThan(0), findClient.Conventions);

query = query.Filter(nestedFilter);


But receiving the MaxSerializationDepthExceededException. 

#332764
Nov 12, 2024 21:43
Vote:
 

You cannot do AND operation between DelegateFilterBuilder and FilterExpression, you could do using two DelegateFilterBuilder but not with a FilterExpression. Try following for the above scenario.

query
.Filter(x => x.Stock(), x => x.WarehouseCode.In(warehouses))
.Filter(x => x.Stock(), x => x.Quantity.GreaterThan(0));

When you use two filters like above, it works like default AND operation. If you need a complex operation you can use FilterBuilder and use FilterBuilder.Or / And function to build your operations.

#332800
Nov 12, 2024 23:46
Vote:
 

Thanks for the suggestion, but it doesn’t resolve the issue in this specific scenario.

Consider this data sample:

"Stock$$nested": [
    {
        "Quantity$$number": 0,
        "___types": [
            "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails",
            "System.Object"
        ],
        "WarehouseCode$$string": "100",
        "$type": "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails, XXX.Foundation.Domain"
    },
    {
        "Quantity$$number": 36,
        "___types": [
            "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails",
            "System.Object"
        ],
        "WarehouseCode$$string": "106",
        "$type": "XXX.Foundation.Domain.Catalog.Variants.VariantStockDetails, XXX.Foundation.Domain"
    }
]

In this example, let’s say my list of warehouses only includes "100". With your proposed approach, the product is still included because there exists another warehouse ("106") with "Quantity > 0". However, the expected outcome would be to exclude this product since the specified warehouse ("100") has "Quantity = 0". 

#332834
Nov 13, 2024 12:36
Sujit Senapati - Nov 13, 2024 13:01
Please try the above code, two filter works just like AND operation in one filter. First filter will include all warehouse codes you are interested in and second filter will further strip all list to only list with quantity more than 0.
Christian Morup - Nov 13, 2024 13:10
I did. It it did not work. Otherwise I would not claim that it didn't resolve the issue. In the above case it does have a stock entry with quantity above 0 AND it does have a warehouse with code 100. However, it does not have a quantity above 0 for warehouse 100, which is what I am looking for. Hence, It doesn't look at the combination as I would expect as when using a nested filter.
Sujit Senapati - Nov 14, 2024 15:46
Let me try to understand once more.
Lets say you are requesting with warehouses = {"100"}
Then first filter will only consider the object that matches warehouse code "100" the one with quantity 0. But the second filter will look for quantity > 0. So the result will be no matching. Is that not expected result you are looking for?
This is what I am thinking you are trying to do. X & (A & B) = (X & A) & (X & B).
Sorry for repeating the same thing, trying to understand the expected output so I can provide another approach using FilterBuilder if this one does not work.
Christian Morup - Nov 15, 2024 7:16
I still think you are missing the key aspect. The thing is, the filter is applied on the BaseProducts, which has a list of these VariantStockDetails in the index. If the filter was on the VariantStockDetails, then it would work - however, this is not the case.

Because the filter is applied on the BaseProduct, the first filter will check: Is there a warehouse with code 100 on this BaseProduct? This is true. The second filter then checks: Is there a warehouse with quantity > 0 on this BaseProduct? This is also true. The issue is, that it is not the same warehouse.
Sujit Senapati - Nov 15, 2024 13:48
So silly of me to understand this and taking so long to catch it. Thanks for explaining. I am adding some code below to see if you can test that.
Vote:
 

probably

        public static FilterExpression<Inventory> MatchSomething(this BaseProduct value,
            IList<string> warehouses)
        {
            var client = ServiceLocator.Current.GetInstance<IClient>();
            new NestedFilterExpression<BaseProductVariantStockDetails>(x => x.Stocks(),  x => x.WarehouseCode.In(warehouses) & x.Quantity.GreaterThan(0)), client.Conventions);
        }

then

.Filter(x => x.MatchSomething(warehouses));

#332847
Nov 13, 2024 13:20
Christian Morup - Nov 13, 2024 13:59
Edit: There seems to be a bug in the comment tool. Everything from my first angle bracket in these comments are missing. I have replaced it with these´{{ }} for now
I tried:

public static NestedFilterExpression{{BaseProduct, VariantStockDetails}} MatchSomething(this BaseProduct value,
IList{{string}} warehouses)
{
var client = ServiceLocator.Current.GetInstance{{IClient}}();
return new NestedFilterExpression{{BaseProduct, VariantStockDetails}}(x =}} x.Stock(), x =}} x.WarehouseCode.In(warehouses) & x.Quantity.GreaterThan(0), client.Conventions);
}

And did
query = query.Filter(x =}} x.MatchSomething(warehouses));


But I am getting this error:
InvalidOperationException: variable 'x' of type 'NDI.Foundation.Commerce.Catalog.BaseProduct' referenced from scope '', but it is not defined
Quan Mai - Nov 13, 2024 14:18
yes it's a "feature" to defend against XSS. I added a post below. I copied and pasted, it should be y, not x
Christian Morup - Nov 13, 2024 14:40
That sounds weird. I tried renaming this part:
query = query.Filter(test =}} test.MatchSomething(warehouses));

And got this error:
InvalidOperationException: variable 'test' of type 'XXX.Foundation.Commerce.Catalog.BaseProduct' referenced from scope '', but it is not defined
Vote:
 

When you use two filters this is how internal EPiServer Find code resolves it to exactly what you are trying to do.

public static ITypeSearch<TSource> Filter<TSource>(this ITypeSearch<TSource> search, Filter filter)
{
    search.ValidateNotNullArgument("search");
    return new Search<TSource, IQuery>(search, delegate (ISearchContext context)
    {
        ApplyFilter(context, filter, (Filter existingFilter, Filter newFilter) => existingFilter & newFilter);
    });
}

May be I am suggesting something wrong but expected result is as above.

#332848
Nov 13, 2024 13:35
Christian Morup - Nov 13, 2024 14:06
I think you might be missing a key aspect: this is about filtering a nested collection within each BaseProduct. The filter you suggested is applied at the BaseProduct level, and as you correctly stated, it performs an AND operation on the list of BaseProducts.

However, the challenge is within the nested list of VariantStockDetails. Each BaseProduct contains a collection of VariantStockDetails, and I need an AND operation inside this list. Currently, the filters are applied separately: one for WarehouseCode and another for Quantity. This means it’s enough for one VariantStockDetails item to pass the WarehouseCode check and a different item to pass the Quantity check.

What I am looking for is a way to ensure that at least one VariantStockDetails item satisfies both conditions simultaneously (i.e., matches the WarehouseCode and has Quantity > 0).

This is why your suggestion does not filter the data correctly in this scenario.
Sujit Senapati - Nov 13, 2024 15:07
Ok. Thanks for trying.
Vote:
 

Sorry, it should be 

 new NestedFilterExpression<BaseProductVariantStockDetails>(x => x.Stocks(),  y => y.WarehouseCode.In(warehouses) & y.Quantity.GreaterThan(0)), client.Conventions);

#332852
Nov 13, 2024 14:17
Christian Morup - Nov 13, 2024 14:37
Still the same issue. It seems to be related to the x-variable in "query = query.Filter(x =}} x.MatchSomething(warehouses)); "
Vote:
 

Here is what I would suggest after understanding the problem. 

Index another property in Stock() that is either string or boolean. Something like IsZeroQuantity or index the QuantityString as string. Once you do that try following code.

query.Filter(x => x.Stock(), x => x.WarehouseCode.In(warehouses) & x.IsZeroQuantity.Match(false));
query.Filter(x => x.Stock(), x => x.WarehouseCode.In(warehouses) & x.QuantityString.Match("0"));

In both of these cases you are trying to use AND operation between two DelegateFilterBuilder which will function correctly vs DelegateFilterBuilder & FilterExpression. Let us know if it works.

#332984
Nov 15, 2024 13:55
Sujit Senapati - Nov 15, 2024 14:27
You should logical negation in the QuantityString Match. Something like this
query.Filter(x => x.Stock(), x => x.WarehouseCode.In(warehouses) & !x.QuantityString.Match("0"));
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.