Fixed at rev. 1558145 Thanks for the report. Jacopo
On Jan 14, 2014, at 6:36 PM, Jacopo Cappellato <[email protected]> wrote: > 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")); >>> >>> >>> >> >> >> >
