The role of the “product promotion engine” in OFBiz, is to apply a store’s promotions to the products in the shopping cart. When more than one promotion is applicable to the same product, the main challenge for the engine is to maximize the overall cart discount by using the best promotion for each product or group of products. This post explains how the recent code refactoring contributed by HotWax Media (OFBiz revision 1554265 by Jacopo Cappellato), improves the capability of the engine to select and apply the best promotions. To begin, we briefly review the previous method of applying promotions in order to highlight the limitations that required an improvement of the engine. To this aim, a simple representative example will be presented. Then, the main characteristics of the new approach will be described and, finally, algorithmic details will be touched on in the Appendix.
Behavior of the previous promotion engine and its limitations
The example in this section is about an online store that sells two types of products: “Product A” and “Product B.” Both “Product A” and “Product B” belong to a category of products called “Product Category 1.” The store has the following two active promotions:
– Promotion 1: 20% discount on all the products in “Product Category 1”
– Promotion 2: 40% discount on “Product A”
Note that both promotions are applicable to “Product A,” but each product is only allowed to benefit from one promotion.
A customer puts in his/her cart:
-1 unit of “Product A” (price $20)
-1 unit of “Product B” (price $40)
Previously the promotions would have been applied as follows.
Step 1. For each available promotion, a discount amount was computed by applying the promotion to the whole cart. In the case of our example:
– application of “Promotion 1” to the cart yields a discount amount = $12 ( = 20% of the price of “Product A” + 20% of the price of “Product B”)
– application of “Promotion 2” to the cart yields a discount amount = $8 (= 40% of the price of “Product A”)
Step 2. The discount amounts computed in step 1 were used to order the promotions from the best to the least. In our example, the ordering would be:
– “Promotion 1” first
– “Promotion 2” second
Step 3. The promotions were applied sequentially to the cart in the order as determined in Step 2, until each unit benefitted from the maximum of one promotion. In our case:
– the application of “Promotion 1” to the cart yields:
* $4 discount for “Product A” ( = 20% of the price of “Product A”)
* $8 discount for “Product B” ( = 20% of the price of “Product B”)
– the application of “Promotion 2” to the cart yields no discount since a promotion has already been applied to each item in the cart
– the total cart discount: $8 + $4= $12
In summary, the application of the previous approach in the case of this example yields a total discount of $12, obtained by using “Promotion 1” for both “Product A” and “Product B.” However, “Promotion 1” is not the largest discount available for “Product A,” the higher discount of $16 could have been obtained by applying “Promotion 1” to “Product B” ( → $8 discount) and “Promotion 2” to “Product A” ( → $8 discount).
Selection and application of the best promotions
Two main ideas characterize the improved way of applying promotions:
i) the value of one promotion with respect to another, is no longer evaluated in terms of the discount produced for the whole cart (see Step 1 in the previous section)
ii) promotions are no longer automatically applied to the cart as a whole (see Step 3 of the previous section).
Instead, the new approach:
– focuses on every minimum set of discountable units in the cart
– determines the best promotion for each set
– applies the best promotion to each set
A minimum set of discountable units may either comprise only one unit, in case the promotion is applicable to single units, e.g., in the case of “Promotion 1” and “Promotion 2” in the above example, or may comprise more than one unit in case the promotion requires the purchase of more than one unit, e.g., x % of discount if two units of a specific product are bought. For each set, the value of a promotion with respect to another is evaluated by computing a “weight” given by the ratio between the total price of the set, and the total amount of the discount given by the application for as a promotion to the set. If the set comprises only one unit, the weight simply corresponds to the portion of the price given as discount, e.g., 0.2 in case of a 20% discount on the product. If the set comprises more than 1 unit, the weight represents the portion of the total price given as discount. For instance, consider the following promotion: buy one unit of “Product A” together with one unit of “Product B”, to get a discount of 20% on the price of “Product A” and 30% on the price of “Product B”. Suppose, also, that “product A” and “product B” have the same price of $20. In this case:
– the minimum discountable set for the promotion comprises 1 unit of “product A” and 1 unit of “product B”
– the set price is $40 = $20 (price of “product A”) + $20 (price of “Product B”)
– the set discount is $10= $4 ( = 20% discount on “Product A”) + $6 (= 30% discount on “product B”)
– the weight relative to the application of the promotion to the discountable set is 0.25 ( = $10/$40).
Among all the promotions applicable to the same set, the one with the highest weight is selected to be applied to the set. This means that the discount yielded by the chosen promotion is the one corresponding to the highest portion of the set price.
As demonstrated in the Appendix for the example in Section 2, applying the most beneficial promotion to each discountable set maximizes the cart discount.
Appendix. Implementation details
Some algorithmic details on the improved engine are outlined below. For more detailed information on the topic, please refer to the commit log and diff: http://svn.apache.org/viewvc?view=revision&revision=1554265
The new approach is implemented by smartly exploiting information generated during Step 1 of the previous approach (see the review in Section 2), also called the test run. We recall that Step 1 consisted of applying all of the available promotions to the cart to determinine the order of their application. Any time a promotion is applied to the cart, several ProductPromoUseInfo objects are created. For each promotion, the number of ProductPromoUseInfo objects is equal to the number of minimum discountable sets associated with the promotion. For instance, with reference to the example in Section 2, application of “Promotion 1” generates two ProductPromoUseInfo objects: one relative to the minimum discountable set containing one unit of “Product A” and one to the set containing one unit of “Product B.” Each ProductPromoUseInfo object contains information about the price of the discountable set and about the discount generated by the application of the promotion to the set. This information allows OFBiz to easily compute the weight introduced in the previous section (i.e., set price/set discount).
The new algorithm works as follows:
1) promotions are applied to the cart one by one in a test run,
2) all ProductPromoUseInfo objects generated in 1) are collected,
3) for each ProductPromoUseInfo the corresponding weight is computed,
4) ProductPromoUseInfo are ordered by weight in decreasing order,
5) promotions corresponding to the ProductPromoUseInfo ordered as in 4) are put in a list (sortedExplodedProductPromoList),
6) promotions in the list are sequentially applied to one minimum discountable set (technical note: application of a promotion to a minimum discountable set is obtained by temporarily setting useLimitPerOrder =1)
As an example, suppose the algorithm is applied to the case presented in Section 2.
Promotion 1 can be applied to both the single unit of “Product A” and to the single unit of “Product B”. Hence two discountable sets are associated with “Promotion 1”:
– set 1 containing “Product A”
– set 2 containing “Product B”
Promotion 2 can be applied only to the single unit of “Product A.” Hence, the only minimum discountable set associated with “Promotion 2” is set to 1.
By applying all of the promotions to the cart (test run) three ProductPromoUseInfo objects are created:
– a ProductPromoUseInfo relative to the application of “Promotion 1” to “Product A” (set 1) → weight 0.2 ( = $4/$20)
– a ProductPromoUseInfo relative to the application of “Promotion 1” to “Product B” (set 2) →weight 0.2 (= $8/$40)
– a ProductPromoUseInfo relative to the application of “Promotion 2” to “Product A” (set 1) → weight 0.4 ( = $8/$20 )
Sorting by weight yields the following order:
1) ProductPromoUseInfo relative to the application of “Promotion 2” to “Product A” – weight 0.4
2) ProductPromoUseInfo relative to the application of “Promotion 1” to “Product A” – weight 0.2
3) ProductPromoUseInfo relative to the application of “Promotion 2” to “Product B” – weight 0.2
The corresponding list of promotions is:
1. Promotion 2
2. Promotion 1
3. Promotion 1
By sequentially applying the promotions to the discountable sets, the following is obtained:
1) application of “Promotion 2” to“ Product A” yields a discount of $8 ( = 40% of price of “Product A” )
2) attempting to apply “Promotion 1” to “Product A” does not yield any discount since a promotion has already been applied to “Product A”
3) application of “Promotion 1“ to “Product B” yields a discount of $8 ( = 20% of price of “Product B”)
4) the total discount is $16.
Jacopo Cappellato is VP of Technology at HotWax Media and has been involved with the OFBiz project since 2003. He is an OFBiz Project Committer and a member of both the OFBiz Project Management Committee and the Apache Software Foundation.