Skip to content
This repository has been archived by the owner on May 9, 2020. It is now read-only.

Commit

Permalink
Applied fix from trunk for revision: 1554265
Browse files Browse the repository at this point in the history
===

This is a refactoring of the product promotion engine in order to overcome some limitations that prevented it to select and apply the best set of promotions under special conditions.

Example: Consider two promotions:
* consider two products: Product A, with price $20, and Product B, with price $40
* Promotion 1: 20% discount on all the products in a category containing Product A and Product B
* Promotion 2: 40% discount on Product A

When Product A and Product B are both in the cart:
* Expected behavior: on Product A the Promotion 2 should be applied i.e. 40% discount, and on Product B Promotion 1 should be applied i.e. 20% discount.
** Summary
Product		Price	Discount		Subtotal
A		$20	$8 (40% discount)	$12
B		$40	$8 (20% discount)	$32
Total Adjustment: $16

* OFBiz behavior (before this fix): Promotion 1 is applied to Product A and Product B; this happens because the total discount of Promotion 1 is greater than the total discount of Promotion 2 and OFBiz applies promotions sorted by discount (desc)
** Summary
Product		Price	Discount		Subtotal
A		$20	$4 (20% discount)	$16
B		$40	$8 (20% discount)	$32
Total Adjustment: $12

The new solution fixes this issue and similar ones.

Here are some details about the new algorithm.

Overview of the flow:
1) run the promotions one by one in a test run
2) collect the ProductPromoUse information
3) sort them by weight (i.e. the ratio between the discount and the value of the products discounted)
4) execute the ProductPromoUse in the given order

In order to understand this solution, and specifically the changes to ProductPromoWorker.java, there is an important concept to consider:
one Promotion can generate more than one ProductPromoUseInfo objects.
For example if I have 2 units of WG-1111 in the cart (in one cart item) and I have the promotion “20% discount on WG-1111 and GZ-1000” then the system will create TWO ProductPromoUseInfo objects both associated to the same promotion one for each of the 2 units discounted.
Similarly if I had two lines: 2 units of WG-1111 and 1 unit of GZ-1000 I would get 3 ProductPromoUseInfo objects 2 objects for WG-1111 and 1 object for GZ-1000

We can sort these ProductPromoUseInfo objects based on their weight (i.e. the ratio between the discount and the value of the products discounted) in desc order
and now we have a sorted list of ProductPromoUseInfo objects ready to be executed
However we only want to execute each of them once and for this reason we set (in memory, not in the DB) the useLimitPerOrder to 1 in the first ProductPromoUseInfo of a given promotion and then to 2 if the same ProductPromoUseInfo is associated to the same promotion etc...
in this way the end result is that the system will generate, as we desire, ONE ProductPromoUseInfo only for each of the ProductPromoUseInfo in the list.

Here is an example:
we have 2 promotions:
PROMO A
PROMO B

After test run:

ProductPromoUseInfo - PROMO A - #1 - weight 0.3
ProductPromoUseInfo - PROMO A - #2 - weight 0.3
ProductPromoUseInfo - PROMO B - #1 - weight 0.4

After sorting:

ProductPromoUseInfo - PROMO B - #1 - weight 0.4
ProductPromoUseInfo - PROMO A - #1 - weight 0.3
ProductPromoUseInfo - PROMO A - #2 - weight 0.3

Based on this we create a list (sortedExplodedProductPromoList) of ProductPromo:

PROMO B - with useLimitPerOrder=1
PROMO A - with useLimitPerOrder=1
PROMO A - with useLimitPerOrder=2

When we apply these to the cart we get the following results:

PROMO B - with useLimitPerOrder=1 APPLIED
PROMO A - with useLimitPerOrder=1 APPLIED
PROMO A - with useLimitPerOrder=2 NOT APPLIED (because PROMO B used the item)




git-svn-id: https://svn.apache.org/repos/asf/ofbiz/branches/release13.07@1554381 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
jacopoc committed Dec 31, 2013
1 parent 1192618 commit e44c025
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2714,7 +2714,7 @@ public BigDecimal getSubTotalForPromotions() {
}
itemsTotal = itemsTotal.add(cartItem.getItemSubTotal());
}
return itemsTotal;
return itemsTotal.add(this.getOrderOtherAdjustmentTotal());
}

