Modeling exerciseThe grocery store’s checkout model process approach
I posted about the grocery store checkout process exercise before. Now I want to see if I can do a short outline on how I would handle this.
The key aspect from my perspective is that we need to separate the notion of the data we have and the processing of the data. That means that we are going to have the following model:
public class ShoppingCart { public List<ProductInShoppingCart> Products {get;set;} public List<Discount> Discounts { get;set; } } public class ProductInShoppingCart { public string ProductId; public Discount Discount; }
Note that we explicitly do not have a quantity field here. If we purchase 6 bottles of milk, that would appear three times in the cart. Why is that?
Let us assume that we have a sale for 2 bottles of milk for 20% discount or a 3 +1 bottles of milk offer. Consider the kind of code you would have to write in the offer code:
- Find all products that have this offer and have 4 items without discount.
- Add the discount to those products.
- After searching for products without discount, need to search for products with a discount, but that we can apply this to and get a better option.
In this case, we start by doing:
- Add bottle of milk
- Add bottle of milk – 2 for 20% discount is triggered.
- Add bottle of milk
- Add bottle of milk – 3+1 offer is triggered, removing the previous discount.
Because this is likely going to be complex, I’m going to be writing this once. A set of offers and the kind of rules that we want. Then we will give the users the ability to define those rules.
Note that we keep the raw data (products) and the transformations (discounts) separate, so we can always reapply everything without losing any data.
More posts in "Modeling exercise" series:
- (28 Oct 2014) The grocery store’s checkout model process approach
- (21 Oct 2014) The grocery store’s checkout model
- (06 Jun 2014) Flights & Travelers
Comments
Quantity becomes important because you also need to concept of Units.
Take bananas, you have 1 kilo of bananas at half price, but you sell individual bananas at an average "each" price. So you store the unit as well, or what you show the user is different to what you store in the backend.
ProductInShoppingCart is just Line (or DocumentLine) and it must have Quantity.
Your iterative approach has to be replay-able when the second bottle of milk is removed at some point (a correction).
Discount rules are usually non-deterministic. A smart POS will optimize on biggest gain for the end customer (or not). It can at least offer a choice on ambiguity.
Ayende,
Would you want a property for
public bool DiscountApplied { get; set; }
so it would be easier to mark items as already participating in a previous discount?How about separation of the state (that's the code you posted) and events and making it an event sourced solution? In this kind of a domain, looking through the event-centric glass is so easy.
How are coupons recorded and applied? Coupons cannot be abstracted as discounts.
Applying rules based on the last item added creates too many "what if..." scenarios. It also completely ignores all of the discount scenarios wherein products don't matter: Spend $100 get $15 off, Senior citizens get a 5% discount, Disabled Vietnam veterans don't pay sales tax, etc...
There are also form-of-payment rules that can apply: You can't use gift certificates to purchase gift cards.
The holistic approach is to take everything you know about the transaction (in a data structure representing current context) against a list of active discount conditions. For the rules whose conditions are met, you gather the resulting adjustments and their types.
Then you let the discount types duke it out by applying collision resolution rules: You can't apply discounts to items with "permanent markdowns", Percent-off discounts are applied AFTER dollar-off discounts, etc...
The adjustments that survive that process get applied to the context price list and returned to the point-of-sale system.
This process is repeated for each item event (adds, removes, quantity changed) and the POS reports the deltas to the customer.
It seems to me the complicated part is the data processing that some rules can be applied with others while some are not, applying the rules in different orders gives different results and it needs to find the best. Letting the non-technical admin users to be able to configure the rule sets makes it even more complicated. Sounds like workflows isn't it?
In my view, a customer should always have the best deal. It is bad customer service and the sign of inferior promotional logic if a customer has to split their shopping cart into two in order to get a better deal.
Also consider that a product could be in multiple promotions, so for example a drink could be in a 'meal deal' promotion where it is offered with a sandwich and snack for a fixed price, and it could also be in a 2 for x promotion.
The best way to deal with this is to look at all of the promotions that apply to the total cart and then calculate the total promotional discount for every permutation. As each permutation is calculated the qty of each product is reduced so that it is not available for any other promotions, and the quantities are then reset for the calculation of the next permutation.
We then use the permutation that gives the customer the best deal.
Comment preview