I have not yet tested nor reviewed, I guess you are aware of http://ci.apache.org/projects/ofbiz/logs/trunk/html/ ? Related to last build http://ci.apache.org/builders/ofbiz-trunk/builds/4247
Jacques On Monday, December 30, 2013 4:53 PM jaco...@apache.org wrote > Author: jacopoc > Date: Mon Dec 30 15:53:07 2013 > New Revision: 1554265 > > URL: http://svn.apache.org/r1554265 > Log: > 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) > > > Modified: > > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCart.java > > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCartServices.java > > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/product/ProductPromoWorker.java > > Modified: > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCart.java > URL: > http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCart.java?rev=1554265&r1=1554264&r2=1554265&view=diff > ============================================================================== > --- > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCart.java > (original) +++ > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCart.java > Mon Dec 30 15:53:07 2013 @@ -2714,7 +2714,7 @@ > public class ShoppingCart implements Ite } > itemsTotal = itemsTotal.add(cartItem.getItemSubTotal()); > } > - return itemsTotal; > + return itemsTotal.add(this.getOrderOtherAdjustmentTotal()); > } > > /** > @@ -3142,12 +3142,12 @@ public class ShoppingCart implements Ite > 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) { > @@ -4385,23 +4385,43 @@ public class ShoppingCart implements Ite > } > } > > - 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 { > > Modified: > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCartServices.java > URL: > http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCartServices.java?rev=1554265&r1=1554264&r2=1554265&view=diff > ============================================================================== > --- > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCartServices.java > (original) +++ > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/ShoppingCartServices.java > Mon Dec 30 15:53:07 2013 @@ -21,6 +21,7 > @@ package org.ofbiz.order.shoppingcart; > 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; > @@ -629,7 +630,7 @@ public class ShoppingCartServices { > 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>()); } } > > > Modified: > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/product/ProductPromoWorker.java > URL: > http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/product/ProductPromoWorker.java?rev=1554265&r1=1554264&r2=1554265&view=diff > ============================================================================== > --- > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/product/ProductPromoWorker.java > (original) +++ > ofbiz/trunk/applications/order/src/org/ofbiz/order/shoppingcart/product/ProductPromoWorker.java > Mon Dec 30 15:53:07 2013 @@ -22,6 > +22,8 @@ import java.math.BigDecimal; > import java.math.MathContext; > import java.sql.Timestamp; > import java.util.ArrayList; > +import java.util.Collections; > +import java.util.HashMap; > import java.util.Iterator; > import java.util.List; > import java.util.Locale; > @@ -51,6 +53,7 @@ import org.ofbiz.entity.condition.Entity > import org.ofbiz.entity.util.EntityUtil; > import org.ofbiz.order.shoppingcart.CartItemModifyException; > import org.ofbiz.order.shoppingcart.ShoppingCart; > +import org.ofbiz.order.shoppingcart.ShoppingCart.ProductPromoUseInfo; > import org.ofbiz.order.shoppingcart.ShoppingCartEvents; > import org.ofbiz.order.shoppingcart.ShoppingCartItem; > import org.ofbiz.product.product.ProductContentWrapper; > @@ -318,44 +321,62 @@ public class ProductPromoWorker { > // do a calculate only run through the promotions, then order by > descending totalDiscountAmount for each promotion > // NOTE: on this run, with isolatedTestRun passed as false it > should not apply any adjustments > // or track which cart items are used for which promotions, but > it will track ProductPromoUseInfo and > - // useLimits; we are basicly just trying to run each promo > "independently" to see how much each is worth > + // useLimits; we are basically just trying to run each promo > "independently" to see how much each is worth > runProductPromos(productPromoList, cart, delegator, dispatcher, > nowTimestamp, true); > > - // NOTE: after that first pass we could remove any that have a 0 > totalDiscountAmount from the run list, but we won't > because by the time they are run the cart may have changed enough to get them > to go; also, certain actions like free shipping > should always be run even though we won't know what the totalDiscountAmount > is at the time the promotion is run > - // each ProductPromoUseInfo on the shopping cart will contain > it's total value, so add up all totals for each > promoId and put them in a List of Maps > - // create a List of Maps with productPromo and > totalDiscountAmount, use the Map sorter to sort them descending by > totalDiscountAmount - > - // before sorting split into two lists and sort each list; one > list for promos that have a order total condition, > and the other list for all promos that don't; then we'll always run the ones > that have no condition on the order total first > - List<Map<Object, Object>> productPromoDiscountMapList = > FastList.newInstance(); > - List<Map<Object, Object>> productPromoDiscountMapListOrderTotal > = FastList.newInstance(); > + // NOTE: we can easily recognize the promos for the order total: > they are the ones with usage set to 0 > + Iterator<ProductPromoUseInfo> promoUses = > cart.getProductPromoUseInfoIter(); > + List<ProductPromoUseInfo> sortedPromoUses = new > ArrayList<ProductPromoUseInfo>(); > + while (promoUses.hasNext()) { > + ProductPromoUseInfo promoUse = promoUses.next(); > + sortedPromoUses.add(promoUse); > + } > + Collections.sort(sortedPromoUses); > + List<GenericValue> sortedExplodedProductPromoList = new > ArrayList<GenericValue>(sortedPromoUses.size()); > + Map<String, Long> usesPerPromo = new HashMap<String, Long>(); > + int indexOfFirstOrderTotalPromo = -1; > + for (ProductPromoUseInfo promoUse: sortedPromoUses) { > + GenericValue productPromo = > delegator.findOne("ProductPromo", UtilMisc.toMap("productPromoId", > promoUse.getProductPromoId()), true); + GenericValue > newProductPromo = (GenericValue)productPromo.clone(); > + if (!usesPerPromo.containsKey(promoUse.getProductPromoId())) > { > + usesPerPromo.put(promoUse.getProductPromoId(), 0l); > + } > + long uses = usesPerPromo.get(promoUse.getProductPromoId()); > + uses = uses + 1; > + long useLimitPerOrder = > (newProductPromo.get("useLimitPerOrder") != null? > newProductPromo.getLong("useLimitPerOrder"): -1); + if > (useLimitPerOrder == -1 || uses < useLimitPerOrder) { > + newProductPromo.set("useLimitPerOrder", uses); > + } > + usesPerPromo.put(promoUse.getProductPromoId(), uses); > + sortedExplodedProductPromoList.add(newProductPromo); > + if (indexOfFirstOrderTotalPromo == -1 && > BigDecimal.ZERO.equals(promoUse.getUsageWeight())) { > + indexOfFirstOrderTotalPromo = > sortedExplodedProductPromoList.size() - 1; > + } > + } > + if (indexOfFirstOrderTotalPromo == -1) { > + indexOfFirstOrderTotalPromo = > sortedExplodedProductPromoList.size() - 1; > + } > + > for (GenericValue productPromo : productPromoList) { > Map<Object, Object> productPromoDiscountMap = > UtilGenerics.checkMap(UtilMisc.toMap("productPromo", productPromo, > "totalDiscountAmount", > cart.getProductPromoUseTotalDiscount(productPromo.getString("productPromoId")))); > if > (hasOrderTotalCondition(productPromo, delegator)) { - > productPromoDiscountMapListOrderTotal.add(productPromoDiscountMap); + > if > (!usesPerPromo.containsKey(productPromo.getString("productPromoId"))) { + > > sortedExplodedProductPromoList.add(productPromo); + } > } else { > - productPromoDiscountMapList.add(productPromoDiscountMap); > + if > (!usesPerPromo.containsKey(productPromo.getString("productPromoId"))) { > + if (indexOfFirstOrderTotalPromo != -1) { > + > sortedExplodedProductPromoList.add(indexOfFirstOrderTotalPromo, productPromo); > + } else { > + sortedExplodedProductPromoList.add(0, > productPromo); > + } > + } > } > } > > - > - // sort the Map List, do it ascending because the discount > amounts will be negative, so the lowest number is really > the highest discount > - productPromoDiscountMapList = > UtilMisc.sortMaps(productPromoDiscountMapList, > UtilMisc.toList("+totalDiscountAmount")); > - productPromoDiscountMapListOrderTotal = > UtilMisc.sortMaps(productPromoDiscountMapListOrderTotal, > UtilMisc.toList("+totalDiscountAmount")); - > - > productPromoDiscountMapList.addAll(productPromoDiscountMapListOrderTotal); > - > - List<GenericValue> sortedProductPromoList = new > ArrayList<GenericValue>(productPromoDiscountMapList.size()); > - Iterator<Map<Object, Object>> productPromoDiscountMapIter = > productPromoDiscountMapList.iterator(); > - while (productPromoDiscountMapIter.hasNext()) { > - Map<Object, Object> productPromoDiscountMap = > UtilGenerics.checkMap(productPromoDiscountMapIter.next()); > - GenericValue productPromo = (GenericValue) > productPromoDiscountMap.get("productPromo"); > - sortedProductPromoList.add(productPromo); > - if (Debug.verboseOn()) Debug.logVerbose("Sorted Promo [" + > productPromo.getString("productPromoId") + "] with > total discount: " + productPromoDiscountMap.get("totalDiscountAmount"), > module); > - } > - > // okay, all ready, do the real run, clearing the temporary > result first... > cart.clearAllPromotionInformation(); > - runProductPromos(sortedProductPromoList, cart, delegator, > dispatcher, nowTimestamp, false); > + runProductPromos(sortedExplodedProductPromoList, cart, > delegator, dispatcher, nowTimestamp, false); > } catch (NumberFormatException e) { > Debug.logError(e, "Number not formatted correctly in promotion > rules, not completed...", module); > } catch (GenericEntityException e) { > @@ -436,7 +457,7 @@ public class ProductPromoWorker { > GenericValue productPromoCode = > productPromoCodeIter.next(); > String productPromoCodeId = > productPromoCode.getString("productPromoCodeId"); > Long codeUseLimit = > getProductPromoCodeUseLimit(productPromoCode, partyId, delegator); > - if (runProductPromoRules(cart, > cartChanged, useLimit, true, productPromoCodeId, > codeUseLimit, maxUseLimit, productPromo, productPromoRules, dispatcher, > delegator, nowTimestamp)) { + > if (runProductPromoRules(cart, > useLimit, true, productPromoCodeId, codeUseLimit, > maxUseLimit, productPromo, > productPromoRules, dispatcher, delegator, nowTimestamp)) { > cartChanged = true; } > > @@ -448,7 +469,7 @@ public class ProductPromoWorker { > } > } else { > try { > - if (runProductPromoRules(cart, cartChanged, > useLimit, false, null, null, maxUseLimit, > productPromo, productPromoRules, dispatcher, delegator, nowTimestamp)) { + > if > (runProductPromoRules(cart, useLimit, > false, null, null, maxUseLimit, productPromo, > productPromoRules, dispatcher, delegator, > nowTimestamp)) { cartChanged = true; } > } catch (RuntimeException e) { > @@ -735,8 +756,10 @@ public class ProductPromoWorker { > return promoDescBuf.toString(); > } > > - protected static boolean runProductPromoRules(ShoppingCart cart, boolean > cartChanged, Long useLimit, boolean requireCode, > String productPromoCodeId, Long codeUseLimit, long maxUseLimit, + > protected static boolean runProductPromoRules(ShoppingCart > cart, Long useLimit, boolean requireCode, String productPromoCodeId, > Long codeUseLimit, long maxUseLimit, GenericValue > productPromo, List<GenericValue> productPromoRules, LocalDispatcher > dispatcher, Delegator delegator, Timestamp nowTimestamp) > throws GenericEntityException, UseLimitException { + boolean > cartChanged = false; + > Map<ShoppingCartItem,BigDecimal> usageInfoMap = > prepareProductUsageInfoMap(cart); String productPromoId = > productPromo.getString("productPromoId"); while ((useLimit == null || > useLimit.longValue() > > cart.getProductPromoUseCount(productPromoId)) && > (!requireCode || UtilValidate.isNotEmpty(productPromoCodeId)) && > @@ -755,17 +778,17 @@ public class ProductPromoWorker { > // loop through conditions for rule, if any false, set > allConditionsTrue to false > List<GenericValue> productPromoConds = > delegator.findByAnd("ProductPromoCond", UtilMisc.toMap("productPromoId", > productPromo.get("productPromoId")), > UtilMisc.toList("productPromoCondSeqId"), true); productPromoConds = > EntityUtil.filterByAnd(productPromoConds, > UtilMisc.toMap("productPromoRuleId", > productPromoRule.get("productPromoRuleId"))); - > // using the other method to consolodate cache entries because the same cache > is used elsewhere: List productPromoConds = > productPromoRule.getRelated("ProductPromoCond", null, > UtilMisc.toList("productPromoCondSeqId"), true); + > // using the other method to consolidate cache entries because the same cache > is used elsewhere: List productPromoConds = > productPromoRule.getRelated("ProductPromoCond", null, > UtilMisc.toList("productPromoCondSeqId"), true); if (Debug.verboseOn()) > Debug.logVerbose("Checking " + productPromoConds.size() + " conditions for > rule " + productPromoRule, module); > > Iterator<GenericValue> productPromoCondIter = > UtilMisc.toIterator(productPromoConds); > while (productPromoCondIter != null && > productPromoCondIter.hasNext()) { > GenericValue productPromoCond = > productPromoCondIter.next(); > > - boolean condResult = checkCondition(productPromoCond, > cart, delegator, dispatcher, nowTimestamp); > + boolean conditionSatisfied = > checkCondition(productPromoCond, cart, delegator, dispatcher, nowTimestamp); > > // any false condition will cause it to NOT perform the > action > - if (condResult == false) { > + if (!conditionSatisfied) { > performActions = false; > break; > } > @@ -797,13 +820,16 @@ public class ProductPromoWorker { > } > > if (promoUsed) { > - > cart.addProductPromoUse(productPromo.getString("productPromoId"), > productPromoCodeId, totalDiscountAmount, > quantityLeftInActions); + // Get product use information from > the cart > + Map<ShoppingCartItem,BigDecimal> newUsageInfoMap = > prepareProductUsageInfoMap(cart); > + Map<ShoppingCartItem,BigDecimal> deltaUsageInfoMap = > prepareDeltaProductUsageInfoMap(usageInfoMap, > newUsageInfoMap); + usageInfoMap = newUsageInfoMap; > + > cart.addProductPromoUse(productPromo.getString("productPromoId"), > productPromoCodeId, totalDiscountAmount, > quantityLeftInActions, deltaUsageInfoMap); } else { > // the promotion was not used, don't try again until we > finish a full pass and come back to see the promo > conditions are now satisfied based on changes to the cart > break; > } > > - > if (cart.getProductPromoUseCount(productPromoId) > maxUseLimit) { > throw new UseLimitException("ERROR: While calculating > promotions the promotion [" + productPromoId + "] action > was applied more than " + maxUseLimit + " times, so the > calculation has been ended. This should generally never > happen unless you have bad rule definitions."); } @@ -812,6 +838,34 @@ public > class ProductPromoWorker { > return cartChanged; > } > > + private static Map<ShoppingCartItem,BigDecimal> > prepareProductUsageInfoMap(ShoppingCart cart) { > + Map<ShoppingCartItem,BigDecimal> usageInfoMap = new > HashMap<ShoppingCartItem, BigDecimal>(); > + List<ShoppingCartItem> lineOrderedByBasePriceList = > cart.getLineListOrderedByBasePrice(false); > + for (ShoppingCartItem cartItem : lineOrderedByBasePriceList) { > + BigDecimal used = cartItem.getPromoQuantityUsed(); > + if (used.compareTo(BigDecimal.ZERO) != 0) { > + usageInfoMap.put(cartItem, used); > + } > + } > + return usageInfoMap; > + } > + > + private static Map<ShoppingCartItem,BigDecimal> > prepareDeltaProductUsageInfoMap(Map<ShoppingCartItem,BigDecimal> oldMap, > Map<ShoppingCartItem,BigDecimal> newMap) { + > Map<ShoppingCartItem,BigDecimal> deltaUsageInfoMap = new > HashMap<ShoppingCartItem, BigDecimal>(newMap); + > Iterator<ShoppingCartItem> cartLines = oldMap.keySet().iterator(); > + while (cartLines.hasNext()) { > + ShoppingCartItem cartLine = cartLines.next(); > + BigDecimal oldUsed = oldMap.get(cartLine); > + BigDecimal newUsed = newMap.get(cartLine); > + if (newUsed.compareTo(oldUsed) > 0) { > + deltaUsageInfoMap.put(cartLine, > newUsed.add(oldUsed.negate())); > + } else { > + deltaUsageInfoMap.remove(cartLine); > + } > + } > + return deltaUsageInfoMap; > + } > + > protected static boolean checkCondition(GenericValue productPromoCond, > ShoppingCart cart, Delegator delegator, > LocalDispatcher dispatcher, Timestamp nowTimestamp) throws > GenericEntityException { String condValue = > productPromoCond.getString("condValue"); String otherValue = > productPromoCond.getString("otherValue"); > @@ -1772,8 +1826,8 @@ public class ProductPromoWorker { > actionResultInfo.ranAction = false; > } > > + // in action, if doesn't have enough quantity to use the promo at > all, remove candidate promo uses and increment > promoQuantityUsed; this should go for all actions, if any action runs > we confirm if (actionResultInfo.ranAction) { > - // in action, if doesn't have enough quantity to use the promo > at all, remove candidate promo uses and increment > promoQuantityUsed; this should go for all actions, if any action > runs we confirm > > cart.confirmPromoRuleUse(productPromoAction.getString("productPromoId"), > productPromoAction.getString("productPromoRuleId")); } else { > cart.resetPromoRuleUse(productPromoAction.getString("productPromoId"), > productPromoAction.getString("productPromoRuleId"));