/**
Expand Down Expand Up @@ -3142,12 +3142,12 @@ public Map<GenericPK, String> getAllDesiredAlternateGiftByActionCopy() {
return new HashMap<GenericPK, String>(this.desiredAlternateGiftByAction);
}

public void addProductPromoUse(String productPromoId, String productPromoCodeId, BigDecimal totalDiscountAmount, BigDecimal quantityLeftInActions) {
public void addProductPromoUse(String productPromoId, String productPromoCodeId, BigDecimal totalDiscountAmount, BigDecimal quantityLeftInActions, Map<ShoppingCartItem,BigDecimal> usageInfoMap) {
if (UtilValidate.isNotEmpty(productPromoCodeId) && !this.productPromoCodes.contains(productPromoCodeId)) {
throw new IllegalStateException("Cannot add a use to a promo code use for a code that has not been entered.");
}
if (Debug.verboseOn()) Debug.logVerbose("Used promotion [" + productPromoId + "] with code [" + productPromoCodeId + "] for total discount [" + totalDiscountAmount + "] and quantity left in actions [" + quantityLeftInActions + "]", module);
this.productPromoUseInfoList.add(new ProductPromoUseInfo(productPromoId, productPromoCodeId, totalDiscountAmount, quantityLeftInActions));
this.productPromoUseInfoList.add(new ProductPromoUseInfo(productPromoId, productPromoCodeId, totalDiscountAmount, quantityLeftInActions, usageInfoMap));
}

public void removeProductPromoUse(String productPromoId) {
Expand Down Expand Up @@ -4385,23 +4385,43 @@ public boolean equals(Object obj) {
}
}

public static class ProductPromoUseInfo implements Serializable {
public static class ProductPromoUseInfo implements Serializable, Comparable<ProductPromoUseInfo> {
public String productPromoId = null;
public String productPromoCodeId = null;
public BigDecimal totalDiscountAmount = BigDecimal.ZERO;
public BigDecimal quantityLeftInActions = BigDecimal.ZERO;
private Map<ShoppingCartItem,BigDecimal> usageInfoMap = null;

public ProductPromoUseInfo(String productPromoId, String productPromoCodeId, BigDecimal totalDiscountAmount, BigDecimal quantityLeftInActions) {
public ProductPromoUseInfo(String productPromoId, String productPromoCodeId, BigDecimal totalDiscountAmount, BigDecimal quantityLeftInActions, Map<ShoppingCartItem,BigDecimal> usageInfoMap) {
this.productPromoId = productPromoId;
this.productPromoCodeId = productPromoCodeId;
this.totalDiscountAmount = totalDiscountAmount;
this.quantityLeftInActions = quantityLeftInActions;
this.usageInfoMap = usageInfoMap;
}

public String getProductPromoId() { return this.productPromoId; }
public String getProductPromoCodeId() { return this.productPromoCodeId; }
public BigDecimal getTotalDiscountAmount() { return this.totalDiscountAmount; }
public BigDecimal getQuantityLeftInActions() { return this.quantityLeftInActions; }
public Map<ShoppingCartItem,BigDecimal> getUsageInfoMap() { return this.usageInfoMap; }
public BigDecimal getUsageWeight() {
Iterator<ShoppingCartItem> lineItems = this.usageInfoMap.keySet().iterator();
BigDecimal totalAmount = BigDecimal.ZERO;
while (lineItems.hasNext()) {
ShoppingCartItem lineItem = lineItems.next();
totalAmount = totalAmount.add(lineItem.getBasePrice().multiply(usageInfoMap.get(lineItem)));
}
if (totalAmount.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
} else {
return getTotalDiscountAmount().negate().divide(totalAmount);
}
}

public int compareTo(ProductPromoUseInfo other) {
return other.getUsageWeight().compareTo(getUsageWeight());
}
}

public static class CartShipInfo implements Serializable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.math.BigDecimal;
import java.math.MathContext;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -629,7 +630,7 @@ public static Map<String, Object> assignItemShipGroup(DispatchContext dctx, Map<
cart.addProductPromoCode(productPromoCode, dispatcher);
}
for (GenericValue productPromoUse: orh.getProductPromoUse()) {
cart.addProductPromoUse(productPromoUse.getString("productPromoId"), productPromoUse.getString("productPromoCodeId"), productPromoUse.getBigDecimal("totalDiscountAmount"), productPromoUse.getBigDecimal("quantityLeftInActions"));
cart.addProductPromoUse(productPromoUse.getString("productPromoId"), productPromoUse.getString("productPromoCodeId"), productPromoUse.getBigDecimal("totalDiscountAmount"), productPromoUse.getBigDecimal("quantityLeftInActions"), new HashMap<ShoppingCartItem, BigDecimal>());
}
}

Expand Down
Loading

0 comments on commit e44c025

Please sign in to comment.