Thanks Adrian, I am looking into this.
Jacopo On Jan 14, 2014, at 6:24 PM, [email protected] wrote: > This commit breaks promotions. > > 1. Create a new sales order, customer DemoCustomer. > 2. Add GZ-8544, qty 1 to the order. > 3. Promo items do not appear in order. > 4. Log shows an exception being thrown: > > 2014-01-14 12:15:56,030 (http-bio-0.0.0.0-8443-exec-1) [ > ProductPromoWorker.java:385:ERROR] > ---- runtime exception report > -------------------------------------------------- > Error running promotions, will ignore: java.lang.ArithmeticException: > Non-terminating decimal expansion; no exact representable decimal result. > Exception: java.lang.ArithmeticException > Message: Non-terminating decimal expansion; no exact representable decimal > result. > ---- stack trace > --------------------------------------------------------------- > java.lang.ArithmeticException: Non-terminating decimal expansion; no exact > representable decimal result. > java.math.BigDecimal.divide(BigDecimal.java:1616) > org.ofbiz.order.shoppingcart.ShoppingCart$ProductPromoUseInfo.getUsageWeight(ShoppingCart.java:4418) > org.ofbiz.order.shoppingcart.ShoppingCart$ProductPromoUseInfo.compareTo(ShoppingCart.java:4423) > org.ofbiz.order.shoppingcart.ShoppingCart$ProductPromoUseInfo.compareTo(ShoppingCart.java:4388) > java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:290) > java.util.ComparableTimSort.sort(ComparableTimSort.java:157) > java.util.ComparableTimSort.sort(ComparableTimSort.java:146) > java.util.Arrays.sort(Arrays.java:472) > java.util.Collections.sort(Collections.java:155) > org.ofbiz.order.shoppingcart.product.ProductPromoWorker.doPromotions(ProductPromoWorker.java:334) > org.ofbiz.order.shoppingcart.product.ProductPromoWorker.doPromotions(ProductPromoWorker.java:293) > org.ofbiz.order.shoppingcart.ShoppingCartItem.setQuantity(ShoppingCartItem.java:1061) > org.ofbiz.order.shoppingcart.ShoppingCartItem.makeItem(ShoppingCartItem.java:502) > org.ofbiz.order.shoppingcart.ShoppingCartItem.makeItem(ShoppingCartItem.java:334) > org.ofbiz.order.shoppingcart.ShoppingCart.addOrIncreaseItem(ShoppingCart.java:590) > org.ofbiz.order.shoppingcart.ShoppingCartHelper.addToCart(ShoppingCartHelper.java:245) > org.ofbiz.order.shoppingcart.ShoppingCartEvents.addToCart(ShoppingCartEvents.java:638) > sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) > sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) > sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) > java.lang.reflect.Method.invoke(Method.java:606) > org.ofbiz.webapp.event.JavaEventHandler.invoke(JavaEventHandler.java:93) > org.ofbiz.webapp.event.JavaEventHandler.invoke(JavaEventHandler.java:79) > org.ofbiz.webapp.control.RequestHandler.runEvent(RequestHandler.java:749) > org.ofbiz.webapp.control.RequestHandler.doRequest(RequestHandler.java:469) > org.ofbiz.webapp.control.ControlServlet.doGet(ControlServlet.java:219) > org.ofbiz.webapp.control.ControlServlet.doPost(ControlServlet.java:91) > javax.servlet.http.HttpServlet.service(HttpServlet.java:641) > javax.servlet.http.HttpServlet.service(HttpServlet.java:722) > org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305) > org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) > org.ofbiz.webapp.control.ContextFilter.doFilter(ContextFilter.java:314) > org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243) > org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) > org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) > org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) > org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) > org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) > org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100) > org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) > org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953) > org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:409) > org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1044) > org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607) > org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:315) > java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) > java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) > java.lang.Thread.run(Thread.java:744) > -------------------------------------------------------------------------------- > > > -Adrian > > > > Quoting [email protected]: > >> 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")); >> >> >> > > >
